file_dialog_win.cc 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. // Copyright (c) 2013 GitHub, Inc.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/ui/file_dialog.h"
  5. #include <windows.h> // windows.h must be included first
  6. #include "base/win/shlwapi.h" // NOLINT(build/include_order)
  7. // atlbase.h for CComPtr
  8. #include <atlbase.h> // NOLINT(build/include_order)
  9. #include <shlobj.h> // NOLINT(build/include_order)
  10. #include <shobjidl.h> // NOLINT(build/include_order)
  11. #include "base/files/file_util.h"
  12. #include "base/i18n/case_conversion.h"
  13. #include "base/strings/string_split.h"
  14. #include "base/strings/string_util.h"
  15. #include "base/strings/utf_string_conversions.h"
  16. #include "base/win/registry.h"
  17. #include "shell/browser/native_window_views.h"
  18. #include "shell/browser/ui/win/dialog_thread.h"
  19. #include "shell/common/gin_converters/file_path_converter.h"
  20. namespace file_dialog {
  21. DialogSettings::DialogSettings() = default;
  22. DialogSettings::DialogSettings(const DialogSettings&) = default;
  23. DialogSettings::~DialogSettings() = default;
  24. namespace {
  25. // Distinguish directories from regular files.
  26. bool IsDirectory(const base::FilePath& path) {
  27. base::File::Info file_info;
  28. return base::GetFileInfo(path, &file_info) ? file_info.is_directory
  29. : path.EndsWithSeparator();
  30. }
  31. void ConvertFilters(const Filters& filters,
  32. std::vector<std::wstring>* buffer,
  33. std::vector<COMDLG_FILTERSPEC>* filterspec) {
  34. if (filters.empty()) {
  35. COMDLG_FILTERSPEC spec = {L"All Files (*.*)", L"*.*"};
  36. filterspec->push_back(spec);
  37. return;
  38. }
  39. buffer->reserve(filters.size() * 2);
  40. for (const Filter& filter : filters) {
  41. COMDLG_FILTERSPEC spec;
  42. buffer->push_back(base::UTF8ToWide(filter.first));
  43. spec.pszName = buffer->back().c_str();
  44. std::vector<std::string> extensions(filter.second);
  45. for (std::string& extension : extensions)
  46. extension.insert(0, "*.");
  47. buffer->push_back(base::UTF8ToWide(base::JoinString(extensions, ";")));
  48. spec.pszSpec = buffer->back().c_str();
  49. filterspec->push_back(spec);
  50. }
  51. }
  52. static HRESULT GetFileNameFromShellItem(IShellItem* pShellItem,
  53. SIGDN type,
  54. LPWSTR lpstr,
  55. size_t cchLength) {
  56. assert(pShellItem != nullptr);
  57. LPWSTR lpstrName = nullptr;
  58. HRESULT hRet = pShellItem->GetDisplayName(type, &lpstrName);
  59. if (SUCCEEDED(hRet)) {
  60. if (wcslen(lpstrName) < cchLength) {
  61. wcscpy_s(lpstr, cchLength, lpstrName);
  62. } else {
  63. NOTREACHED();
  64. }
  65. ::CoTaskMemFree(lpstrName);
  66. }
  67. return hRet;
  68. }
  69. static void SetDefaultFolder(IFileDialog* dialog,
  70. const base::FilePath file_path) {
  71. std::wstring directory =
  72. IsDirectory(file_path) ? file_path.value() : file_path.DirName().value();
  73. ATL::CComPtr<IShellItem> folder_item;
  74. HRESULT hr = SHCreateItemFromParsingName(directory.c_str(), nullptr,
  75. IID_PPV_ARGS(&folder_item));
  76. if (SUCCEEDED(hr))
  77. dialog->SetFolder(folder_item);
  78. }
  79. static HRESULT ShowFileDialog(IFileDialog* dialog,
  80. const DialogSettings& settings) {
  81. HWND parent_window =
  82. settings.parent_window
  83. ? static_cast<electron::NativeWindowViews*>(settings.parent_window)
  84. ->GetAcceleratedWidget()
  85. : nullptr;
  86. return dialog->Show(parent_window);
  87. }
  88. static void ApplySettings(IFileDialog* dialog, const DialogSettings& settings) {
  89. std::wstring file_part;
  90. if (!IsDirectory(settings.default_path))
  91. file_part = settings.default_path.BaseName().value();
  92. dialog->SetFileName(file_part.c_str());
  93. if (!settings.title.empty())
  94. dialog->SetTitle(base::UTF8ToWide(settings.title).c_str());
  95. if (!settings.button_label.empty())
  96. dialog->SetOkButtonLabel(base::UTF8ToWide(settings.button_label).c_str());
  97. std::vector<std::wstring> buffer;
  98. std::vector<COMDLG_FILTERSPEC> filterspec;
  99. ConvertFilters(settings.filters, &buffer, &filterspec);
  100. if (!filterspec.empty()) {
  101. dialog->SetFileTypes(filterspec.size(), filterspec.data());
  102. }
  103. // By default, *.* will be added to the file name if file type is "*.*". In
  104. // Electron, we disable it to make a better experience.
  105. //
  106. // From MSDN: https://msdn.microsoft.com/en-us/library/windows/desktop/
  107. // bb775970(v=vs.85).aspx
  108. //
  109. // If SetDefaultExtension is not called, the dialog will not update
  110. // automatically when user choose a new file type in the file dialog.
  111. //
  112. // We set file extension to the first none-wildcard extension to make
  113. // sure the dialog will update file extension automatically.
  114. for (size_t i = 0; i < filterspec.size(); ++i) {
  115. if (std::wstring(filterspec[i].pszSpec) != L"*.*") {
  116. // SetFileTypeIndex is regarded as one-based index.
  117. dialog->SetFileTypeIndex(i + 1);
  118. dialog->SetDefaultExtension(filterspec[i].pszSpec);
  119. break;
  120. }
  121. }
  122. if (settings.default_path.IsAbsolute()) {
  123. SetDefaultFolder(dialog, settings.default_path);
  124. }
  125. }
  126. } // namespace
  127. bool ShowOpenDialogSync(const DialogSettings& settings,
  128. std::vector<base::FilePath>* paths) {
  129. ATL::CComPtr<IFileOpenDialog> file_open_dialog;
  130. HRESULT hr = file_open_dialog.CoCreateInstance(CLSID_FileOpenDialog);
  131. if (FAILED(hr))
  132. return false;
  133. DWORD options = FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST;
  134. if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
  135. options |= FOS_PICKFOLDERS;
  136. if (settings.properties & OPEN_DIALOG_MULTI_SELECTIONS)
  137. options |= FOS_ALLOWMULTISELECT;
  138. if (settings.properties & OPEN_DIALOG_SHOW_HIDDEN_FILES)
  139. options |= FOS_FORCESHOWHIDDEN;
  140. if (settings.properties & OPEN_DIALOG_PROMPT_TO_CREATE)
  141. options |= FOS_CREATEPROMPT;
  142. if (settings.properties & FILE_DIALOG_DONT_ADD_TO_RECENT)
  143. options |= FOS_DONTADDTORECENT;
  144. file_open_dialog->SetOptions(options);
  145. ApplySettings(file_open_dialog, settings);
  146. hr = ShowFileDialog(file_open_dialog, settings);
  147. if (FAILED(hr))
  148. return false;
  149. ATL::CComPtr<IShellItemArray> items;
  150. hr = file_open_dialog->GetResults(&items);
  151. if (FAILED(hr))
  152. return false;
  153. ATL::CComPtr<IShellItem> item;
  154. DWORD count = 0;
  155. hr = items->GetCount(&count);
  156. if (FAILED(hr))
  157. return false;
  158. paths->reserve(count);
  159. for (DWORD i = 0; i < count; ++i) {
  160. hr = items->GetItemAt(i, &item);
  161. if (FAILED(hr))
  162. return false;
  163. wchar_t file_name[MAX_PATH];
  164. hr = GetFileNameFromShellItem(item, SIGDN_FILESYSPATH, file_name,
  165. std::size(file_name));
  166. if (FAILED(hr))
  167. return false;
  168. paths->push_back(base::FilePath(file_name));
  169. }
  170. return true;
  171. }
  172. void ShowOpenDialog(const DialogSettings& settings,
  173. gin_helper::Promise<gin_helper::Dictionary> promise) {
  174. auto done = [](gin_helper::Promise<gin_helper::Dictionary> promise,
  175. bool success, std::vector<base::FilePath> result) {
  176. v8::HandleScope handle_scope(promise.isolate());
  177. auto dict = gin::Dictionary::CreateEmpty(promise.isolate());
  178. dict.Set("canceled", !success);
  179. dict.Set("filePaths", result);
  180. promise.Resolve(dict);
  181. };
  182. dialog_thread::Run(base::BindOnce(ShowOpenDialogSync, settings),
  183. base::BindOnce(done, std::move(promise)));
  184. }
  185. bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
  186. ATL::CComPtr<IFileSaveDialog> file_save_dialog;
  187. HRESULT hr = file_save_dialog.CoCreateInstance(CLSID_FileSaveDialog);
  188. if (FAILED(hr))
  189. return false;
  190. DWORD options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT;
  191. if (settings.properties & SAVE_DIALOG_SHOW_HIDDEN_FILES)
  192. options |= FOS_FORCESHOWHIDDEN;
  193. if (settings.properties & SAVE_DIALOG_DONT_ADD_TO_RECENT)
  194. options |= FOS_DONTADDTORECENT;
  195. file_save_dialog->SetOptions(options);
  196. ApplySettings(file_save_dialog, settings);
  197. hr = ShowFileDialog(file_save_dialog, settings);
  198. if (FAILED(hr))
  199. return false;
  200. CComPtr<IShellItem> pItem;
  201. hr = file_save_dialog->GetResult(&pItem);
  202. if (FAILED(hr))
  203. return false;
  204. PWSTR result_path = nullptr;
  205. hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &result_path);
  206. if (!SUCCEEDED(hr))
  207. return false;
  208. *path = base::FilePath(result_path);
  209. CoTaskMemFree(result_path);
  210. return true;
  211. }
  212. void ShowSaveDialog(const DialogSettings& settings,
  213. gin_helper::Promise<gin_helper::Dictionary> promise) {
  214. auto done = [](gin_helper::Promise<gin_helper::Dictionary> promise,
  215. bool success, base::FilePath result) {
  216. v8::HandleScope handle_scope(promise.isolate());
  217. auto dict = gin::Dictionary::CreateEmpty(promise.isolate());
  218. dict.Set("canceled", !success);
  219. dict.Set("filePath", result);
  220. promise.Resolve(dict);
  221. };
  222. dialog_thread::Run(base::BindOnce(ShowSaveDialogSync, settings),
  223. base::BindOnce(done, std::move(promise)));
  224. }
  225. } // namespace file_dialog