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.
5 #import "chrome/browser/ui/cocoa/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/GTM/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 // 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;
42 [view setFrameOrigin:origin];
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) {
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;
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]);
63 [text attribute:NSFontAttributeName
66 NSFontManager* fontManager = [NSFontManager sharedFontManager];
67 NSFont* boldFont = [fontManager convertFont:currentFont
68 toHaveTrait:NSBoldFontMask];
70 [text addAttribute:NSFontAttributeName
72 range:NSMakeRange(offset, length)];
74 [textField setAttributedStringValue:text];
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]
89 // Create a new NSTextView and add it to the specified parent.
90 NSTextView* AddTextView(
92 id<NSTextViewDelegate> delegate,
93 const string16& message,
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)
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();
115 // Create a new NSTextField and add it to the specified parent.
116 NSTextField* AddTextField(
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),
125 NSNaturalTextAlignment,
126 NSLineBreakByWordWrapping)];
127 [parent addSubview:textField];
131 // Create a new link button and add it to the specified parent.
132 NSButton* AddLinkButton(
134 const string16& message,
138 [HyperlinkButtonCell buttonWithString:SysUTF16ToNSString(message)];
139 [button setTarget:target];
140 [button setAction:selector];
141 HyperlinkButtonCell* cell = [button cell];
143 gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor());
144 cell.shouldUnderline = NO;
145 [parent addSubview:button];
151 @interface ProfileSigninConfirmationViewController ()
153 - (void)addButton:(NSButton*)button
154 withTitle:(int)resourceID
157 shouldAutoSize:(BOOL)shouldAutoSize;
160 @implementation ProfileSigninConfirmationViewController
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])) {
169 username_ = username;
170 delegate_ = delegate;
171 closeDialogCallback_ = closeDialogCallback;
172 offerProfileCreation_ = offer;
178 self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease];
180 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
182 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
183 if (offerProfileCreation_) {
184 createProfileButton_.reset(
185 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]);
188 [[NSBox alloc] initWithFrame:NSZeroRect]);
190 [[WebUIHoverCloseButton alloc] initWithFrame:NSZeroRect]);
192 // -------------------------------
194 // |-----------------------------| (1 px border)
196 // |-----------------------------| (1 px border)
199 // | [create] [cancel] [ok] |
200 // -------------------------------
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.
208 [self addButton:okButton_
209 withTitle:IDS_ENTERPRISE_SIGNIN_CONTINUE_NEW_STYLE
211 action:@selector(ok:)
215 [self addButton:cancelButton_
216 withTitle:IDS_ENTERPRISE_SIGNIN_CANCEL
218 action:@selector(cancel:)
221 // Add the close button.
222 [self addButton:closeButton_
225 action:@selector(close:)
227 NSRect closeButtonFrame = [closeButton_ frame];
228 closeButtonFrame.size.width = chrome_style::GetCloseButtonSize();
229 closeButtonFrame.size.height = chrome_style::GetCloseButtonSize();
230 [closeButton_ setFrame:closeButtonFrame];
232 // Create Profile link.
233 if (offerProfileCreation_) {
234 [self addButton:createProfileButton_
235 withTitle:IDS_ENTERPRISE_SIGNIN_CREATE_NEW_PROFILE_NEW_STYLE
237 action:@selector(createProfile:)
241 // Add the title label.
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)];
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;
264 // Now setup the prompt and explanation text using the computed width.
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_];
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,
287 [AddTextField(promptBox_, prompt_text, chrome_style::kTextFontStyle)
289 MakeTextBold(promptField_, offset, domain.size());
290 [promptField_ setFrame:ComputeFrame(
291 [promptField_ attributedStringValue], width, 0.0)];
293 // Set the height of the prompt box from the prompt text, padding, and border.
295 kDialogAlertBarBorderWidth +
296 chrome_style::kRowPadding +
297 NSHeight([promptField_ frame]) +
298 chrome_style::kRowPadding +
299 kDialogAlertBarBorderWidth;
300 [promptBox_ setFrame:NSMakeRect(0, 0, dialogWidth, boxHeight)];
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
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]);
324 [explanationField_ setFrame:ComputeFrame(
325 [explanationField_ attributedString], width, 0.0)];
327 // Layout the elements, starting at the bottom and moving up.
329 CGFloat curX = dialogWidth - chrome_style::kHorizontalPadding;
330 CGFloat curY = chrome_style::kClientBottomPadding;
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)];
339 curX -= NSWidth([okButton_ frame]);
340 [okButton_ setFrameOrigin:NSMakePoint(curX, curY)];
341 curX -= (kButtonGap + NSWidth([cancelButton_ frame]));
342 [cancelButton_ setFrameOrigin:NSMakePoint(curX, curY)];
344 curY += NSHeight([cancelButton_ frame]);
347 curY += chrome_style::kRowPadding;
349 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
350 curY += NSHeight([explanationField_ frame]);
352 // Prompt box goes all the way to the edges.
354 curY += chrome_style::kRowPadding;
355 [promptBox_ setFrameOrigin:NSMakePoint(curX, curY)];
356 curY += NSHeight([promptBox_ frame]);
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)];
368 // Title goes at the top.
369 curY += chrome_style::kRowPadding;
371 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)];
372 curY += NSHeight([titleField_ frame]);
374 // Find the height required to fit everything with the necessary padding.
375 CGFloat dialogHeight = curY + chrome_style::kTitleTopPadding;
377 // Update the dialog frame with the computed dimensions.
378 [[self view] setFrame:NSMakeRect(0, 0, dialogWidth, dialogHeight)];
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];
389 - (IBAction)cancel:(id)sender {
391 delegate_->OnCancelSignin();
393 closeDialogCallback_.Run();
397 - (IBAction)ok:(id)sender {
399 delegate_->OnContinueSignin();
401 closeDialogCallback_.Run();
405 - (IBAction)close:(id)sender {
407 delegate_->OnCancelSignin();
410 closeDialogCallback_.Run();
413 - (IBAction)createProfile:(id)sender {
415 delegate_->OnSigninWithNewProfile();
417 closeDialogCallback_.Run();
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(¶ms);
430 - (BOOL)textView:(NSTextView*)textView
431 clickedOnLink:(id)link
432 atIndex:(NSUInteger)charIndex {
433 if (textView == explanationField_.get()) {
440 - (void)addButton:(NSButton*)button
441 withTitle:(int)resourceID
444 shouldAutoSize:(BOOL)shouldAutoSize {
446 [button setTitle:base::SysUTF16ToNSString(
447 l10n_util::GetStringUTF16(resourceID))];
448 [button setTarget:target];
449 [button setAction:action];
450 [[self view] addSubview:button];
452 [GTMUILocalizerAndLayoutTweaker sizeToFitView:button];
457 @implementation ProfileSigninConfirmationViewController (TestingAPI)
459 - (ui::ProfileSigninConfirmationDelegate*)delegate {
463 - (NSButton*)createProfileButton {
464 return createProfileButton_.get();
467 - (NSTextView*)explanationField {
468 return explanationField_.get();