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(&params);
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