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"
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"
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) {
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;
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]);
55 [text attribute:NSFontAttributeName
58 NSFontManager* fontManager = [NSFontManager sharedFontManager];
59 NSFont* boldFont = [fontManager convertFont:currentFont
60 toHaveTrait:NSBoldFontMask];
62 [text addAttribute:NSFontAttributeName
64 range:NSMakeRange(offset, length)];
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]
81 // Create a new NSTextView and add it to the specified parent.
82 NSTextView* AddTextView(
84 id<NSTextViewDelegate> delegate,
85 const base::string16& message,
86 const base::string16& link,
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
102 messageColor:[NSColor blackColor]];
103 [textView addLinkRange:NSMakeRange(offset, [linkString length])
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(
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),
122 NSNaturalTextAlignment,
123 NSLineBreakByWordWrapping)];
124 [parent addSubview:textField];
130 @interface ProfileSigninConfirmationViewController ()
132 - (void)addButton:(NSButton*)button
133 withTitle:(int)resourceID
136 shouldAutoSize:(BOOL)shouldAutoSize;
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])) {
148 username_ = username;
149 delegate_ = delegate;
150 closeDialogCallback_ = closeDialogCallback;
151 offerProfileCreation_ = offer;
157 self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
159 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
161 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
162 if (offerProfileCreation_) {
163 createProfileButton_.reset(
164 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
167 [[NSBox alloc] initWithFrame:NSZeroRect]);
169 [[WebUIHoverCloseButton alloc] initWithFrame:NSZeroRect]);
171 // -------------------------------
173 // |-----------------------------| (1 px border)
175 // |-----------------------------| (1 px border)
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.
187 [self addButton:okButton_
188 withTitle:IDS_ENTERPRISE_SIGNIN_CONTINUE_NEW_STYLE
190 action:@selector(ok:)
194 [self addButton:cancelButton_
195 withTitle:IDS_ENTERPRISE_SIGNIN_CANCEL
197 action:@selector(cancel:)
200 // Add the close button.
201 [self addButton:closeButton_
204 action:@selector(close:)
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
216 action:@selector(createProfile:)
220 // Add the title label.
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.
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_];
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,
267 [AddTextField(promptBox_, prompt_text, chrome_style::kTextFontStyle)
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.
275 kDialogAlertBarBorderWidth +
276 chrome_style::kRowPadding +
277 NSHeight([promptField_ frame]) +
278 chrome_style::kRowPadding +
279 kDialogAlertBarBorderWidth;
280 [promptBox_ setFrame:NSMakeRect(0, 0, dialogWidth, boxHeight)];
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
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)];
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]);
327 curY += chrome_style::kRowPadding;
329 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
330 curY += NSHeight([explanationField_ frame]);
332 // Prompt box goes all the way to the edges.
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;
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 {
371 delegate_->OnCancelSignin();
373 closeDialogCallback_.Run();
377 - (IBAction)ok:(id)sender {
379 delegate_->OnContinueSignin();
381 closeDialogCallback_.Run();
385 - (IBAction)close:(id)sender {
387 delegate_->OnCancelSignin();
390 closeDialogCallback_.Run();
393 - (IBAction)createProfile:(id)sender {
395 delegate_->OnSigninWithNewProfile();
397 closeDialogCallback_.Run();
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(¶ms);
410 - (BOOL)textView:(NSTextView*)textView
411 clickedOnLink:(id)link
412 atIndex:(NSUInteger)charIndex {
413 if (textView == explanationField_.get()) {
420 - (void)addButton:(NSButton*)button
421 withTitle:(int)resourceID
424 shouldAutoSize:(BOOL)shouldAutoSize {
426 [button setTitle:base::SysUTF16ToNSString(
427 l10n_util::GetStringUTF16(resourceID))];
428 [button setTarget:target];
429 [button setAction:action];
430 [[self view] addSubview:button];
432 [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
437 @implementation ProfileSigninConfirmationViewController (TestingAPI)
439 - (ui::ProfileSigninConfirmationDelegate*)delegate {
443 - (NSButton*)createProfileButton {
444 return createProfileButton_.get();
447 - (NSTextView*)explanationField {
448 return explanationField_.get();