- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / profile_signin_confirmation_view_controller.mm
1 // Copyright 2013 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 #import "chrome/browser/ui/cocoa/profile_signin_confirmation_view_controller.h"
6
7 #include <algorithm>
8 #include <cmath>
9
10 #include "base/callback_helpers.h"
11 #include "base/mac/bundle_locations.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/browser/ui/browser_finder.h"
15 #include "chrome/browser/ui/browser_navigator.h"
16 #import "chrome/browser/ui/chrome_style.h"
17 #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_control_utils.h"
18 #import "chrome/browser/ui/cocoa/hover_close_button.h"
19 #import "chrome/browser/ui/cocoa/hyperlink_text_view.h"
20 #include "chrome/browser/ui/host_desktop.h"
21 #include "chrome/browser/ui/sync/profile_signin_confirmation_helper.h"
22 #include "chrome/common/url_constants.h"
23 #include "google_apis/gaia/gaia_auth_util.h"
24 #include "grit/chromium_strings.h"
25 #include "grit/generated_resources.h"
26 #include "skia/ext/skia_utils_mac.h"
27 #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h"
28 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
29 #include "ui/base/l10n/l10n_util.h"
30
31 namespace {
32
33 const CGFloat kWindowMinWidth = 500;
34 const CGFloat kButtonGap = 6;
35 const CGFloat kDialogAlertBarBorderWidth = 1;
36
37 // Shift the origin of |view|'s frame by the given amount in the
38 // positive y direction (up).
39 void ShiftOriginY(NSView* view, CGFloat amount) {
40   NSPoint origin = [view frame].origin;
41   origin.y += amount;
42   [view setFrameOrigin:origin];
43 }
44
45 // Determine the frame required to fit the content of a string.  Uses the
46 // provided height and width as preferred dimensions, where a value of
47 // 0.0 indicates no preference.
48 NSRect ComputeFrame(NSAttributedString* text, CGFloat width, CGFloat height) {
49   NSRect frame =
50       [text boundingRectWithSize:NSMakeSize(width, height)
51                          options:NSStringDrawingUsesLineFragmentOrigin];
52   // boundingRectWithSize is known to underestimate the width.
53   static const CGFloat kTextViewPadding = 10;
54   frame.size.width += kTextViewPadding;
55   return frame;
56 }
57
58 // Make the indicated range of characters in a text view bold.
59 void MakeTextBold(NSTextField* textField, int offset, int length) {
60   base::scoped_nsobject<NSMutableAttributedString> text(
61       [[textField attributedStringValue] mutableCopy]);
62   NSFont* currentFont =
63       [text attribute:NSFontAttributeName
64               atIndex:offset
65                effectiveRange:NULL];
66   NSFontManager* fontManager = [NSFontManager sharedFontManager];
67   NSFont* boldFont = [fontManager convertFont:currentFont
68                                   toHaveTrait:NSBoldFontMask];
69   [text beginEditing];
70   [text addAttribute:NSFontAttributeName
71                value:boldFont
72                range:NSMakeRange(offset, length)];
73   [text endEditing];
74   [textField setAttributedStringValue:text];
75 }
76
77 // Remove underlining from the specified range of characters in a text view.
78 void RemoveUnderlining(NSTextView* textView, int offset, int length) {
79   // Clear the default link attributes that were set by the
80   // HyperlinkTextView, otherwise removing the underline doesn't matter.
81   [textView setLinkTextAttributes:nil];
82   NSTextStorage* text = [textView textStorage];
83   NSRange range = NSMakeRange(offset, length);
84   [text addAttribute:NSUnderlineStyleAttributeName
85                value:[NSNumber numberWithInt:NSUnderlineStyleNone]
86                range:range];
87 }
88
89 // Create a new NSTextView and add it to the specified parent.
90 NSTextView* AddTextView(
91     NSView* parent,
92     id<NSTextViewDelegate> delegate,
93     const string16& message,
94     const string16& link,
95     int offset,
96     const ui::ResourceBundle::FontStyle& font_style) {
97   base::scoped_nsobject<HyperlinkTextView> textView(
98       [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]);
99   NSFont* font = ui::ResourceBundle::GetSharedInstance().GetFont(
100       font_style).GetNativeFont();
101   NSColor* linkColor = gfx::SkColorToCalibratedNSColor(
102       chrome_style::GetLinkColor());
103   [textView setMessageAndLink:base::SysUTF16ToNSString(message)
104                      withLink:base::SysUTF16ToNSString(link)
105                      atOffset:offset
106                          font:font
107                  messageColor:[NSColor blackColor]
108                     linkColor:linkColor];
109   RemoveUnderlining(textView, offset, link.size());
110   [textView setDelegate:delegate];
111   [parent addSubview:textView];
112   return textView.autorelease();
113 }
114
115 // Create a new NSTextField and add it to the specified parent.
116 NSTextField* AddTextField(
117     NSView* parent,
118     const string16& message,
119     const ui::ResourceBundle::FontStyle& font_style) {
120   NSTextField* textField = constrained_window::CreateLabel();
121   [textField setAttributedStringValue:
122       constrained_window::GetAttributedLabelString(
123           SysUTF16ToNSString(message),
124           font_style,
125           NSNaturalTextAlignment,
126           NSLineBreakByWordWrapping)];
127   [parent addSubview:textField];
128   return textField;
129 }
130
131 // Create a new link button and add it to the specified parent.
132  NSButton* AddLinkButton(
133      NSView* parent,
134      const string16& message,
135      id target,
136      SEL selector) {
137    NSButton* button =
138        [HyperlinkButtonCell buttonWithString:SysUTF16ToNSString(message)];
139    [button setTarget:target];
140    [button setAction:selector];
141    HyperlinkButtonCell* cell = [button cell];
142    cell.textColor =
143        gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor());
144    cell.shouldUnderline = NO;
145    [parent addSubview:button];
146    return button;
147  }
148
149 }  // namespace
150
151 @interface ProfileSigninConfirmationViewController ()
152 - (void)learnMore;
153 - (void)addButton:(NSButton*)button
154         withTitle:(int)resourceID
155            target:(id)target
156            action:(SEL)action
157    shouldAutoSize:(BOOL)shouldAutoSize;
158 @end
159
160 @implementation ProfileSigninConfirmationViewController
161
162 - (id)initWithBrowser:(Browser*)browser
163              username:(const std::string&)username
164              delegate:(ui::ProfileSigninConfirmationDelegate*)delegate
165   closeDialogCallback:(const base::Closure&)closeDialogCallback
166  offerProfileCreation:(bool)offer {
167   if ((self = [super initWithNibName:nil bundle:nil])) {
168     browser_ = browser;
169     username_ = username;
170     delegate_ = delegate;
171     closeDialogCallback_ = closeDialogCallback;
172     offerProfileCreation_ = offer;
173   }
174   return self;
175 }
176
177 - (void)loadView {
178   self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
179   cancelButton_.reset(
180       [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
181   okButton_.reset(
182       [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
183   if (offerProfileCreation_) {
184     createProfileButton_.reset(
185         [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
186   }
187   promptBox_.reset(
188       [[NSBox alloc] initWithFrame:NSZeroRect]);
189   closeButton_.reset(
190       [[WebUIHoverCloseButton alloc] initWithFrame:NSZeroRect]);
191
192   // -------------------------------
193   // | Title                     x |
194   // |-----------------------------| (1 px border)
195   // | Prompt    (box)             |
196   // |-----------------------------| (1 px border)
197   // | Explanation                 |
198   // |                             |
199   // | [create]      [cancel] [ok] |
200   // -------------------------------
201
202   // The width of the dialog should be sufficient to fit the buttons on
203   // one line and the title and the close button on one line, but not
204   // smaller than kWindowMinWidth.  Therefore we first layout the title
205   // and the buttons and then compute the necessary width.
206
207   // OK button.
208   [self addButton:okButton_
209         withTitle:IDS_ENTERPRISE_SIGNIN_CONTINUE_NEW_STYLE
210            target:self
211            action:@selector(ok:)
212    shouldAutoSize:YES];
213
214   // Cancel button.
215   [self addButton:cancelButton_
216         withTitle:IDS_ENTERPRISE_SIGNIN_CANCEL
217            target:self
218            action:@selector(cancel:)
219    shouldAutoSize:YES];
220
221   // Add the close button.
222   [self addButton:closeButton_
223         withTitle:0
224            target:self
225            action:@selector(close:)
226    shouldAutoSize:NO];
227   NSRect closeButtonFrame = [closeButton_ frame];
228   closeButtonFrame.size.width = chrome_style::GetCloseButtonSize();
229   closeButtonFrame.size.height = chrome_style::GetCloseButtonSize();
230   [closeButton_ setFrame:closeButtonFrame];
231
232   // Create Profile link.
233   if (offerProfileCreation_) {
234     [self addButton:createProfileButton_
235           withTitle:IDS_ENTERPRISE_SIGNIN_CREATE_NEW_PROFILE_NEW_STYLE
236              target:self
237              action:@selector(createProfile:)
238      shouldAutoSize:YES];
239   }
240
241   // Add the title label.
242   titleField_.reset(
243       [AddTextField([self view],
244                     l10n_util::GetStringUTF16(
245                         IDS_ENTERPRISE_SIGNIN_TITLE_NEW_STYLE),
246                     chrome_style::kTitleFontStyle) retain]);
247   [titleField_ setFrame:ComputeFrame(
248       [titleField_ attributedStringValue], 0.0, 0.0)];
249
250   // Compute the dialog width using the title and buttons.
251   const CGFloat buttonsWidth =
252       (offerProfileCreation_ ? NSWidth([createProfileButton_ frame]) : 0) +
253       kButtonGap + NSWidth([cancelButton_ frame]) +
254       kButtonGap + NSWidth([okButton_ frame]);
255   const CGFloat titleWidth =
256       NSWidth([titleField_ frame]) + NSWidth([closeButton_ frame]);
257   // Dialog minimum width must include the padding.
258   const CGFloat minWidth =
259       kWindowMinWidth - 2 * chrome_style::kHorizontalPadding;
260   const CGFloat width = std::max(minWidth,
261                                  std::max(buttonsWidth, titleWidth));
262   const CGFloat dialogWidth = width + 2 * chrome_style::kHorizontalPadding;
263
264   // Now setup the prompt and explanation text using the computed width.
265
266   // Prompt box.
267   [promptBox_ setBorderColor:gfx::SkColorToCalibratedNSColor(
268       ui::GetSigninConfirmationPromptBarColor(
269           ui::kSigninConfirmationPromptBarBorderAlpha))];
270   [promptBox_ setBorderWidth:kDialogAlertBarBorderWidth];
271   [promptBox_ setFillColor:gfx::SkColorToCalibratedNSColor(
272       ui::GetSigninConfirmationPromptBarColor(
273           ui::kSigninConfirmationPromptBarBackgroundAlpha))];
274   [promptBox_ setBoxType:NSBoxCustom];
275   [promptBox_ setTitlePosition:NSNoTitle];
276   [[self view] addSubview:promptBox_];
277
278   // Prompt text.
279   size_t offset;
280   const string16 domain = ASCIIToUTF16(gaia::ExtractDomainName(username_));
281   const string16 username = ASCIIToUTF16(username_);
282   const string16 prompt_text =
283       l10n_util::GetStringFUTF16(
284           IDS_ENTERPRISE_SIGNIN_ALERT_NEW_STYLE,
285           domain, &offset);
286   promptField_.reset(
287       [AddTextField(promptBox_, prompt_text, chrome_style::kTextFontStyle)
288           retain]);
289   MakeTextBold(promptField_, offset, domain.size());
290   [promptField_ setFrame:ComputeFrame(
291         [promptField_ attributedStringValue], width, 0.0)];
292
293   // Set the height of the prompt box from the prompt text, padding, and border.
294   CGFloat boxHeight =
295       kDialogAlertBarBorderWidth +
296       chrome_style::kRowPadding +
297       NSHeight([promptField_ frame]) +
298       chrome_style::kRowPadding +
299       kDialogAlertBarBorderWidth;
300   [promptBox_ setFrame:NSMakeRect(0, 0, dialogWidth, boxHeight)];
301
302   // Explanation text.
303   std::vector<size_t> offsets;
304   const string16 learn_more_text =
305       l10n_util::GetStringUTF16(
306           IDS_ENTERPRISE_SIGNIN_PROFILE_LINK_LEARN_MORE);
307   const string16 explanation_text =
308       l10n_util::GetStringFUTF16(
309           offerProfileCreation_ ?
310           IDS_ENTERPRISE_SIGNIN_EXPLANATION_WITH_PROFILE_CREATION_NEW_STYLE :
311           IDS_ENTERPRISE_SIGNIN_EXPLANATION_WITHOUT_PROFILE_CREATION_NEW_STYLE,
312           username, learn_more_text, &offsets);
313   // HyperlinkTextView requires manually inserting the link text
314   // into the middle of the message text.  To do this we slice out
315   // the "learn more" string from the message so that it can be
316   // inserted again.
317   const string16 explanation_message_text =
318     explanation_text.substr(0, offsets[1]) +
319     explanation_text.substr(offsets[1] + learn_more_text.size());
320   explanationField_.reset(
321       [AddTextView([self view], self, explanation_message_text, learn_more_text,
322                    offsets[1], chrome_style::kTextFontStyle) retain]);
323
324   [explanationField_ setFrame:ComputeFrame(
325         [explanationField_ attributedString], width, 0.0)];
326
327   // Layout the elements, starting at the bottom and moving up.
328
329   CGFloat curX = dialogWidth - chrome_style::kHorizontalPadding;
330   CGFloat curY = chrome_style::kClientBottomPadding;
331
332   // Buttons should go |Cancel|Continue|CreateProfile|, unless
333   // |CreateProfile| isn't shown.
334   if (offerProfileCreation_) {
335     curX -= NSWidth([createProfileButton_ frame]);
336     [createProfileButton_ setFrameOrigin:NSMakePoint(curX, curY)];
337     curX -= kButtonGap;
338   }
339   curX -= NSWidth([okButton_ frame]);
340   [okButton_ setFrameOrigin:NSMakePoint(curX, curY)];
341   curX -= (kButtonGap + NSWidth([cancelButton_ frame]));
342   [cancelButton_ setFrameOrigin:NSMakePoint(curX, curY)];
343
344   curY += NSHeight([cancelButton_ frame]);
345
346   // Explanation text.
347   curY += chrome_style::kRowPadding;
348   [explanationField_
349       setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
350   curY += NSHeight([explanationField_ frame]);
351
352   // Prompt box goes all the way to the edges.
353   curX = 0;
354   curY += chrome_style::kRowPadding;
355   [promptBox_ setFrameOrigin:NSMakePoint(curX, curY)];
356   curY += NSHeight([promptBox_ frame]);
357
358   // Prompt label fits in the middle of the box.
359   NSRect boxClientFrame = [[promptBox_ contentView] bounds];
360   CGFloat boxHorizontalMargin =
361       roundf((dialogWidth - NSWidth(boxClientFrame)) / 2);
362   CGFloat boxVerticalMargin =
363       roundf((boxHeight - NSHeight(boxClientFrame)) / 2);
364   [promptField_ setFrameOrigin:NSMakePoint(
365       chrome_style::kHorizontalPadding - boxHorizontalMargin,
366       chrome_style::kRowPadding - boxVerticalMargin)];
367
368   // Title goes at the top.
369   curY += chrome_style::kRowPadding;
370   [titleField_
371       setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
372   curY += NSHeight([titleField_ frame]);
373
374   // Find the height required to fit everything with the necessary padding.
375   CGFloat dialogHeight = curY + chrome_style::kTitleTopPadding;
376
377   // Update the dialog frame with the computed dimensions.
378   [[self view] setFrame:NSMakeRect(0, 0, dialogWidth, dialogHeight)];
379
380   // Close button goes in the top-right corner.
381   NSPoint closeOrigin = NSMakePoint(
382       dialogWidth - chrome_style::kCloseButtonPadding -
383           NSWidth(closeButtonFrame),
384       dialogHeight - chrome_style::kCloseButtonPadding -
385           NSWidth(closeButtonFrame));
386   [closeButton_ setFrameOrigin:closeOrigin];
387 }
388
389 - (IBAction)cancel:(id)sender {
390   if (delegate_) {
391     delegate_->OnCancelSignin();
392     delegate_ = NULL;
393     closeDialogCallback_.Run();
394   }
395 }
396
397 - (IBAction)ok:(id)sender {
398   if (delegate_) {
399     delegate_->OnContinueSignin();
400     delegate_ = NULL;
401     closeDialogCallback_.Run();
402   }
403 }
404
405 - (IBAction)close:(id)sender {
406   if (delegate_) {
407     delegate_->OnCancelSignin();
408     delegate_ = NULL;
409   }
410   closeDialogCallback_.Run();
411 }
412
413 - (IBAction)createProfile:(id)sender {
414   if (delegate_) {
415     delegate_->OnSigninWithNewProfile();
416     delegate_ = NULL;
417     closeDialogCallback_.Run();
418   }
419 }
420
421 - (void)learnMore {
422   chrome::NavigateParams params(
423       browser_, GURL(chrome::kChromeEnterpriseSignInLearnMoreURL),
424       content::PAGE_TRANSITION_AUTO_TOPLEVEL);
425   params.disposition = NEW_POPUP;
426   params.window_action = chrome::NavigateParams::SHOW_WINDOW;
427   chrome::Navigate(&params);
428 }
429
430 - (BOOL)textView:(NSTextView*)textView
431    clickedOnLink:(id)link
432          atIndex:(NSUInteger)charIndex {
433   if (textView == explanationField_.get()) {
434     [self learnMore];
435     return YES;
436   }
437   return NO;
438 }
439
440 - (void)addButton:(NSButton*)button
441         withTitle:(int)resourceID
442            target:(id)target
443            action:(SEL)action
444    shouldAutoSize:(BOOL)shouldAutoSize {
445   if (resourceID)
446     [button setTitle:base::SysUTF16ToNSString(
447         l10n_util::GetStringUTF16(resourceID))];
448   [button setTarget:target];
449   [button setAction:action];
450   [[self view] addSubview:button];
451   if (shouldAutoSize)
452     [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
453 }
454
455 @end
456
457 @implementation ProfileSigninConfirmationViewController (TestingAPI)
458
459 - (ui::ProfileSigninConfirmationDelegate*)delegate {
460   return delegate_;
461 }
462
463 - (NSButton*)createProfileButton {
464   return createProfileButton_.get();
465 }
466
467 - (NSTextView*)explanationField {
468   return explanationField_.get();
469 }
470
471 @end