new_user_view.cc revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
1// Copyright (c) 2010 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#include "chrome/browser/chromeos/login/new_user_view.h"
6
7#include <signal.h>
8#include <sys/types.h>
9
10#include <algorithm>
11#include <vector>
12
13#include "app/keyboard_codes.h"
14#include "app/l10n_util.h"
15#include "app/resource_bundle.h"
16#include "base/callback.h"
17#include "base/command_line.h"
18#include "base/message_loop.h"
19#include "base/process_util.h"
20#include "base/string_util.h"
21#include "base/utf_string_conversions.h"
22#include "chrome/browser/chromeos/cros/cros_library.h"
23#include "chrome/browser/chromeos/login/rounded_rect_painter.h"
24#include "chrome/browser/chromeos/login/textfield_with_margin.h"
25#include "chrome/browser/chromeos/login/wizard_accessibility_helper.h"
26#include "chrome/browser/chromeos/user_cros_settings_provider.h"
27#include "chrome/browser/chromeos/views/copy_background.h"
28#include "gfx/font.h"
29#include "grit/app_resources.h"
30#include "grit/chromium_strings.h"
31#include "grit/generated_resources.h"
32#include "views/controls/label.h"
33#include "views/controls/throbber.h"
34#include "views/widget/widget_gtk.h"
35
36using views::View;
37
38namespace {
39
40const int kTextfieldWidth = 230;
41const int kSplitterHeight = 1;
42const int kTitlePad = 20;
43const int kRowPad = 13;
44const int kBottomPad = 33;
45const int kLeftPad = 33;
46const int kColumnPad = 7;
47const int kLanguagesMenuHeight = 25;
48const int kLanguagesMenuPad = 5;
49const SkColor kLanguagesMenuTextColor = 0xFF999999;
50const SkColor kErrorColor = 0xFF8F384F;
51const SkColor kSplitterUp1Color = 0xFFD0D2D3;
52const SkColor kSplitterUp2Color = 0xFFE1E3E4;
53const SkColor kSplitterDown1Color = 0xFFE3E6E8;
54const SkColor kSplitterDown2Color = 0xFFEAEDEE;
55const char kDefaultDomain[] = "@gmail.com";
56
57// Textfield that adds domain to the entered username if focus is lost and
58// username doesn't have full domain.
59class UsernameField : public chromeos::TextfieldWithMargin {
60 public:
61  explicit UsernameField(chromeos::NewUserView* controller)
62      : controller_(controller) {}
63
64  // views::Textfield overrides:
65  virtual void WillLoseFocus() {
66    if (!text().empty()) {
67      std::string username = UTF16ToUTF8(text());
68
69      if (username.find('@') == std::string::npos) {
70        username += kDefaultDomain;
71        SetText(UTF8ToUTF16(username));
72      }
73    }
74  }
75
76  // Overridden from views::View:
77  virtual bool OnKeyPressed(const views::KeyEvent& e) {
78    if (e.GetKeyCode() == app::VKEY_LEFT) {
79      return controller_->NavigateAway();
80    }
81    return false;
82  }
83
84 private:
85  chromeos::NewUserView* controller_;
86  DISALLOW_COPY_AND_ASSIGN(UsernameField);
87};
88
89}  // namespace
90
91namespace chromeos {
92
93NewUserView::NewUserView(Delegate* delegate,
94                         bool need_border,
95                         bool need_guest_link)
96    : username_field_(NULL),
97      password_field_(NULL),
98      title_label_(NULL),
99      title_hint_label_(NULL),
100      splitter_up1_(NULL),
101      splitter_up2_(NULL),
102      splitter_down1_(NULL),
103      splitter_down2_(NULL),
104      sign_in_button_(NULL),
105      create_account_link_(NULL),
106      guest_link_(NULL),
107      languages_menubutton_(NULL),
108      accel_focus_pass_(views::Accelerator(app::VKEY_P, false, false, true)),
109      accel_focus_user_(views::Accelerator(app::VKEY_U, false, false, true)),
110      accel_login_off_the_record_(
111          views::Accelerator(app::VKEY_B, false, false, true)),
112      accel_toggle_accessibility_(WizardAccessibilityHelper::GetAccelerator()),
113      delegate_(delegate),
114      ALLOW_THIS_IN_INITIALIZER_LIST(focus_grabber_factory_(this)),
115      focus_delayed_(false),
116      login_in_process_(false),
117      need_border_(need_border),
118      need_guest_link_(false),
119      need_create_account_(false),
120      languages_menubutton_order_(-1),
121      sign_in_button_order_(-1) {
122  if (need_guest_link && UserCrosSettingsProvider::cached_allow_guest())
123    need_guest_link_ = true;
124}
125
126NewUserView::~NewUserView() {
127}
128
129void NewUserView::Init() {
130  if (need_border_) {
131    // Use rounded rect background.
132    set_border(CreateWizardBorder(&BorderDefinition::kUserBorder));
133    views::Painter* painter = CreateWizardPainter(
134        &BorderDefinition::kUserBorder);
135    set_background(views::Background::CreateBackgroundPainter(true, painter));
136  }
137
138  // Set up fonts.
139  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
140  gfx::Font title_font = rb.GetFont(ResourceBundle::MediumBoldFont).DeriveFont(
141      kFontSizeCorrectionDelta);
142  gfx::Font title_hint_font = rb.GetFont(ResourceBundle::BoldFont);
143
144  title_label_ = new views::Label();
145  title_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
146  title_label_->SetFont(title_font);
147  title_label_->SetMultiLine(true);
148  AddChildView(title_label_);
149
150  title_hint_label_ = new views::Label();
151  title_hint_label_->SetHorizontalAlignment(views::Label::ALIGN_LEFT);
152  title_hint_label_->SetFont(title_hint_font);
153  title_hint_label_->SetColor(SK_ColorGRAY);
154  title_hint_label_->SetMultiLine(true);
155  AddChildView(title_hint_label_);
156
157  splitter_up1_ = CreateSplitter(kSplitterUp1Color);
158  splitter_up2_ = CreateSplitter(kSplitterUp2Color);
159  splitter_down1_ = CreateSplitter(kSplitterDown1Color);
160  splitter_down2_ = CreateSplitter(kSplitterDown2Color);
161
162  username_field_ = new UsernameField(this);
163  username_field_->set_background(new CopyBackground(this));
164  username_field_->SetAccessibleName(
165      ASCIIToWide(l10n_util::GetStringUTF8(IDS_CHROMEOS_ACC_USERNAME_LABEL)));
166  AddChildView(username_field_);
167
168  password_field_ = new TextfieldWithMargin(views::Textfield::STYLE_PASSWORD);
169  password_field_->set_background(new CopyBackground(this));
170  AddChildView(password_field_);
171
172  language_switch_menu_.InitLanguageMenu();
173
174  RecreatePeculiarControls();
175
176  AddChildView(sign_in_button_);
177  if (need_create_account_) {
178    InitLink(&create_account_link_);
179  }
180  if (need_guest_link_) {
181    InitLink(&guest_link_);
182  }
183  AddChildView(languages_menubutton_);
184
185  // Set up accelerators.
186  AddAccelerator(accel_focus_user_);
187  AddAccelerator(accel_focus_pass_);
188  AddAccelerator(accel_login_off_the_record_);
189  AddAccelerator(accel_toggle_accessibility_);
190
191  OnLocaleChanged();
192
193  // Controller to handle events from textfields
194  username_field_->SetController(this);
195  password_field_->SetController(this);
196  if (!CrosLibrary::Get()->EnsureLoaded()) {
197    EnableInputControls(false);
198  }
199
200  // The 'Sign in' button should be disabled when there is no text in the
201  // username and password fields.
202  sign_in_button_->SetEnabled(false);
203}
204
205bool NewUserView::AcceleratorPressed(const views::Accelerator& accelerator) {
206  if (accelerator == accel_focus_user_) {
207    username_field_->RequestFocus();
208  } else if (accelerator == accel_focus_pass_) {
209    password_field_->RequestFocus();
210  } else if (accelerator == accel_login_off_the_record_) {
211    delegate_->OnLoginOffTheRecord();
212  } else if (accelerator == accel_toggle_accessibility_) {
213    WizardAccessibilityHelper::GetInstance()->ToggleAccessibility(this);
214  } else {
215    return false;
216  }
217  return true;
218}
219
220void NewUserView::RecreatePeculiarControls() {
221  // PreferredSize reported by MenuButton (and TextField) is not able
222  // to shrink, only grow; so recreate on text change.
223  delete languages_menubutton_;
224  languages_menubutton_ = new views::MenuButton(
225      NULL, std::wstring(), &language_switch_menu_, true);
226  languages_menubutton_->set_menu_marker(
227      ResourceBundle::GetSharedInstance().GetBitmapNamed(
228          IDR_MENU_DROPARROW_SHARP));
229  languages_menubutton_->SetEnabledColor(kLanguagesMenuTextColor);
230  languages_menubutton_->SetFocusable(true);
231
232  // There is no way to get native button preferred size after the button was
233  // sized so delete and recreate the button on text update.
234  delete sign_in_button_;
235  sign_in_button_ = new login::WideButton(this, std::wstring());
236  UpdateSignInButtonState();
237
238  if (!CrosLibrary::Get()->EnsureLoaded())
239    sign_in_button_->SetEnabled(false);
240}
241
242void NewUserView::UpdateSignInButtonState() {
243  bool enabled = !username_field_->text().empty() &&
244                 !password_field_->text().empty();
245  sign_in_button_->SetEnabled(enabled);
246}
247
248views::View* NewUserView::CreateSplitter(SkColor color) {
249  views::View* splitter = new views::View();
250  splitter->set_background(views::Background::CreateSolidBackground(color));
251  AddChildView(splitter);
252  return splitter;
253}
254
255void NewUserView::AddChildView(View* view) {
256  // languages_menubutton_ and sign_in_button_ are recreated on text change,
257  // so we restore their original position in layout.
258  if (view == languages_menubutton_) {
259    if (languages_menubutton_order_ < 0) {
260      languages_menubutton_order_ = GetChildViewCount();
261    }
262    views::View::AddChildView(languages_menubutton_order_, view);
263  } else if (view == sign_in_button_) {
264    if (sign_in_button_order_ < 0) {
265      sign_in_button_order_ = GetChildViewCount();
266    }
267    views::View::AddChildView(sign_in_button_order_, view);
268  } else {
269    views::View::AddChildView(view);
270  }
271}
272
273void NewUserView::UpdateLocalizedStrings() {
274  title_label_->SetText(l10n_util::GetString(IDS_LOGIN_TITLE));
275  title_hint_label_->SetText(l10n_util::GetString(IDS_LOGIN_TITLE_HINT));
276  username_field_->set_text_to_display_when_empty(
277      l10n_util::GetStringUTF16(IDS_LOGIN_USERNAME));
278  password_field_->set_text_to_display_when_empty(
279      l10n_util::GetStringUTF16(IDS_LOGIN_PASSWORD));
280  sign_in_button_->SetLabel(l10n_util::GetString(IDS_LOGIN_BUTTON));
281  if (need_create_account_) {
282    create_account_link_->SetText(
283        l10n_util::GetString(IDS_CREATE_ACCOUNT_BUTTON));
284  }
285  if (need_guest_link_) {
286    guest_link_->SetText(
287        l10n_util::GetString(IDS_BROWSE_WITHOUT_SIGNING_IN_BUTTON));
288  }
289  delegate_->ClearErrors();
290  languages_menubutton_->SetText(language_switch_menu_.GetCurrentLocaleName());
291}
292
293void NewUserView::OnLocaleChanged() {
294  RecreatePeculiarControls();
295  UpdateLocalizedStrings();
296  AddChildView(sign_in_button_);
297  AddChildView(languages_menubutton_);
298
299  Layout();
300  SchedulePaint();
301  RequestFocus();
302}
303
304void NewUserView::RequestFocus() {
305  MessageLoop::current()->PostTask(FROM_HERE,
306      focus_grabber_factory_.NewRunnableMethod(
307          &NewUserView::FocusFirstField));
308}
309
310void NewUserView::ViewHierarchyChanged(bool is_add,
311                                       View *parent,
312                                       View *child) {
313  if (is_add && child == this) {
314    MessageLoop::current()->PostTask(FROM_HERE,
315        focus_grabber_factory_.NewRunnableMethod(
316            &NewUserView::FocusFirstField));
317    WizardAccessibilityHelper::GetInstance()->MaybeEnableAccessibility(this);
318  } else if (is_add && (child == username_field_ || child == password_field_)) {
319    MessageLoop::current()->PostTask(FROM_HERE,
320        focus_grabber_factory_.NewRunnableMethod(
321            &NewUserView::Layout));
322  }
323}
324
325void NewUserView::NativeViewHierarchyChanged(bool attached,
326                                             gfx::NativeView native_view,
327                                             views::RootView* root_view) {
328  if (focus_delayed_ && attached) {
329    focus_delayed_ = false;
330    MessageLoop::current()->PostTask(FROM_HERE,
331        focus_grabber_factory_.NewRunnableMethod(
332            &NewUserView::FocusFirstField));
333  }
334}
335
336void NewUserView::FocusFirstField() {
337  if (GetFocusManager()) {
338    if (username_field_->text().empty())
339      username_field_->RequestFocus();
340    else
341      password_field_->RequestFocus();
342  } else {
343    // We are invisible - delay until it is no longer the case.
344    focus_delayed_ = true;
345  }
346}
347
348// Sets the bounds of the view, using x and y as the origin.
349// The width is determined by the min of width and the preferred size
350// of the view, unless force_width is true in which case it is always used.
351// The height is gotten from the preferred size and returned.
352static int setViewBounds(
353    views::View* view, int x, int y, int width, bool force_width) {
354  gfx::Size pref_size = view->GetPreferredSize();
355  if (!force_width) {
356    if (pref_size.width() < width) {
357      width = pref_size.width();
358    }
359  }
360  int height = pref_size.height();
361  view->SetBounds(x, y, width, height);
362  return height;
363}
364
365void NewUserView::Layout() {
366  gfx::Insets insets = GetInsets();
367
368  // Place language selection in top right corner.
369  int x = std::max(0,
370      this->width() - insets.right() -
371          languages_menubutton_->GetPreferredSize().width() - kColumnPad);
372  int y = insets.top() + kLanguagesMenuPad;
373  int width = std::min(this->width() - insets.width() - 2 * kColumnPad,
374                       languages_menubutton_->GetPreferredSize().width());
375  int height = kLanguagesMenuHeight;
376  languages_menubutton_->SetBounds(x, y, width, height);
377  y += height + kTitlePad;
378
379  width = std::min(this->width() - insets.width() - 2 * kColumnPad,
380                   kTextfieldWidth);
381  x = insets.left() + kLeftPad;
382  int max_width = this->width() - x - std::max(insets.right(), x);
383  title_label_->SizeToFit(max_width);
384  title_hint_label_->SizeToFit(max_width);
385
386  // Top align title and title hint.
387  y += setViewBounds(title_label_, x, y, max_width, false);
388  y += setViewBounds(title_hint_label_, x, y, max_width, false);
389  int title_end = y + kTitlePad;
390
391  splitter_up1_->SetBounds(0, title_end, this->width(), kSplitterHeight);
392  splitter_up2_->SetBounds(0, title_end + 1, this->width(), kSplitterHeight);
393
394  // Bottom controls.
395  int links_height = 0;
396  if (need_create_account_)
397    links_height += create_account_link_->GetPreferredSize().height();
398  if (need_guest_link_)
399    links_height += guest_link_->GetPreferredSize().height();
400  if (need_create_account_ && need_guest_link_)
401    links_height += kRowPad;
402  y = this->height() - insets.bottom() - kBottomPad;
403  if (links_height)
404    y -= links_height + kBottomPad;
405  int bottom_start = y;
406
407  splitter_down1_->SetBounds(0, y, this->width(), kSplitterHeight);
408  splitter_down2_->SetBounds(0, y + 1, this->width(), kSplitterHeight);
409
410  if (need_guest_link_) {
411    y -= setViewBounds(guest_link_,
412                       x, y + kBottomPad, max_width, false) + kRowPad;
413  }
414  if (need_create_account_) {
415    y += setViewBounds(create_account_link_, x, y, max_width, false);
416  }
417
418  // Center main controls.
419  height = username_field_->GetPreferredSize().height() +
420           password_field_->GetPreferredSize().height() +
421           sign_in_button_->GetPreferredSize().height() +
422           2 * kRowPad;
423  y = title_end + (bottom_start - title_end - height) / 2;
424
425  y += setViewBounds(username_field_, x, y, width, true) + kRowPad;
426  y += setViewBounds(password_field_, x, y, width, true) + kRowPad;
427
428  int sign_in_button_width = sign_in_button_->GetPreferredSize().width();
429  setViewBounds(sign_in_button_, x, y, sign_in_button_width,true);
430
431  SchedulePaint();
432}
433
434gfx::Size NewUserView::GetPreferredSize() {
435  return need_guest_link_ ?
436      gfx::Size(kNewUserPodFullWidth, kNewUserPodFullHeight) :
437      gfx::Size(kNewUserPodSmallWidth, kNewUserPodSmallHeight);
438}
439
440void NewUserView::SetUsername(const std::string& username) {
441  username_field_->SetText(UTF8ToUTF16(username));
442}
443
444void NewUserView::SetPassword(const std::string& password) {
445  password_field_->SetText(UTF8ToUTF16(password));
446}
447
448void NewUserView::Login() {
449  if (login_in_process_ || username_field_->text().empty())
450    return;
451
452  login_in_process_ = true;
453  std::string username = UTF16ToUTF8(username_field_->text());
454  // todo(cmasone) Need to sanitize memory used to store password.
455  std::string password = UTF16ToUTF8(password_field_->text());
456
457  if (username.find('@') == std::string::npos) {
458    username += kDefaultDomain;
459    username_field_->SetText(UTF8ToUTF16(username));
460  }
461
462  delegate_->OnLogin(username, password);
463}
464
465// Sign in button causes a login attempt.
466void NewUserView::ButtonPressed(views::Button* sender,
467                                const views::Event& event) {
468  DCHECK(sender == sign_in_button_);
469  Login();
470}
471
472void NewUserView::LinkActivated(views::Link* source, int event_flags) {
473  if (source == create_account_link_) {
474    delegate_->OnCreateAccount();
475  } else if (source == guest_link_) {
476    delegate_->OnLoginOffTheRecord();
477  }
478}
479
480void NewUserView::ClearAndFocusControls() {
481  login_in_process_ = false;
482  SetUsername(std::string());
483  SetPassword(std::string());
484  username_field_->RequestFocus();
485}
486
487void NewUserView::ClearAndFocusPassword() {
488  login_in_process_ = false;
489  SetPassword(std::string());
490  password_field_->RequestFocus();
491}
492
493gfx::Rect NewUserView::GetMainInputScreenBounds() const {
494  return GetUsernameBounds();
495}
496
497gfx::Rect NewUserView::CalculateThrobberBounds(views::Throbber* throbber) {
498  DCHECK(password_field_);
499  DCHECK(sign_in_button_);
500
501  gfx::Size throbber_size = throbber->GetPreferredSize();
502  int x = password_field_->x();
503  x += password_field_->width() - throbber_size.width();
504  int y = sign_in_button_->y();
505  y += (sign_in_button_->height() - throbber_size.height()) / 2;
506
507  return gfx::Rect(gfx::Point(x, y), throbber_size);
508}
509
510gfx::Rect NewUserView::GetPasswordBounds() const {
511  return password_field_->GetScreenBounds();
512}
513
514gfx::Rect NewUserView::GetUsernameBounds() const {
515  return username_field_->GetScreenBounds();
516}
517
518bool NewUserView::HandleKeystroke(views::Textfield* s,
519    const views::Textfield::Keystroke& keystroke) {
520  if (!CrosLibrary::Get()->EnsureLoaded() || login_in_process_)
521    return false;
522
523  if (keystroke.GetKeyboardCode() == app::VKEY_RETURN) {
524    if (!username_field_->text().empty() && !password_field_->text().empty())
525      Login();
526    // Return true so that processing ends
527    return true;
528  }
529  delegate_->ClearErrors();
530  // Return false so that processing does not end
531  return false;
532}
533
534void NewUserView::ContentsChanged(views::Textfield* sender,
535                                  const string16& new_contents) {
536  UpdateSignInButtonState();
537}
538
539void NewUserView::EnableInputControls(bool enabled) {
540  languages_menubutton_->SetEnabled(enabled);
541  username_field_->SetEnabled(enabled);
542  password_field_->SetEnabled(enabled);
543  sign_in_button_->SetEnabled(enabled);
544  if (need_create_account_) {
545    create_account_link_->SetEnabled(enabled);
546  }
547  if (need_guest_link_) {
548    guest_link_->SetEnabled(enabled);
549  }
550}
551
552bool NewUserView::NavigateAway() {
553  if (username_field_->text().empty() &&
554      password_field_->text().empty()) {
555    delegate_->NavigateAway();
556    return true;
557  } else {
558    return false;
559  }
560}
561
562void NewUserView::InitLink(views::Link** link) {
563  *link = new views::Link(std::wstring());
564  (*link)->SetController(this);
565  (*link)->SetNormalColor(login::kLinkColor);
566  (*link)->SetHighlightedColor(login::kLinkColor);
567  AddChildView(*link);
568}
569
570}  // namespace chromeos
571