file_dialog_win.cc 9.1 KB

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