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 #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/google_toolbox_for_mac/src/AppKit/GTMUILocalizerAndLayoutTweaker.h"
28 #import "ui/base/cocoa/controls/hyperlink_button_cell.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 [textView setMessageAndLink:base::SysUTF16ToNSString(message)
96 withLink:base::SysUTF16ToNSString(link)
99 messageColor:[NSColor blackColor]
100 linkColor:linkColor];
101 RemoveUnderlining(textView, offset, link.size());
102 [textView setDelegate:delegate];
103 [parent addSubview:textView];
104 return textView.autorelease();
107 // Create a new NSTextField and add it to the specified parent.
108 NSTextField* AddTextField(
110 const base::string16& message,
111 const ui::ResourceBundle::FontStyle& font_style) {
112 NSTextField* textField = constrained_window::CreateLabel();
113 [textField setAttributedStringValue:
114 constrained_window::GetAttributedLabelString(
115 SysUTF16ToNSString(message),
117 NSNaturalTextAlignment,
118 NSLineBreakByWordWrapping)];
119 [parent addSubview:textField];
125 @interface ProfileSigninConfirmationViewController ()
127 - (void)addButton:(NSButton*)button
128 withTitle:(int)resourceID
131 shouldAutoSize:(BOOL)shouldAutoSize;
134 @implementation ProfileSigninConfirmationViewController
136 - (id)initWithBrowser:(Browser*)browser
137 username:(const std::string&)username
138 delegate:(ui::ProfileSigninConfirmationDelegate*)delegate
139 closeDialogCallback:(const base::Closure&)closeDialogCallback
140 offerProfileCreation:(bool)offer {
141 if ((self = [super initWithNibName:nil bundle:nil])) {
143 username_ = username;
144 delegate_ = delegate;
145 closeDialogCallback_ = closeDialogCallback;
146 offerProfileCreation_ = offer;
152 self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
154 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
156 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
157 if (offerProfileCreation_) {
158 createProfileButton_.reset(
159 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
162 [[NSBox alloc] initWithFrame:NSZeroRect]);
164 [[WebUIHoverCloseButton alloc] initWithFrame:NSZeroRect]);
166 // -------------------------------
168 // |-----------------------------| (1 px border)
170 // |-----------------------------| (1 px border)
173 // | [create] [cancel] [ok] |
174 // -------------------------------
176 // The width of the dialog should be sufficient to fit the buttons on
177 // one line and the title and the close button on one line, but not
178 // smaller than kWindowMinWidth. Therefore we first layout the title
179 // and the buttons and then compute the necessary width.
182 [self addButton:okButton_
183 withTitle:IDS_ENTERPRISE_SIGNIN_CONTINUE_NEW_STYLE
185 action:@selector(ok:)
189 [self addButton:cancelButton_
190 withTitle:IDS_ENTERPRISE_SIGNIN_CANCEL
192 action:@selector(cancel:)
195 // Add the close button.
196 [self addButton:closeButton_
199 action:@selector(close:)
201 NSRect closeButtonFrame = [closeButton_ frame];
202 closeButtonFrame.size.width = chrome_style::GetCloseButtonSize();
203 closeButtonFrame.size.height = chrome_style::GetCloseButtonSize();
204 [closeButton_ setFrame:closeButtonFrame];
206 // Create Profile link.
207 if (offerProfileCreation_) {
208 [self addButton:createProfileButton_
209 withTitle:IDS_ENTERPRISE_SIGNIN_CREATE_NEW_PROFILE_NEW_STYLE
211 action:@selector(createProfile:)
215 // Add the title label.
217 [AddTextField([self view],
218 l10n_util::GetStringUTF16(
219 IDS_ENTERPRISE_SIGNIN_TITLE_NEW_STYLE),
220 chrome_style::kTitleFontStyle) retain]);
221 [titleField_ setFrame:ComputeFrame(
222 [titleField_ attributedStringValue], 0.0, 0.0)];
224 // Compute the dialog width using the title and buttons.
225 const CGFloat buttonsWidth =
226 (offerProfileCreation_ ? NSWidth([createProfileButton_ frame]) : 0) +
227 kButtonGap + NSWidth([cancelButton_ frame]) +
228 kButtonGap + NSWidth([okButton_ frame]);
229 const CGFloat titleWidth =
230 NSWidth([titleField_ frame]) + NSWidth([closeButton_ frame]);
231 // Dialog minimum width must include the padding.
232 const CGFloat minWidth =
233 kWindowMinWidth - 2 * chrome_style::kHorizontalPadding;
234 const CGFloat width = std::max(minWidth,
235 std::max(buttonsWidth, titleWidth));
236 const CGFloat dialogWidth = width + 2 * chrome_style::kHorizontalPadding;
238 // Now setup the prompt and explanation text using the computed width.
241 [promptBox_ setBorderColor:gfx::SkColorToCalibratedNSColor(
242 ui::GetSigninConfirmationPromptBarColor(
243 ui::kSigninConfirmationPromptBarBorderAlpha))];
244 [promptBox_ setBorderWidth:kDialogAlertBarBorderWidth];
245 [promptBox_ setFillColor:gfx::SkColorToCalibratedNSColor(
246 ui::GetSigninConfirmationPromptBarColor(
247 ui::kSigninConfirmationPromptBarBackgroundAlpha))];
248 [promptBox_ setBoxType:NSBoxCustom];
249 [promptBox_ setTitlePosition:NSNoTitle];
250 [[self view] addSubview:promptBox_];
254 const base::string16 domain =
255 base::ASCIIToUTF16(gaia::ExtractDomainName(username_));
256 const base::string16 username = base::ASCIIToUTF16(username_);
257 const base::string16 prompt_text =
258 l10n_util::GetStringFUTF16(
259 IDS_ENTERPRISE_SIGNIN_ALERT_NEW_STYLE,
262 [AddTextField(promptBox_, prompt_text, chrome_style::kTextFontStyle)
264 MakeTextBold(promptField_, offset, domain.size());
265 [promptField_ setFrame:ComputeFrame(
266 [promptField_ attributedStringValue], width, 0.0)];
268 // Set the height of the prompt box from the prompt text, padding, and border.
270 kDialogAlertBarBorderWidth +
271 chrome_style::kRowPadding +
272 NSHeight([promptField_ frame]) +
273 chrome_style::kRowPadding +
274 kDialogAlertBarBorderWidth;
275 [promptBox_ setFrame:NSMakeRect(0, 0, dialogWidth, boxHeight)];
278 std::vector<size_t> offsets;
279 const base::string16 learn_more_text =
280 l10n_util::GetStringUTF16(
281 IDS_ENTERPRISE_SIGNIN_PROFILE_LINK_LEARN_MORE);
282 const base::string16 explanation_text =
283 l10n_util::GetStringFUTF16(
284 offerProfileCreation_ ?
285 IDS_ENTERPRISE_SIGNIN_EXPLANATION_WITH_PROFILE_CREATION_NEW_STYLE :
286 IDS_ENTERPRISE_SIGNIN_EXPLANATION_WITHOUT_PROFILE_CREATION_NEW_STYLE,
287 username, learn_more_text, &offsets);
288 // HyperlinkTextView requires manually inserting the link text
289 // into the middle of the message text. To do this we slice out
290 // the "learn more" string from the message so that it can be
292 const base::string16 explanation_message_text =
293 explanation_text.substr(0, offsets[1]) +
294 explanation_text.substr(offsets[1] + learn_more_text.size());
295 explanationField_.reset(
296 [AddTextView([self view], self, explanation_message_text, learn_more_text,
297 offsets[1], chrome_style::kTextFontStyle) retain]);
299 [explanationField_ setFrame:ComputeFrame(
300 [explanationField_ attributedString], width, 0.0)];
302 // Layout the elements, starting at the bottom and moving up.
304 CGFloat curX = dialogWidth - chrome_style::kHorizontalPadding;
305 CGFloat curY = chrome_style::kClientBottomPadding;
307 // Buttons should go |Cancel|Continue|CreateProfile|, unless
308 // |CreateProfile| isn't shown.
309 if (offerProfileCreation_) {
310 curX -= NSWidth([createProfileButton_ frame]);
311 [createProfileButton_ setFrameOrigin:NSMakePoint(curX, curY)];
314 curX -= NSWidth([okButton_ frame]);
315 [okButton_ setFrameOrigin:NSMakePoint(curX, curY)];
316 curX -= (kButtonGap + NSWidth([cancelButton_ frame]));
317 [cancelButton_ setFrameOrigin:NSMakePoint(curX, curY)];
319 curY += NSHeight([cancelButton_ frame]);
322 curY += chrome_style::kRowPadding;
324 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
325 curY += NSHeight([explanationField_ frame]);
327 // Prompt box goes all the way to the edges.
329 curY += chrome_style::kRowPadding;
330 [promptBox_ setFrameOrigin:NSMakePoint(curX, curY)];
331 curY += NSHeight([promptBox_ frame]);
333 // Prompt label fits in the middle of the box.
334 NSRect boxClientFrame = [[promptBox_ contentView] bounds];
335 CGFloat boxHorizontalMargin =
336 roundf((dialogWidth - NSWidth(boxClientFrame)) / 2);
337 CGFloat boxVerticalMargin =
338 roundf((boxHeight - NSHeight(boxClientFrame)) / 2);
339 [promptField_ setFrameOrigin:NSMakePoint(
340 chrome_style::kHorizontalPadding - boxHorizontalMargin,
341 chrome_style::kRowPadding - boxVerticalMargin)];
343 // Title goes at the top.
344 curY += chrome_style::kRowPadding;
346 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
347 curY += NSHeight([titleField_ frame]);
349 // Find the height required to fit everything with the necessary padding.
350 CGFloat dialogHeight = curY + chrome_style::kTitleTopPadding;
352 // Update the dialog frame with the computed dimensions.
353 [[self view] setFrame:NSMakeRect(0, 0, dialogWidth, dialogHeight)];
355 // Close button goes in the top-right corner.
356 NSPoint closeOrigin = NSMakePoint(
357 dialogWidth - chrome_style::kCloseButtonPadding -
358 NSWidth(closeButtonFrame),
359 dialogHeight - chrome_style::kCloseButtonPadding -
360 NSWidth(closeButtonFrame));
361 [closeButton_ setFrameOrigin:closeOrigin];
364 - (IBAction)cancel:(id)sender {
366 delegate_->OnCancelSignin();
368 closeDialogCallback_.Run();
372 - (IBAction)ok:(id)sender {
374 delegate_->OnContinueSignin();
376 closeDialogCallback_.Run();
380 - (IBAction)close:(id)sender {
382 delegate_->OnCancelSignin();
385 closeDialogCallback_.Run();
388 - (IBAction)createProfile:(id)sender {
390 delegate_->OnSigninWithNewProfile();
392 closeDialogCallback_.Run();
397 chrome::NavigateParams params(
398 browser_, GURL(chrome::kChromeEnterpriseSignInLearnMoreURL),
399 content::PAGE_TRANSITION_AUTO_TOPLEVEL);
400 params.disposition = NEW_POPUP;
401 params.window_action = chrome::NavigateParams::SHOW_WINDOW;
402 chrome::Navigate(¶ms);
405 - (BOOL)textView:(NSTextView*)textView
406 clickedOnLink:(id)link
407 atIndex:(NSUInteger)charIndex {
408 if (textView == explanationField_.get()) {
415 - (void)addButton:(NSButton*)button
416 withTitle:(int)resourceID
419 shouldAutoSize:(BOOL)shouldAutoSize {
421 [button setTitle:base::SysUTF16ToNSString(
422 l10n_util::GetStringUTF16(resourceID))];
423 [button setTarget:target];
424 [button setAction:action];
425 [[self view] addSubview:button];
427 [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
432 @implementation ProfileSigninConfirmationViewController (TestingAPI)
434 - (ui::ProfileSigninConfirmationDelegate*)delegate {
438 - (NSButton*)createProfileButton {
439 return createProfileButton_.get();
442 - (NSTextView*)explanationField {
443 return explanationField_.get();