1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/cocoa/javascript_app_modal_dialog_cocoa.h"
7 #import <Cocoa/Cocoa.h>
9 #include "base/i18n/rtl.h"
10 #include "base/logging.h"
11 #import "base/mac/foundation_util.h"
12 #include "base/strings/sys_string_conversions.h"
13 #import "chrome/browser/chrome_browser_application_mac.h"
14 #include "chrome/browser/ui/app_modal/chrome_javascript_native_dialog_factory.h"
15 #include "chrome/browser/ui/blocked_content/app_modal_dialog_helper.h"
16 #include "components/app_modal/javascript_app_modal_dialog.h"
17 #include "components/app_modal/javascript_dialog_manager.h"
18 #include "components/app_modal/javascript_native_dialog_factory.h"
19 #include "content/public/browser/web_contents.h"
20 #include "content/public/browser/web_contents_delegate.h"
21 #include "grit/components_strings.h"
22 #include "ui/base/l10n/l10n_util_mac.h"
23 #include "ui/base/ui_base_types.h"
24 #include "ui/gfx/text_elider.h"
25 #include "ui/strings/grit/ui_strings.h"
29 const int kSlotsPerLine = 50;
30 const int kMessageTextMaxSlots = 2000;
34 // Helper object that receives the notification that the dialog/sheet is
35 // going away. Is responsible for cleaning itself up.
36 @interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
38 base::scoped_nsobject<NSAlert> alert_;
39 JavaScriptAppModalDialogCocoa* nativeDialog_; // Weak.
40 base::scoped_nsobject<NSTextField> textField_;
44 // Creates an NSAlert if one does not already exist. Otherwise returns the
47 - (void)addTextFieldWithPrompt:(NSString*)prompt;
49 // Presents an AppKit blocking dialog.
52 // Selects the first button of the alert, which should accept it.
55 // Selects the second button of the alert, which should cancel it.
58 // Closes the window, and the alert along with it.
61 // Designated initializer.
62 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog;
66 @implementation JavaScriptAppModalDialogHelper
68 - (instancetype)init {
73 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog {
77 nativeDialog_ = dialog;
83 alert_.reset([[NSAlert alloc] init]);
87 - (void)addTextFieldWithPrompt:(NSString*)prompt {
90 [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)]);
91 [[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
92 [[self alert] setAccessoryView:textField_];
94 [textField_ setStringValue:prompt];
97 // |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
98 - (void)alertDidEnd:(NSAlert*)alert
99 returnCode:(int)returnCode
100 contextInfo:(void*)contextInfo {
101 switch (returnCode) {
102 case NSAlertFirstButtonReturn: { // OK
103 [self sendAcceptToNativeDialog];
106 case NSAlertSecondButtonReturn: { // Cancel
107 // If the user wants to stay on this page, stop quitting (if a quit is in
109 [self sendCancelToNativeDialog];
112 case NSRunStoppedResponse: { // Window was closed underneath us
113 // Need to call OnClose() because there is some cleanup that needs
114 // to be done. It won't call back to the javascript since the
115 // JavaScriptAppModalDialog knows that the WebContents was destroyed.
116 [self sendCloseToNativeDialog];
126 DCHECK(nativeDialog_);
127 DCHECK(!alertShown_);
129 NSAlert* alert = [self alert];
130 [alert beginSheetModalForWindow:nil // nil here makes it app-modal
132 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
135 if ([alert accessoryView])
136 [[alert window] makeFirstResponder:[alert accessoryView]];
139 - (void)acceptAlert {
140 DCHECK(nativeDialog_);
142 [self sendAcceptToNativeDialog];
145 NSButton* first = [[[self alert] buttons] objectAtIndex:0];
146 [first performClick:nil];
149 - (void)cancelAlert {
150 DCHECK(nativeDialog_);
152 [self sendCancelToNativeDialog];
155 DCHECK_GE([[[self alert] buttons] count], 2U);
156 NSButton* second = [[[self alert] buttons] objectAtIndex:1];
157 [second performClick:nil];
160 - (void)closeWindow {
161 DCHECK(nativeDialog_);
163 [self sendCloseToNativeDialog];
166 [NSApp endSheet:[[self alert] window]];
169 - (void)sendAcceptToNativeDialog {
170 DCHECK(nativeDialog_);
171 nativeDialog_->dialog()->OnAccept([self input], [self shouldSuppress]);
172 [self destroyNativeDialog];
175 - (void)sendCancelToNativeDialog {
176 DCHECK(nativeDialog_);
177 // If the user wants to stay on this page, stop quitting (if a quit is in
179 if (nativeDialog_->dialog()->is_before_unload_dialog())
180 chrome_browser_application_mac::CancelTerminate();
181 nativeDialog_->dialog()->OnCancel([self shouldSuppress]);
182 [self destroyNativeDialog];
185 - (void)sendCloseToNativeDialog {
186 DCHECK(nativeDialog_);
187 nativeDialog_->dialog()->OnClose();
188 [self destroyNativeDialog];
191 - (void)destroyNativeDialog {
192 DCHECK(nativeDialog_);
193 JavaScriptAppModalDialogCocoa* nativeDialog = nativeDialog_;
194 nativeDialog_ = nil; // Need to fail on DCHECK if something wrong happens.
195 delete nativeDialog; // Careful, this will delete us.
198 - (base::string16)input {
200 return base::SysNSStringToUTF16([textField_ stringValue]);
201 return base::string16();
204 - (bool)shouldSuppress {
205 if ([[self alert] showsSuppressionButton])
206 return [[[self alert] suppressionButton] state] == NSOnState;
212 ////////////////////////////////////////////////////////////////////////////////
213 // JavaScriptAppModalDialogCocoa, public:
215 JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
216 app_modal::JavaScriptAppModalDialog* dialog)
218 popup_helper_(new AppModalDialogHelper(dialog->web_contents())),
220 // Determine the names of the dialog buttons based on the flags. "Default"
221 // is the OK button. "Other" is the cancel button. We don't use the
222 // "Alternate" button in NSRunAlertPanel.
223 NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
224 NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
225 bool text_field = false;
226 bool one_button = false;
227 switch (dialog_->javascript_message_type()) {
228 case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
231 case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
232 if (dialog_->is_before_unload_dialog()) {
233 if (dialog_->is_reload()) {
234 default_button = l10n_util::GetNSStringWithFixup(
235 IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
236 other_button = l10n_util::GetNSStringWithFixup(
237 IDS_BEFORERELOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
239 default_button = l10n_util::GetNSStringWithFixup(
240 IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
241 other_button = l10n_util::GetNSStringWithFixup(
242 IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
246 case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
254 // Create a helper which will receive the sheet ended selector. It will
255 // delete itself when done.
257 [[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]);
259 // Show the modal dialog.
261 [helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString(
262 dialog_->default_prompt_text())];
264 [GetAlert() setDelegate:helper_];
265 NSString* informative_text =
266 base::SysUTF16ToNSString(dialog_->message_text());
268 // Truncate long JS alerts - crbug.com/331219
269 NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet];
270 for (size_t index = 0, slots_count = 0; index < informative_text.length;
272 unichar current_char = [informative_text characterAtIndex:index];
273 if ([newline_char_set characterIsMember:current_char])
274 slots_count += kSlotsPerLine;
277 if (slots_count > kMessageTextMaxSlots) {
278 base::string16 info_text = base::SysNSStringToUTF16(informative_text);
279 informative_text = base::SysUTF16ToNSString(
280 gfx::TruncateString(info_text, index, gfx::WORD_BREAK));
285 [GetAlert() setInformativeText:informative_text];
286 NSString* message_text =
287 base::SysUTF16ToNSString(dialog_->title());
288 [GetAlert() setMessageText:message_text];
289 [GetAlert() addButtonWithTitle:default_button];
291 NSButton* other = [GetAlert() addButtonWithTitle:other_button];
292 [other setKeyEquivalent:@"\e"];
294 if (dialog_->display_suppress_checkbox()) {
295 [GetAlert() setShowsSuppressionButton:YES];
296 NSString* suppression_title = l10n_util::GetNSStringWithFixup(
297 IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
298 [[GetAlert() suppressionButton] setTitle:suppression_title];
303 // Mac OS X will always display NSAlert strings as LTR. A workaround is to
304 // manually set the text as attributed strings in the implementing
305 // NSTextFields. This is a basic correctness issue.
307 // In addition, for readability, the overall alignment is set based on the
308 // directionality of the first strongly-directional character.
310 // If the dialog fields are selectable then they will scramble when clicked.
311 // Therefore, selectability is disabled.
313 // See http://crbug.com/70806 for more details.
315 bool message_has_rtl =
316 base::i18n::StringContainsStrongRTLChars(dialog_->title());
317 bool informative_has_rtl =
318 base::i18n::StringContainsStrongRTLChars(dialog_->message_text());
320 NSTextField* message_text_field = nil;
321 NSTextField* informative_text_field = nil;
322 if (message_has_rtl || informative_has_rtl) {
323 // Force layout of the dialog. NSAlert leaves its dialog alone once laid
324 // out; if this is not done then all the modifications that are to come will
325 // be un-done when the dialog is finally displayed.
328 // Locate the NSTextFields that implement the text display. These are
329 // actually available as the ivars |_messageField| and |_informationField|
330 // of the NSAlert, but it is safer (and more forward-compatible) to search
331 // for them in the subviews.
332 for (NSView* view in [[[GetAlert() window] contentView] subviews]) {
333 NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
334 if ([[text_field stringValue] isEqualTo:message_text])
335 message_text_field = text_field;
336 else if ([[text_field stringValue] isEqualTo:informative_text])
337 informative_text_field = text_field;
340 // This may fail in future OS releases, but it will still work for shipped
341 // versions of Chromium.
342 DCHECK(message_text_field);
343 DCHECK(informative_text_field);
346 if (message_has_rtl && message_text_field) {
347 base::scoped_nsobject<NSMutableParagraphStyle> alignment(
348 [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
349 [alignment setAlignment:NSRightTextAlignment];
351 NSDictionary* alignment_attributes =
352 @{ NSParagraphStyleAttributeName : alignment };
353 base::scoped_nsobject<NSAttributedString> attr_string(
354 [[NSAttributedString alloc] initWithString:message_text
355 attributes:alignment_attributes]);
357 [message_text_field setAttributedStringValue:attr_string];
358 [message_text_field setSelectable:NO];
361 if (informative_has_rtl && informative_text_field) {
362 base::i18n::TextDirection direction =
363 base::i18n::GetFirstStrongCharacterDirection(dialog_->message_text());
364 base::scoped_nsobject<NSMutableParagraphStyle> alignment(
365 [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
366 [alignment setAlignment:
367 (direction == base::i18n::RIGHT_TO_LEFT) ? NSRightTextAlignment
368 : NSLeftTextAlignment];
370 NSDictionary* alignment_attributes =
371 @{ NSParagraphStyleAttributeName : alignment };
372 base::scoped_nsobject<NSAttributedString> attr_string(
373 [[NSAttributedString alloc] initWithString:informative_text
374 attributes:alignment_attributes]);
376 [informative_text_field setAttributedStringValue:attr_string];
377 [informative_text_field setSelectable:NO];
381 JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {
382 [NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()];
385 ////////////////////////////////////////////////////////////////////////////////
386 // JavaScriptAppModalDialogCocoa, private:
388 NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
389 return [helper_ alert];
392 ////////////////////////////////////////////////////////////////////////////////
393 // JavaScriptAppModalDialogCocoa, NativeAppModalDialog implementation:
395 int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
396 // From the above, it is the case that if there is 1 button, it is always the
397 // OK button. The second button, if it exists, is always the Cancel button.
398 int num_buttons = [[GetAlert() buttons] count];
399 switch (num_buttons) {
401 return ui::DIALOG_BUTTON_OK;
403 return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
410 void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
413 // Dispatch the method to show the alert back to the top of the CFRunLoop.
414 // This fixes an interaction bug with NSSavePanel. http://crbug.com/375785
415 // When this object is destroyed, outstanding performSelector: requests
416 // should be cancelled.
417 [helper_.get() performSelector:@selector(showAlert)
422 void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
425 void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
426 [helper_ closeWindow];
429 void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
430 [helper_ acceptAlert];
433 void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
434 [helper_ cancelAlert];
437 bool JavaScriptAppModalDialogCocoa::IsShowing() const {
443 class ChromeJavaScriptNativeDialogCocoaFactory
444 : public app_modal::JavaScriptNativeDialogFactory {
446 ChromeJavaScriptNativeDialogCocoaFactory() {}
447 ~ChromeJavaScriptNativeDialogCocoaFactory() override {}
450 app_modal::NativeAppModalDialog* CreateNativeJavaScriptDialog(
451 app_modal::JavaScriptAppModalDialog* dialog) override {
452 app_modal::NativeAppModalDialog* d =
453 new JavaScriptAppModalDialogCocoa(dialog);
454 dialog->web_contents()->GetDelegate()->ActivateContents(
455 dialog->web_contents());
459 DISALLOW_COPY_AND_ASSIGN(ChromeJavaScriptNativeDialogCocoaFactory);
464 void InstallChromeJavaScriptNativeDialogFactory() {
465 app_modal::JavaScriptDialogManager::GetInstance()->
466 SetNativeDialogFactory(
467 make_scoped_ptr(new ChromeJavaScriptNativeDialogCocoaFactory));