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