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_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/browser/unresponsive_suppressor.h"
  20. #include "shell/common/gin_converters/file_path_converter.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 != NULL);
  58. LPWSTR lpstrName = NULL;
  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. hRet = DISP_E_BUFFERTOOSMALL;
  66. }
  67. ::CoTaskMemFree(lpstrName);
  68. }
  69. return hRet;
  70. }
  71. static void SetDefaultFolder(IFileDialog* dialog,
  72. const base::FilePath file_path) {
  73. std::wstring directory =
  74. IsDirectory(file_path) ? file_path.value() : file_path.DirName().value();
  75. ATL::CComPtr<IShellItem> folder_item;
  76. HRESULT hr = SHCreateItemFromParsingName(directory.c_str(), NULL,
  77. IID_PPV_ARGS(&folder_item));
  78. if (SUCCEEDED(hr))
  79. dialog->SetFolder(folder_item);
  80. }
  81. static HRESULT ShowFileDialog(IFileDialog* dialog,
  82. const DialogSettings& settings) {
  83. electron::UnresponsiveSuppressor suppressor;
  84. HWND parent_window =
  85. settings.parent_window
  86. ? static_cast<electron::NativeWindowViews*>(settings.parent_window)
  87. ->GetAcceleratedWidget()
  88. : NULL;
  89. return dialog->Show(parent_window);
  90. }
  91. static void ApplySettings(IFileDialog* dialog, const DialogSettings& settings) {
  92. std::wstring file_part;
  93. if (!IsDirectory(settings.default_path))
  94. file_part = settings.default_path.BaseName().value();
  95. dialog->SetFileName(file_part.c_str());
  96. if (!settings.title.empty())
  97. dialog->SetTitle(base::UTF8ToWide(settings.title).c_str());
  98. if (!settings.button_label.empty())
  99. dialog->SetOkButtonLabel(base::UTF8ToWide(settings.button_label).c_str());
  100. std::vector<std::wstring> buffer;
  101. std::vector<COMDLG_FILTERSPEC> filterspec;
  102. ConvertFilters(settings.filters, &buffer, &filterspec);
  103. if (!filterspec.empty()) {
  104. dialog->SetFileTypes(filterspec.size(), filterspec.data());
  105. }
  106. // By default, *.* will be added to the file name if file type is "*.*". In
  107. // Electron, we disable it to make a better experience.
  108. //
  109. // From MSDN: https://msdn.microsoft.com/en-us/library/windows/desktop/
  110. // bb775970(v=vs.85).aspx
  111. //
  112. // If SetDefaultExtension is not called, the dialog will not update
  113. // automatically when user choose a new file type in the file dialog.
  114. //
  115. // We set file extension to the first none-wildcard extension to make
  116. // sure the dialog will update file extension automatically.
  117. for (size_t i = 0; i < filterspec.size(); ++i) {
  118. if (std::wstring(filterspec[i].pszSpec) != L"*.*") {
  119. // SetFileTypeIndex is regarded as one-based index.
  120. dialog->SetFileTypeIndex(i + 1);
  121. dialog->SetDefaultExtension(filterspec[i].pszSpec);
  122. break;
  123. }
  124. }
  125. if (settings.default_path.IsAbsolute()) {
  126. SetDefaultFolder(dialog, settings.default_path);
  127. }
  128. }
  129. } // namespace
  130. bool ShowOpenDialogSync(const DialogSettings& settings,
  131. std::vector<base::FilePath>* paths) {
  132. ATL::CComPtr<IFileOpenDialog> file_open_dialog;
  133. HRESULT hr = file_open_dialog.CoCreateInstance(CLSID_FileOpenDialog);
  134. if (FAILED(hr))
  135. return false;
  136. DWORD options = FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST;
  137. if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
  138. options |= FOS_PICKFOLDERS;
  139. if (settings.properties & OPEN_DIALOG_MULTI_SELECTIONS)
  140. options |= FOS_ALLOWMULTISELECT;
  141. if (settings.properties & OPEN_DIALOG_SHOW_HIDDEN_FILES)
  142. options |= FOS_FORCESHOWHIDDEN;
  143. if (settings.properties & OPEN_DIALOG_PROMPT_TO_CREATE)
  144. options |= FOS_CREATEPROMPT;
  145. if (settings.properties & FILE_DIALOG_DONT_ADD_TO_RECENT)
  146. options |= FOS_DONTADDTORECENT;
  147. file_open_dialog->SetOptions(options);
  148. ApplySettings(file_open_dialog, settings);
  149. hr = ShowFileDialog(file_open_dialog, settings);
  150. if (FAILED(hr))
  151. return false;
  152. ATL::CComPtr<IShellItemArray> items;
  153. hr = file_open_dialog->GetResults(&items);
  154. if (FAILED(hr))
  155. return false;
  156. ATL::CComPtr<IShellItem> item;
  157. DWORD count = 0;
  158. hr = items->GetCount(&count);
  159. if (FAILED(hr))
  160. return false;
  161. paths->reserve(count);
  162. for (DWORD i = 0; i < count; ++i) {
  163. hr = items->GetItemAt(i, &item);
  164. if (FAILED(hr))
  165. return false;
  166. wchar_t file_name[MAX_PATH];
  167. hr = GetFileNameFromShellItem(item, SIGDN_FILESYSPATH, file_name,
  168. std::size(file_name));
  169. if (FAILED(hr))
  170. return false;
  171. paths->push_back(base::FilePath(file_name));
  172. }
  173. return true;
  174. }
  175. void ShowOpenDialog(const DialogSettings& settings,
  176. gin_helper::Promise<gin_helper::Dictionary> promise) {
  177. auto done = [](gin_helper::Promise<gin_helper::Dictionary> promise,
  178. bool success, std::vector<base::FilePath> result) {
  179. v8::Locker locker(promise.isolate());
  180. v8::HandleScope handle_scope(promise.isolate());
  181. gin::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  182. dict.Set("canceled", !success);
  183. dict.Set("filePaths", result);
  184. promise.Resolve(dict);
  185. };
  186. dialog_thread::Run(base::BindOnce(ShowOpenDialogSync, settings),
  187. base::BindOnce(done, std::move(promise)));
  188. }
  189. bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
  190. ATL::CComPtr<IFileSaveDialog> file_save_dialog;
  191. HRESULT hr = file_save_dialog.CoCreateInstance(CLSID_FileSaveDialog);
  192. if (FAILED(hr))
  193. return false;
  194. DWORD options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT;
  195. if (settings.properties & SAVE_DIALOG_SHOW_HIDDEN_FILES)
  196. options |= FOS_FORCESHOWHIDDEN;
  197. if (settings.properties & SAVE_DIALOG_DONT_ADD_TO_RECENT)
  198. options |= FOS_DONTADDTORECENT;
  199. file_save_dialog->SetOptions(options);
  200. ApplySettings(file_save_dialog, settings);
  201. hr = ShowFileDialog(file_save_dialog, settings);
  202. if (FAILED(hr))
  203. return false;
  204. CComPtr<IShellItem> pItem;
  205. hr = file_save_dialog->GetResult(&pItem);
  206. if (FAILED(hr))
  207. return false;
  208. PWSTR result_path = nullptr;
  209. hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &result_path);
  210. if (!SUCCEEDED(hr))
  211. return false;
  212. *path = base::FilePath(result_path);
  213. CoTaskMemFree(result_path);
  214. return true;
  215. }
  216. void ShowSaveDialog(const DialogSettings& settings,
  217. gin_helper::Promise<gin_helper::Dictionary> promise) {
  218. auto done = [](gin_helper::Promise<gin_helper::Dictionary> promise,
  219. bool success, base::FilePath result) {
  220. v8::Locker locker(promise.isolate());
  221. v8::HandleScope handle_scope(promise.isolate());
  222. gin::Dictionary 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