[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / javascript_app_modal_dialog_cocoa.mm
bloba60703b4d0c6a43b0d5d7f5e754056af3cdc40a6
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_dialogs/javascript_app_modal_dialog.h"
15 #include "grit/generated_resources.h"
16 #include "grit/ui_strings.h"
17 #include "ui/base/l10n/l10n_util_mac.h"
18 #include "ui/base/ui_base_types.h"
19 #include "ui/gfx/text_elider.h"
21 const int kSlotsPerLine = 50;
22 const int kMessageTextMaxSlots = 2000;
24 // Helper object that receives the notification that the dialog/sheet is
25 // going away. Is responsible for cleaning itself up.
26 @interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
27  @private
28   base::scoped_nsobject<NSAlert> alert_;
29   NSTextField* textField_;  // WEAK; owned by alert_
32 - (NSAlert*)alert;
33 - (NSTextField*)textField;
34 - (void)alertDidEnd:(NSAlert*)alert
35          returnCode:(int)returnCode
36         contextInfo:(void*)contextInfo;
38 @end
40 @implementation JavaScriptAppModalDialogHelper
42 - (NSAlert*)alert {
43   alert_.reset([[NSAlert alloc] init]);
44   return alert_;
47 - (NSTextField*)textField {
48   textField_ = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)];
49   [[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
50   [alert_ setAccessoryView:textField_];
51   [textField_ release];
53   return textField_;
56 // |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
57 - (void)alertDidEnd:(NSAlert*)alert
58          returnCode:(int)returnCode
59         contextInfo:(void*)contextInfo {
60   scoped_ptr<JavaScriptAppModalDialogCocoa> native_dialog(
61       reinterpret_cast<JavaScriptAppModalDialogCocoa*>(contextInfo));
62   base::string16 input;
63   if (textField_)
64     input = base::SysNSStringToUTF16([textField_ stringValue]);
65   bool shouldSuppress = false;
66   if ([alert showsSuppressionButton])
67     shouldSuppress = [[alert suppressionButton] state] == NSOnState;
68   switch (returnCode) {
69     case NSAlertFirstButtonReturn:  {  // OK
70       native_dialog->dialog()->OnAccept(input, shouldSuppress);
71       break;
72     }
73     case NSAlertSecondButtonReturn:  {  // Cancel
74       // If the user wants to stay on this page, stop quitting (if a quit is in
75       // progress).
76       if (native_dialog->dialog()->is_before_unload_dialog())
77         chrome_browser_application_mac::CancelTerminate();
78       native_dialog->dialog()->OnCancel(shouldSuppress);
79       break;
80     }
81     case NSRunStoppedResponse: {  // Window was closed underneath us
82       // Need to call OnCancel() because there is some cleanup that needs
83       // to be done.  It won't call back to the javascript since the
84       // JavaScriptAppModalDialog knows that the WebContents was destroyed.
85       native_dialog->dialog()->OnCancel(shouldSuppress);
86       break;
87     }
88     default:  {
89       NOTREACHED();
90     }
91   }
94 @end
96 ////////////////////////////////////////////////////////////////////////////////
97 // JavaScriptAppModalDialogCocoa, public:
99 JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
100     JavaScriptAppModalDialog* dialog)
101     : dialog_(dialog),
102       helper_(NULL) {
103   // Determine the names of the dialog buttons based on the flags. "Default"
104   // is the OK button. "Other" is the cancel button. We don't use the
105   // "Alternate" button in NSRunAlertPanel.
106   NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
107   NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
108   bool text_field = false;
109   bool one_button = false;
110   switch (dialog_->javascript_message_type()) {
111     case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
112       one_button = true;
113       break;
114     case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
115       if (dialog_->is_before_unload_dialog()) {
116         if (dialog_->is_reload()) {
117           default_button = l10n_util::GetNSStringWithFixup(
118               IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
119           other_button = l10n_util::GetNSStringWithFixup(
120               IDS_BEFORERELOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
121         } else {
122           default_button = l10n_util::GetNSStringWithFixup(
123               IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
124           other_button = l10n_util::GetNSStringWithFixup(
125               IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
126         }
127       }
128       break;
129     case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
130       text_field = true;
131       break;
133     default:
134       NOTREACHED();
135   }
137   // Create a helper which will receive the sheet ended selector. It will
138   // delete itself when done. It doesn't need anything passed to its init
139   // as it will get a contextInfo parameter.
140   helper_.reset([[JavaScriptAppModalDialogHelper alloc] init]);
142   // Show the modal dialog.
143   alert_ = [helper_ alert];
144   NSTextField* field = nil;
145   if (text_field) {
146     field = [helper_ textField];
147     [field setStringValue:base::SysUTF16ToNSString(
148         dialog_->default_prompt_text())];
149   }
150   [alert_ setDelegate:helper_];
151   NSString* informative_text =
152       base::SysUTF16ToNSString(dialog_->message_text());
154   // Truncate long JS alerts - crbug.com/331219
155   NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet];
156   for (size_t index = 0, slots_count = 0; index < informative_text.length;
157       ++index) {
158     unichar current_char = [informative_text characterAtIndex:index];
159     if ([newline_char_set characterIsMember:current_char])
160       slots_count += kSlotsPerLine;
161     else
162       slots_count++;
163     if (slots_count > kMessageTextMaxSlots) {
164       base::string16 info_text = base::SysNSStringToUTF16(informative_text);
165       informative_text = base::SysUTF16ToNSString(
166           gfx::TruncateString(info_text, index));
167       break;
168     }
169   }
171   [alert_ setInformativeText:informative_text];
172   NSString* message_text =
173       base::SysUTF16ToNSString(dialog_->title());
174   [alert_ setMessageText:message_text];
175   [alert_ addButtonWithTitle:default_button];
176   if (!one_button) {
177     NSButton* other = [alert_ addButtonWithTitle:other_button];
178     [other setKeyEquivalent:@"\e"];
179   }
180   if (dialog_->display_suppress_checkbox()) {
181     [alert_ setShowsSuppressionButton:YES];
182     NSString* suppression_title = l10n_util::GetNSStringWithFixup(
183         IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
184     [[alert_ suppressionButton] setTitle:suppression_title];
185   }
187   // Fix RTL dialogs.
188   //
189   // Mac OS X will always display NSAlert strings as LTR. A workaround is to
190   // manually set the text as attributed strings in the implementing
191   // NSTextFields. This is a basic correctness issue.
192   //
193   // In addition, for readability, the overall alignment is set based on the
194   // directionality of the first strongly-directional character.
195   //
196   // If the dialog fields are selectable then they will scramble when clicked.
197   // Therefore, selectability is disabled.
198   //
199   // See http://crbug.com/70806 for more details.
201   bool message_has_rtl =
202       base::i18n::StringContainsStrongRTLChars(dialog_->title());
203   bool informative_has_rtl =
204       base::i18n::StringContainsStrongRTLChars(dialog_->message_text());
206   NSTextField* message_text_field = nil;
207   NSTextField* informative_text_field = nil;
208   if (message_has_rtl || informative_has_rtl) {
209     // Force layout of the dialog. NSAlert leaves its dialog alone once laid
210     // out; if this is not done then all the modifications that are to come will
211     // be un-done when the dialog is finally displayed.
212     [alert_ layout];
214     // Locate the NSTextFields that implement the text display. These are
215     // actually available as the ivars |_messageField| and |_informationField|
216     // of the NSAlert, but it is safer (and more forward-compatible) to search
217     // for them in the subviews.
218     for (NSView* view in [[[alert_ window] contentView] subviews]) {
219       NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
220       if ([[text_field stringValue] isEqualTo:message_text])
221         message_text_field = text_field;
222       else if ([[text_field stringValue] isEqualTo:informative_text])
223         informative_text_field = text_field;
224     }
226     // This may fail in future OS releases, but it will still work for shipped
227     // versions of Chromium.
228     DCHECK(message_text_field);
229     DCHECK(informative_text_field);
230   }
232   if (message_has_rtl && message_text_field) {
233     base::scoped_nsobject<NSMutableParagraphStyle> alignment(
234         [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
235     [alignment setAlignment:NSRightTextAlignment];
237     NSDictionary* alignment_attributes =
238         @{ NSParagraphStyleAttributeName : alignment };
239     base::scoped_nsobject<NSAttributedString> attr_string(
240         [[NSAttributedString alloc] initWithString:message_text
241                                         attributes:alignment_attributes]);
243     [message_text_field setAttributedStringValue:attr_string];
244     [message_text_field setSelectable:NO];
245   }
247   if (informative_has_rtl && informative_text_field) {
248     base::i18n::TextDirection direction =
249         base::i18n::GetFirstStrongCharacterDirection(dialog_->message_text());
250     base::scoped_nsobject<NSMutableParagraphStyle> alignment(
251         [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
252     [alignment setAlignment:
253         (direction == base::i18n::RIGHT_TO_LEFT) ? NSRightTextAlignment
254                                                  : NSLeftTextAlignment];
256     NSDictionary* alignment_attributes =
257         @{ NSParagraphStyleAttributeName : alignment };
258     base::scoped_nsobject<NSAttributedString> attr_string(
259         [[NSAttributedString alloc] initWithString:informative_text
260                                         attributes:alignment_attributes]);
262     [informative_text_field setAttributedStringValue:attr_string];
263     [informative_text_field setSelectable:NO];
264   }
267 JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {
270 ////////////////////////////////////////////////////////////////////////////////
271 // JavaScriptAppModalDialogCocoa, NativeAppModalDialog implementation:
273 int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
274   // From the above, it is the case that if there is 1 button, it is always the
275   // OK button.  The second button, if it exists, is always the Cancel button.
276   int num_buttons = [[alert_ buttons] count];
277   switch (num_buttons) {
278     case 1:
279       return ui::DIALOG_BUTTON_OK;
280     case 2:
281       return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
282     default:
283       NOTREACHED();
284       return 0;
285   }
288 void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
289   [alert_
290       beginSheetModalForWindow:nil  // nil here makes it app-modal
291                  modalDelegate:helper_.get()
292                 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
293                    contextInfo:this];
295   if ([alert_ accessoryView])
296     [[alert_ window] makeFirstResponder:[alert_ accessoryView]];
299 void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
302 void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
303   DCHECK([alert_ isKindOfClass:[NSAlert class]]);
305   // Note: the call below will delete |this|,
306   // see JavaScriptAppModalDialogHelper's alertDidEnd.
307   [NSApp endSheet:[alert_ window]];
310 void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
311   NSButton* first = [[alert_ buttons] objectAtIndex:0];
312   [first performClick:nil];
315 void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
316   DCHECK([[alert_ buttons] count] >= 2);
317   NSButton* second = [[alert_ buttons] objectAtIndex:1];
318   [second performClick:nil];
321 ////////////////////////////////////////////////////////////////////////////////
322 // NativeAppModalDialog, public:
324 // static
325 NativeAppModalDialog* NativeAppModalDialog::CreateNativeJavaScriptPrompt(
326     JavaScriptAppModalDialog* dialog,
327     gfx::NativeWindow parent_window) {
328   return new JavaScriptAppModalDialogCocoa(dialog);