file_dialog_win.cc 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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 != NULL);
  57. LPWSTR lpstrName = NULL;
  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. hRet = DISP_E_BUFFERTOOSMALL;
  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(), NULL,
  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. : NULL;
  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. if (std::wstring(filterspec[i].pszSpec) != L"*.*") {
  117. // SetFileTypeIndex is regarded as one-based index.
  118. dialog->SetFileTypeIndex(i + 1);
  119. dialog->SetDefaultExtension(filterspec[i].pszSpec);
  120. break;
  121. }
  122. }
  123. if (settings.default_path.IsAbsolute()) {
  124. SetDefaultFolder(dialog, settings.default_path);
  125. }
  126. }
  127. } // namespace
  128. bool ShowOpenDialogSync(const DialogSettings& settings,
  129. std::vector<base::FilePath>* paths) {
  130. ATL::CComPtr<IFileOpenDialog> file_open_dialog;
  131. HRESULT hr = file_open_dialog.CoCreateInstance(CLSID_FileOpenDialog);
  132. if (FAILED(hr))
  133. return false;
  134. DWORD options = FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST;
  135. if (settings.properties & OPEN_DIALOG_OPEN_DIRECTORY)
  136. options |= FOS_PICKFOLDERS;
  137. if (settings.properties & OPEN_DIALOG_MULTI_SELECTIONS)
  138. options |= FOS_ALLOWMULTISELECT;
  139. if (settings.properties & OPEN_DIALOG_SHOW_HIDDEN_FILES)
  140. options |= FOS_FORCESHOWHIDDEN;
  141. if (settings.properties & OPEN_DIALOG_PROMPT_TO_CREATE)
  142. options |= FOS_CREATEPROMPT;
  143. if (settings.properties & FILE_DIALOG_DONT_ADD_TO_RECENT)
  144. options |= FOS_DONTADDTORECENT;
  145. file_open_dialog->SetOptions(options);
  146. ApplySettings(file_open_dialog, settings);
  147. hr = ShowFileDialog(file_open_dialog, settings);
  148. if (FAILED(hr))
  149. return false;
  150. ATL::CComPtr<IShellItemArray> items;
  151. hr = file_open_dialog->GetResults(&items);
  152. if (FAILED(hr))
  153. return false;
  154. ATL::CComPtr<IShellItem> item;
  155. DWORD count = 0;
  156. hr = items->GetCount(&count);
  157. if (FAILED(hr))
  158. return false;
  159. paths->reserve(count);
  160. for (DWORD i = 0; i < count; ++i) {
  161. hr = items->GetItemAt(i, &item);
  162. if (FAILED(hr))
  163. return false;
  164. wchar_t file_name[MAX_PATH];
  165. hr = GetFileNameFromShellItem(item, SIGDN_FILESYSPATH, file_name,
  166. std::size(file_name));
  167. if (FAILED(hr))
  168. return false;
  169. paths->push_back(base::FilePath(file_name));
  170. }
  171. return true;
  172. }
  173. void ShowOpenDialog(const DialogSettings& settings,
  174. gin_helper::Promise<gin_helper::Dictionary> promise) {
  175. auto done = [](gin_helper::Promise<gin_helper::Dictionary> promise,
  176. bool success, std::vector<base::FilePath> result) {
  177. v8::HandleScope handle_scope(promise.isolate());
  178. gin::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  179. dict.Set("canceled", !success);
  180. dict.Set("filePaths", result);
  181. promise.Resolve(dict);
  182. };
  183. dialog_thread::Run(base::BindOnce(ShowOpenDialogSync, settings),
  184. base::BindOnce(done, std::move(promise)));
  185. }
  186. bool ShowSaveDialogSync(const DialogSettings& settings, base::FilePath* path) {
  187. ATL::CComPtr<IFileSaveDialog> file_save_dialog;
  188. HRESULT hr = file_save_dialog.CoCreateInstance(CLSID_FileSaveDialog);
  189. if (FAILED(hr))
  190. return false;
  191. DWORD options = FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_OVERWRITEPROMPT;
  192. if (settings.properties & SAVE_DIALOG_SHOW_HIDDEN_FILES)
  193. options |= FOS_FORCESHOWHIDDEN;
  194. if (settings.properties & SAVE_DIALOG_DONT_ADD_TO_RECENT)
  195. options |= FOS_DONTADDTORECENT;
  196. file_save_dialog->SetOptions(options);
  197. ApplySettings(file_save_dialog, settings);
  198. hr = ShowFileDialog(file_save_dialog, settings);
  199. if (FAILED(hr))
  200. return false;
  201. CComPtr<IShellItem> pItem;
  202. hr = file_save_dialog->GetResult(&pItem);
  203. if (FAILED(hr))
  204. return false;
  205. PWSTR result_path = nullptr;
  206. hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &result_path);
  207. if (!SUCCEEDED(hr))
  208. return false;
  209. *path = base::FilePath(result_path);
  210. CoTaskMemFree(result_path);
  211. return true;
  212. }
  213. void ShowSaveDialog(const DialogSettings& settings,
  214. gin_helper::Promise<gin_helper::Dictionary> promise) {
  215. auto done = [](gin_helper::Promise<gin_helper::Dictionary> promise,
  216. bool success, base::FilePath result) {
  217. v8::HandleScope handle_scope(promise.isolate());
  218. gin::Dictionary dict = gin::Dictionary::CreateEmpty(promise.isolate());
  219. dict.Set("canceled", !success);
  220. dict.Set("filePath", result);
  221. promise.Resolve(dict);
  222. };
  223. dialog_thread::Run(base::BindOnce(ShowSaveDialogSync, settings),
  224. base::BindOnce(done, std::move(promise)));
  225. }
  226. } // namespace file_dialog