Check USB device path access when prompting users to select a device.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / javascript_app_modal_dialog_cocoa.mm
blob73ee1c649fa21b755a47df3dff9bd7da90a3698f
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 // The presentation of the NSAlert is delayed, due to an AppKit bug. See
33 // JavaScriptAppModalDialogCocoa::ShowAppModalDialog for more details.  If the
34 // NSAlert has not yet been presented, then actions that affect the NSAlert
35 // should be delayed as well. Due to the destructive nature of these actions,
36 // at most one action should be queued.
37 enum AlertAction {
38   ACTION_NONE,
39   ACTION_CLOSE,
40   ACTION_ACCEPT,
41   ACTION_CANCEL
44 }  // namespace
46 // Helper object that receives the notification that the dialog/sheet is
47 // going away. Is responsible for cleaning itself up.
48 @interface JavaScriptAppModalDialogHelper : NSObject<NSAlertDelegate> {
49  @private
50   base::scoped_nsobject<NSAlert> alert_;
51   JavaScriptAppModalDialogCocoa* nativeDialog_;  // Weak.
52   base::scoped_nsobject<NSTextField> textField_;
53   BOOL alertShown_;
54   AlertAction queuedAction_;
57 // Creates an NSAlert if one does not already exist. Otherwise returns the
58 // existing NSAlert.
59 - (NSAlert*)alert;
60 - (void)addTextFieldWithPrompt:(NSString*)prompt;
61 - (void)alertDidEnd:(NSAlert*)alert
62          returnCode:(int)returnCode
63         contextInfo:(void*)contextInfo;
65 // If the alert has been presented, immediately play the action. Otherwise
66 // queue the action for replay immediately after the alert is presented.
67 - (void)playOrQueueAction:(AlertAction)action;
68 - (void)queueAction:(AlertAction)action;
70 // Presents an AppKit blocking dialog.
71 - (void)showAlert;
73 // Selects the first button of the alert, which should accept it.
74 - (void)acceptAlert;
76 // Selects the second button of the alert, which should cancel it.
77 - (void)cancelAlert;
79 // Closes the window, and the alert along with it.
80 - (void)closeWindow;
82 // Designated initializer.
83 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog;
85 @end
87 @implementation JavaScriptAppModalDialogHelper
89 - (instancetype)init {
90   NOTREACHED();
91   return nil;
94 - (instancetype)initWithNativeDialog:(JavaScriptAppModalDialogCocoa*)dialog {
95   DCHECK(dialog);
96   self = [super init];
97   if (self) {
98     nativeDialog_ = dialog;
99     queuedAction_ = ACTION_NONE;
100   }
101   return self;
104 - (NSAlert*)alert {
105   if (!alert_)
106     alert_.reset([[NSAlert alloc] init]);
107   return alert_;
110 - (void)addTextFieldWithPrompt:(NSString*)prompt {
111   DCHECK(!textField_);
112   textField_.reset(
113       [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 22)]);
114   [[textField_ cell] setLineBreakMode:NSLineBreakByTruncatingTail];
115   [[self alert] setAccessoryView:textField_];
117   [textField_ setStringValue:prompt];
120 // |contextInfo| is the JavaScriptAppModalDialogCocoa that owns us.
121 - (void)alertDidEnd:(NSAlert*)alert
122          returnCode:(int)returnCode
123         contextInfo:(void*)contextInfo {
124   DCHECK(nativeDialog_);
125   base::string16 input;
126   if (textField_)
127     input = base::SysNSStringToUTF16([textField_ stringValue]);
128   bool shouldSuppress = false;
129   if ([alert showsSuppressionButton])
130     shouldSuppress = [[alert suppressionButton] state] == NSOnState;
131   switch (returnCode) {
132     case NSAlertFirstButtonReturn:  {  // OK
133       nativeDialog_->dialog()->OnAccept(input, shouldSuppress);
134       break;
135     }
136     case NSAlertSecondButtonReturn:  {  // Cancel
137       // If the user wants to stay on this page, stop quitting (if a quit is in
138       // progress).
139       if (nativeDialog_->dialog()->is_before_unload_dialog())
140         chrome_browser_application_mac::CancelTerminate();
141       nativeDialog_->dialog()->OnCancel(shouldSuppress);
142       break;
143     }
144     case NSRunStoppedResponse: {  // Window was closed underneath us
145       // Need to call OnCancel() because there is some cleanup that needs
146       // to be done.  It won't call back to the javascript since the
147       // JavaScriptAppModalDialog knows that the WebContents was destroyed.
148       nativeDialog_->dialog()->OnCancel(shouldSuppress);
149       break;
150     }
151     default:  {
152       NOTREACHED();
153     }
154   }
156   delete nativeDialog_;  // Careful, this will delete us.
159 - (void)playOrQueueAction:(AlertAction)action {
160   if (alertShown_)
161     [self playAlertAction:action];
162   else
163     [self queueAction:action];
166 - (void)queueAction:(AlertAction)action {
167   DCHECK(!alertShown_);
168   DCHECK(queuedAction_ == ACTION_NONE);
170   queuedAction_ = action;
173 - (void)playAlertAction:(AlertAction)action {
174   switch (action) {
175     case ACTION_NONE:
176       break;
177     case ACTION_CLOSE:
178       [self closeWindow];
179       break;
180     case ACTION_CANCEL:
181       [self cancelAlert];
182       break;
183     case ACTION_ACCEPT:
184       [self acceptAlert];
185       break;
186   }
189 - (void)showAlert {
190   alertShown_ = YES;
191   NSAlert* alert = [self alert];
192   [alert beginSheetModalForWindow:nil  // nil here makes it app-modal
193                     modalDelegate:self
194                    didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
195                       contextInfo:NULL];
197   if ([alert accessoryView])
198     [[alert window] makeFirstResponder:[alert accessoryView]];
200   [self playAlertAction:queuedAction_];
203 - (void)acceptAlert {
204   NSButton* first = [[[self alert] buttons] objectAtIndex:0];
205   [first performClick:nil];
208 - (void)cancelAlert {
209   NSAlert* alert = [self alert];
210   DCHECK([[alert buttons] count] >= 2);
211   NSButton* second = [[alert buttons] objectAtIndex:1];
212   [second performClick:nil];
215 - (void)closeWindow {
216   DCHECK([self alert]);
218   [NSApp endSheet:[[self alert] window]];
221 @end
223 ////////////////////////////////////////////////////////////////////////////////
224 // JavaScriptAppModalDialogCocoa, public:
226 JavaScriptAppModalDialogCocoa::JavaScriptAppModalDialogCocoa(
227     app_modal::JavaScriptAppModalDialog* dialog)
228     : dialog_(dialog),
229       popup_helper_(new AppModalDialogHelper(dialog->web_contents())),
230       is_showing_(false) {
231   // Determine the names of the dialog buttons based on the flags. "Default"
232   // is the OK button. "Other" is the cancel button. We don't use the
233   // "Alternate" button in NSRunAlertPanel.
234   NSString* default_button = l10n_util::GetNSStringWithFixup(IDS_APP_OK);
235   NSString* other_button = l10n_util::GetNSStringWithFixup(IDS_APP_CANCEL);
236   bool text_field = false;
237   bool one_button = false;
238   switch (dialog_->javascript_message_type()) {
239     case content::JAVASCRIPT_MESSAGE_TYPE_ALERT:
240       one_button = true;
241       break;
242     case content::JAVASCRIPT_MESSAGE_TYPE_CONFIRM:
243       if (dialog_->is_before_unload_dialog()) {
244         if (dialog_->is_reload()) {
245           default_button = l10n_util::GetNSStringWithFixup(
246               IDS_BEFORERELOAD_MESSAGEBOX_OK_BUTTON_LABEL);
247           other_button = l10n_util::GetNSStringWithFixup(
248               IDS_BEFORERELOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
249         } else {
250           default_button = l10n_util::GetNSStringWithFixup(
251               IDS_BEFOREUNLOAD_MESSAGEBOX_OK_BUTTON_LABEL);
252           other_button = l10n_util::GetNSStringWithFixup(
253               IDS_BEFOREUNLOAD_MESSAGEBOX_CANCEL_BUTTON_LABEL);
254         }
255       }
256       break;
257     case content::JAVASCRIPT_MESSAGE_TYPE_PROMPT:
258       text_field = true;
259       break;
261     default:
262       NOTREACHED();
263   }
265   // Create a helper which will receive the sheet ended selector. It will
266   // delete itself when done.
267   helper_.reset(
268       [[JavaScriptAppModalDialogHelper alloc] initWithNativeDialog:this]);
270   // Show the modal dialog.
271   if (text_field) {
272     [helper_ addTextFieldWithPrompt:base::SysUTF16ToNSString(
273         dialog_->default_prompt_text())];
274   }
275   [GetAlert() setDelegate:helper_];
276   NSString* informative_text =
277       base::SysUTF16ToNSString(dialog_->message_text());
279   // Truncate long JS alerts - crbug.com/331219
280   NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet];
281   for (size_t index = 0, slots_count = 0; index < informative_text.length;
282       ++index) {
283     unichar current_char = [informative_text characterAtIndex:index];
284     if ([newline_char_set characterIsMember:current_char])
285       slots_count += kSlotsPerLine;
286     else
287       slots_count++;
288     if (slots_count > kMessageTextMaxSlots) {
289       base::string16 info_text = base::SysNSStringToUTF16(informative_text);
290       informative_text = base::SysUTF16ToNSString(
291           gfx::TruncateString(info_text, index, gfx::WORD_BREAK));
292       break;
293     }
294   }
296   [GetAlert() setInformativeText:informative_text];
297   NSString* message_text =
298       base::SysUTF16ToNSString(dialog_->title());
299   [GetAlert() setMessageText:message_text];
300   [GetAlert() addButtonWithTitle:default_button];
301   if (!one_button) {
302     NSButton* other = [GetAlert() addButtonWithTitle:other_button];
303     [other setKeyEquivalent:@"\e"];
304   }
305   if (dialog_->display_suppress_checkbox()) {
306     [GetAlert() setShowsSuppressionButton:YES];
307     NSString* suppression_title = l10n_util::GetNSStringWithFixup(
308         IDS_JAVASCRIPT_MESSAGEBOX_SUPPRESS_OPTION);
309     [[GetAlert() suppressionButton] setTitle:suppression_title];
310   }
312   // Fix RTL dialogs.
313   //
314   // Mac OS X will always display NSAlert strings as LTR. A workaround is to
315   // manually set the text as attributed strings in the implementing
316   // NSTextFields. This is a basic correctness issue.
317   //
318   // In addition, for readability, the overall alignment is set based on the
319   // directionality of the first strongly-directional character.
320   //
321   // If the dialog fields are selectable then they will scramble when clicked.
322   // Therefore, selectability is disabled.
323   //
324   // See http://crbug.com/70806 for more details.
326   bool message_has_rtl =
327       base::i18n::StringContainsStrongRTLChars(dialog_->title());
328   bool informative_has_rtl =
329       base::i18n::StringContainsStrongRTLChars(dialog_->message_text());
331   NSTextField* message_text_field = nil;
332   NSTextField* informative_text_field = nil;
333   if (message_has_rtl || informative_has_rtl) {
334     // Force layout of the dialog. NSAlert leaves its dialog alone once laid
335     // out; if this is not done then all the modifications that are to come will
336     // be un-done when the dialog is finally displayed.
337     [GetAlert() layout];
339     // Locate the NSTextFields that implement the text display. These are
340     // actually available as the ivars |_messageField| and |_informationField|
341     // of the NSAlert, but it is safer (and more forward-compatible) to search
342     // for them in the subviews.
343     for (NSView* view in [[[GetAlert() window] contentView] subviews]) {
344       NSTextField* text_field = base::mac::ObjCCast<NSTextField>(view);
345       if ([[text_field stringValue] isEqualTo:message_text])
346         message_text_field = text_field;
347       else if ([[text_field stringValue] isEqualTo:informative_text])
348         informative_text_field = text_field;
349     }
351     // This may fail in future OS releases, but it will still work for shipped
352     // versions of Chromium.
353     DCHECK(message_text_field);
354     DCHECK(informative_text_field);
355   }
357   if (message_has_rtl && message_text_field) {
358     base::scoped_nsobject<NSMutableParagraphStyle> alignment(
359         [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
360     [alignment setAlignment:NSRightTextAlignment];
362     NSDictionary* alignment_attributes =
363         @{ NSParagraphStyleAttributeName : alignment };
364     base::scoped_nsobject<NSAttributedString> attr_string(
365         [[NSAttributedString alloc] initWithString:message_text
366                                         attributes:alignment_attributes]);
368     [message_text_field setAttributedStringValue:attr_string];
369     [message_text_field setSelectable:NO];
370   }
372   if (informative_has_rtl && informative_text_field) {
373     base::i18n::TextDirection direction =
374         base::i18n::GetFirstStrongCharacterDirection(dialog_->message_text());
375     base::scoped_nsobject<NSMutableParagraphStyle> alignment(
376         [[NSParagraphStyle defaultParagraphStyle] mutableCopy]);
377     [alignment setAlignment:
378         (direction == base::i18n::RIGHT_TO_LEFT) ? NSRightTextAlignment
379                                                  : NSLeftTextAlignment];
381     NSDictionary* alignment_attributes =
382         @{ NSParagraphStyleAttributeName : alignment };
383     base::scoped_nsobject<NSAttributedString> attr_string(
384         [[NSAttributedString alloc] initWithString:informative_text
385                                         attributes:alignment_attributes]);
387     [informative_text_field setAttributedStringValue:attr_string];
388     [informative_text_field setSelectable:NO];
389   }
392 JavaScriptAppModalDialogCocoa::~JavaScriptAppModalDialogCocoa() {
393   [NSObject cancelPreviousPerformRequestsWithTarget:helper_.get()];
396 ////////////////////////////////////////////////////////////////////////////////
397 // JavaScriptAppModalDialogCocoa, private:
399 NSAlert* JavaScriptAppModalDialogCocoa::GetAlert() const {
400   return [helper_ alert];
403 ////////////////////////////////////////////////////////////////////////////////
404 // JavaScriptAppModalDialogCocoa, NativeAppModalDialog implementation:
406 int JavaScriptAppModalDialogCocoa::GetAppModalDialogButtons() const {
407   // From the above, it is the case that if there is 1 button, it is always the
408   // OK button.  The second button, if it exists, is always the Cancel button.
409   int num_buttons = [[GetAlert() buttons] count];
410   switch (num_buttons) {
411     case 1:
412       return ui::DIALOG_BUTTON_OK;
413     case 2:
414       return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
415     default:
416       NOTREACHED();
417       return 0;
418   }
421 void JavaScriptAppModalDialogCocoa::ShowAppModalDialog() {
422   is_showing_ = true;
424   // Dispatch the method to show the alert back to the top of the CFRunLoop.
425   // This fixes an interaction bug with NSSavePanel. http://crbug.com/375785
426   // When this object is destroyed, outstanding performSelector: requests
427   // should be cancelled.
428   [helper_.get() performSelector:@selector(showAlert)
429                       withObject:nil
430                       afterDelay:0];
433 void JavaScriptAppModalDialogCocoa::ActivateAppModalDialog() {
436 void JavaScriptAppModalDialogCocoa::CloseAppModalDialog() {
437   [helper_ playOrQueueAction:ACTION_CLOSE];
440 void JavaScriptAppModalDialogCocoa::AcceptAppModalDialog() {
441   [helper_ playOrQueueAction:ACTION_ACCEPT];
444 void JavaScriptAppModalDialogCocoa::CancelAppModalDialog() {
445   [helper_ playOrQueueAction:ACTION_CANCEL];
448 bool JavaScriptAppModalDialogCocoa::IsShowing() const {
449   return is_showing_;
452 namespace {
454 class ChromeJavaScriptNativeDialogCocoaFactory
455     : public app_modal::JavaScriptNativeDialogFactory {
456  public:
457   ChromeJavaScriptNativeDialogCocoaFactory() {}
458   ~ChromeJavaScriptNativeDialogCocoaFactory() override {}
460  private:
461   app_modal::NativeAppModalDialog* CreateNativeJavaScriptDialog(
462       app_modal::JavaScriptAppModalDialog* dialog) override {
463     app_modal::NativeAppModalDialog* d =
464         new JavaScriptAppModalDialogCocoa(dialog);
465     dialog->web_contents()->GetDelegate()->ActivateContents(
466         dialog->web_contents());
467     return d;
468   }
470   DISALLOW_COPY_AND_ASSIGN(ChromeJavaScriptNativeDialogCocoaFactory);
473 }  // namespace
475 void InstallChromeJavaScriptNativeDialogFactory() {
476   app_modal::JavaScriptDialogManager::GetInstance()->
477       SetNativeDialogFactory(
478           make_scoped_ptr(new ChromeJavaScriptNativeDialogCocoaFactory));