Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / javascript_app_modal_dialog_cocoa.mm
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.
4
5 #include "chrome/browser/ui/cocoa/javascript_app_modal_dialog_cocoa.h"
6
7 #import <Cocoa/Cocoa.h>
8
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_dialogs/javascript_app_modal_dialog.h"
15 #include "chrome/grit/generated_resources.h"
16 #include "ui/base/l10n/l10n_util_mac.h"
17 #include "ui/base/ui_base_types.h"
18 #include "ui/gfx/text_elider.h"
19 #include "ui/strings/grit/ui_strings.h"
20
21 namespace {
22
23 const int kSlotsPerLine = 50;
24 const int kMessageTextMaxSlots = 2000;
25
26 // The presentation of the NSAlert is delayed, due to an AppKit bug. See
27 // JavaScriptAppModalDialogCocoa::ShowAppModalDialog for more details.  If the
28 // NSAlert has not yet been presented, then actions that affect the NSAlert
29 // should be delayed as well. Due to the destructive nature of these actions,
30 // at most one action should be queued.
31 enum AlertAction {
32   ACTION_NONE,
33   ACTION_CLOSE,
34   ACTION_ACCEPT,
35   ACTION_CANCEL
36 };
37
38 }  // namespace
39
40 // Helper object that receives the notification that the dialog/sheet is
41 // going away. Is responsible for cleaning itself up.
42 @interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
43  @private
44   base::scoped_nsobject<NSAlert> alert_;
45   JavaScriptAppModalDialogCocoa* nativeDialog_;  // Weak.
46   base::scoped_nsobject<NSTextField> textField_;
47   BOOL alertShown_;
48   AlertAction queuedAction_;
49 }
50
51 // Creates an NSAlert if one does not already exist. Otherwise returns the
52 // existing NSAlert.
53 - (NSAlert*)alert;
54 - (void)addTextFieldWithPrompt:(NSString*)prompt;
55 - (void)alertDidEnd:(NSAlert*)alert
56          returnCode:(int)returnCode
57         contextInfo:(void*)contextInfo;
58
59 // If the alert has been presented, immediately play the action. Otherwise
60 // queue the action for replay immediately after the alert is presented.
61 - (void)playOrQueueAction:(AlertAction)action;
62 - (void)queueAction:(AlertAction)action;
63
64 // Presents an AppKit blocking dialog.
65 - (void)showAlert;
66
67 // Selects the first button of the alert, which should accept it.
68 - (void)acceptAlert;
69
70 // Selects the second button of the alert, which should cancel it.
71 - (void)cancelAlert;
72
73 // Closes the window, and the alert along with it.
74 - (void)closeWindow;
75
76 // Designated initializer.
77 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog;
78
79 @end
80
81 @implementation JavaScriptAppModalDialogHelper
82
83 - (instancetype)init {
84   NOTREACHED();
85   return nil;
86 }
87
88 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog {
89   DCHECK(dialog);
90   self = [super init];
91   if (self) {
92     nativeDialog_ = dialog;
93     queuedAction_ = ACTION_NONE;
94   }
95   return self;
96 }
97
98 - (NSAlert*)alert {
99   if (!alert_)
100     alert_.reset([[NSAlert alloc] init]);
101   return alert_;
102 }
103
104 - (void)addTextFieldWithPrompt:(NSString*)prompt {
105   DCHECK(!textField_);
106   textField_.reset(
107       [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)]);
108   [[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
109   [[self alert] setAccessoryView:textField_];
110
111   [textField_ setStringValue:prompt];
112 }
113
114 // |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
115 - (void)alertDidEnd:(NSAlert*)alert
116          returnCode:(int)returnCode
117         contextInfo:(void*)contextInfo {
118   DCHECK(nativeDialog_);
119   base::string16 input;
120   if (textField_)
121     input = base::SysNSStringToUTF16([textField_ stringValue]);
122   bool shouldSuppress = false;
123   if ([alert showsSuppressionButton])
124     shouldSuppress = [[alert suppressionButton] state] == NSOnState;
125   switch (returnCode) {
126     case NSAlertFirstButtonReturn:  {  // OK
127       nativeDialog_->dialog()->OnAccept(input, shouldSuppress);
128       break;
129     }
130     case NSAlertSecondButtonReturn:  {  // Cancel
131       // If the user wants to stay on this page, stop quitting (if a quit is in
132       // progress).
133       if (nativeDialog_->dialog()->is_before_unload_dialog())
134         chrome_browser_application_mac::CancelTerminate();
135       nativeDialog_->dialog()->OnCancel(shouldSuppress);
136       break;
137     }
138     case NSRunStoppedResponse: {  // Window was closed underneath us
139       // Need to call OnCancel() because there is some cleanup that needs
140       // to be done.  It won't call back to the javascript since the
141       // JavaScriptAppModalDialog knows that the WebContents was destroyed.
142       nativeDialog_->dialog()->OnCancel(shouldSuppress);
143       break;
144     }
145     default:  {
146       NOTREACHED();
147     }
148   }
149 }
150
151 - (void)playOrQueueAction:(AlertAction)action {
152   if (alertShown_)
153     [self playAlertAction:action];
154   else
155     [self queueAction:action];
156 }
157
158 - (void)queueAction:(AlertAction)action {
159   DCHECK(!alertShown_);
160   DCHECK(queuedAction_ == ACTION_NONE);
161
162   queuedAction_ = action;
163 }
164
165 - (void)playAlertAction:(AlertAction)action {
166   switch (action) {
167     case ACTION_NONE:
168       break;
169     case ACTION_CLOSE:
170       [self closeWindow];
171       break;
172     case ACTION_CANCEL:
173       [self cancelAlert];
174       break;
175     case ACTION_ACCEPT:
176       [self acceptAlert];
177       break;
178   }
179 }
180
181 - (void)showAlert {
182   alertShown_ = YES;
183   NSAlert* alert = [self alert];
184   [alert beginSheetModalForWindow:nil  // nil here makes it app-modal
185                     modalDelegate:self
186                    didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
187                       contextInfo:NULL];
188
189   if ([alert accessoryView])
190     [[alert window] makeFirstResponder:[alert accessoryView]];
191
192   [self playAlertAction:queuedAction_];
193 }
194
195 - (void)acceptAlert {
196   NSButton* first = [[[self alert] buttons] objectAtIndex:0];
197   [first performClick:nil];
198 }
199
200 - (void)cancelAlert {
201   NSAlert* alert = [self alert];
202   DCHECK([[alert buttons] count] >= 2);
203   NSButton* second = [[alert buttons] objectAtIndex:1];
204   [second performClick:nil];
205 }
206
207 - (void)closeWindow {
208   DCHECK([self alert]);
209
210   [NSApp endSheet:[[self alert] window]];
211 }
212
213 @end
214
215 ////////////////////////////////////////////////////////////////////////////////
216 // JavaScriptAppModalDialogCocoa, public:
217
218 JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
219     JavaScriptAppModalDialog* dialog)
220     : dialog_(dialog),
221       helper_(NULL) {
222   // Determine the names of the dialog buttons based on the flags. "Default"
223   // is the OK button. "Other" is the cancel button. We don't use the
224   // "Alternate" button in NSRunAlertPanel.
225   NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
226   NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
227   bool text_field = false;
228   bool one_button = false;
229   switch (dialog_->javascript_message_type()) {
230     case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
231       one_button = true;
232       break;
233     case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
234       if (dialog_->is_before_unload_dialog()) {
235         if (dialog_->is_reload()) {
236           default_button = l10n_util::GetNSStringWithFixup(
237               IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
238           other_button = l10n_util::GetNSStringWithFixup(
239               IDS_BEFORERELOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
240         } else {
241           default_button = l10n_util::GetNSStringWithFixup(
242               IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
243           other_button = l10n_util::GetNSStringWithFixup(
244               IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
245         }
246       }
247       break;
248     case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
249       text_field = true;
250       break;
251
252     default:
253       NOTREACHED();
254   }
255
256   // Create a helper which will receive the sheet ended selector. It will
257   // delete itself when done.
258   helper_.reset(
259       [[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]);
260
261   // Show the modal dialog.
262   if (text_field) {
263     [helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString(
264         dialog_->default_prompt_text())];
265   }
266   [GetAlert() setDelegate:helper_];
267   NSString* informative_text =
268       base::SysUTF16ToNSString(dialog_->message_text());
269
270   // Truncate long JS alerts - crbug.com/331219
271   NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet];
272   for (size_t index = 0, slots_count = 0; index < informative_text.length;
273       ++index) {
274     unichar current_char = [informative_text characterAtIndex:index];
275     if ([newline_char_set characterIsMember:current_char])
276       slots_count += kSlotsPerLine;
277     else
278       slots_count++;
279     if (slots_count > kMessageTextMaxSlots) {
280       base::string16 info_text = base::SysNSStringToUTF16(informative_text);
281       informative_text = base::SysUTF16ToNSString(
282           gfx::TruncateString(info_text, index, gfx::WORD_BREAK));
283       break;
284     }
285   }
286
287   [GetAlert() setInformativeText:informative_text];
288   NSString* message_text =
289       base::SysUTF16ToNSString(dialog_->title());
290   [GetAlert() setMessageText:message_text];
291   [GetAlert() addButtonWithTitle:default_button];
292   if (!one_button) {
293     NSButton* other = [GetAlert() addButtonWithTitle:other_button];
294     [other setKeyEquivalent:@"\e"];
295   }
296   if (dialog_->display_suppress_checkbox()) {
297     [GetAlert() setShowsSuppressionButton:YES];
298     NSString* suppression_title = l10n_util::GetNSStringWithFixup(
299         IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
300     [[GetAlert() suppressionButton] setTitle:suppression_title];
301   }
302
303   // Fix RTL dialogs.
304   //
305   // Mac OS X will always display NSAlert strings as LTR. A workaround is to
306   // manually set the text as attributed strings in the implementing
307   // NSTextFields. This is a basic correctness issue.
308   //
309   // In addition, for readability, the overall alignment is set based on the
310   // directionality of the first strongly-directional character.
311   //
312   // If the dialog fields are selectable then they will scramble when clicked.
313   // Therefore, selectability is disabled.
314   //
315   // See http://crbug.com/70806 for more details.
316
317   bool message_has_rtl =
318       base::i18n::StringContainsStrongRTLChars(dialog_->title());
319   bool informative_has_rtl =
320       base::i18n::StringContainsStrongRTLChars(dialog_->message_text());
321
322   NSTextField* message_text_field = nil;
323   NSTextField* informative_text_field = nil;
324   if (message_has_rtl || informative_has_rtl) {
325     // Force layout of the dialog. NSAlert leaves its dialog alone once laid
326     // out; if this is not done then all the modifications that are to come will
327     // be un-done when the dialog is finally displayed.
328     [GetAlert() layout];
329
330     // Locate the NSTextFields that implement the text display. These are
331     // actually available as the ivars |_messageField| and |_informationField|
332     // of the NSAlert, but it is safer (and more forward-compatible) to search
333     // for them in the subviews.
334     for (NSView* view in [[[GetAlert() window] contentView] subviews]) {
335       NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
336       if ([[text_field stringValue] isEqualTo:message_text])
337         message_text_field = text_field;
338       else if ([[text_field stringValue] isEqualTo:informative_text])
339         informative_text_field = text_field;
340     }
341
342     // This may fail in future OS releases, but it will still work for shipped
343     // versions of Chromium.
344     DCHECK(message_text_field);
345     DCHECK(informative_text_field);
346   }
347
348   if (message_has_rtl && message_text_field) {
349     base::scoped_nsobject<NSMutableParagraphStyle> alignment(
350         [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
351     [alignment setAlignment:NSRightTextAlignment];
352
353     NSDictionary* alignment_attributes =
354         @{ NSParagraphStyleAttributeName : alignment };
355     base::scoped_nsobject<NSAttributedString> attr_string(
356         [[NSAttributedString alloc] initWithString:message_text
357                                         attributes:alignment_attributes]);
358
359     [message_text_field setAttributedStringValue:attr_string];
360     [message_text_field setSelectable:NO];
361   }
362
363   if (informative_has_rtl && informative_text_field) {
364     base::i18n::TextDirection direction =
365         base::i18n::GetFirstStrongCharacterDirection(dialog_->message_text());
366     base::scoped_nsobject<NSMutableParagraphStyle> alignment(
367         [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
368     [alignment setAlignment:
369         (direction == base::i18n::RIGHT_TO_LEFT) ? NSRightTextAlignment
370                                                  : NSLeftTextAlignment];
371
372     NSDictionary* alignment_attributes =
373         @{ NSParagraphStyleAttributeName : alignment };
374     base::scoped_nsobject<NSAttributedString> attr_string(
375         [[NSAttributedString alloc] initWithString:informative_text
376                                         attributes:alignment_attributes]);
377
378     [informative_text_field setAttributedStringValue:attr_string];
379     [informative_text_field setSelectable:NO];
380   }
381 }
382
383 JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {
384   [NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()];
385 }
386
387 ////////////////////////////////////////////////////////////////////////////////
388 // JavaScriptAppModalDialogCocoa, private:
389
390 NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
391   return [helper_ alert];
392 }
393
394 ////////////////////////////////////////////////////////////////////////////////
395 // JavaScriptAppModalDialogCocoa, NativeAppModalDialog implementation:
396
397 int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
398   // From the above, it is the case that if there is 1 button, it is always the
399   // OK button.  The second button, if it exists, is always the Cancel button.
400   int num_buttons = [[GetAlert() buttons] count];
401   switch (num_buttons) {
402     case 1:
403       return ui::DIALOG_BUTTON_OK;
404     case 2:
405       return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
406     default:
407       NOTREACHED();
408       return 0;
409   }
410 }
411
412 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)
418                       withObject:nil
419                       afterDelay:0];
420 }
421
422 void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
423 }
424
425 void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
426   [helper_ playOrQueueAction:ACTION_CLOSE];
427 }
428
429 void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
430   [helper_ playOrQueueAction:ACTION_ACCEPT];
431 }
432
433 void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
434   [helper_ playOrQueueAction:ACTION_CANCEL];
435 }
436
437 ////////////////////////////////////////////////////////////////////////////////
438 // NativeAppModalDialog, public:
439
440 // static
441 NativeAppModalDialog* NativeAppModalDialog::CreateNativeJavaScriptPrompt(
442     JavaScriptAppModalDialog* dialog,
443     gfx::NativeWindow parent_window) {
444   return new JavaScriptAppModalDialogCocoa(dialog);
445 }