| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/profile_signin_confirmation_view_controller.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "base/callback_helpers.h" |
| #include "base/mac/bundle_locations.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #import "chrome/browser/ui/chrome_style.h" |
| #import "chrome/browser/ui/cocoa/constrained_window/constrained_window_control_utils.h" |
| #import "chrome/browser/ui/cocoa/hover_close_button.h" |
| #import "chrome/browser/ui/cocoa/hyperlink_text_view.h" |
| #include "chrome/browser/ui/host_desktop.h" |
| #include "chrome/browser/ui/sync/profile_signin_confirmation_helper.h" |
| #include "chrome/common/url_constants.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "grit/chromium_strings.h" |
| #include "grit/generated_resources.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/GTM/AppKit/GTMUILocalizerAndLayoutTweaker.h" |
| #import "ui/base/cocoa/controls/hyperlink_button_cell.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace { |
| |
| const CGFloat kWindowMinWidth = 500; |
| const CGFloat kButtonGap = 6; |
| const SkColor kDialogAlertBarBackgroundColor = 0xFFF6F6F6; |
| const SkColor kDialogAlertBarBorderColor = 0xFFE1E1E1; |
| const CGFloat kDialogAlertBarBorderWidth = 1; |
| |
| // Shift the origin of |view|'s frame by the given amount in the |
| // positive y direction (up). |
| void ShiftOriginY(NSView* view, CGFloat amount) { |
| NSPoint origin = [view frame].origin; |
| origin.y += amount; |
| [view setFrameOrigin:origin]; |
| } |
| |
| // Determine the frame required to fit the content of a string. Uses the |
| // provided height and width as preferred dimensions, where a value of |
| // 0.0 indicates no preference. |
| NSRect ComputeFrame(NSAttributedString* text, CGFloat width, CGFloat height) { |
| NSRect frame = |
| [text boundingRectWithSize:NSMakeSize(width, height) |
| options:NSStringDrawingUsesLineFragmentOrigin]; |
| // boundingRectWithSize is known to underestimate the width. |
| static const CGFloat kTextViewPadding = 10; |
| frame.size.width += kTextViewPadding; |
| return frame; |
| } |
| |
| // Make the indicated range of characters in a text view bold. |
| void MakeTextBold(NSTextField* textField, int offset, int length) { |
| base::scoped_nsobject<NSMutableAttributedString> text( |
| [[textField attributedStringValue] mutableCopy]); |
| NSFont* currentFont = |
| [text attribute:NSFontAttributeName |
| atIndex:offset |
| effectiveRange:NULL]; |
| NSFontManager* fontManager = [NSFontManager sharedFontManager]; |
| NSFont* boldFont = [fontManager convertFont:currentFont |
| toHaveTrait:NSBoldFontMask]; |
| [text beginEditing]; |
| [text addAttribute:NSFontAttributeName |
| value:boldFont |
| range:NSMakeRange(offset, length)]; |
| [text endEditing]; |
| [textField setAttributedStringValue:text]; |
| } |
| |
| // Remove underlining from the specified range of characters in a text view. |
| void RemoveUnderlining(NSTextView* textView, int offset, int length) { |
| // Clear the default link attributes that were set by the |
| // HyperlinkTextView, otherwise removing the underline doesn't matter. |
| [textView setLinkTextAttributes:nil]; |
| NSTextStorage* text = [textView textStorage]; |
| NSRange range = NSMakeRange(offset, length); |
| [text addAttribute:NSUnderlineStyleAttributeName |
| value:[NSNumber numberWithInt:NSUnderlineStyleNone] |
| range:range]; |
| } |
| |
| // Create a new NSTextView and add it to the specified parent. |
| NSTextView* AddTextView( |
| NSView* parent, |
| id<NSTextViewDelegate> delegate, |
| const string16& message, |
| const string16& link, |
| int offset, |
| const ui::ResourceBundle::FontStyle& font_style) { |
| base::scoped_nsobject<HyperlinkTextView> textView( |
| [[HyperlinkTextView alloc] initWithFrame:NSZeroRect]); |
| NSFont* font = ui::ResourceBundle::GetSharedInstance().GetFont( |
| font_style).GetNativeFont(); |
| NSColor* linkColor = gfx::SkColorToCalibratedNSColor( |
| chrome_style::GetLinkColor()); |
| [textView setMessageAndLink:base::SysUTF16ToNSString(message) |
| withLink:base::SysUTF16ToNSString(link) |
| atOffset:offset |
| font:font |
| messageColor:[NSColor blackColor] |
| linkColor:linkColor]; |
| RemoveUnderlining(textView, offset, link.size()); |
| [textView setDelegate:delegate]; |
| [parent addSubview:textView]; |
| return textView.autorelease(); |
| } |
| |
| // Create a new NSTextField and add it to the specified parent. |
| NSTextField* AddTextField( |
| NSView* parent, |
| const string16& message, |
| const ui::ResourceBundle::FontStyle& font_style) { |
| NSTextField* textField = constrained_window::CreateLabel(); |
| [textField setAttributedStringValue: |
| constrained_window::GetAttributedLabelString( |
| SysUTF16ToNSString(message), |
| font_style, |
| NSNaturalTextAlignment, |
| NSLineBreakByWordWrapping)]; |
| [parent addSubview:textField]; |
| return textField; |
| } |
| |
| // Create a new link button and add it to the specified parent. |
| NSButton* AddLinkButton( |
| NSView* parent, |
| const string16& message, |
| id target, |
| SEL selector) { |
| NSButton* button = |
| [HyperlinkButtonCell buttonWithString:SysUTF16ToNSString(message)]; |
| [button setTarget:target]; |
| [button setAction:selector]; |
| HyperlinkButtonCell* cell = [button cell]; |
| cell.textColor = |
| gfx::SkColorToCalibratedNSColor(chrome_style::GetLinkColor()); |
| cell.shouldUnderline = NO; |
| [parent addSubview:button]; |
| return button; |
| } |
| |
| } // namespace |
| |
| @interface ProfileSigninConfirmationViewController () |
| - (void)learnMore; |
| - (void)addButton:(NSButton*)button |
| withTitle:(int)resourceID |
| target:(id)target |
| action:(SEL)action |
| shouldAutoSize:(BOOL)shouldAutoSize; |
| @end |
| |
| @implementation ProfileSigninConfirmationViewController |
| |
| - (id)initWithBrowser:(Browser*)browser |
| username:(const std::string&)username |
| delegate:(ui::ProfileSigninConfirmationDelegate*)delegate |
| closeDialogCallback:(const base::Closure&)closeDialogCallback |
| offerProfileCreation:(bool)offer { |
| if ((self = [super initWithNibName:nil bundle:nil])) { |
| browser_ = browser; |
| username_ = username; |
| delegate_ = delegate; |
| closeDialogCallback_ = closeDialogCallback; |
| offerProfileCreation_ = offer; |
| } |
| return self; |
| } |
| |
| - (void)loadView { |
| self.view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease]; |
| cancelButton_.reset( |
| [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); |
| okButton_.reset( |
| [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); |
| if (offerProfileCreation_) { |
| createProfileButton_.reset( |
| [[ConstrainedWindowButton alloc] initWithFrame:NSZeroRect]); |
| } |
| promptBox_.reset( |
| [[NSBox alloc] initWithFrame:NSZeroRect]); |
| closeButton_.reset( |
| [[WebUIHoverCloseButton alloc] initWithFrame:NSZeroRect]); |
| |
| // ------------------------------- |
| // | Title x | |
| // |-----------------------------| (1 px border) |
| // | Prompt (box) | |
| // |-----------------------------| (1 px border) |
| // | Explanation | |
| // | | |
| // | [create] [cancel] [ok] | |
| // ------------------------------- |
| |
| // The width of the dialog should be sufficient to fit the buttons on |
| // one line and the title and the close button on one line, but not |
| // smaller than kWindowMinWidth. Therefore we first layout the title |
| // and the buttons and then compute the necessary width. |
| |
| // OK button. |
| [self addButton:okButton_ |
| withTitle:IDS_ENTERPRISE_SIGNIN_CONTINUE_NEW_STYLE |
| target:self |
| action:@selector(ok:) |
| shouldAutoSize:YES]; |
| |
| // Cancel button. |
| [self addButton:cancelButton_ |
| withTitle:IDS_ENTERPRISE_SIGNIN_CANCEL |
| target:self |
| action:@selector(cancel:) |
| shouldAutoSize:YES]; |
| |
| // Add the close button. |
| [self addButton:closeButton_ |
| withTitle:0 |
| target:self |
| action:@selector(close:) |
| shouldAutoSize:NO]; |
| NSRect closeButtonFrame = [closeButton_ frame]; |
| closeButtonFrame.size.width = chrome_style::GetCloseButtonSize(); |
| closeButtonFrame.size.height = chrome_style::GetCloseButtonSize(); |
| [closeButton_ setFrame:closeButtonFrame]; |
| |
| // Create Profile link. |
| if (offerProfileCreation_) { |
| [self addButton:createProfileButton_ |
| withTitle:IDS_ENTERPRISE_SIGNIN_CREATE_NEW_PROFILE_NEW_STYLE |
| target:self |
| action:@selector(createProfile:) |
| shouldAutoSize:YES]; |
| } |
| |
| // Add the title label. |
| titleField_.reset( |
| [AddTextField([self view], |
| l10n_util::GetStringUTF16( |
| IDS_ENTERPRISE_SIGNIN_TITLE_NEW_STYLE), |
| chrome_style::kTitleFontStyle) retain]); |
| [titleField_ setFrame:ComputeFrame( |
| [titleField_ attributedStringValue], 0.0, 0.0)]; |
| |
| // Compute the dialog width using the title and buttons. |
| const CGFloat buttonsWidth = |
| (offerProfileCreation_ ? NSWidth([createProfileButton_ frame]) : 0) + |
| kButtonGap + NSWidth([cancelButton_ frame]) + |
| kButtonGap + NSWidth([okButton_ frame]); |
| const CGFloat titleWidth = |
| NSWidth([titleField_ frame]) + NSWidth([closeButton_ frame]); |
| // Dialog minimum width must include the padding. |
| const CGFloat minWidth = |
| kWindowMinWidth - 2 * chrome_style::kHorizontalPadding; |
| const CGFloat width = std::max(minWidth, |
| std::max(buttonsWidth, titleWidth)); |
| const CGFloat dialogWidth = width + 2 * chrome_style::kHorizontalPadding; |
| |
| // Now setup the prompt and explanation text using the computed width. |
| |
| // Prompt box. |
| [promptBox_ setBorderColor:gfx::SkColorToCalibratedNSColor( |
| ui::GetSigninConfirmationPromptBarColor( |
| ui::kSigninConfirmationPromptBarBorderAlpha))]; |
| [promptBox_ setBorderWidth:kDialogAlertBarBorderWidth]; |
| [promptBox_ setFillColor:gfx::SkColorToCalibratedNSColor( |
| ui::GetSigninConfirmationPromptBarColor( |
| ui::kSigninConfirmationPromptBarBackgroundAlpha))]; |
| [promptBox_ setBoxType:NSBoxCustom]; |
| [promptBox_ setTitlePosition:NSNoTitle]; |
| [[self view] addSubview:promptBox_]; |
| |
| // Prompt text. |
| size_t offset; |
| const string16 domain = ASCIIToUTF16(gaia::ExtractDomainName(username_)); |
| const string16 username = ASCIIToUTF16(username_); |
| const string16 prompt_text = |
| l10n_util::GetStringFUTF16( |
| IDS_ENTERPRISE_SIGNIN_ALERT_NEW_STYLE, |
| domain, &offset); |
| promptField_.reset( |
| [AddTextField(promptBox_, prompt_text, chrome_style::kTextFontStyle) |
| retain]); |
| MakeTextBold(promptField_, offset, domain.size()); |
| [promptField_ setFrame:ComputeFrame( |
| [promptField_ attributedStringValue], width, 0.0)]; |
| |
| // Set the height of the prompt box from the prompt text, padding, and border. |
| CGFloat boxHeight = |
| kDialogAlertBarBorderWidth + |
| chrome_style::kRowPadding + |
| NSHeight([promptField_ frame]) + |
| chrome_style::kRowPadding + |
| kDialogAlertBarBorderWidth; |
| [promptBox_ setFrame:NSMakeRect(0, 0, dialogWidth, boxHeight)]; |
| |
| // Explanation text. |
| std::vector<size_t> offsets; |
| const string16 learn_more_text = |
| l10n_util::GetStringUTF16( |
| IDS_ENTERPRISE_SIGNIN_PROFILE_LINK_LEARN_MORE); |
| const string16 explanation_text = |
| l10n_util::GetStringFUTF16( |
| offerProfileCreation_ ? |
| IDS_ENTERPRISE_SIGNIN_EXPLANATION_WITH_PROFILE_CREATION_NEW_STYLE : |
| IDS_ENTERPRISE_SIGNIN_EXPLANATION_WITHOUT_PROFILE_CREATION_NEW_STYLE, |
| username, learn_more_text, &offsets); |
| // HyperlinkTextView requires manually inserting the link text |
| // into the middle of the message text. To do this we slice out |
| // the "learn more" string from the message so that it can be |
| // inserted again. |
| const string16 explanation_message_text = |
| explanation_text.substr(0, offsets[1]) + |
| explanation_text.substr(offsets[1] + learn_more_text.size()); |
| explanationField_.reset( |
| [AddTextView([self view], self, explanation_message_text, learn_more_text, |
| offsets[1], chrome_style::kTextFontStyle) retain]); |
| |
| [explanationField_ setFrame:ComputeFrame( |
| [explanationField_ attributedString], width, 0.0)]; |
| |
| // Layout the elements, starting at the bottom and moving up. |
| |
| CGFloat curX = dialogWidth - chrome_style::kHorizontalPadding; |
| CGFloat curY = chrome_style::kClientBottomPadding; |
| |
| // Buttons should go |Cancel|Continue|CreateProfile|, unless |
| // |CreateProfile| isn't shown. |
| if (offerProfileCreation_) { |
| curX -= NSWidth([createProfileButton_ frame]); |
| [createProfileButton_ setFrameOrigin:NSMakePoint(curX, curY)]; |
| curX -= kButtonGap; |
| } |
| curX -= NSWidth([okButton_ frame]); |
| [okButton_ setFrameOrigin:NSMakePoint(curX, curY)]; |
| curX -= (kButtonGap + NSWidth([cancelButton_ frame])); |
| [cancelButton_ setFrameOrigin:NSMakePoint(curX, curY)]; |
| |
| curY += NSHeight([cancelButton_ frame]); |
| |
| // Explanation text. |
| curY += chrome_style::kRowPadding; |
| [explanationField_ |
| setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)]; |
| curY += NSHeight([explanationField_ frame]); |
| |
| // Prompt box goes all the way to the edges. |
| curX = 0; |
| curY += chrome_style::kRowPadding; |
| [promptBox_ setFrameOrigin:NSMakePoint(curX, curY)]; |
| curY += NSHeight([promptBox_ frame]); |
| |
| // Prompt label fits in the middle of the box. |
| NSRect boxClientFrame = [[promptBox_ contentView] bounds]; |
| CGFloat boxHorizontalMargin = |
| roundf((dialogWidth - NSWidth(boxClientFrame)) / 2); |
| CGFloat boxVerticalMargin = |
| roundf((boxHeight - NSHeight(boxClientFrame)) / 2); |
| [promptField_ setFrameOrigin:NSMakePoint( |
| chrome_style::kHorizontalPadding - boxHorizontalMargin, |
| chrome_style::kRowPadding - boxVerticalMargin)]; |
| |
| // Title goes at the top. |
| curY += chrome_style::kRowPadding; |
| [titleField_ |
| setFrameOrigin:NSMakePoint(chrome_style::kHorizontalPadding, curY)]; |
| curY += NSHeight([titleField_ frame]); |
| |
| // Find the height required to fit everything with the necessary padding. |
| CGFloat dialogHeight = curY + chrome_style::kTitleTopPadding; |
| |
| // Update the dialog frame with the computed dimensions. |
| [[self view] setFrame:NSMakeRect(0, 0, dialogWidth, dialogHeight)]; |
| |
| // Close button goes in the top-right corner. |
| NSPoint closeOrigin = NSMakePoint( |
| dialogWidth - chrome_style::kCloseButtonPadding - |
| NSWidth(closeButtonFrame), |
| dialogHeight - chrome_style::kCloseButtonPadding - |
| NSWidth(closeButtonFrame)); |
| [closeButton_ setFrameOrigin:closeOrigin]; |
| } |
| |
| - (IBAction)cancel:(id)sender { |
| if (delegate_) { |
| delegate_->OnCancelSignin(); |
| delegate_ = NULL; |
| closeDialogCallback_.Run(); |
| } |
| } |
| |
| - (IBAction)ok:(id)sender { |
| if (delegate_) { |
| delegate_->OnContinueSignin(); |
| delegate_ = NULL; |
| closeDialogCallback_.Run(); |
| } |
| } |
| |
| - (IBAction)close:(id)sender { |
| if (delegate_) { |
| delegate_->OnCancelSignin(); |
| delegate_ = NULL; |
| } |
| closeDialogCallback_.Run(); |
| } |
| |
| - (IBAction)createProfile:(id)sender { |
| if (delegate_) { |
| delegate_->OnSigninWithNewProfile(); |
| delegate_ = NULL; |
| closeDialogCallback_.Run(); |
| } |
| } |
| |
| - (void)learnMore { |
| chrome::NavigateParams params( |
| browser_, GURL(chrome::kChromeEnterpriseSignInLearnMoreURL), |
| content::PAGE_TRANSITION_AUTO_TOPLEVEL); |
| params.disposition = NEW_POPUP; |
| params.window_action = chrome::NavigateParams::SHOW_WINDOW; |
| chrome::Navigate(¶ms); |
| } |
| |
| - (BOOL)textView:(NSTextView*)textView |
| clickedOnLink:(id)link |
| atIndex:(NSUInteger)charIndex { |
| if (textView == explanationField_.get()) { |
| [self learnMore]; |
| return YES; |
| } |
| return NO; |
| } |
| |
| - (void)addButton:(NSButton*)button |
| withTitle:(int)resourceID |
| target:(id)target |
| action:(SEL)action |
| shouldAutoSize:(BOOL)shouldAutoSize { |
| if (resourceID) |
| [button setTitle:base::SysUTF16ToNSString( |
| l10n_util::GetStringUTF16(resourceID))]; |
| [button setTarget:target]; |
| [button setAction:action]; |
| [[self view] addSubview:button]; |
| if (shouldAutoSize) |
| [GTMUILocalizerAndLayoutTweaker sizeToFitView:button]; |
| } |
| |
| @end |
| |
| @implementation ProfileSigninConfirmationViewController (TestingAPI) |
| |
| - (ui::ProfileSigninConfirmationDelegate*)delegate { |
| return delegate_; |
| } |
| |
| - (NSButton*)createProfileButton { |
| return createProfileButton_.get(); |
| } |
| |
| - (NSTextView*)explanationField { |
| return explanationField_.get(); |
| } |
| |
| @end |