// 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)); }