feat: add signal option to dialog.showMessageBox (#26102)

* mac: add dialog.closeMessageBox API

* win: Implement dialog.closeMessageBox

* mac: Return cancelId with closeMessageBox

* gtk: Implement dialog.closeMessageBox

* win: Fix 32bit build

* win: Reduce the scope of lock

* fix: Build error after rebase

* feat: Use AbortSignal to close message box

* chore: silently handle duplicate ID

* win: Add more notes about the threads

* chore: apply reviews

* fix: base::NoDestructor should be warpped in function

* chore: fix style on windows
This commit is contained in:
Cheng Zhao 2021-07-15 07:59:27 +09:00 committed by GitHub
parent 4b780f9770
commit 05ba6359d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 276 additions and 24 deletions

View file

@ -11,8 +11,11 @@
#include <map>
#include <vector>
#include "base/containers/contains.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/post_task.h"
#include "base/win/scoped_gdi_object.h"
#include "shell/browser/browser.h"
@ -30,6 +33,41 @@ MessageBoxSettings::~MessageBoxSettings() = default;
namespace {
struct DialogResult {
int button_id;
bool verification_flag_checked;
};
// <ID, messageBox> map.
//
// Note that the HWND is stored in a unique_ptr, because the pointer of HWND
// will be passed between threads and we need to ensure the memory of HWND is
// not changed while dialogs map is modified.
std::map<int, std::unique_ptr<HWND>>& GetDialogsMap() {
static base::NoDestructor<std::map<int, std::unique_ptr<HWND>>> dialogs;
return *dialogs;
}
// Speical HWND used by the dialogs map.
//
// - ID is used but window has not been created yet.
const HWND kHwndReserve = reinterpret_cast<HWND>(-1);
// - Notification to cancel message box.
const HWND kHwndCancel = reinterpret_cast<HWND>(-2);
// Lock used for modifying HWND between threads.
//
// Note that there might be multiple dialogs being opened at the same time, but
// we only use one lock for them all, because each dialog is independent from
// each other and there is no need to use different lock for each one.
// Also note that the |GetDialogsMap| is only used in the main thread, what is
// shared between threads is the memory of HWND, so there is no need to use lock
// when accessing dialogs map.
base::Lock& GetHWNDLock() {
static base::NoDestructor<base::Lock> lock;
return *lock;
}
// Small command ID values are already taken by Windows, we have to start from
// a large number to avoid conflicts with Windows.
const int kIDStart = 100;
@ -76,6 +114,31 @@ void MapToCommonID(const std::vector<std::wstring>& buttons,
}
}
// Callback of the task dialog. The TaskDialogIndirect API does not provide the
// HWND of the dialog, and we have to listen to the TDN_CREATED message to get
// it.
// Note that this callback runs in dialog thread instead of main thread, so it
// is possible for CloseMessageBox to be called before or all after the dialog
// window is created.
HRESULT CALLBACK
TaskDialogCallback(HWND hwnd, UINT msg, WPARAM, LPARAM, LONG_PTR data) {
if (msg == TDN_CREATED) {
HWND* target = reinterpret_cast<HWND*>(data);
// Lock since CloseMessageBox might be called.
base::AutoLock lock(GetHWNDLock());
if (*target == kHwndCancel) {
// The dialog is cancelled before it is created, close it directly.
::PostMessage(hwnd, WM_CLOSE, 0, 0);
} else if (*target == kHwndReserve) {
// Otherwise save the hwnd.
*target = hwnd;
} else {
NOTREACHED();
}
}
return S_OK;
}
DialogResult ShowTaskDialogWstr(NativeWindow* parent,
MessageBoxType type,
const std::vector<std::wstring>& buttons,
@ -87,7 +150,8 @@ DialogResult ShowTaskDialogWstr(NativeWindow* parent,
const std::u16string& detail,
const std::u16string& checkbox_label,
bool checkbox_checked,
const gfx::ImageSkia& icon) {
const gfx::ImageSkia& icon,
HWND* hwnd) {
TASKDIALOG_FLAGS flags =
TDF_SIZE_TO_CONTENT | // Show all content.
TDF_ALLOW_DIALOG_CANCELLATION; // Allow canceling the dialog.
@ -169,12 +233,17 @@ DialogResult ShowTaskDialogWstr(NativeWindow* parent,
config.dwFlags |= TDF_USE_COMMAND_LINKS; // custom buttons as links.
}
int button_id;
// Pass a callback to receive the HWND of the message box.
if (hwnd) {
config.pfCallback = &TaskDialogCallback;
config.lpCallbackData = reinterpret_cast<LONG_PTR>(hwnd);
}
int id = 0;
BOOL verificationFlagChecked = FALSE;
TaskDialogIndirect(&config, &id, nullptr, &verificationFlagChecked);
BOOL verification_flag_checked = FALSE;
TaskDialogIndirect(&config, &id, nullptr, &verification_flag_checked);
int button_id;
if (id_map.find(id) != id_map.end()) // common button.
button_id = id_map[id];
else if (id >= kIDStart) // custom button.
@ -182,10 +251,11 @@ DialogResult ShowTaskDialogWstr(NativeWindow* parent,
else
button_id = cancel_id;
return std::make_pair(button_id, verificationFlagChecked);
return {button_id, static_cast<bool>(verification_flag_checked)};
}
DialogResult ShowTaskDialogUTF8(const MessageBoxSettings& settings) {
DialogResult ShowTaskDialogUTF8(const MessageBoxSettings& settings,
HWND* hwnd) {
std::vector<std::wstring> buttons;
for (const auto& button : settings.buttons)
buttons.push_back(base::UTF8ToWide(button));
@ -199,31 +269,67 @@ DialogResult ShowTaskDialogUTF8(const MessageBoxSettings& settings) {
return ShowTaskDialogWstr(
settings.parent_window, settings.type, buttons, settings.default_id,
settings.cancel_id, settings.no_link, title, message, detail,
checkbox_label, settings.checkbox_checked, settings.icon);
checkbox_label, settings.checkbox_checked, settings.icon, hwnd);
}
} // namespace
int ShowMessageBoxSync(const MessageBoxSettings& settings) {
electron::UnresponsiveSuppressor suppressor;
DialogResult result = ShowTaskDialogUTF8(settings);
return result.first;
DialogResult result = ShowTaskDialogUTF8(settings, nullptr);
return result.button_id;
}
void ShowMessageBox(const MessageBoxSettings& settings,
MessageBoxCallback callback) {
dialog_thread::Run(base::BindOnce(&ShowTaskDialogUTF8, settings),
base::BindOnce(
[](MessageBoxCallback callback, DialogResult result) {
std::move(callback).Run(result.first, result.second);
},
std::move(callback)));
// The dialog is created in a new thread so we don't know its HWND yet, put
// kHwndReserve in the dialogs map for now.
HWND* hwnd = nullptr;
if (settings.id) {
if (base::Contains(GetDialogsMap(), *settings.id))
CloseMessageBox(*settings.id);
auto it = GetDialogsMap().emplace(*settings.id,
std::make_unique<HWND>(kHwndReserve));
hwnd = it.first->second.get();
}
dialog_thread::Run(
base::BindOnce(&ShowTaskDialogUTF8, settings, base::Unretained(hwnd)),
base::BindOnce(
[](MessageBoxCallback callback, absl::optional<int> id,
DialogResult result) {
if (id)
GetDialogsMap().erase(*id);
std::move(callback).Run(result.button_id,
result.verification_flag_checked);
},
std::move(callback), settings.id));
}
void CloseMessageBox(int id) {
auto it = GetDialogsMap().find(id);
if (it == GetDialogsMap().end()) {
LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
return;
}
HWND* hwnd = it->second.get();
// Lock since the TaskDialogCallback might be saving the dialog's HWND.
base::AutoLock lock(GetHWNDLock());
DCHECK(*hwnd != kHwndCancel);
if (*hwnd == kHwndReserve) {
// If the dialog window has not been created yet, tell it to cancel.
*hwnd = kHwndCancel;
} else {
// Otherwise send a message to close it.
::PostMessage(*hwnd, WM_CLOSE, 0, 0);
}
}
void ShowErrorBox(const std::u16string& title, const std::u16string& content) {
electron::UnresponsiveSuppressor suppressor;
ShowTaskDialogWstr(nullptr, MessageBoxType::kError, {}, -1, 0, false,
u"Error", title, content, u"", false, gfx::ImageSkia());
u"Error", title, content, u"", false, gfx::ImageSkia(),
nullptr);
}
} // namespace electron