blob: 8b91c3ed28c8c0ac5f09853af52f83bff0369e88 [file] [log] [blame]
// Copyright 2017 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.
#include "ash/login/ui/login_password_view.h"
#include "ash/login/ui/horizontal_image_sequence_animation_decoder.h"
#include "ash/login/ui/hover_notifier.h"
#include "ash/login/ui/lock_screen.h"
#include "ash/login/ui/login_button.h"
#include "ash/login/ui/non_accessible_view.h"
#include "ash/public/cpp/login_constants.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/timer.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
// TODO(jdufault): On two user view the password prompt is visible to
// accessibility using special navigation keys even though it is invisible. We
// probably need to customize the text box quite a bit, we may want to do
// something similar to SearchBoxView.
namespace ash {
namespace {
// How long the user must hover over the easy unlock icon before showing the
// tooltip.
const int kDelayBeforeShowingTooltipMs = 500;
// Margin above/below the password view.
constexpr const int kMarginAboveBelowPasswordIconsDp = 8;
// Total width of the password view.
constexpr int kPasswordTotalWidthDp = 204;
// Size (width/height) of the submit button.
constexpr int kSubmitButtonSizeDp = 20;
// Size (width/height) of the caps lock hint icon.
constexpr int kCapsLockIconSizeDp = 20;
// Width and height of the easy unlock icon.
constexpr const int kEasyUnlockIconSizeDp = 20;
// Horizontal distance/margin between the easy unlock icon and the start of
// the password view.
constexpr const int kHorizontalDistanceBetweenEasyUnlockAndPasswordDp = 12;
// Non-empty height, useful for debugging/visualization.
constexpr const int kNonEmptyHeight = 1;
constexpr const char kLoginPasswordViewName[] = "LoginPasswordView";
class NonAccessibleSeparator : public views::Separator {
public:
NonAccessibleSeparator() = default;
~NonAccessibleSeparator() override = default;
// views::Separator:
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
views::Separator::GetAccessibleNodeData(node_data);
node_data->AddState(ax::mojom::State::kInvisible);
}
private:
DISALLOW_COPY_AND_ASSIGN(NonAccessibleSeparator);
};
// A textfield that selects all text on focus.
class LoginTextfield : public views::Textfield {
public:
LoginTextfield() {}
~LoginTextfield() override {}
// views::Textfield:
void OnFocus() override {
views::Textfield::OnFocus();
SelectAll(false /*reverse*/);
}
private:
DISALLOW_COPY_AND_ASSIGN(LoginTextfield);
};
// Set of resources for an easy unlock icon.
struct IconBundle {
// Creates an IconBundle for a static image.
IconBundle(int normal, int hover, int pressed)
: normal(normal), hover(hover), pressed(pressed) {}
// Creates an IconBundle instance for an animation.
IconBundle(int resource, base::TimeDelta duration, int num_frames)
: normal(resource),
hover(resource),
pressed(resource),
duration(duration),
num_frames(num_frames) {}
// Icons for different button states.
const int normal;
const int hover;
const int pressed;
// Animation metadata. If these are set then |normal| == |hover| == |pressed|.
const base::TimeDelta duration;
const int num_frames = 0;
};
// Construct an IconBundle instance for a given mojom::EasyUnlockIconId value.
IconBundle GetEasyUnlockResources(mojom::EasyUnlockIconId id) {
switch (id) {
case mojom::EasyUnlockIconId::NONE:
break;
case mojom::EasyUnlockIconId::HARDLOCKED:
return IconBundle(IDR_EASY_UNLOCK_HARDLOCKED,
IDR_EASY_UNLOCK_HARDLOCKED_HOVER,
IDR_EASY_UNLOCK_HARDLOCKED_PRESSED);
case mojom::EasyUnlockIconId::LOCKED:
return IconBundle(IDR_EASY_UNLOCK_LOCKED, IDR_EASY_UNLOCK_LOCKED_HOVER,
IDR_EASY_UNLOCK_LOCKED_PRESSED);
case mojom::EasyUnlockIconId::LOCKED_TO_BE_ACTIVATED:
return IconBundle(IDR_EASY_UNLOCK_LOCKED_TO_BE_ACTIVATED,
IDR_EASY_UNLOCK_LOCKED_TO_BE_ACTIVATED_HOVER,
IDR_EASY_UNLOCK_LOCKED_TO_BE_ACTIVATED_PRESSED);
case mojom::EasyUnlockIconId::LOCKED_WITH_PROXIMITY_HINT:
return IconBundle(IDR_EASY_UNLOCK_LOCKED_WITH_PROXIMITY_HINT,
IDR_EASY_UNLOCK_LOCKED_WITH_PROXIMITY_HINT_HOVER,
IDR_EASY_UNLOCK_LOCKED_WITH_PROXIMITY_HINT_PRESSED);
case mojom::EasyUnlockIconId::UNLOCKED:
return IconBundle(IDR_EASY_UNLOCK_UNLOCKED,
IDR_EASY_UNLOCK_UNLOCKED_HOVER,
IDR_EASY_UNLOCK_UNLOCKED_PRESSED);
case mojom::EasyUnlockIconId::SPINNER:
return IconBundle(IDR_EASY_UNLOCK_SPINNER,
base::TimeDelta::FromSeconds(2), 45 /*num_frames*/);
}
NOTREACHED();
return IconBundle(IDR_EASY_UNLOCK_LOCKED, IDR_EASY_UNLOCK_LOCKED_HOVER,
IDR_EASY_UNLOCK_LOCKED_PRESSED);
}
} // namespace
class LoginPasswordView::EasyUnlockIcon : public views::Button,
public views::ButtonListener {
public:
EasyUnlockIcon(const gfx::Size& size, int corner_radius)
: views::Button(this) {
SetPreferredSize(size);
SetLayoutManager(std::make_unique<views::FillLayout>());
icon_ = new AnimatedRoundedImageView(size, corner_radius);
AddChildView(icon_);
}
~EasyUnlockIcon() override = default;
void Init(const OnEasyUnlockIconHovered& on_hovered,
const OnEasyUnlockIconTapped& on_tapped) {
DCHECK(on_hovered);
DCHECK(on_tapped);
on_hovered_ = on_hovered;
on_tapped_ = on_tapped;
hover_notifier_ = std::make_unique<HoverNotifier>(
this,
base::Bind(&LoginPasswordView::EasyUnlockIcon::OnHoverStateChanged,
base::Unretained(this)));
}
void SetEasyUnlockIcon(mojom::EasyUnlockIconId icon_id,
const base::string16& accessibility_label) {
bool changed_states = icon_id != icon_id_;
icon_id_ = icon_id;
UpdateImage(changed_states);
SetAccessibleName(accessibility_label);
}
void set_immediately_hover_for_test() { immediately_hover_for_test_ = true; }
// views::Button:
void StateChanged(ButtonState old_state) override {
// Stop showing tooltip, as we most likely exited hover state.
invoke_hover_.Stop();
switch (state()) {
case ButtonState::STATE_NORMAL:
UpdateImage(false /*changed_states*/);
break;
case ButtonState::STATE_HOVERED:
UpdateImage(false /*changed_states*/);
if (immediately_hover_for_test_) {
on_hovered_.Run();
} else {
invoke_hover_.Start(
FROM_HERE,
base::TimeDelta::FromMilliseconds(kDelayBeforeShowingTooltipMs),
on_hovered_);
}
break;
case ButtonState::STATE_PRESSED:
UpdateImage(false /*changed_states*/);
break;
case ButtonState::STATE_DISABLED:
break;
case ButtonState::STATE_COUNT:
break;
}
}
// views::ButtonListener:
void ButtonPressed(Button* sender, const ui::Event& event) override {
on_tapped_.Run();
}
private:
void OnHoverStateChanged(bool has_hover) {
SetState(has_hover ? ButtonState::STATE_HOVERED
: ButtonState::STATE_NORMAL);
}
void UpdateImage(bool changed_states) {
// Ignore any calls happening while the view is not attached;
// IsMouseHovered() will CHECK(false) in that scenario. GetWidget() may be
// null during construction and GetWidget()->GetRootView() may be null
// during destruction. Both scenarios only happen in tests.
if (!GetWidget() || !GetWidget()->GetRootView())
return;
if (icon_id_ == mojom::EasyUnlockIconId::NONE)
return;
IconBundle resources = GetEasyUnlockResources(icon_id_);
int active_resource = resources.normal;
if (IsMouseHovered())
active_resource = resources.hover;
if (state() == ButtonState::STATE_PRESSED)
active_resource = resources.pressed;
// Image to show. It may or may not be an animation, depending on
// |resources.duration|.
gfx::ImageSkia* image =
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
active_resource);
if (!resources.duration.is_zero()) {
// Only change the animation if the state itself has changed, otherwise
// the active animation frame is reset and there is a lot of unecessary
// decoding/image resizing work. This optimization only is valid only if
// all three resource assets are the same.
DCHECK_EQ(resources.normal, resources.hover);
DCHECK_EQ(resources.normal, resources.pressed);
if (changed_states) {
icon_->SetAnimationDecoder(
std::make_unique<HorizontalImageSequenceAnimationDecoder>(
*image, resources.duration, resources.num_frames),
AnimatedRoundedImageView::Playback::kRepeat);
}
} else {
icon_->SetImage(*image);
}
}
// Icon we are currently displaying.
mojom::EasyUnlockIconId icon_id_ = mojom::EasyUnlockIconId::NONE;
// View which renders the icon.
AnimatedRoundedImageView* icon_;
// Callbacks run when icon is hovered or tapped.
OnEasyUnlockIconHovered on_hovered_;
OnEasyUnlockIconTapped on_tapped_;
std::unique_ptr<HoverNotifier> hover_notifier_;
// Timer used to control when we invoke |on_hover_|.
base::OneShotTimer invoke_hover_;
// If true, the tooltip/hover timer will be skipped and |on_hover_| will be
// run immediately.
bool immediately_hover_for_test_ = false;
DISALLOW_COPY_AND_ASSIGN(EasyUnlockIcon);
};
LoginPasswordView::TestApi::TestApi(LoginPasswordView* view) : view_(view) {}
LoginPasswordView::TestApi::~TestApi() = default;
void LoginPasswordView::TestApi::SubmitPassword(const std::string& password) {
view_->textfield_->SetText(base::ASCIIToUTF16(password));
view_->UpdateUiState();
view_->SubmitPassword();
}
views::Textfield* LoginPasswordView::TestApi::textfield() const {
return view_->textfield_;
}
views::View* LoginPasswordView::TestApi::submit_button() const {
return view_->submit_button_;
}
views::View* LoginPasswordView::TestApi::easy_unlock_icon() const {
return view_->easy_unlock_icon_;
}
void LoginPasswordView::TestApi::set_immediately_hover_easy_unlock_icon() {
view_->easy_unlock_icon_->set_immediately_hover_for_test();
}
LoginPasswordView::LoginPasswordView() {
Shell::Get()->ime_controller()->AddObserver(this);
auto* root_layout = SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
root_layout->set_main_axis_alignment(
views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER);
password_row_ = new NonAccessibleView();
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal,
gfx::Insets(kMarginAboveBelowPasswordIconsDp, 0));
layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER);
layout->set_cross_axis_alignment(
views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
auto* layout_ptr = password_row_->SetLayoutManager(std::move(layout));
AddChildView(password_row_);
// Add easy unlock icon.
easy_unlock_icon_ = new EasyUnlockIcon(
gfx::Size(kEasyUnlockIconSizeDp, kEasyUnlockIconSizeDp),
0 /*corner_radius*/);
password_row_->AddChildView(easy_unlock_icon_);
easy_unlock_right_margin_ = new NonAccessibleView();
easy_unlock_right_margin_->SetPreferredSize(gfx::Size(
kHorizontalDistanceBetweenEasyUnlockAndPasswordDp, kNonEmptyHeight));
password_row_->AddChildView(easy_unlock_right_margin_);
// Easy unlock starts invisible. There will be an event later to show it if
// needed.
easy_unlock_icon_->SetVisible(false);
easy_unlock_right_margin_->SetVisible(false);
// Password textfield. We control the textfield size by sizing the parent
// view, as the textfield will expand to fill it.
textfield_ = new LoginTextfield();
textfield_->set_controller(this);
textfield_->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
textfield_->SetTextColor(login_constants::kAuthMethodsTextColor);
textfield_->SetFontList(views::Textfield::GetDefaultFontList().Derive(
5, gfx::Font::FontStyle::NORMAL, gfx::Font::Weight::NORMAL));
textfield_->set_placeholder_font_list(views::Textfield::GetDefaultFontList());
textfield_->set_placeholder_text_color(
login_constants::kAuthMethodsTextColor);
textfield_->SetGlyphSpacing(6);
textfield_->SetBorder(nullptr);
textfield_->SetBackgroundColor(SK_ColorTRANSPARENT);
password_row_->AddChildView(textfield_);
layout_ptr->SetFlexForView(textfield_, 1);
// Caps lock hint icon.
capslock_icon_ = new views::ImageView();
capslock_icon_->SetImage(gfx::CreateVectorIcon(
kLockScreenCapsLockIcon, kCapsLockIconSizeDp,
SkColorSetA(login_constants::kButtonEnabledColor,
login_constants::kButtonDisabledAlpha)));
password_row_->AddChildView(capslock_icon_);
// Caps lock hint starts invisible. This constructor will call
// OnCapsLockChanged with the actual caps lock state.
capslock_icon_->SetVisible(false);
// Submit button.
submit_button_ = new LoginButton(this);
submit_button_->SetImage(
views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(kLockScreenArrowIcon, kSubmitButtonSizeDp,
login_constants::kButtonEnabledColor));
submit_button_->SetImage(
views::Button::STATE_DISABLED,
gfx::CreateVectorIcon(
kLockScreenArrowIcon, kSubmitButtonSizeDp,
SkColorSetA(login_constants::kButtonEnabledColor,
login_constants::kButtonDisabledAlpha)));
submit_button_->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ASH_LOGIN_SUBMIT_BUTTON_ACCESSIBLE_NAME));
password_row_->AddChildView(submit_button_);
// Separator on bottom.
separator_ = new NonAccessibleSeparator();
AddChildView(separator_);
// Make sure the textfield always starts with focus.
textfield_->RequestFocus();
// Initialize with the initial state of caps lock.
OnCapsLockChanged(Shell::Get()->ime_controller()->IsCapsLockEnabled());
// Make sure the UI start with the correct states.
UpdateUiState();
}
LoginPasswordView::~LoginPasswordView() {
Shell::Get()->ime_controller()->RemoveObserver(this);
}
void LoginPasswordView::Init(
const OnPasswordSubmit& on_submit,
const OnPasswordTextChanged& on_password_text_changed,
const OnEasyUnlockIconHovered& on_easy_unlock_icon_hovered,
const OnEasyUnlockIconTapped& on_easy_unlock_icon_tapped) {
DCHECK(on_submit);
DCHECK(on_password_text_changed);
on_submit_ = on_submit;
on_password_text_changed_ = on_password_text_changed;
easy_unlock_icon_->Init(on_easy_unlock_icon_hovered,
on_easy_unlock_icon_tapped);
}
void LoginPasswordView::SetEnabledOnEmptyPassword(bool enabled) {
enabled_on_empty_password_ = enabled;
UpdateUiState();
}
void LoginPasswordView::SetEasyUnlockIcon(
mojom::EasyUnlockIconId id,
const base::string16& accessibility_label) {
// Update icon.
easy_unlock_icon_->SetEasyUnlockIcon(id, accessibility_label);
// Update icon visiblity.
bool has_icon = id != mojom::EasyUnlockIconId::NONE;
easy_unlock_icon_->SetVisible(has_icon);
easy_unlock_right_margin_->SetVisible(has_icon);
password_row_->Layout();
}
void LoginPasswordView::UpdateForUser(const mojom::LoginUserInfoPtr& user) {
textfield_->SetAccessibleName(l10n_util::GetStringFUTF16(
IDS_ASH_LOGIN_POD_PASSWORD_FIELD_ACCESSIBLE_NAME,
base::UTF8ToUTF16(user->basic_user_info->display_email)));
}
void LoginPasswordView::SetFocusEnabledForChildViews(bool enable) {
auto behavior = enable ? FocusBehavior::ALWAYS : FocusBehavior::NEVER;
textfield_->SetFocusBehavior(behavior);
}
void LoginPasswordView::Clear() {
textfield_->SetText(base::string16());
// |ContentsChanged| won't be called by |Textfield| if the text is changed
// by |Textfield::SetText()|.
ContentsChanged(textfield_, textfield_->text());
}
void LoginPasswordView::InsertNumber(int value) {
textfield_->InsertOrReplaceText(base::NumberToString16(value));
}
void LoginPasswordView::Backspace() {
// Instead of just adjusting textfield_ text directly, fire a backspace key
// event as this handles the various edge cases (ie, selected text).
// views::Textfield::OnKeyPressed is private, so we call it via views::View.
auto* view = static_cast<views::View*>(textfield_);
view->OnKeyPressed(ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_BACK,
ui::DomCode::BACKSPACE, ui::EF_NONE));
view->OnKeyPressed(ui::KeyEvent(ui::ET_KEY_RELEASED, ui::VKEY_BACK,
ui::DomCode::BACKSPACE, ui::EF_NONE));
}
void LoginPasswordView::SetPlaceholderText(
const base::string16& placeholder_text) {
textfield_->set_placeholder_text(placeholder_text);
SchedulePaint();
}
void LoginPasswordView::SetReadOnly(bool read_only) {
textfield_->SetReadOnly(read_only);
textfield_->SetCursorEnabled(!read_only);
UpdateUiState();
}
const char* LoginPasswordView::GetClassName() const {
return kLoginPasswordViewName;
}
gfx::Size LoginPasswordView::CalculatePreferredSize() const {
gfx::Size size = views::View::CalculatePreferredSize();
size.set_width(kPasswordTotalWidthDp);
return size;
}
void LoginPasswordView::RequestFocus() {
textfield_->RequestFocus();
}
bool LoginPasswordView::OnKeyPressed(const ui::KeyEvent& event) {
if (event.key_code() == ui::KeyboardCode::VKEY_RETURN &&
submit_button_->enabled()) {
SubmitPassword();
return true;
}
return false;
}
void LoginPasswordView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
DCHECK_EQ(sender, submit_button_);
SubmitPassword();
}
void LoginPasswordView::ContentsChanged(views::Textfield* sender,
const base::string16& new_contents) {
DCHECK_EQ(sender, textfield_);
UpdateUiState();
on_password_text_changed_.Run(new_contents.empty() /*is_empty*/);
}
// Implements swapping active user with arrow keys
bool LoginPasswordView::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
// Treat the password field as normal if it has text
if (!textfield_->text().empty())
return false;
if (key_event.type() != ui::ET_KEY_PRESSED)
return false;
if (key_event.is_repeat())
return false;
switch (key_event.key_code()) {
case ui::VKEY_LEFT:
LockScreen::Get()->FocusPreviousUser();
break;
case ui::VKEY_RIGHT:
LockScreen::Get()->FocusNextUser();
break;
default:
return false;
}
return true;
}
void LoginPasswordView::UpdateUiState() {
bool is_enabled = !textfield_->read_only() &&
(enabled_on_empty_password_ || !textfield_->text().empty());
submit_button_->SetEnabled(is_enabled);
SkColor color = is_enabled
? login_constants::kButtonEnabledColor
: SkColorSetA(login_constants::kButtonEnabledColor,
login_constants::kButtonDisabledAlpha);
separator_->SetColor(color);
capslock_icon_->SetImage(gfx::CreateVectorIcon(kLockScreenCapsLockIcon,
kCapsLockIconSizeDp, color));
}
void LoginPasswordView::OnCapsLockChanged(bool enabled) {
capslock_icon_->SetVisible(enabled);
password_row_->Layout();
}
void LoginPasswordView::SubmitPassword() {
DCHECK(submit_button_->enabled());
if (textfield_->read_only())
return;
on_submit_.Run(textfield_->text());
}
} // namespace ash