From c8c11e68c6c519789ba686783c3d653d5f790a51 Mon Sep 17 00:00:00 2001 From: Birunthan Mohanathas Date: Mon, 6 Feb 2017 15:35:36 +0000 Subject: [PATCH] Add support for checkbox with dialog.showMessageBox This adds the `checkboxLabel` and `checkboxChecked` options to display a checkbox in the message box. Fixes #6048. --- atom/browser/api/atom_api_dialog.cc | 6 +- .../browser/atom_javascript_dialog_manager.cc | 15 ++-- atom/browser/atom_javascript_dialog_manager.h | 3 +- atom/browser/ui/message_box.h | 5 +- atom/browser/ui/message_box_gtk.cc | 45 ++++++++-- atom/browser/ui/message_box_mac.mm | 25 ++++-- atom/browser/ui/message_box_win.cc | 89 +++++++++++++------ docs/api/dialog.md | 7 ++ lib/browser/api/dialog.js | 17 +++- spec/api-dialog-spec.js | 4 + 10 files changed, 155 insertions(+), 61 deletions(-) diff --git a/atom/browser/api/atom_api_dialog.cc b/atom/browser/api/atom_api_dialog.cc index 5d853e2d5900..41cad524ea25 100644 --- a/atom/browser/api/atom_api_dialog.cc +++ b/atom/browser/api/atom_api_dialog.cc @@ -47,6 +47,8 @@ void ShowMessageBox(int type, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon, atom::NativeWindow* window, mate::Arguments* args) { @@ -56,8 +58,8 @@ void ShowMessageBox(int type, peek, &callback)) { atom::ShowMessageBox(window, (atom::MessageBoxType)type, buttons, - default_id, cancel_id, options, title, - message, detail, icon, callback); + default_id, cancel_id, options, title, message, detail, + checkbox_label, checkbox_checked, icon, callback); } else { int chosen = atom::ShowMessageBox(window, (atom::MessageBoxType)type, buttons, default_id, cancel_id, diff --git a/atom/browser/atom_javascript_dialog_manager.cc b/atom/browser/atom_javascript_dialog_manager.cc index 24197915e61e..f874a99db55a 100644 --- a/atom/browser/atom_javascript_dialog_manager.cc +++ b/atom/browser/atom_javascript_dialog_manager.cc @@ -38,14 +38,9 @@ void AtomJavaScriptDialogManager::RunJavaScriptDialog( } atom::ShowMessageBox(NativeWindow::FromWebContents(web_contents), - atom::MessageBoxType::MESSAGE_BOX_TYPE_NONE, - buttons, - -1, - 0, - atom::MessageBoxOptions::MESSAGE_BOX_NONE, - "", - base::UTF16ToUTF8(message_text), - "", + atom::MessageBoxType::MESSAGE_BOX_TYPE_NONE, buttons, -1, + 0, atom::MessageBoxOptions::MESSAGE_BOX_NONE, "", + base::UTF16ToUTF8(message_text), "", "", false, gfx::ImageSkia(), base::Bind(&OnMessageBoxCallback, callback)); } @@ -66,7 +61,9 @@ void AtomJavaScriptDialogManager::CancelDialogs( // static void AtomJavaScriptDialogManager::OnMessageBoxCallback( - const DialogClosedCallback& callback, int code) { + const DialogClosedCallback& callback, + int code, + bool checkbox_checked) { callback.Run(code == 0, base::string16()); } diff --git a/atom/browser/atom_javascript_dialog_manager.h b/atom/browser/atom_javascript_dialog_manager.h index e5bb6114bc92..01cc76248c87 100644 --- a/atom/browser/atom_javascript_dialog_manager.h +++ b/atom/browser/atom_javascript_dialog_manager.h @@ -32,7 +32,8 @@ class AtomJavaScriptDialogManager : public content::JavaScriptDialogManager { private: static void OnMessageBoxCallback(const DialogClosedCallback& callback, - int code); + int code, + bool checkbox_checked); }; } // namespace atom diff --git a/atom/browser/ui/message_box.h b/atom/browser/ui/message_box.h index 16eef3c46350..6c826719ee15 100644 --- a/atom/browser/ui/message_box.h +++ b/atom/browser/ui/message_box.h @@ -32,7 +32,8 @@ enum MessageBoxOptions { MESSAGE_BOX_NO_LINK = 1 << 0, }; -typedef base::Callback MessageBoxCallback; +typedef base::Callback + MessageBoxCallback; int ShowMessageBox(NativeWindow* parent_window, MessageBoxType type, @@ -54,6 +55,8 @@ void ShowMessageBox(NativeWindow* parent_window, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon, const MessageBoxCallback& callback); diff --git a/atom/browser/ui/message_box_gtk.cc b/atom/browser/ui/message_box_gtk.cc index 0717d4d06d6c..40d23f19eb5b 100644 --- a/atom/browser/ui/message_box_gtk.cc +++ b/atom/browser/ui/message_box_gtk.cc @@ -36,8 +36,11 @@ class GtkMessageBox : public NativeWindowObserver { const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon) : cancel_id_(cancel_id), + checkbox_checked_(false), parent_(static_cast(parent_window)) { // Create dialog. dialog_ = gtk_message_dialog_new( @@ -68,6 +71,18 @@ class GtkMessageBox : public NativeWindowObserver { g_object_unref(pixbuf); } + if (!checkbox_label.empty()) { + GtkWidget* message_area = + gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dialog_)); + GtkWidget* check_button = + gtk_check_button_new_with_label(checkbox_label.c_str()); + g_signal_connect(check_button, "toggled", + G_CALLBACK(OnCheckboxToggledThunk), this); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_button), + checkbox_checked); + gtk_container_add(GTK_CONTAINER(message_area), check_button); + } + // Add buttons. for (size_t i = 0; i < buttons.size(); ++i) { GtkWidget* button = gtk_dialog_add_button( @@ -154,6 +169,7 @@ class GtkMessageBox : public NativeWindowObserver { } CHROMEGTK_CALLBACK_1(GtkMessageBox, void, OnResponseDialog, int); + CHROMEGTK_CALLBACK_1(GtkMessageBox, void, OnCheckboxToggled, gpointer); private: atom::UnresponsiveSuppressor unresponsive_suppressor_; @@ -161,6 +177,8 @@ class GtkMessageBox : public NativeWindowObserver { // The id to return when the dialog is closed without pressing buttons. int cancel_id_; + bool checkbox_checked_; + NativeWindowViews* parent_; GtkWidget* dialog_; MessageBoxCallback callback_; @@ -172,12 +190,16 @@ void GtkMessageBox::OnResponseDialog(GtkWidget* widget, int response) { gtk_widget_hide(dialog_); if (response < 0) - callback_.Run(cancel_id_); + callback_.Run(cancel_id_, checkbox_checked_); else - callback_.Run(response); + callback_.Run(response, checkbox_checked_); delete this; } +void GtkMessageBox::OnCheckboxToggled(GtkWidget* widget, gpointer data) { + checkbox_checked_ = GTK_TOGGLE_BUTTON(widget)->active; +} + } // namespace int ShowMessageBox(NativeWindow* parent, @@ -190,8 +212,9 @@ int ShowMessageBox(NativeWindow* parent, const std::string& message, const std::string& detail, const gfx::ImageSkia& icon) { - return GtkMessageBox(parent, type, buttons, default_id, cancel_id, - title, message, detail, icon).RunSynchronous(); + return GtkMessageBox(parent, type, buttons, default_id, cancel_id, title, + message, detail, "", false, icon) + .RunSynchronous(); } void ShowMessageBox(NativeWindow* parent, @@ -203,18 +226,22 @@ void ShowMessageBox(NativeWindow* parent, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon, const MessageBoxCallback& callback) { - (new GtkMessageBox(parent, type, buttons, default_id, cancel_id, - title, message, detail, icon))->RunAsynchronous(callback); + (new GtkMessageBox(parent, type, buttons, default_id, cancel_id, title, + message, detail, checkbox_label, checkbox_checked, icon)) + ->RunAsynchronous(callback); } void ShowErrorBox(const base::string16& title, const base::string16& content) { if (Browser::Get()->is_ready()) { - GtkMessageBox(nullptr, MESSAGE_BOX_TYPE_ERROR, { "OK" }, -1, 0, "Error", + GtkMessageBox(nullptr, MESSAGE_BOX_TYPE_ERROR, {"OK"}, -1, 0, "Error", base::UTF16ToUTF8(title).c_str(), - base::UTF16ToUTF8(content).c_str(), - gfx::ImageSkia()).RunSynchronous(); + base::UTF16ToUTF8(content).c_str(), "", false, + gfx::ImageSkia()) + .RunSynchronous(); } else { fprintf(stderr, ANSI_TEXT_BOLD ANSI_BACKGROUND_GRAY diff --git a/atom/browser/ui/message_box_mac.mm b/atom/browser/ui/message_box_mac.mm index 8fd28e244630..0d03eefc20ad 100644 --- a/atom/browser/ui/message_box_mac.mm +++ b/atom/browser/ui/message_box_mac.mm @@ -39,7 +39,7 @@ - (void)alertDidEnd:(NSAlert*)alert returnCode:(NSInteger)returnCode contextInfo:(void*)contextInfo { - callback_.Run(returnCode); + callback_.Run(returnCode, alert.suppressionButton.state == NSOnState); [alert_ release]; [self release]; @@ -60,6 +60,8 @@ NSAlert* CreateNSAlert(NativeWindow* parent_window, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon) { // Ignore the title; it's the window title on other platforms and ignorable. NSAlert* alert = [[NSAlert alloc] init]; @@ -95,6 +97,12 @@ NSAlert* CreateNSAlert(NativeWindow* parent_window, [[ns_buttons objectAtIndex:default_id] setKeyEquivalent:@"\r"]; } + if (!checkbox_label.empty()) { + alert.showsSuppressionButton = YES; + alert.suppressionButton.title = base::SysUTF8ToNSString(checkbox_label); + alert.suppressionButton.state = checkbox_checked ? NSOnState : NSOffState; + } + if (!icon.isNull()) { NSImage* image = skia::SkBitmapToNSImageWithColorSpace( *icon.bitmap(), base::mac::GetGenericRGBColorSpace()); @@ -104,7 +112,7 @@ NSAlert* CreateNSAlert(NativeWindow* parent_window, return alert; } -void SetReturnCode(int* ret_code, int result) { +void SetReturnCode(int* ret_code, int result, bool checkbox_checked) { *ret_code = result; } @@ -120,9 +128,8 @@ int ShowMessageBox(NativeWindow* parent_window, const std::string& message, const std::string& detail, const gfx::ImageSkia& icon) { - NSAlert* alert = CreateNSAlert( - parent_window, type, buttons, default_id, title, message, - detail, icon); + NSAlert* alert = CreateNSAlert(parent_window, type, buttons, default_id, + title, message, detail, "", false, icon); // Use runModal for synchronous alert without parent, since we don't have a // window to wait for. @@ -154,11 +161,13 @@ void ShowMessageBox(NativeWindow* parent_window, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon, const MessageBoxCallback& callback) { - NSAlert* alert = CreateNSAlert( - parent_window, type, buttons, default_id, title, message, - detail, icon); + NSAlert* alert = + CreateNSAlert(parent_window, type, buttons, default_id, title, message, + detail, checkbox_label, checkbox_checked, icon); ModalDelegate* delegate = [[ModalDelegate alloc] initWithCallback:callback andAlert:alert callEndModal:false]; diff --git a/atom/browser/ui/message_box_win.cc b/atom/browser/ui/message_box_win.cc index b6777fb1da23..844a057ae6fd 100644 --- a/atom/browser/ui/message_box_win.cc +++ b/atom/browser/ui/message_box_win.cc @@ -72,7 +72,7 @@ void MapToCommonID(const std::vector& buttons, } } -int ShowMessageBoxUTF16(HWND parent, +int ShowTaskDialogUTF16(NativeWindow* parent, MessageBoxType type, const std::vector& buttons, int default_id, @@ -81,6 +81,8 @@ int ShowMessageBoxUTF16(HWND parent, const base::string16& title, const base::string16& message, const base::string16& detail, + const base::string16& checkbox_label, + bool* checkbox_checked, const gfx::ImageSkia& icon) { TASKDIALOG_FLAGS flags = TDF_SIZE_TO_CONTENT | // Show all content. @@ -88,10 +90,14 @@ int ShowMessageBoxUTF16(HWND parent, TASKDIALOGCONFIG config = { 0 }; config.cbSize = sizeof(config); - config.hwndParent = parent; config.hInstance = GetModuleHandle(NULL); config.dwFlags = flags; + if (parent) { + config.hwndParent = + static_cast(parent)->GetAcceleratedWidget(); + } + if (default_id > 0) config.nDefaultButton = kIDStart + default_id; @@ -132,6 +138,14 @@ int ShowMessageBoxUTF16(HWND parent, config.pszContent = detail.c_str(); } + if (!checkbox_label.empty()) { + config.pszVerificationText = checkbox_label.c_str(); + + if (checkbox_checked && *checkbox_checked) { + config.dwFlags |= TDF_VERIFICATION_FLAG_CHECKED; + } + } + // Iterate through the buttons, put common buttons in dwCommonButtons // and custom buttons in pButtons. std::map id_map; @@ -151,7 +165,12 @@ int ShowMessageBoxUTF16(HWND parent, } int id = 0; - TaskDialogIndirect(&config, &id, NULL, NULL); + BOOL verificationFlagChecked = FALSE; + TaskDialogIndirect(&config, &id, nullptr, &verificationFlagChecked); + if (checkbox_checked) { + *checkbox_checked = verificationFlagChecked; + } + if (id_map.find(id) != id_map.end()) // common button. return id_map[id]; else if (id >= kIDStart) // custom button. @@ -160,6 +179,29 @@ int ShowMessageBoxUTF16(HWND parent, return cancel_id; } +int ShowTaskDialogUTF8(NativeWindow* parent, + MessageBoxType type, + const std::vector& buttons, + int default_id, + int cancel_id, + int options, + const std::string& title, + const std::string& message, + const std::string& detail, + const std::string& checkbox_label, + bool* checkbox_checked, + const gfx::ImageSkia& icon) { + std::vector utf16_buttons; + for (const auto& button : buttons) + utf16_buttons.push_back(base::UTF8ToUTF16(button)); + + return ShowTaskDialogUTF16( + parent, type, utf16_buttons, default_id, cancel_id, options, + base::UTF8ToUTF16(title), base::UTF8ToUTF16(message), + base::UTF8ToUTF16(detail), base::UTF8ToUTF16(checkbox_label), + checkbox_checked, icon); +} + void RunMessageBoxInNewThread(base::Thread* thread, NativeWindow* parent, MessageBoxType type, @@ -170,12 +212,16 @@ void RunMessageBoxInNewThread(base::Thread* thread, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon, const MessageBoxCallback& callback) { - int result = ShowMessageBox(parent, type, buttons, default_id, - cancel_id, options, title, message, detail, icon); + int result = ShowTaskDialogUTF8(parent, type, buttons, default_id, cancel_id, + options, title, message, detail, + checkbox_label, &checkbox_checked, icon); content::BrowserThread::PostTask( - content::BrowserThread::UI, FROM_HERE, base::Bind(callback, result)); + content::BrowserThread::UI, FROM_HERE, + base::Bind(callback, result, checkbox_checked)); content::BrowserThread::DeleteSoon( content::BrowserThread::UI, FROM_HERE, thread); } @@ -192,25 +238,9 @@ int ShowMessageBox(NativeWindow* parent, const std::string& message, const std::string& detail, const gfx::ImageSkia& icon) { - std::vector utf16_buttons; - for (const auto& button : buttons) - utf16_buttons.push_back(base::UTF8ToUTF16(button)); - - HWND hwnd_parent = parent ? - static_cast(parent)->GetAcceleratedWidget() : - NULL; - atom::UnresponsiveSuppressor suppressor; - return ShowMessageBoxUTF16(hwnd_parent, - type, - utf16_buttons, - default_id, - cancel_id, - options, - base::UTF8ToUTF16(title), - base::UTF8ToUTF16(message), - base::UTF8ToUTF16(detail), - icon); + return ShowTaskDialogUTF8(parent, type, buttons, default_id, cancel_id, + options, title, message, detail, "", nullptr, icon); } void ShowMessageBox(NativeWindow* parent, @@ -222,13 +252,15 @@ void ShowMessageBox(NativeWindow* parent, const std::string& title, const std::string& message, const std::string& detail, + const std::string& checkbox_label, + bool checkbox_checked, const gfx::ImageSkia& icon, const MessageBoxCallback& callback) { std::unique_ptr thread( new base::Thread(ATOM_PRODUCT_NAME "MessageBoxThread")); thread->init_com_with_mta(false); if (!thread->Start()) { - callback.Run(cancel_id); + callback.Run(cancel_id, checkbox_checked); return; } @@ -237,13 +269,14 @@ void ShowMessageBox(NativeWindow* parent, FROM_HERE, base::Bind(&RunMessageBoxInNewThread, base::Unretained(unretained), parent, type, buttons, default_id, cancel_id, options, title, - message, detail, icon, callback)); + message, detail, checkbox_label, checkbox_checked, icon, + callback)); } void ShowErrorBox(const base::string16& title, const base::string16& content) { atom::UnresponsiveSuppressor suppressor; - ShowMessageBoxUTF16(NULL, MESSAGE_BOX_TYPE_ERROR, {}, -1, 0, 0, L"Error", - title, content, gfx::ImageSkia()); + ShowTaskDialogUTF16(nullptr, MESSAGE_BOX_TYPE_ERROR, {}, -1, 0, 0, L"Error", + title, content, L"", nullptr, gfx::ImageSkia()); } } // namespace atom diff --git a/docs/api/dialog.md b/docs/api/dialog.md index 00a7e92ef160..6a7cdb225b9d 100644 --- a/docs/api/dialog.md +++ b/docs/api/dialog.md @@ -122,6 +122,11 @@ will be passed via `callback(filename)` * `title` String (optional) - Title of the message box, some platforms will not show it. * `message` String - Content of the message box. * `detail` String (optional) - Extra information of the message. + * `checkboxLabel` String (optional) - If provided, the message box will + include a checkbox with the given label. The checkbox state can be + inspected only when using `callback`. + * `checkboxChecked` Boolean (optional) - Initial checked state of the + checkbox. `false` by default. * `icon` [NativeImage](native-image.md) (optional) * `cancelId` Integer (optional) - The value will be returned when user cancels the dialog instead of clicking the buttons of the dialog. By default it is the index @@ -135,6 +140,8 @@ will be passed via `callback(filename)` set `noLink` to `true`. * `callback` Function (optional) * `response` Number - The index of the button that was clicked + * `checkboxChecked` Boolean - The checked state of the checkbox if + `checkboxLabel` was set. Otherwise `false`. Returns `Integer`, the index of the clicked button, if a callback is provided it returns undefined. diff --git a/lib/browser/api/dialog.js b/lib/browser/api/dialog.js index 85572b3b4fba..d0266d912d31 100644 --- a/lib/browser/api/dialog.js +++ b/lib/browser/api/dialog.js @@ -178,7 +178,10 @@ module.exports = { } } - let {buttons, cancelId, defaultId, detail, icon, message, title, type} = options + let { + buttons, cancelId, checkboxLabel, checkboxChecked, defaultId, detail, + icon, message, title, type + } = options if (type == null) { type = 'none' @@ -217,6 +220,14 @@ module.exports = { throw new TypeError('Detail must be a string') } + checkboxChecked = !!checkboxChecked + + if (checkboxLabel == null) { + checkboxLabel = '' + } else if (typeof checkboxLabel !== 'string') { + throw new TypeError('checkboxLabel must be a string') + } + if (icon == null) { icon = null } @@ -239,8 +250,8 @@ module.exports = { const flags = options.noLink ? messageBoxOptions.noLink : 0 return binding.showMessageBox(messageBoxType, buttons, defaultId, cancelId, - flags, title, message, detail, icon, window, - callback) + flags, title, message, detail, checkboxLabel, + checkboxChecked, icon, window, callback) }, showErrorBox: function (...args) { diff --git a/spec/api-dialog-spec.js b/spec/api-dialog-spec.js index 9b90b0353dc2..966cbc4beed8 100644 --- a/spec/api-dialog-spec.js +++ b/spec/api-dialog-spec.js @@ -59,6 +59,10 @@ describe('dialog module', () => { assert.throws(() => { dialog.showMessageBox({detail: 3.14}) }, /Detail must be a string/) + + assert.throws(() => { + dialog.showMessageBox({checkboxLabel: false}) + }, /checkboxLabel must be a string/) }) })