Skip to content

Commit

Permalink
feat: add signal option to dialog.showMessageBox (electron#26102)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zcbenz authored Jul 14, 2021
1 parent 4b780f9 commit 05ba635
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 24 deletions.
7 changes: 7 additions & 0 deletions docs/api/dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ If `browserWindow` is not shown dialog will not be attached to it. In such case
will result in one button labeled "OK".
* `defaultId` Integer (optional) - Index of the button in the buttons array which will
be selected by default when the message box opens.
* `signal` AbortSignal (optional) - Pass an instance of [AbortSignal][] to
optionally close the message box, the message box will behave as if it was
cancelled by the user. On macOS, `signal` does not work with message boxes
that do not have a parent window, since those message boxes run
synchronously due to platform limitations.
* `title` String (optional) - Title of the message box, some platforms will not show it.
* `detail` String (optional) - Extra information of the message.
* `checkboxLabel` String (optional) - If provided, the message box will
Expand Down Expand Up @@ -360,3 +365,5 @@ window is provided.

You can call `BrowserWindow.getCurrentWindow().setSheetOffset(offset)` to change
the offset from the window frame where sheets are attached.

[AbortSignal]: https://nodejs.org/api/globals.html#globals_class_abortsignal
17 changes: 17 additions & 0 deletions lib/browser/api/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ enum OpenFileDialogProperties {
dontAddToRecent = 1 << 8 // Windows
}

let nextId = 0;
const getNextId = function () {
return ++nextId;
};

const normalizeAccessKey = (text: string) => {
if (typeof text !== 'string') return text;

Expand Down Expand Up @@ -157,6 +162,7 @@ const messageBox = (sync: boolean, window: BrowserWindow | null, options?: Messa
let {
buttons = [],
cancelId,
signal,
checkboxLabel = '',
checkboxChecked,
defaultId = -1,
Expand Down Expand Up @@ -196,10 +202,21 @@ const messageBox = (sync: boolean, window: BrowserWindow | null, options?: Messa
}
}

// AbortSignal processing.
let id: number | undefined;
if (signal) {
// Generate an ID used for closing the message box.
id = getNextId();
// Close the message box when signal is aborted.
if (signal.aborted) { return Promise.resolve({ cancelId, checkboxChecked }); }
signal.addEventListener('abort', () => dialogBinding._closeMessageBox(id));
}

const settings = {
window,
messageBoxType,
buttons,
id,
defaultId,
cancelId,
noLink,
Expand Down
1 change: 1 addition & 0 deletions shell/browser/api/electron_api_dialog.cc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ void Initialize(v8::Local<v8::Object> exports,
gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("showMessageBoxSync", &ShowMessageBoxSync);
dict.SetMethod("showMessageBox", &ShowMessageBox);
dict.SetMethod("_closeMessageBox", &electron::CloseMessageBox);
dict.SetMethod("showErrorBox", &electron::ShowErrorBox);
dict.SetMethod("showOpenDialogSync", &ShowOpenDialogSync);
dict.SetMethod("showOpenDialog", &ShowOpenDialog);
Expand Down
7 changes: 4 additions & 3 deletions shell/browser/ui/message_box.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
#define SHELL_BROWSER_UI_MESSAGE_BOX_H_

#include <string>
#include <utility>
#include <vector>

#include "base/callback_forward.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/gfx/image/image_skia.h"

namespace electron {
Expand All @@ -24,12 +24,11 @@ enum class MessageBoxType {
kQuestion,
};

using DialogResult = std::pair<int, bool>;

struct MessageBoxSettings {
electron::NativeWindow* parent_window = nullptr;
MessageBoxType type = electron::MessageBoxType::kNone;
std::vector<std::string> buttons;
absl::optional<int> id;
int default_id;
int cancel_id;
bool no_link = false;
Expand All @@ -53,6 +52,8 @@ typedef base::OnceCallback<void(int code, bool checkbox_checked)>
void ShowMessageBox(const MessageBoxSettings& settings,
MessageBoxCallback callback);

void CloseMessageBox(int id);

// Like ShowMessageBox with simplest settings, but safe to call at very early
// stage of application.
void ShowErrorBox(const std::u16string& title, const std::u16string& content);
Expand Down
33 changes: 31 additions & 2 deletions shell/browser/ui/message_box_gtk.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/browser/ui/gtk_util.h"
#include "shell/browser/ui/message_box.h"

#include <map>

#include "base/callback.h"
#include "base/containers/contains.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/browser.h"
#include "shell/browser/native_window_observer.h"
#include "shell/browser/native_window_views.h"
#include "shell/browser/ui/gtk_util.h"
#include "shell/browser/unresponsive_suppressor.h"
#include "ui/base/glib/glib_signal.h"
#include "ui/gfx/image/image_skia.h"
Expand Down Expand Up @@ -38,10 +42,17 @@ MessageBoxSettings::~MessageBoxSettings() = default;

namespace {

// <ID, messageBox> map
std::map<int, GtkWidget*>& GetDialogsMap() {
static base::NoDestructor<std::map<int, GtkWidget*>> dialogs;
return *dialogs;
}

class GtkMessageBox : public NativeWindowObserver {
public:
explicit GtkMessageBox(const MessageBoxSettings& settings)
: cancel_id_(settings.cancel_id),
: id_(settings.id),
cancel_id_(settings.cancel_id),
parent_(static_cast<NativeWindow*>(settings.parent_window)) {
// Create dialog.
dialog_ =
Expand All @@ -50,6 +61,8 @@ class GtkMessageBox : public NativeWindowObserver {
GetMessageType(settings.type), // type
GTK_BUTTONS_NONE, // no buttons
"%s", settings.message.c_str());
if (id_)
GetDialogsMap()[*id_] = dialog_;
if (!settings.detail.empty())
gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog_),
"%s", settings.detail.c_str());
Expand Down Expand Up @@ -183,6 +196,9 @@ class GtkMessageBox : public NativeWindowObserver {
private:
electron::UnresponsiveSuppressor unresponsive_suppressor_;

// The id of the dialog.
absl::optional<int> id_;

// The id to return when the dialog is closed without pressing buttons.
int cancel_id_ = 0;

Expand All @@ -196,6 +212,8 @@ class GtkMessageBox : public NativeWindowObserver {
};

void GtkMessageBox::OnResponseDialog(GtkWidget* widget, int response) {
if (id_)
GetDialogsMap().erase(*id_);
gtk_widget_hide(dialog_);

if (response < 0)
Expand All @@ -217,9 +235,20 @@ int ShowMessageBoxSync(const MessageBoxSettings& settings) {

void ShowMessageBox(const MessageBoxSettings& settings,
MessageBoxCallback callback) {
if (settings.id && base::Contains(GetDialogsMap(), *settings.id))
CloseMessageBox(*settings.id);
(new GtkMessageBox(settings))->RunAsynchronous(std::move(callback));
}

void CloseMessageBox(int id) {
auto it = GetDialogsMap().find(id);
if (it == GetDialogsMap().end()) {
LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
return;
}
gtk_window_close(GTK_WINDOW(it->second));
}

void ShowErrorBox(const std::u16string& title, const std::u16string& content) {
if (Browser::Get()->is_ready()) {
electron::MessageBoxSettings settings;
Expand Down
34 changes: 34 additions & 0 deletions shell/browser/ui/message_box_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

#include "shell/browser/ui/message_box.h"

#include <map>
#include <string>
#include <utility>
#include <vector>

#import <Cocoa/Cocoa.h>

#include "base/callback.h"
#include "base/containers/contains.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "shell/browser/native_window.h"
#include "skia/ext/skia_utils_mac.h"
Expand All @@ -26,6 +29,12 @@

namespace {

// <ID, messageBox> map
std::map<int, NSAlert*>& GetDialogsMap() {
static base::NoDestructor<std::map<int, NSAlert*>> dialogs;
return *dialogs;
}

NSAlert* CreateNSAlert(const MessageBoxSettings& settings) {
// Ignore the title; it's the window title on other platforms and ignorable.
NSAlert* alert = [[NSAlert alloc] init];
Expand Down Expand Up @@ -128,6 +137,12 @@ void ShowMessageBox(const MessageBoxSettings& settings,
int ret = [[alert autorelease] runModal];
std::move(callback).Run(ret, alert.suppressionButton.state == NSOnState);
} else {
if (settings.id) {
if (base::Contains(GetDialogsMap(), *settings.id))
CloseMessageBox(*settings.id);
GetDialogsMap()[*settings.id] = alert;
}

NSWindow* window =
settings.parent_window
? settings.parent_window->GetNativeWindow().GetNativeNSWindow()
Expand All @@ -136,16 +151,35 @@ void ShowMessageBox(const MessageBoxSettings& settings,
// Duplicate the callback object here since c is a reference and gcd would
// only store the pointer, by duplication we can force gcd to store a copy.
__block MessageBoxCallback callback_ = std::move(callback);
__block absl::optional<int> id = std::move(settings.id);
__block int cancel_id = settings.cancel_id;

[alert beginSheetModalForWindow:window
completionHandler:^(NSModalResponse response) {
if (id)
GetDialogsMap().erase(*id);
// When the alert is cancelled programmatically, the
// response would be something like -1000. This currently
// only happens when users call CloseMessageBox API, and we
// should return cancelId as result.
if (response < 0)
response = cancel_id;
std::move(callback_).Run(
response, alert.suppressionButton.state == NSOnState);
[alert release];
}];
}
}

void CloseMessageBox(int id) {
auto it = GetDialogsMap().find(id);
if (it == GetDialogsMap().end()) {
LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
return;
}
[NSApp endSheet:it->second.window];
}

void ShowErrorBox(const std::u16string& title, const std::u16string& content) {
NSAlert* alert = [[NSAlert alloc] init];
[alert setMessageText:base::SysUTF16ToNSString(title)];
Expand Down
Loading

0 comments on commit 05ba635

Please sign in to comment.