BookmarkManager: Fix 'new folder text field size changes on clicking it' issue.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / javascript_app_modal_dialog_cocoa.mm
blob1854d2e0836db20bd66d9241c8cedf163a1854a3
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"
27 namespace {
29 const int kSlotsPerLine = 50;
30 const int kMessageTextMaxSlots = 2000;
32 }  // namespace
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> {
37  @private
38   base::scoped_nsobject<NSAlert> alert_;
39   JavaScriptAppModalDialogCocoa* nativeDialog_;  // Weak.
40   base::scoped_nsobject<NSTextField> textField_;
41   BOOL alertShown_;
44 // Creates an NSAlert if one does not already exist. Otherwise returns the
45 // existing NSAlert.
46 - (NSAlert*)alert;
47 - (void)addTextFieldWithPrompt:(NSString*)prompt;
49 // Presents an AppKit blocking dialog.
50 - (void)showAlert;
52 // Selects the first button of the alert, which should accept it.
53 - (void)acceptAlert;
55 // Selects the second button of the alert, which should cancel it.
56 - (void)cancelAlert;
58 // Closes the window, and the alert along with it.
59 - (void)closeWindow;
61 // Designated initializer.
62 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog;
64 @end
66 @implementation JavaScriptAppModalDialogHelper
68 - (instancetype)init {
69   NOTREACHED();
70   return nil;
73 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog {
74   DCHECK(dialog);
75   self = [super init];
76   if (self)
77     nativeDialog_ = dialog;
78   return self;
81 - (NSAlert*)alert {
82   if (!alert_)
83     alert_.reset([[NSAlert alloc] init]);
84   return alert_;
87 - (void)addTextFieldWithPrompt:(NSString*)prompt {
88   DCHECK(!textField_);
89   textField_.reset(
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];
104       break;
105     }
106     case NSAlertSecondButtonReturn:  {  // Cancel
107       // If the user wants to stay on this page, stop quitting (if a quit is in
108       // progress).
109       [self sendCancelToNativeDialog];
110       break;
111     }
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];
117       break;
118     }
119     default:  {
120       NOTREACHED();
121     }
122   }
125 - (void)showAlert {
126   DCHECK(nativeDialog_);
127   DCHECK(!alertShown_);
128   alertShown_ = YES;
129   NSAlert* alert = [self alert];
130   [alert beginSheetModalForWindow:nil  // nil here makes it app-modal
131                     modalDelegate:self
132                    didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
133                       contextInfo:NULL];
135   if ([alert accessoryView])
136     [[alert window] makeFirstResponder:[alert accessoryView]];
139 - (void)acceptAlert {
140   DCHECK(nativeDialog_);
141   if (!alertShown_) {
142     [self sendAcceptToNativeDialog];
143     return;
144   }
145   NSButton* first = [[[self alert] buttons] objectAtIndex:0];
146   [first performClick:nil];
149 - (void)cancelAlert {
150   DCHECK(nativeDialog_);
151   if (!alertShown_) {
152     [self sendCancelToNativeDialog];
153     return;
154   }
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_);
162   if (!alertShown_) {
163     [self sendCloseToNativeDialog];
164     return;
165   }
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
178   // progress).
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 {
199   if (textField_)
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;
207   return false;
210 @end
212 ////////////////////////////////////////////////////////////////////////////////
213 // JavaScriptAppModalDialogCocoa, public:
215 JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
216     app_modal::JavaScriptAppModalDialog* dialog)
217     : dialog_(dialog),
218       popup_helper_(new AppModalDialogHelper(dialog->web_contents())),
219       is_showing_(false) {
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:
229       one_button = true;
230       break;
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);
238         } else {
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);
243         }
244       }
245       break;
246     case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
247       text_field = true;
248       break;
250     default:
251       NOTREACHED();
252   }
254   // Create a helper which will receive the sheet ended selector. It will
255   // delete itself when done.
256   helper_.reset(
257       [[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]);
259   // Show the modal dialog.
260   if (text_field) {
261     [helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString(
262         dialog_->default_prompt_text())];
263   }
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;
271       ++index) {
272     unichar current_char = [informative_text characterAtIndex:index];
273     if ([newline_char_set characterIsMember:current_char])
274       slots_count += kSlotsPerLine;
275     else
276       slots_count++;
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));
281       break;
282     }
283   }
285   [GetAlert() setInformativeText:informative_text];
286   NSString* message_text =
287       base::SysUTF16ToNSString(dialog_->title());
288   [GetAlert() setMessageText:message_text];
289   [GetAlert() addButtonWithTitle:default_button];
290   if (!one_button) {
291     NSButton* other = [GetAlert() addButtonWithTitle:other_button];
292     [other setKeyEquivalent:@"\e"];
293   }
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];
299   }
301   // Fix RTL dialogs.
302   //
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.
306   //
307   // In addition, for readability, the overall alignment is set based on the
308   // directionality of the first strongly-directional character.
309   //
310   // If the dialog fields are selectable then they will scramble when clicked.
311   // Therefore, selectability is disabled.
312   //
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.
326     [GetAlert() layout];
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;
338     }
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);
344   }
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];
359   }
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];
378   }
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) {
400     case 1:
401       return ui::DIALOG_BUTTON_OK;
402     case 2:
403       return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
404     default:
405       NOTREACHED();
406       return 0;
407   }
410 void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
411   is_showing_ = true;
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];
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 {
438   return is_showing_;
441 namespace {
443 class ChromeJavaScriptNativeDialogCocoaFactory
444     : public app_modal::JavaScriptNativeDialogFactory {
445  public:
446   ChromeJavaScriptNativeDialogCocoaFactory() {}
447   ~ChromeJavaScriptNativeDialogCocoaFactory() override {}
449  private:
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());
456     return d;
457   }
459   DISALLOW_COPY_AND_ASSIGN(ChromeJavaScriptNativeDialogCocoaFactory);
462 }  // namespace
464 void InstallChromeJavaScriptNativeDialogFactory() {
465   app_modal::JavaScriptDialogManager::GetInstance()->
466       SetNativeDialogFactory(
467           make_scoped_ptr(new ChromeJavaScriptNativeDialogCocoaFactory));