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. 4 5#import "chrome/browser/ui/cocoa/profiles/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/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" 30 31namespace { 32 33const CGFloat kWindowMinWidth = 500; 34const CGFloat kButtonGap = 6; 35const CGFloat kDialogAlertBarBorderWidth = 1; 36 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. 40NSRect 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; 48} 49 50// Make the indicated range of characters in a text view bold. 51void 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]; 67} 68 69// Remove underlining from the specified range of characters in a text view. 70void 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]; 79} 80 81// Create a new NSTextView and add it to the specified parent. 82NSTextView* 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 [textView setMessageAndLink:base::SysUTF16ToNSString(message) 96 withLink:base::SysUTF16ToNSString(link) 97 atOffset:offset 98 font:font 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(); 105} 106 107// Create a new NSTextField and add it to the specified parent. 108NSTextField* AddTextField( 109 NSView* parent, 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), 116 font_style, 117 NSNaturalTextAlignment, 118 NSLineBreakByWordWrapping)]; 119 [parent addSubview:textField]; 120 return textField; 121} 122 123} // namespace 124 125@interface ProfileSigninConfirmationViewController () 126- (void)learnMore; 127- (void)addButton:(NSButton*)button 128 withTitle:(int)resourceID 129 target:(id)target 130 action:(SEL)action 131 shouldAutoSize:(BOOL)shouldAutoSize; 132@end 133 134@implementation ProfileSigninConfirmationViewController 135 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])) { 142 browser_ = browser; 143 username_ = username; 144 delegate_ = delegate; 145 closeDialogCallback_ = closeDialogCallback; 146 offerProfileCreation_ = offer; 147 } 148 return self; 149} 150 151- (void)loadView { 152 self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease]; 153 cancelButton_.reset( 154 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); 155 okButton_.reset( 156 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); 157 if (offerProfileCreation_) { 158 createProfileButton_.reset( 159 [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); 160 } 161 promptBox_.reset( 162 [[NSBox alloc] initWithFrame:NSZeroRect]); 163 closeButton_.reset( 164 [[WebUIHoverCloseButton alloc] initWithFrame:NSZeroRect]); 165 166 // ------------------------------- 167 // | Title x | 168 // |-----------------------------| (1 px border) 169 // | Prompt (box) | 170 // |-----------------------------| (1 px border) 171 // | Explanation | 172 // | | 173 // | [create] [cancel] [ok] | 174 // ------------------------------- 175 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. 180 181 // OK button. 182 [self addButton:okButton_ 183 withTitle:IDS_ENTERPRISE_SIGNIN_CONTINUE_NEW_STYLE 184 target:self 185 action:@selector(ok:) 186 shouldAutoSize:YES]; 187 188 // Cancel button. 189 [self addButton:cancelButton_ 190 withTitle:IDS_ENTERPRISE_SIGNIN_CANCEL 191 target:self 192 action:@selector(cancel:) 193 shouldAutoSize:YES]; 194 195 // Add the close button. 196 [self addButton:closeButton_ 197 withTitle:0 198 target:self 199 action:@selector(close:) 200 shouldAutoSize:NO]; 201 NSRect closeButtonFrame = [closeButton_ frame]; 202 closeButtonFrame.size.width = chrome_style::GetCloseButtonSize(); 203 closeButtonFrame.size.height = chrome_style::GetCloseButtonSize(); 204 [closeButton_ setFrame:closeButtonFrame]; 205 206 // Create Profile link. 207 if (offerProfileCreation_) { 208 [self addButton:createProfileButton_ 209 withTitle:IDS_ENTERPRISE_SIGNIN_CREATE_NEW_PROFILE_NEW_STYLE 210 target:self 211 action:@selector(createProfile:) 212 shouldAutoSize:YES]; 213 } 214 215 // Add the title label. 216 titleField_.reset( 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)]; 223 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; 237 238 // Now setup the prompt and explanation text using the computed width. 239 240 // Prompt box. 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_]; 251 252 // Prompt text. 253 size_t offset; 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, 260 domain, &offset); 261 promptField_.reset( 262 [AddTextField(promptBox_, prompt_text, chrome_style::kTextFontStyle) 263 retain]); 264 MakeTextBold(promptField_, offset, domain.size()); 265 [promptField_ setFrame:ComputeFrame( 266 [promptField_ attributedStringValue], width, 0.0)]; 267 268 // Set the height of the prompt box from the prompt text, padding, and border. 269 CGFloat boxHeight = 270 kDialogAlertBarBorderWidth + 271 chrome_style::kRowPadding + 272 NSHeight([promptField_ frame]) + 273 chrome_style::kRowPadding + 274 kDialogAlertBarBorderWidth; 275 [promptBox_ setFrame:NSMakeRect(0, 0, dialogWidth, boxHeight)]; 276 277 // Explanation text. 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 291 // inserted again. 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]); 298 299 [explanationField_ setFrame:ComputeFrame( 300 [explanationField_ attributedString], width, 0.0)]; 301 302 // Layout the elements, starting at the bottom and moving up. 303 304 CGFloat curX = dialogWidth - chrome_style::kHorizontalPadding; 305 CGFloat curY = chrome_style::kClientBottomPadding; 306 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)]; 312 curX -= kButtonGap; 313 } 314 curX -= NSWidth([okButton_ frame]); 315 [okButton_ setFrameOrigin:NSMakePoint(curX, curY)]; 316 curX -= (kButtonGap + NSWidth([cancelButton_ frame])); 317 [cancelButton_ setFrameOrigin:NSMakePoint(curX, curY)]; 318 319 curY += NSHeight([cancelButton_ frame]); 320 321 // Explanation text. 322 curY += chrome_style::kRowPadding; 323 [explanationField_ 324 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)]; 325 curY += NSHeight([explanationField_ frame]); 326 327 // Prompt box goes all the way to the edges. 328 curX = 0; 329 curY += chrome_style::kRowPadding; 330 [promptBox_ setFrameOrigin:NSMakePoint(curX, curY)]; 331 curY += NSHeight([promptBox_ frame]); 332 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)]; 342 343 // Title goes at the top. 344 curY += chrome_style::kRowPadding; 345 [titleField_ 346 setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)]; 347 curY += NSHeight([titleField_ frame]); 348 349 // Find the height required to fit everything with the necessary padding. 350 CGFloat dialogHeight = curY + chrome_style::kTitleTopPadding; 351 352 // Update the dialog frame with the computed dimensions. 353 [[self view] setFrame:NSMakeRect(0, 0, dialogWidth, dialogHeight)]; 354 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]; 362} 363 364- (IBAction)cancel:(id)sender { 365 if (delegate_) { 366 delegate_->OnCancelSignin(); 367 delegate_ = NULL; 368 closeDialogCallback_.Run(); 369 } 370} 371 372- (IBAction)ok:(id)sender { 373 if (delegate_) { 374 delegate_->OnContinueSignin(); 375 delegate_ = NULL; 376 closeDialogCallback_.Run(); 377 } 378} 379 380- (IBAction)close:(id)sender { 381 if (delegate_) { 382 delegate_->OnCancelSignin(); 383 delegate_ = NULL; 384 } 385 closeDialogCallback_.Run(); 386} 387 388- (IBAction)createProfile:(id)sender { 389 if (delegate_) { 390 delegate_->OnSigninWithNewProfile(); 391 delegate_ = NULL; 392 closeDialogCallback_.Run(); 393 } 394} 395 396- (void)learnMore { 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); 403} 404 405- (BOOL)textView:(NSTextView*)textView 406 clickedOnLink:(id)link 407 atIndex:(NSUInteger)charIndex { 408 if (textView == explanationField_.get()) { 409 [self learnMore]; 410 return YES; 411 } 412 return NO; 413} 414 415- (void)addButton:(NSButton*)button 416 withTitle:(int)resourceID 417 target:(id)target 418 action:(SEL)action 419 shouldAutoSize:(BOOL)shouldAutoSize { 420 if (resourceID) 421 [button setTitle:base::SysUTF16ToNSString( 422 l10n_util::GetStringUTF16(resourceID))]; 423 [button setTarget:target]; 424 [button setAction:action]; 425 [[self view] addSubview:button]; 426 if (shouldAutoSize) 427 [GTMUILocalizerAndLayoutTweaker sizeToFitView:button]; 428} 429 430@end 431 432@implementation ProfileSigninConfirmationViewController (TestingAPI) 433 434- (ui::ProfileSigninConfirmationDelegate*)delegate { 435 return delegate_; 436} 437 438- (NSButton*)createProfileButton { 439 return createProfileButton_.get(); 440} 441 442- (NSTextView*)explanationField { 443 return explanationField_.get(); 444} 445 446@end 447