Commit ba65d322 authored by Christopher Cameron's avatar Christopher Cameron Committed by Commit Bot

MacPWAs: Create NSAlerts in the appropriate process

Split the Cocoa-facing parts of JavaScriptAppModalDialogCocoa into a
separate views_bridge_mac::AlertBridge class, implementing a mojo
interface of the same name.

The mojo interface has a single method, Show, which has a single
callback, indicating the disposition of the alert.

Make JavaScriptAppModalDialogCocoa instantiate
views_bridge_mac::AlertBridge across mojo either in-process or
out-of-process as needed. Note that this subtly changes the behavior of
JavaScriptAppModalDialogCocoa::AcceptAppModalDialog in that it will no
longer attempt to read the dialog's text when programmatically
quitting (note that this feature may be dead code).

This makes alerts appear in the appropriate process. Note that the
alerts of PWAs and of Chrome are still interconnected. Only one alert
may be present at once, and an active alert will take focus from Chrome
and all PWAs.

Bug: 898604
Change-Id: I37267cf7aa41d8e68cb2d342a5058e257b2d4385
Reviewed-on: https://chromium-review.googlesource.com/c/1390757Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarAvi Drissman <avi@chromium.org>
Commit-Queue: ccameron <ccameron@chromium.org>
Cr-Commit-Position: refs/heads/master@{#619812}
parent 48c7ebc2
...@@ -22,13 +22,6 @@ import("//rlz/buildflags/buildflags.gni") ...@@ -22,13 +22,6 @@ import("//rlz/buildflags/buildflags.gni")
import("//ui/base/ui_features.gni") import("//ui/base/ui_features.gni")
import("//ui/views/features.gni") import("//ui/views/features.gni")
config("ui_warnings") {
if (is_mac) {
# TODO(thakis): Remove this once http://crbug.com/383820 is figured out
cflags = [ "-Wno-nonnull" ]
}
}
# Use a static library here because many test binaries depend on this but don't # Use a static library here because many test binaries depend on this but don't
# require many files from it. This makes linking more efficient. # require many files from it. This makes linking more efficient.
jumbo_split_static_library("ui") { jumbo_split_static_library("ui") {
...@@ -347,7 +340,6 @@ jumbo_split_static_library("ui") { ...@@ -347,7 +340,6 @@ jumbo_split_static_library("ui") {
libs = [] libs = []
configs += [ configs += [
":ui_warnings",
"//build/config:precompiled_headers", "//build/config:precompiled_headers",
"//build/config/compiler:wexit_time_destructors", "//build/config/compiler:wexit_time_destructors",
] ]
...@@ -2120,6 +2112,7 @@ jumbo_split_static_library("ui") { ...@@ -2120,6 +2112,7 @@ jumbo_split_static_library("ui") {
"//third_party/google_toolbox_for_mac", "//third_party/google_toolbox_for_mac",
"//third_party/mozilla", "//third_party/mozilla",
"//ui/accelerated_widget_mac:accelerated_widget_mac", "//ui/accelerated_widget_mac:accelerated_widget_mac",
"//ui/views_bridge_mac",
] ]
include_dirs = [ "$target_gen_dir" ] include_dirs = [ "$target_gen_dir" ]
libs += [ libs += [
......
...@@ -10,7 +10,10 @@ ...@@ -10,7 +10,10 @@
#include "base/logging.h" #include "base/logging.h"
#include "base/mac/scoped_nsobject.h" #include "base/mac/scoped_nsobject.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "components/app_modal/native_app_modal_dialog.h" #include "components/app_modal/native_app_modal_dialog.h"
#include "ui/views_bridge_mac/alert.h"
#include "ui/views_bridge_mac/mojo/alert.mojom.h"
class PopunderPreventer; class PopunderPreventer;
...@@ -18,19 +21,10 @@ namespace app_modal { ...@@ -18,19 +21,10 @@ namespace app_modal {
class JavaScriptAppModalDialog; class JavaScriptAppModalDialog;
} }
#if __OBJC__
@class NSAlert;
@class JavaScriptAppModalDialogHelper;
#else
class NSAlert;
class JavaScriptAppModalDialogHelper;
#endif
class JavaScriptAppModalDialogCocoa : public app_modal::NativeAppModalDialog { class JavaScriptAppModalDialogCocoa : public app_modal::NativeAppModalDialog {
public: public:
explicit JavaScriptAppModalDialogCocoa( explicit JavaScriptAppModalDialogCocoa(
app_modal::JavaScriptAppModalDialog* dialog); app_modal::JavaScriptAppModalDialog* dialog);
~JavaScriptAppModalDialogCocoa() override;
// Overridden from NativeAppModalDialog: // Overridden from NativeAppModalDialog:
int GetAppModalDialogButtons() const override; int GetAppModalDialogButtons() const override;
...@@ -46,17 +40,30 @@ class JavaScriptAppModalDialogCocoa : public app_modal::NativeAppModalDialog { ...@@ -46,17 +40,30 @@ class JavaScriptAppModalDialogCocoa : public app_modal::NativeAppModalDialog {
} }
private: private:
// Returns the NSAlert associated with the modal dialog. ~JavaScriptAppModalDialogCocoa() override;
NSAlert* GetAlert() const;
// Return the parameters to use for the alert.
views_bridge_mac::mojom::AlertBridgeInitParamsPtr GetAlertParams();
// Called when the alert completes. Deletes |this|.
void OnAlertFinished(views_bridge_mac::mojom::AlertDisposition disposition,
const base::string16& prompt_text,
bool suppress_js_messages);
// Called if there is an error connecting to the alert process. Deletes
// |this|.
void OnConnectionError();
// Mojo interface to the NSAlert.
views_bridge_mac::mojom::AlertBridgePtr alert_bridge_;
std::unique_ptr<app_modal::JavaScriptAppModalDialog> dialog_; std::unique_ptr<app_modal::JavaScriptAppModalDialog> dialog_;
std::unique_ptr<PopunderPreventer> popunder_preventer_; std::unique_ptr<PopunderPreventer> popunder_preventer_;
// Created in the constructor and destroyed in the destructor. int num_buttons_ = 0;
base::scoped_nsobject<JavaScriptAppModalDialogHelper> helper_; bool is_showing_ = false;
bool is_showing_;
base::WeakPtrFactory<JavaScriptAppModalDialogCocoa> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(JavaScriptAppModalDialogCocoa); DISALLOW_COPY_AND_ASSIGN(JavaScriptAppModalDialogCocoa);
}; };
......
...@@ -7,12 +7,9 @@ ...@@ -7,12 +7,9 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#include <stddef.h> #include <stddef.h>
#include "base/i18n/rtl.h"
#include "base/logging.h" #include "base/logging.h"
#import "base/mac/foundation_util.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/ptr_util.h" #include "base/memory/ptr_util.h"
#include "base/strings/sys_string_conversions.h"
#import "chrome/browser/chrome_browser_application_mac.h" #import "chrome/browser/chrome_browser_application_mac.h"
#include "chrome/browser/ui/blocked_content/popunder_preventer.h" #include "chrome/browser/ui/blocked_content/popunder_preventer.h"
#include "chrome/browser/ui/javascript_dialogs/chrome_javascript_native_dialog_factory.h" #include "chrome/browser/ui/javascript_dialogs/chrome_javascript_native_dialog_factory.h"
...@@ -22,381 +19,99 @@ ...@@ -22,381 +19,99 @@
#include "components/strings/grit/components_strings.h" #include "components/strings/grit/components_strings.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_delegate.h"
#include "ui/base/l10n/l10n_util_mac.h" #include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_types.h" #include "ui/base/ui_base_types.h"
#include "ui/gfx/text_elider.h"
#include "ui/strings/grit/ui_strings.h" #include "ui/strings/grit/ui_strings.h"
#include "ui/views/cocoa/bridge_factory_host.h"
#include "ui/views/cocoa/bridged_native_widget_host_impl.h"
#include "ui/views_bridge_mac/alert.h"
namespace { using views_bridge_mac::mojom::AlertDisposition;
const int kSlotsPerLine = 50;
const int kMessageTextMaxSlots = 2000;
} // namespace
// Helper object that receives the notification that the dialog/sheet is
// going away. Is responsible for cleaning itself up.
@interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
@private
base::scoped_nsobject<NSAlert> alert_;
JavaScriptAppModalDialogCocoa* nativeDialog_; // Weak.
base::scoped_nsobject<NSTextField> textField_;
BOOL alertShown_;
}
// Creates an NSAlert if one does not already exist. Otherwise returns the
// existing NSAlert.
- (NSAlert*)alert;
- (void)addTextFieldWithPrompt:(NSString*)prompt;
// Presents an AppKit blocking dialog.
- (void)showAlert;
// Selects the first button of the alert, which should accept it.
- (void)acceptAlert;
// Selects the second button of the alert, which should cancel it.
- (void)cancelAlert;
// Closes the window, and the alert along with it.
- (void)closeWindow;
// Designated initializer.
- (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog;
@end
@implementation JavaScriptAppModalDialogHelper
- (instancetype)init {
NOTREACHED();
return nil;
}
- (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog {
DCHECK(dialog);
self = [super init];
if (self)
nativeDialog_ = dialog;
return self;
}
- (NSAlert*)alert {
if (!alert_) {
alert_.reset([[NSAlert alloc] init]);
if (!nativeDialog_->dialog()->is_before_unload_dialog()) {
// Set a blank icon for dialogs with text provided by the page.
// "onbeforeunload" dialogs don't have text provided by the page, so it's
// OK to use the app icon.
NSImage* image =
[[[NSImage alloc] initWithSize:NSMakeSize(1, 1)] autorelease];
[alert_ setIcon:image];
}
}
return alert_;
}
- (void)addTextFieldWithPrompt:(NSString*)prompt {
DCHECK(!textField_);
textField_.reset(
[[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)]);
[[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
[[self alert] setAccessoryView:textField_];
[[alert_ window] setInitialFirstResponder:textField_];
[textField_ setStringValue:prompt];
}
// |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
- (void)alertDidEnd:(NSAlert*)alert
returnCode:(int)returnCode
contextInfo:(void*)contextInfo {
switch (returnCode) {
case NSAlertFirstButtonReturn: { // OK
[self sendAcceptToNativeDialog];
break;
}
case NSAlertSecondButtonReturn: { // Cancel
// If the user wants to stay on this page, stop quitting (if a quit is in
// progress).
[self sendCancelToNativeDialog];
break;
}
case NSRunStoppedResponse: { // Window was closed underneath us
// Need to call OnClose() because there is some cleanup that needs
// to be done. It won't call back to the javascript since the
// JavaScriptAppModalDialog knows that the WebContents was destroyed.
[self sendCloseToNativeDialog];
break;
}
default: {
NOTREACHED();
}
}
}
- (void)showAlert {
DCHECK(nativeDialog_);
DCHECK(!alertShown_);
alertShown_ = YES;
NSAlert* alert = [self alert];
[alert layout];
[[alert window] recalculateKeyViewLoop];
[alert beginSheetModalForWindow:nil // nil here makes it app-modal
modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
- (void)acceptAlert {
DCHECK(nativeDialog_);
if (!alertShown_) {
[self sendAcceptToNativeDialog];
return;
}
NSButton* first = [[[self alert] buttons] objectAtIndex:0];
[first performClick:nil];
}
- (void)cancelAlert {
DCHECK(nativeDialog_);
if (!alertShown_) {
[self sendCancelToNativeDialog];
return;
}
DCHECK_GE([[[self alert] buttons] count], 2U);
NSButton* second = [[[self alert] buttons] objectAtIndex:1];
[second performClick:nil];
}
- (void)closeWindow {
DCHECK(nativeDialog_);
if (!alertShown_) {
[self sendCloseToNativeDialog];
return;
}
[NSApp endSheet:[[self alert] window]];
}
- (void)sendAcceptToNativeDialog {
DCHECK(nativeDialog_);
nativeDialog_->dialog()->OnAccept([self input], [self shouldSuppress]);
[self destroyNativeDialog];
}
- (void)sendCancelToNativeDialog {
DCHECK(nativeDialog_);
// If the user wants to stay on this page, stop quitting (if a quit is in
// progress).
if (nativeDialog_->dialog()->is_before_unload_dialog())
chrome_browser_application_mac::CancelTerminate();
nativeDialog_->dialog()->OnCancel([self shouldSuppress]);
[self destroyNativeDialog];
}
- (void)sendCloseToNativeDialog {
DCHECK(nativeDialog_);
nativeDialog_->dialog()->OnClose();
[self destroyNativeDialog];
}
- (void)destroyNativeDialog {
DCHECK(nativeDialog_);
JavaScriptAppModalDialogCocoa* nativeDialog = nativeDialog_;
nativeDialog_ = nil; // Need to fail on DCHECK if something wrong happens.
delete nativeDialog; // Careful, this will delete us.
}
- (base::string16)input {
if (textField_)
return base::SysNSStringToUTF16([textField_ stringValue]);
return base::string16();
}
- (bool)shouldSuppress {
if ([[self alert] showsSuppressionButton])
return [[[self alert] suppressionButton] state] == NSOnState;
return false;
}
@end
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, public: // JavaScriptAppModalDialogCocoa:
JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa( JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
app_modal::JavaScriptAppModalDialog* dialog) app_modal::JavaScriptAppModalDialog* dialog)
: dialog_(dialog), : dialog_(dialog),
popunder_preventer_(new PopunderPreventer(dialog->web_contents())), popunder_preventer_(new PopunderPreventer(dialog->web_contents())),
is_showing_(false) { weak_factory_(this) {}
JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {}
views_bridge_mac::mojom::AlertBridgeInitParamsPtr
JavaScriptAppModalDialogCocoa::GetAlertParams() {
views_bridge_mac::mojom::AlertBridgeInitParamsPtr params =
views_bridge_mac::mojom::AlertBridgeInitParams::New();
params->title = dialog_->title();
params->message_text = dialog_->message_text();
// Set a blank icon for dialogs with text provided by the page.
// "onbeforeunload" dialogs don't have text provided by the page, so it's
// OK to use the app icon.
params->hide_application_icon = !dialog_->is_before_unload_dialog();
// Determine the names of the dialog buttons based on the flags. "Default" // Determine the names of the dialog buttons based on the flags. "Default"
// is the OK button. "Other" is the cancel button. We don't use the // is the OK button. "Other" is the cancel button. We don't use the
// "Alternate" button in NSRunAlertPanel. // "Alternate" button in NSRunAlertPanel.
NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK); params->primary_button_text = l10n_util::GetStringUTF16(IDS_APP_OK);
NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
bool text_field = false;
bool one_button = false;
switch (dialog_->javascript_dialog_type()) { switch (dialog_->javascript_dialog_type()) {
case content::JAVASCRIPT_DIALOG_TYPE_ALERT: case content::JAVASCRIPT_DIALOG_TYPE_ALERT:
one_button = true; num_buttons_ = 1;
break; break;
case content::JAVASCRIPT_DIALOG_TYPE_CONFIRM: case content::JAVASCRIPT_DIALOG_TYPE_CONFIRM:
num_buttons_ = 2;
if (dialog_->is_before_unload_dialog()) { if (dialog_->is_before_unload_dialog()) {
if (dialog_->is_reload()) { if (dialog_->is_reload()) {
default_button = l10n_util::GetNSStringWithFixup( params->primary_button_text = l10n_util::GetStringUTF16(
IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL); IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
} else { } else {
default_button = l10n_util::GetNSStringWithFixup( params->primary_button_text = l10n_util::GetStringUTF16(
IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL); IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
} }
} }
params->secondary_button_text.emplace(
l10n_util::GetStringUTF16(IDS_APP_CANCEL));
break; break;
case content::JAVASCRIPT_DIALOG_TYPE_PROMPT: case content::JAVASCRIPT_DIALOG_TYPE_PROMPT:
text_field = true; num_buttons_ = 2;
params->secondary_button_text.emplace(
l10n_util::GetStringUTF16(IDS_APP_CANCEL));
params->text_field_text.emplace(dialog_->default_prompt_text());
break; break;
default: default:
NOTREACHED(); NOTREACHED();
} }
return params;
}
// Create a helper which will receive the sheet ended selector. It will void JavaScriptAppModalDialogCocoa::OnAlertFinished(
// delete itself when done. AlertDisposition disposition,
helper_.reset( const base::string16& text_field_value,
[[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]); bool check_box_value) {
switch (disposition) {
// Show the modal dialog. case AlertDisposition::PRIMARY_BUTTON:
if (text_field) { dialog_->OnAccept(text_field_value, check_box_value);
[helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString( break;
dialog_->default_prompt_text())]; case AlertDisposition::SECONDARY_BUTTON:
} // If the user wants to stay on this page, stop quitting (if a quit is in
[GetAlert() setDelegate:helper_]; // progress).
NSString* informative_text = if (dialog_->is_before_unload_dialog())
base::SysUTF16ToNSString(dialog_->message_text()); chrome_browser_application_mac::CancelTerminate();
dialog_->OnCancel(check_box_value);
// Truncate long JS alerts - crbug.com/331219 break;
NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet]; case AlertDisposition::CLOSE:
for (size_t index = 0, slots_count = 0; index < informative_text.length; dialog_->OnClose();
++index) {
unichar current_char = [informative_text characterAtIndex:index];
if ([newline_char_set characterIsMember:current_char])
slots_count += kSlotsPerLine;
else
slots_count++;
if (slots_count > kMessageTextMaxSlots) {
base::string16 info_text = base::SysNSStringToUTF16(informative_text);
informative_text = base::SysUTF16ToNSString(
gfx::TruncateString(info_text, index, gfx::WORD_BREAK));
break; break;
}
}
[GetAlert() setInformativeText:informative_text];
NSString* message_text =
base::SysUTF16ToNSString(dialog_->title());
[GetAlert() setMessageText:message_text];
[GetAlert() addButtonWithTitle:default_button];
if (!one_button) {
NSButton* other = [GetAlert() addButtonWithTitle:other_button];
[other setKeyEquivalent:@"\e"];
}
if (dialog_->display_suppress_checkbox()) {
[GetAlert() setShowsSuppressionButton:YES];
NSString* suppression_title = l10n_util::GetNSStringWithFixup(
IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
[[GetAlert() suppressionButton] setTitle:suppression_title];
}
// Fix RTL dialogs.
//
// Mac OS X will always display NSAlert strings as LTR. A workaround is to
// manually set the text as attributed strings in the implementing
// NSTextFields. This is a basic correctness issue.
//
// In addition, for readability, the overall alignment is set based on the
// directionality of the first strongly-directional character.
//
// If the dialog fields are selectable then they will scramble when clicked.
// Therefore, selectability is disabled.
//
// See http://crbug.com/70806 for more details.
bool message_has_rtl =
base::i18n::StringContainsStrongRTLChars(dialog_->title());
bool informative_has_rtl =
base::i18n::StringContainsStrongRTLChars(dialog_->message_text());
NSTextField* message_text_field = nil;
NSTextField* informative_text_field = nil;
if (message_has_rtl || informative_has_rtl) {
// Force layout of the dialog. NSAlert leaves its dialog alone once laid
// out; if this is not done then all the modifications that are to come will
// be un-done when the dialog is finally displayed.
[GetAlert() layout];
// Locate the NSTextFields that implement the text display. These are
// actually available as the ivars |_messageField| and |_informationField|
// of the NSAlert, but it is safer (and more forward-compatible) to search
// for them in the subviews.
for (NSView* view in [[[GetAlert() window] contentView] subviews]) {
NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
if ([[text_field stringValue] isEqualTo:message_text])
message_text_field = text_field;
else if ([[text_field stringValue] isEqualTo:informative_text])
informative_text_field = text_field;
}
// This may fail in future OS releases, but it will still work for shipped
// versions of Chromium.
DCHECK(message_text_field);
DCHECK(informative_text_field);
}
if (message_has_rtl && message_text_field) {
base::scoped_nsobject<NSMutableParagraphStyle> alignment(
[[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
[alignment setAlignment:NSRightTextAlignment];
NSDictionary* alignment_attributes =
@{ NSParagraphStyleAttributeName : alignment };
base::scoped_nsobject<NSAttributedString> attr_string(
[[NSAttributedString alloc] initWithString:message_text
attributes:alignment_attributes]);
[message_text_field setAttributedStringValue:attr_string];
[message_text_field setSelectable:NO];
}
if (informative_has_rtl && informative_text_field) {
base::i18n::TextDirection direction =
base::i18n::GetFirstStrongCharacterDirection(dialog_->message_text());
base::scoped_nsobject<NSMutableParagraphStyle> alignment(
[[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
[alignment setAlignment:
(direction == base::i18n::RIGHT_TO_LEFT) ? NSRightTextAlignment
: NSLeftTextAlignment];
NSDictionary* alignment_attributes =
@{ NSParagraphStyleAttributeName : alignment };
base::scoped_nsobject<NSAttributedString> attr_string(
[[NSAttributedString alloc] initWithString:informative_text
attributes:alignment_attributes]);
[informative_text_field setAttributedStringValue:attr_string];
[informative_text_field setSelectable:NO];
} }
delete this;
} }
JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() { void JavaScriptAppModalDialogCocoa::OnConnectionError() {
[NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()]; dialog()->OnClose();
} delete this;
////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, private:
NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
return [helper_ alert];
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
...@@ -405,8 +120,7 @@ NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const { ...@@ -405,8 +120,7 @@ NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const { int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
// From the above, it is the case that if there is 1 button, it is always the // From the above, it is the case that if there is 1 button, it is always the
// OK button. The second button, if it exists, is always the Cancel button. // OK button. The second button, if it exists, is always the Cancel button.
int num_buttons = [[GetAlert() buttons] count]; switch (num_buttons_) {
switch (num_buttons) {
case 1: case 1:
return ui::DIALOG_BUTTON_OK; return ui::DIALOG_BUTTON_OK;
case 2: case 2:
...@@ -420,28 +134,54 @@ int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const { ...@@ -420,28 +134,54 @@ int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() { void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
is_showing_ = true; is_showing_ = true;
// Dispatch the method to show the alert back to the top of the CFRunLoop. views_bridge_mac::mojom::AlertBridgeRequest bridge_request =
// This fixes an interaction bug with NSSavePanel. http://crbug.com/375785 mojo::MakeRequest(&alert_bridge_);
// When this object is destroyed, outstanding performSelector: requests alert_bridge_.set_connection_error_handler(
// should be cancelled. base::BindOnce(&JavaScriptAppModalDialogCocoa::OnConnectionError,
[helper_.get() performSelector:@selector(showAlert) weak_factory_.GetWeakPtr()));
withObject:nil // If the alert is from a window that is out of process then use the
afterDelay:0]; // views::BridgeFactoryHost for that window to create the alert. Otherwise
// create an AlertBridge in-process (but still communicate with it over
// mojo).
auto* bridged_native_widget_host =
views::BridgedNativeWidgetHostImpl::GetFromNativeView(
dialog_->web_contents()->GetNativeView());
views::BridgeFactoryHost* bridge_factory_host =
bridged_native_widget_host
? bridged_native_widget_host->bridge_factory_host()
: nullptr;
if (bridge_factory_host)
bridge_factory_host->GetFactory()->CreateAlert(std::move(bridge_request));
else
ignore_result(new views_bridge_mac::AlertBridge(std::move(bridge_request)));
alert_bridge_->Show(
GetAlertParams(),
base::BindOnce(&JavaScriptAppModalDialogCocoa::OnAlertFinished,
weak_factory_.GetWeakPtr()));
} }
void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() { void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
} }
void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() { void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
[helper_ closeWindow]; // This function expects that dialog_->OnClose will be called before this
// function completes.
OnAlertFinished(AlertDisposition::CLOSE, base::string16(),
false /* check_box_value */);
} }
void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() { void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
[helper_ acceptAlert]; // Note that for out-of-process dialogs, we cannot find out the actual
// prompt text or suppression checkbox state in time (because the caller
// expects that OnAlertFinished be called before the function ends), so just
// use the initial values.
OnAlertFinished(AlertDisposition::PRIMARY_BUTTON,
dialog_->default_prompt_text(), false /* check_box_value */);
} }
void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() { void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
[helper_ cancelAlert]; OnAlertFinished(AlertDisposition::SECONDARY_BUTTON, base::string16(), false
/* check_box_value */);
} }
bool JavaScriptAppModalDialogCocoa::IsShowing() const { bool JavaScriptAppModalDialogCocoa::IsShowing() const {
......
...@@ -4,10 +4,20 @@ ...@@ -4,10 +4,20 @@
import("//mojo/public/tools/bindings/mojom.gni") import("//mojo/public/tools/bindings/mojom.gni")
config("views_bridge_mac_warnings") {
if (is_mac) {
# TODO(thakis): Remove this once http://crbug.com/383820 is figured out
cflags = [ "-Wno-nonnull" ]
}
}
component("views_bridge_mac") { component("views_bridge_mac") {
assert(is_mac) assert(is_mac)
configs += [ ":views_bridge_mac_warnings" ]
sources = [ sources = [
"alert.h",
"alert.mm",
"bridged_native_widget_host_helper.h", "bridged_native_widget_host_helper.h",
"cocoa_mouse_capture.h", "cocoa_mouse_capture.h",
"cocoa_mouse_capture.mm", "cocoa_mouse_capture.mm",
...@@ -17,7 +27,11 @@ component("views_bridge_mac") { ...@@ -17,7 +27,11 @@ component("views_bridge_mac") {
] ]
defines = [ "VIEWS_BRIDGE_MAC_IMPLEMENTATION" ] defines = [ "VIEWS_BRIDGE_MAC_IMPLEMENTATION" ]
deps = [ deps = [
":mojo",
"//base", "//base",
"//base:i18n",
"//mojo/public/cpp/bindings",
"//ui/accelerated_widget_mac",
"//ui/base", "//ui/base",
"//ui/events", "//ui/events",
"//ui/gfx", "//ui/gfx",
...@@ -29,6 +43,7 @@ mojom("mojo") { ...@@ -29,6 +43,7 @@ mojom("mojo") {
assert(is_mac) assert(is_mac)
sources = [ sources = [
"mojo/alert.mojom",
"mojo/bridge_factory.mojom", "mojo/bridge_factory.mojom",
"mojo/bridged_native_widget.mojom", "mojo/bridged_native_widget.mojom",
"mojo/bridged_native_widget_host.mojom", "mojo/bridged_native_widget_host.mojom",
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_VIEWS_BRIDGE_MAC_ALERT_H_
#define UI_VIEWS_BRIDGE_MAC_ALERT_H_
#import <Cocoa/Cocoa.h>
#include "base/mac/scoped_nsobject.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "ui/gfx/text_elider.h"
#include "ui/views_bridge_mac/mojo/alert.mojom.h"
#include "ui/views_bridge_mac/views_bridge_mac_export.h"
@class AlertBridgeHelper;
namespace views_bridge_mac {
// Class that displays an NSAlert with associated UI as described by the mojo
// AlertBridge interface.
class VIEWS_BRIDGE_MAC_EXPORT AlertBridge
: public views_bridge_mac::mojom::AlertBridge {
public:
// Creates a new alert which controls its own lifetime. It will destroy itself
// once its NSAlert goes away.
AlertBridge(mojom::AlertBridgeRequest bridge_request);
// Send the specified disposition via the Show callback, then destroy |this|.
void SendResultAndDestroy(mojom::AlertDisposition disposition);
// Called by Cocoa to indicate when the NSAlert is visible (and can be
// programmatically updated by Accept, Cancel, and Close).
void SetAlertHasShown();
private:
// Private destructor is called only through SendResultAndDestroy.
~AlertBridge() override;
// Handle being disconnected (e.g, because the alert was programmatically
// dismissed).
void OnConnectionError();
// views_bridge_mac::mojom::Alert:
void Show(mojom::AlertBridgeInitParamsPtr params,
ShowCallback callback) override;
// The NSAlert's owner and delegate.
base::scoped_nsobject<AlertBridgeHelper> helper_;
// Set once the alert window is showing (needed because showing is done in a
// posted task).
bool alert_shown_ = false;
// The callback to make when the dialog has finished running.
ShowCallback callback_;
mojo::Binding<views_bridge_mac::mojom::AlertBridge> mojo_binding_;
base::WeakPtrFactory<AlertBridge> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(AlertBridge);
};
} // namespace views_bridge_mac
#endif // UI_VIEWS_BRIDGE_MAC_ALERT_H_
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views_bridge_mac/alert.h"
#include "base/i18n/rtl.h"
#import "base/mac/foundation_util.h"
#include "base/strings/sys_string_conversions.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/l10n/l10n_util_mac.h"
using views_bridge_mac::mojom::AlertBridgeInitParams;
using views_bridge_mac::mojom::AlertDisposition;
namespace {
const int kSlotsPerLine = 50;
const int kMessageTextMaxSlots = 2000;
} // namespace
////////////////////////////////////////////////////////////////////////////////
// AlertBridgeHelper:
// Helper object that receives the notification that the dialog/sheet is
// going away. Is responsible for cleaning itself up.
@interface AlertBridgeHelper : NSObject <NSAlertDelegate> {
@private
base::scoped_nsobject<NSAlert> alert_;
views_bridge_mac::AlertBridge* alertBridge_; // Weak.
base::scoped_nsobject<NSTextField> textField_;
}
@property(assign, nonatomic) views_bridge_mac::AlertBridge* alertBridge;
// Returns the underlying alert.
- (NSAlert*)alert;
// Set a blank icon for dialogs with text provided by the page.
- (void)setBlankIcon;
// Add a text field to the alert.
- (void)addTextFieldWithPrompt:(NSString*)prompt;
// Presents an AppKit blocking dialog.
- (void)showAlert;
@end
@implementation AlertBridgeHelper
@synthesize alertBridge = alertBridge_;
- (void)initAlert:(AlertBridgeInitParams*)params {
alert_.reset([[NSAlert alloc] init]);
[alert_ setDelegate:self];
if (params->hide_application_icon)
[self setBlankIcon];
if (params->text_field_text) {
[self addTextFieldWithPrompt:base::SysUTF16ToNSString(
*params->text_field_text)];
}
NSString* informative_text = base::SysUTF16ToNSString(params->message_text);
// Truncate long JS alerts - crbug.com/331219
NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet];
for (size_t index = 0, slots_count = 0; index < informative_text.length;
++index) {
unichar current_char = [informative_text characterAtIndex:index];
if ([newline_char_set characterIsMember:current_char])
slots_count += kSlotsPerLine;
else
slots_count++;
if (slots_count > kMessageTextMaxSlots) {
base::string16 info_text = base::SysNSStringToUTF16(informative_text);
informative_text = base::SysUTF16ToNSString(
gfx::TruncateString(info_text, index, gfx::WORD_BREAK));
break;
}
}
[alert_ setInformativeText:informative_text];
NSString* message_text = l10n_util::FixUpWindowsStyleLabel(params->title);
[alert_ setMessageText:message_text];
[alert_ addButtonWithTitle:l10n_util::FixUpWindowsStyleLabel(
params->primary_button_text)];
if (params->secondary_button_text) {
NSButton* other =
[alert_ addButtonWithTitle:l10n_util::FixUpWindowsStyleLabel(
*params->secondary_button_text)];
[other setKeyEquivalent:@"\e"];
}
if (params->check_box_text) {
[alert_ setShowsSuppressionButton:YES];
NSString* suppression_title =
l10n_util::FixUpWindowsStyleLabel(*params->check_box_text);
[[alert_ suppressionButton] setTitle:suppression_title];
}
// Fix RTL dialogs.
//
// Mac OS X will always display NSAlert strings as LTR. A workaround is to
// manually set the text as attributed strings in the implementing
// NSTextFields. This is a basic correctness issue.
//
// In addition, for readability, the overall alignment is set based on the
// directionality of the first strongly-directional character.
//
// If the dialog fields are selectable then they will scramble when clicked.
// Therefore, selectability is disabled.
//
// See http://crbug.com/70806 for more details.
bool message_has_rtl =
base::i18n::StringContainsStrongRTLChars(params->title);
bool informative_has_rtl =
base::i18n::StringContainsStrongRTLChars(params->message_text);
NSTextField* message_text_field = nil;
NSTextField* informative_text_field = nil;
if (message_has_rtl || informative_has_rtl) {
// Force layout of the dialog. NSAlert leaves its dialog alone once laid
// out; if this is not done then all the modifications that are to come will
// be un-done when the dialog is finally displayed.
[alert_ layout];
// Locate the NSTextFields that implement the text display. These are
// actually available as the ivars |_messageField| and |_informationField|
// of the NSAlert, but it is safer (and more forward-compatible) to search
// for them in the subviews.
for (NSView* view in [[[alert_ window] contentView] subviews]) {
NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
if ([[text_field stringValue] isEqualTo:message_text])
message_text_field = text_field;
else if ([[text_field stringValue] isEqualTo:informative_text])
informative_text_field = text_field;
}
// This may fail in future OS releases, but it will still work for shipped
// versions of Chromium.
DCHECK(message_text_field);
DCHECK(informative_text_field);
}
if (message_has_rtl && message_text_field) {
base::scoped_nsobject<NSMutableParagraphStyle> alignment(
[[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
[alignment setAlignment:NSRightTextAlignment];
NSDictionary* alignment_attributes =
@{NSParagraphStyleAttributeName : alignment};
base::scoped_nsobject<NSAttributedString> attr_string(
[[NSAttributedString alloc] initWithString:message_text
attributes:alignment_attributes]);
[message_text_field setAttributedStringValue:attr_string];
[message_text_field setSelectable:NO];
}
if (informative_has_rtl && informative_text_field) {
base::i18n::TextDirection direction =
base::i18n::GetFirstStrongCharacterDirection(params->message_text);
base::scoped_nsobject<NSMutableParagraphStyle> alignment(
[[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
[alignment setAlignment:(direction == base::i18n::RIGHT_TO_LEFT)
? NSRightTextAlignment
: NSLeftTextAlignment];
NSDictionary* alignment_attributes =
@{NSParagraphStyleAttributeName : alignment};
base::scoped_nsobject<NSAttributedString> attr_string(
[[NSAttributedString alloc] initWithString:informative_text
attributes:alignment_attributes]);
[informative_text_field setAttributedStringValue:attr_string];
[informative_text_field setSelectable:NO];
}
}
- (void)setBlankIcon {
NSImage* image =
[[[NSImage alloc] initWithSize:NSMakeSize(1, 1)] autorelease];
[alert_ setIcon:image];
}
- (NSAlert*)alert {
return alert_;
}
- (void)addTextFieldWithPrompt:(NSString*)prompt {
DCHECK(!textField_);
textField_.reset(
[[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)]);
[[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
[[self alert] setAccessoryView:textField_];
[[alert_ window] setInitialFirstResponder:textField_];
[textField_ setStringValue:prompt];
}
// |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
- (void)alertDidEnd:(NSAlert*)alert
returnCode:(int)returnCode
contextInfo:(void*)contextInfo {
switch (returnCode) {
case NSAlertFirstButtonReturn: // OK
alertBridge_->SendResultAndDestroy(AlertDisposition::PRIMARY_BUTTON);
break;
case NSAlertSecondButtonReturn: // Cancel
alertBridge_->SendResultAndDestroy(AlertDisposition::SECONDARY_BUTTON);
break;
case NSRunStoppedResponse: // Window was closed underneath us
alertBridge_->SendResultAndDestroy(AlertDisposition::CLOSE);
break;
default:
NOTREACHED();
}
}
- (void)showAlert {
DCHECK(alertBridge_);
alertBridge_->SetAlertHasShown();
NSAlert* alert = [self alert];
[alert layout];
[[alert window] recalculateKeyViewLoop];
[alert beginSheetModalForWindow:nil // nil here makes it app-modal
modalDelegate:self
didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
- (void)closeWindow {
DCHECK(alertBridge_);
[NSApp endSheet:[[self alert] window]];
}
- (base::string16)input {
if (textField_)
return base::SysNSStringToUTF16([textField_ stringValue]);
return base::string16();
}
- (bool)shouldSuppress {
if ([[self alert] showsSuppressionButton])
return [[[self alert] suppressionButton] state] == NSOnState;
return false;
}
@end
namespace views_bridge_mac {
////////////////////////////////////////////////////////////////////////////////
// AlertBridge:
AlertBridge::AlertBridge(mojom::AlertBridgeRequest bridge_request)
: mojo_binding_(this), weak_factory_(this) {
mojo_binding_.Bind(std::move(bridge_request),
ui::WindowResizeHelperMac::Get()->task_runner());
mojo_binding_.set_connection_error_handler(base::BindOnce(
&AlertBridge::OnConnectionError, weak_factory_.GetWeakPtr()));
}
AlertBridge::~AlertBridge() {
[helper_ setAlertBridge:nil];
[NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()];
}
void AlertBridge::OnConnectionError() {
// If the alert has been shown, then close the window, and |this| will delete
// itself after the window is closed. Otherwise, just delete |this|
// immediately.
if (alert_shown_)
[helper_ closeWindow];
else
delete this;
}
void AlertBridge::SendResultAndDestroy(AlertDisposition disposition) {
DCHECK(callback_);
std::move(callback_).Run(disposition, [helper_ input],
[helper_ shouldSuppress]);
delete this;
}
void AlertBridge::SetAlertHasShown() {
DCHECK(!alert_shown_);
alert_shown_ = true;
}
////////////////////////////////////////////////////////////////////////////////
// AlertBridge, mojo::AlertBridge:
void AlertBridge::Show(mojom::AlertBridgeInitParamsPtr params,
ShowCallback callback) {
callback_ = std::move(callback);
// Create a helper which will receive the sheet ended selector. It will
// delete itself when done.
helper_.reset([[AlertBridgeHelper alloc] init]);
[helper_ setAlertBridge:this];
[helper_ initAlert:params.get()];
// Dispatch the method to show the alert back to the top of the CFRunLoop.
// This fixes an interaction bug with NSSavePanel. http://crbug.com/375785
// When this object is destroyed, outstanding performSelector: requests
// should be cancelled.
[helper_.get() performSelector:@selector(showAlert)
withObject:nil
afterDelay:0];
}
} // namespace views_bridge_mac
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include "mojo/public/cpp/bindings/associated_binding.h" #include "mojo/public/cpp/bindings/associated_binding.h"
#include "ui/views/views_export.h" #include "ui/views/views_export.h"
#include "ui/views_bridge_mac/mojo/alert.mojom.h"
#include "ui/views_bridge_mac/mojo/bridge_factory.mojom.h" #include "ui/views_bridge_mac/mojo/bridge_factory.mojom.h"
#include "ui/views_bridge_mac/mojo/bridged_native_widget.mojom.h" #include "ui/views_bridge_mac/mojo/bridged_native_widget.mojom.h"
#include "ui/views_bridge_mac/mojo/bridged_native_widget_host.mojom.h" #include "ui/views_bridge_mac/mojo/bridged_native_widget_host.mojom.h"
...@@ -23,6 +24,7 @@ class VIEWS_EXPORT BridgeFactoryImpl : public mojom::BridgeFactory { ...@@ -23,6 +24,7 @@ class VIEWS_EXPORT BridgeFactoryImpl : public mojom::BridgeFactory {
void BindRequest(mojom::BridgeFactoryAssociatedRequest request); void BindRequest(mojom::BridgeFactoryAssociatedRequest request);
// mojom::BridgeFactory: // mojom::BridgeFactory:
void CreateAlert(mojom::AlertBridgeRequest bridge_request) override;
void CreateBridgedNativeWidget( void CreateBridgedNativeWidget(
uint64_t bridge_id, uint64_t bridge_id,
mojom::BridgedNativeWidgetAssociatedRequest bridge_request, mojom::BridgedNativeWidgetAssociatedRequest bridge_request,
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
#include "base/no_destructor.h" #include "base/no_destructor.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h" #include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/cocoa/remote_accessibility_api.h" #include "ui/base/cocoa/remote_accessibility_api.h"
#include "ui/views_bridge_mac/alert.h"
#include "ui/views_bridge_mac/bridged_native_widget_host_helper.h" #include "ui/views_bridge_mac/bridged_native_widget_host_helper.h"
#include "ui/views_bridge_mac/bridged_native_widget_impl.h" #include "ui/views_bridge_mac/bridged_native_widget_impl.h"
...@@ -104,6 +105,11 @@ void BridgeFactoryImpl::BindRequest( ...@@ -104,6 +105,11 @@ void BridgeFactoryImpl::BindRequest(
ui::WindowResizeHelperMac::Get()->task_runner()); ui::WindowResizeHelperMac::Get()->task_runner());
} }
void BridgeFactoryImpl::CreateAlert(mojom::AlertBridgeRequest bridge_request) {
// The resulting object manages its own lifetime.
ignore_result(new AlertBridge(std::move(bridge_request)));
}
void BridgeFactoryImpl::CreateBridgedNativeWidget( void BridgeFactoryImpl::CreateBridgedNativeWidget(
uint64_t bridge_id, uint64_t bridge_id,
mojom::BridgedNativeWidgetAssociatedRequest bridge_request, mojom::BridgedNativeWidgetAssociatedRequest bridge_request,
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
module views_bridge_mac.mojom;
import "mojo/public/mojom/base/string16.mojom";
struct AlertBridgeInitParams {
// The dialog title and text.
mojo_base.mojom.String16 title;
mojo_base.mojom.String16 message_text;
// Set if the application icon should be hidden.
bool hide_application_icon;
// Text for the primary button (which is also the default button).
mojo_base.mojom.String16 primary_button_text;
// Text for the secondary (non-default) button. If not set, then there is only
// one button for this alert.
mojo_base.mojom.String16? secondary_button_text;
// Default text for the text field. If not set, then there is no text field
// for this alert.
mojo_base.mojom.String16? text_field_text;
// The text for the checkbox. If not set, then there is no checkbox for this
// alert.
mojo_base.mojom.String16? check_box_text;
};
// Disposition of alert window.
enum AlertDisposition {
// Default button was pressed.
PRIMARY_BUTTON,
// Secondary button was pressed.
SECONDARY_BUTTON,
// The window was closed without a selection being made.
CLOSE,
};
interface AlertBridge {
// Initialize and show the alert. Return in |disposition| is how the window
// was dismissed. Return in |text_field_value| the value of the text field
// shown in the alert (if any). Return true in |check_box_value| only if the
// alert had a checkbox and it was checked.
Show(AlertBridgeInitParams params) =>
(AlertDisposition disposition, mojo_base.mojom.String16 text_field_value,
bool check_box_value);
};
...@@ -4,11 +4,15 @@ ...@@ -4,11 +4,15 @@
module views_bridge_mac.mojom; module views_bridge_mac.mojom;
import "ui/views_bridge_mac/mojo/alert.mojom";
import "ui/views_bridge_mac/mojo/bridged_native_widget.mojom"; import "ui/views_bridge_mac/mojo/bridged_native_widget.mojom";
import "ui/views_bridge_mac/mojo/bridged_native_widget_host.mojom"; import "ui/views_bridge_mac/mojo/bridged_native_widget_host.mojom";
// The interface through which a bridge is created and connected to its host. // The interface through which a bridge is created and connected to its host.
interface BridgeFactory { interface BridgeFactory {
// Create a bridge for an NSAlert. The resulting object owns its own lifetime.
CreateAlert(AlertBridge& alert_bridge_request);
// Create a bridge for a native widget. The resulting object will be owned by // Create a bridge for a native widget. The resulting object will be owned by
// the connection for |host|. Closing that connection will result in deleting // the connection for |host|. Closing that connection will result in deleting
// the bridge. // the bridge.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment