message_box_mac.mm 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  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 <utility>
  6. #import <Cocoa/Cocoa.h>
  7. #include "base/containers/contains.h"
  8. #include "base/containers/flat_map.h"
  9. #include "base/mac/mac_util.h"
  10. #include "base/no_destructor.h"
  11. #include "base/strings/sys_string_conversions.h"
  12. #include "content/public/browser/browser_task_traits.h"
  13. #include "content/public/browser/browser_thread.h"
  14. #include "shell/browser/native_window.h"
  15. #include "skia/ext/skia_utils_mac.h"
  16. #include "ui/gfx/image/image_skia.h"
  17. namespace electron {
  18. MessageBoxSettings::MessageBoxSettings() = default;
  19. MessageBoxSettings::MessageBoxSettings(const MessageBoxSettings&) = default;
  20. MessageBoxSettings::~MessageBoxSettings() = default;
  21. namespace {
  22. // <ID, messageBox> map
  23. base::flat_map<int, NSAlert*>& GetDialogsMap() {
  24. static base::NoDestructor<base::flat_map<int, NSAlert*>> dialogs;
  25. return *dialogs;
  26. }
  27. NSAlert* CreateNSAlert(const MessageBoxSettings& settings) {
  28. // Ignore the title; it's the window title on other platforms and ignorable.
  29. NSAlert* alert = [[NSAlert alloc] init];
  30. [alert setMessageText:base::SysUTF8ToNSString(settings.message)];
  31. [alert setInformativeText:base::SysUTF8ToNSString(settings.detail)];
  32. switch (settings.type) {
  33. case MessageBoxType::kInformation:
  34. alert.alertStyle = NSAlertStyleInformational;
  35. break;
  36. case MessageBoxType::kWarning:
  37. case MessageBoxType::kError:
  38. // NSWarningAlertStyle shows the app icon while NSAlertStyleCritical
  39. // shows a warning icon with an app icon badge. Since there is no
  40. // error variant, lets just use NSAlertStyleCritical.
  41. alert.alertStyle = NSAlertStyleCritical;
  42. break;
  43. default:
  44. break;
  45. }
  46. for (size_t i = 0; i < settings.buttons.size(); ++i) {
  47. NSString* title = base::SysUTF8ToNSString(settings.buttons[i]);
  48. // An empty title causes crash on macOS.
  49. if (settings.buttons[i].empty())
  50. title = @"(empty)";
  51. NSButton* button = [alert addButtonWithTitle:title];
  52. [button setTag:i];
  53. }
  54. NSArray* ns_buttons = [alert buttons];
  55. int button_count = static_cast<int>([ns_buttons count]);
  56. if (settings.default_id >= 0 && settings.default_id < button_count) {
  57. // The first button added gets set as the default selected, so remove
  58. // that and set the button @ default_id to be default.
  59. [[ns_buttons objectAtIndex:0] setKeyEquivalent:@""];
  60. [[ns_buttons objectAtIndex:settings.default_id] setKeyEquivalent:@"\r"];
  61. }
  62. if (button_count > 1 && settings.cancel_id >= 0) {
  63. // Bind cancel id button to escape key if there is more than one button.
  64. if (settings.cancel_id < button_count) {
  65. [[ns_buttons objectAtIndex:settings.cancel_id] setKeyEquivalent:@"\e"];
  66. }
  67. // TODO(@codebytere): This behavior violates HIG & should be deprecated.
  68. if (settings.cancel_id == settings.default_id) {
  69. [(NSButton*)[ns_buttons objectAtIndex:settings.default_id] highlight:YES];
  70. }
  71. }
  72. if (!settings.checkbox_label.empty()) {
  73. alert.showsSuppressionButton = YES;
  74. alert.suppressionButton.title =
  75. base::SysUTF8ToNSString(settings.checkbox_label);
  76. alert.suppressionButton.state = settings.checkbox_checked
  77. ? NSControlStateValueOn
  78. : NSControlStateValueOff;
  79. }
  80. if (!settings.icon.isNull()) {
  81. NSImage* image = skia::SkBitmapToNSImage(*settings.icon.bitmap());
  82. [alert setIcon:image];
  83. }
  84. if (settings.text_width > 0) {
  85. NSRect rect = NSMakeRect(0, 0, settings.text_width, 0);
  86. NSView* accessoryView = [[NSView alloc] initWithFrame:rect];
  87. [alert setAccessoryView:accessoryView];
  88. }
  89. return alert;
  90. }
  91. } // namespace
  92. int ShowMessageBoxSync(const MessageBoxSettings& settings) {
  93. NSAlert* alert(CreateNSAlert(settings));
  94. // Use runModal for synchronous alert without parent, since we don't have a
  95. // window to wait for. Also use it when window is provided but it is not
  96. // shown as it would be impossible to dismiss the alert if it is connected
  97. // to invisible window (see #22671).
  98. if (!settings.parent_window || !settings.parent_window->IsVisible())
  99. return [alert runModal];
  100. __block int ret_code = -1;
  101. NSWindow* window =
  102. settings.parent_window->GetNativeWindow().GetNativeNSWindow();
  103. [alert beginSheetModalForWindow:window
  104. completionHandler:^(NSModalResponse response) {
  105. ret_code = response;
  106. [NSApp stopModal];
  107. }];
  108. [NSApp runModalForWindow:window];
  109. return ret_code;
  110. }
  111. void ShowMessageBox(const MessageBoxSettings& settings,
  112. MessageBoxCallback callback) {
  113. NSAlert* alert = CreateNSAlert(settings);
  114. // Use runModal for synchronous alert without parent, since we don't have a
  115. // window to wait for.
  116. if (!settings.parent_window) {
  117. int ret = [alert runModal];
  118. std::move(callback).Run(
  119. ret, alert.suppressionButton.state == NSControlStateValueOn);
  120. } else {
  121. if (settings.id) {
  122. if (base::Contains(GetDialogsMap(), *settings.id))
  123. CloseMessageBox(*settings.id);
  124. GetDialogsMap()[*settings.id] = alert;
  125. }
  126. NSWindow* window =
  127. settings.parent_window
  128. ? settings.parent_window->GetNativeWindow().GetNativeNSWindow()
  129. : nil;
  130. // Duplicate the callback object here since c is a reference and gcd would
  131. // only store the pointer, by duplication we can force gcd to store a copy.
  132. __block MessageBoxCallback callback_ = std::move(callback);
  133. __block std::optional<int> id = std::move(settings.id);
  134. __block int cancel_id = settings.cancel_id;
  135. auto handler = ^(NSModalResponse response) {
  136. if (id)
  137. GetDialogsMap().erase(*id);
  138. // When the alert is cancelled programmatically, the response would be
  139. // something like -1000. This currently only happens when users call
  140. // CloseMessageBox API, and we should return cancelId as result.
  141. if (response < 0)
  142. response = cancel_id;
  143. bool suppressed = alert.suppressionButton.state == NSControlStateValueOn;
  144. // The completionHandler runs inside a transaction commit, and we should
  145. // not do any runModal inside it. However since we can not control what
  146. // users will run in the callback, we have to delay running the callback
  147. // until next tick, otherwise crash like this may happen:
  148. // https://github.com/electron/electron/issues/26884
  149. content::GetUIThreadTaskRunner({})->PostTask(
  150. FROM_HERE,
  151. base::BindOnce(std::move(callback_), response, suppressed));
  152. };
  153. [alert beginSheetModalForWindow:window completionHandler:handler];
  154. }
  155. }
  156. void CloseMessageBox(int id) {
  157. auto it = GetDialogsMap().find(id);
  158. if (it == GetDialogsMap().end()) {
  159. LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
  160. return;
  161. }
  162. [NSApp endSheet:it->second.window];
  163. }
  164. void ShowErrorBox(const std::u16string& title, const std::u16string& content) {
  165. NSAlert* alert = [[NSAlert alloc] init];
  166. [alert setMessageText:base::SysUTF16ToNSString(title)];
  167. [alert setInformativeText:base::SysUTF16ToNSString(content)];
  168. [alert setAlertStyle:NSAlertStyleCritical];
  169. [alert runModal];
  170. }
  171. } // namespace electron