message_box_win.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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/message_box.h"
  5. #include <windows.h> // windows.h must be included first
  6. #include <commctrl.h>
  7. #include <vector>
  8. #include "base/containers/contains.h"
  9. #include "base/containers/flat_map.h"
  10. #include "base/no_destructor.h"
  11. #include "base/strings/string_util.h"
  12. #include "base/strings/utf_string_conversions.h"
  13. #include "base/synchronization/lock.h"
  14. #include "base/win/scoped_gdi_object.h"
  15. #include "shell/browser/browser.h"
  16. #include "shell/browser/native_window_views.h"
  17. #include "shell/browser/ui/win/dialog_thread.h"
  18. #include "ui/gfx/icon_util.h"
  19. #include "ui/gfx/image/image_skia.h"
  20. namespace electron {
  21. MessageBoxSettings::MessageBoxSettings() = default;
  22. MessageBoxSettings::MessageBoxSettings(const MessageBoxSettings&) = default;
  23. MessageBoxSettings::~MessageBoxSettings() = default;
  24. namespace {
  25. struct DialogResult {
  26. int button_id;
  27. bool verification_flag_checked;
  28. };
  29. // <ID, messageBox> map.
  30. //
  31. // Note that the HWND is stored in a unique_ptr, because the pointer of HWND
  32. // will be passed between threads and we need to ensure the memory of HWND is
  33. // not changed while dialogs map is modified.
  34. base::flat_map<int, std::unique_ptr<HWND>>& GetDialogsMap() {
  35. static base::NoDestructor<base::flat_map<int, std::unique_ptr<HWND>>> dialogs;
  36. return *dialogs;
  37. }
  38. // Special HWND used by the dialogs map.
  39. //
  40. // - ID is used but window has not been created yet.
  41. const HWND kHwndReserve = reinterpret_cast<HWND>(-1);
  42. // - Notification to cancel message box.
  43. const HWND kHwndCancel = reinterpret_cast<HWND>(-2);
  44. // Lock used for modifying HWND between threads.
  45. //
  46. // Note that there might be multiple dialogs being opened at the same time, but
  47. // we only use one lock for them all, because each dialog is independent from
  48. // each other and there is no need to use different lock for each one.
  49. // Also note that the |GetDialogsMap| is only used in the main thread, what is
  50. // shared between threads is the memory of HWND, so there is no need to use lock
  51. // when accessing dialogs map.
  52. base::Lock& GetHWNDLock() {
  53. static base::NoDestructor<base::Lock> lock;
  54. return *lock;
  55. }
  56. // Small command ID values are already taken by Windows, we have to start from
  57. // a large number to avoid conflicts with Windows.
  58. const int kIDStart = 100;
  59. // Get the common ID from button's name.
  60. struct CommonButtonID {
  61. int button;
  62. int id;
  63. };
  64. CommonButtonID GetCommonID(const std::wstring& button) {
  65. std::wstring lower = base::ToLowerASCII(button);
  66. if (lower == L"ok")
  67. return {TDCBF_OK_BUTTON, IDOK};
  68. else if (lower == L"yes")
  69. return {TDCBF_YES_BUTTON, IDYES};
  70. else if (lower == L"no")
  71. return {TDCBF_NO_BUTTON, IDNO};
  72. else if (lower == L"cancel")
  73. return {TDCBF_CANCEL_BUTTON, IDCANCEL};
  74. else if (lower == L"retry")
  75. return {TDCBF_RETRY_BUTTON, IDRETRY};
  76. else if (lower == L"close")
  77. return {TDCBF_CLOSE_BUTTON, IDCLOSE};
  78. return {-1, -1};
  79. }
  80. // Determine whether the buttons are common buttons, if so map common ID
  81. // to button ID.
  82. void MapToCommonID(const std::vector<std::wstring>& buttons,
  83. base::flat_map<int, int>* id_map,
  84. TASKDIALOG_COMMON_BUTTON_FLAGS* button_flags,
  85. std::vector<TASKDIALOG_BUTTON>* dialog_buttons) {
  86. for (size_t i = 0; i < buttons.size(); ++i) {
  87. auto common = GetCommonID(buttons[i]);
  88. if (common.button != -1) {
  89. // It is a common button.
  90. (*id_map)[common.id] = i;
  91. (*button_flags) |= common.button;
  92. } else {
  93. // It is a custom button.
  94. dialog_buttons->push_back(
  95. {static_cast<int>(i + kIDStart), buttons[i].c_str()});
  96. }
  97. }
  98. }
  99. // Callback of the task dialog. The TaskDialogIndirect API does not provide the
  100. // HWND of the dialog, and we have to listen to the TDN_CREATED message to get
  101. // it.
  102. // Note that this callback runs in dialog thread instead of main thread, so it
  103. // is possible for CloseMessageBox to be called before or all after the dialog
  104. // window is created.
  105. HRESULT CALLBACK
  106. TaskDialogCallback(HWND hwnd, UINT msg, WPARAM, LPARAM, LONG_PTR data) {
  107. if (msg == TDN_CREATED) {
  108. HWND* target = reinterpret_cast<HWND*>(data);
  109. // Lock since CloseMessageBox might be called.
  110. base::AutoLock lock(GetHWNDLock());
  111. if (*target == kHwndCancel) {
  112. // The dialog is cancelled before it is created, close it directly.
  113. ::PostMessage(hwnd, WM_CLOSE, 0, 0);
  114. } else if (*target == kHwndReserve) {
  115. // Otherwise save the hwnd.
  116. *target = hwnd;
  117. } else {
  118. NOTREACHED();
  119. }
  120. }
  121. return S_OK;
  122. }
  123. DialogResult ShowTaskDialogWstr(gfx::AcceleratedWidget parent,
  124. MessageBoxType type,
  125. const std::vector<std::wstring>& buttons,
  126. int default_id,
  127. int cancel_id,
  128. bool no_link,
  129. const std::u16string& title,
  130. const std::u16string& message,
  131. const std::u16string& detail,
  132. const std::u16string& checkbox_label,
  133. bool checkbox_checked,
  134. const gfx::ImageSkia& icon,
  135. HWND* hwnd) {
  136. TASKDIALOG_FLAGS flags =
  137. TDF_SIZE_TO_CONTENT | // Show all content.
  138. TDF_ALLOW_DIALOG_CANCELLATION; // Allow canceling the dialog.
  139. TASKDIALOGCONFIG config = {0};
  140. config.cbSize = sizeof(config);
  141. config.hInstance = GetModuleHandle(nullptr);
  142. config.dwFlags = flags;
  143. if (parent) {
  144. config.hwndParent = parent;
  145. }
  146. if (default_id > 0)
  147. config.nDefaultButton = kIDStart + default_id;
  148. // TaskDialogIndirect doesn't allow empty name, if we set empty title it
  149. // will show "electron.exe" in title.
  150. std::wstring app_name;
  151. if (title.empty()) {
  152. app_name = base::UTF8ToWide(Browser::Get()->GetName());
  153. config.pszWindowTitle = app_name.c_str();
  154. } else {
  155. config.pszWindowTitle = base::as_wcstr(title);
  156. }
  157. base::win::ScopedHICON hicon;
  158. if (!icon.isNull()) {
  159. hicon = IconUtil::CreateHICONFromSkBitmap(*icon.bitmap());
  160. config.dwFlags |= TDF_USE_HICON_MAIN;
  161. config.hMainIcon = hicon.get();
  162. } else {
  163. // Show icon according to dialog's type.
  164. switch (type) {
  165. case MessageBoxType::kInformation:
  166. case MessageBoxType::kQuestion:
  167. config.pszMainIcon = TD_INFORMATION_ICON;
  168. break;
  169. case MessageBoxType::kWarning:
  170. config.pszMainIcon = TD_WARNING_ICON;
  171. break;
  172. case MessageBoxType::kError:
  173. config.pszMainIcon = TD_ERROR_ICON;
  174. break;
  175. case MessageBoxType::kNone:
  176. break;
  177. }
  178. }
  179. // If "detail" is empty then don't make message highlighted.
  180. if (detail.empty()) {
  181. config.pszContent = base::as_wcstr(message);
  182. } else {
  183. config.pszMainInstruction = base::as_wcstr(message);
  184. config.pszContent = base::as_wcstr(detail);
  185. }
  186. if (!checkbox_label.empty()) {
  187. config.pszVerificationText = base::as_wcstr(checkbox_label);
  188. if (checkbox_checked)
  189. config.dwFlags |= TDF_VERIFICATION_FLAG_CHECKED;
  190. }
  191. // Iterate through the buttons, put common buttons in dwCommonButtons
  192. // and custom buttons in pButtons.
  193. base::flat_map<int, int> id_map;
  194. std::vector<TASKDIALOG_BUTTON> dialog_buttons;
  195. if (no_link) {
  196. for (size_t i = 0; i < buttons.size(); ++i)
  197. dialog_buttons.push_back(
  198. {static_cast<int>(i + kIDStart), buttons[i].c_str()});
  199. } else {
  200. MapToCommonID(buttons, &id_map, &config.dwCommonButtons, &dialog_buttons);
  201. }
  202. if (!dialog_buttons.empty()) {
  203. config.pButtons = &dialog_buttons.front();
  204. config.cButtons = dialog_buttons.size();
  205. if (!no_link)
  206. config.dwFlags |= TDF_USE_COMMAND_LINKS; // custom buttons as links.
  207. }
  208. // Pass a callback to receive the HWND of the message box.
  209. if (hwnd) {
  210. config.pfCallback = &TaskDialogCallback;
  211. config.lpCallbackData = reinterpret_cast<LONG_PTR>(hwnd);
  212. }
  213. int id = 0;
  214. BOOL verification_flag_checked = FALSE;
  215. TaskDialogIndirect(&config, &id, nullptr, &verification_flag_checked);
  216. int button_id;
  217. if (base::Contains(id_map, id)) // common button.
  218. button_id = id_map[id];
  219. else if (id >= kIDStart) // custom button.
  220. button_id = id - kIDStart;
  221. else
  222. button_id = cancel_id;
  223. return {button_id, static_cast<bool>(verification_flag_checked)};
  224. }
  225. DialogResult ShowTaskDialogUTF8(const MessageBoxSettings& settings,
  226. gfx::AcceleratedWidget parent_widget,
  227. HWND* hwnd) {
  228. std::vector<std::wstring> buttons;
  229. for (const auto& button : settings.buttons)
  230. buttons.push_back(base::UTF8ToWide(button));
  231. const std::u16string title = base::UTF8ToUTF16(settings.title);
  232. const std::u16string message = base::UTF8ToUTF16(settings.message);
  233. const std::u16string detail = base::UTF8ToUTF16(settings.detail);
  234. const std::u16string checkbox_label =
  235. base::UTF8ToUTF16(settings.checkbox_label);
  236. return ShowTaskDialogWstr(
  237. parent_widget, settings.type, buttons, settings.default_id,
  238. settings.cancel_id, settings.no_link, title, message, detail,
  239. checkbox_label, settings.checkbox_checked, settings.icon, hwnd);
  240. }
  241. } // namespace
  242. int ShowMessageBoxSync(const MessageBoxSettings& settings) {
  243. gfx::AcceleratedWidget parent_widget =
  244. settings.parent_window
  245. ? static_cast<electron::NativeWindowViews*>(settings.parent_window)
  246. ->GetAcceleratedWidget()
  247. : nullptr;
  248. DialogResult result = ShowTaskDialogUTF8(settings, parent_widget, nullptr);
  249. return result.button_id;
  250. }
  251. void ShowMessageBox(const MessageBoxSettings& settings,
  252. MessageBoxCallback callback) {
  253. // The dialog is created in a new thread so we don't know its HWND yet, put
  254. // kHwndReserve in the dialogs map for now.
  255. HWND* hwnd = nullptr;
  256. if (settings.id) {
  257. if (base::Contains(GetDialogsMap(), *settings.id))
  258. CloseMessageBox(*settings.id);
  259. auto it = GetDialogsMap().emplace(*settings.id,
  260. std::make_unique<HWND>(kHwndReserve));
  261. hwnd = it.first->second.get();
  262. }
  263. gfx::AcceleratedWidget parent_widget =
  264. settings.parent_window
  265. ? static_cast<electron::NativeWindowViews*>(settings.parent_window)
  266. ->GetAcceleratedWidget()
  267. : nullptr;
  268. dialog_thread::Run(base::BindOnce(&ShowTaskDialogUTF8, settings,
  269. parent_widget, base::Unretained(hwnd)),
  270. base::BindOnce(
  271. [](MessageBoxCallback callback, std::optional<int> id,
  272. DialogResult result) {
  273. if (id)
  274. GetDialogsMap().erase(*id);
  275. std::move(callback).Run(
  276. result.button_id,
  277. result.verification_flag_checked);
  278. },
  279. std::move(callback), settings.id));
  280. }
  281. void CloseMessageBox(int id) {
  282. auto it = GetDialogsMap().find(id);
  283. if (it == GetDialogsMap().end()) {
  284. LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
  285. return;
  286. }
  287. HWND* hwnd = it->second.get();
  288. // Lock since the TaskDialogCallback might be saving the dialog's HWND.
  289. base::AutoLock lock(GetHWNDLock());
  290. DCHECK(*hwnd != kHwndCancel);
  291. if (*hwnd == kHwndReserve) {
  292. // If the dialog window has not been created yet, tell it to cancel.
  293. *hwnd = kHwndCancel;
  294. } else {
  295. // Otherwise send a message to close it.
  296. ::PostMessage(*hwnd, WM_CLOSE, 0, 0);
  297. }
  298. }
  299. void ShowErrorBox(const std::u16string& title, const std::u16string& content) {
  300. ShowTaskDialogWstr(nullptr, MessageBoxType::kError, {}, -1, 0, false,
  301. u"Error", title, content, u"", false, gfx::ImageSkia(),
  302. nullptr);
  303. }
  304. } // namespace electron