blob: a9c7a833c60f8dada5ee5327634d7a8fe5c0481c [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/ui/cocoa/javascript_app_modal_dialog_cocoa.h"
#import <Cocoa/Cocoa.h>
#include <stddef.h>
#include "base/i18n/rtl.h"
#include "base/logging.h"
#import "base/mac/foundation_util.h"
#include "base/macros.h"
#include "base/strings/sys_string_conversions.h"
#import "chrome/browser/chrome_browser_application_mac.h"
#include "chrome/browser/ui/app_modal/chrome_javascript_native_dialog_factory.h"
#include "chrome/browser/ui/blocked_content/app_modal_dialog_helper.h"
#include "components/app_modal/javascript_app_modal_dialog.h"
#include "components/app_modal/javascript_dialog_manager.h"
#include "components/app_modal/javascript_native_dialog_factory.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "grit/components_strings.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/ui_base_types.h"
#include "ui/gfx/text_elider.h"
#include "ui/strings/grit/ui_strings.h"
namespace {
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 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(
app_modal::JavaScriptAppModalDialog* dialog)
: dialog_(dialog),
popup_helper_(new AppModalDialogHelper(dialog->web_contents())),
is_showing_(false) {
// 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
// "Alternate" button in NSRunAlertPanel.
NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
bool text_field = false;
bool one_button = false;
switch (dialog_->javascript_message_type()) {
case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
one_button = true;
break;
case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
if (dialog_->is_before_unload_dialog()) {
if (dialog_->is_reload()) {
default_button = l10n_util::GetNSStringWithFixup(
IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
other_button = l10n_util::GetNSStringWithFixup(
IDS_BEFORERELOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
} else {
default_button = l10n_util::GetNSStringWithFixup(
IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
other_button = l10n_util::GetNSStringWithFixup(
IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
}
}
break;
case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
text_field = true;
break;
default:
NOTREACHED();
}
// Create a helper which will receive the sheet ended selector. It will
// delete itself when done.
helper_.reset(
[[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]);
// Show the modal dialog.
if (text_field) {
[helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString(
dialog_->default_prompt_text())];
}
[GetAlert() setDelegate:helper_];
NSString* informative_text =
base::SysUTF16ToNSString(dialog_->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;
}
}
[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];
}
}
JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {
[NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()];
}
////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, private:
NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
return [helper_ alert];
}
////////////////////////////////////////////////////////////////////////////////
// JavaScriptAppModalDialogCocoa, NativeAppModalDialog implementation:
int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
// 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.
int num_buttons = [[GetAlert() buttons] count];
switch (num_buttons) {
case 1:
return ui::DIALOG_BUTTON_OK;
case 2:
return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
default:
NOTREACHED();
return 0;
}
}
void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
is_showing_ = true;
// 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];
}
void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
}
void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
[helper_ closeWindow];
}
void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
[helper_ acceptAlert];
}
void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
[helper_ cancelAlert];
}
bool JavaScriptAppModalDialogCocoa::IsShowing() const {
return is_showing_;
}
namespace {
class ChromeJavaScriptNativeDialogCocoaFactory
: public app_modal::JavaScriptNativeDialogFactory {
public:
ChromeJavaScriptNativeDialogCocoaFactory() {}
~ChromeJavaScriptNativeDialogCocoaFactory() override {}
private:
app_modal::NativeAppModalDialog* CreateNativeJavaScriptDialog(
app_modal::JavaScriptAppModalDialog* dialog) override {
app_modal::NativeAppModalDialog* d =
new JavaScriptAppModalDialogCocoa(dialog);
dialog->web_contents()->GetDelegate()->ActivateContents(
dialog->web_contents());
return d;
}
DISALLOW_COPY_AND_ASSIGN(ChromeJavaScriptNativeDialogCocoaFactory);
};
} // namespace
void InstallChromeJavaScriptNativeDialogFactory() {
app_modal::JavaScriptDialogManager::GetInstance()->
SetNativeDialogFactory(
make_scoped_ptr(new ChromeJavaScriptNativeDialogCocoaFactory));
}