jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 1 | // Copyright 2017 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 "ash/login/ui/login_password_view.h" |
| 6 | |
Sarah Hu | e16b6ef | 2018-08-06 17:50:22 | [diff] [blame] | 7 | #include "ash/login/ui/horizontal_image_sequence_animation_decoder.h" |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 8 | #include "ash/login/ui/hover_notifier.h" |
Quan Nguyen | 59b4cd57 | 2018-07-10 20:49:23 | [diff] [blame] | 9 | #include "ash/login/ui/lock_screen.h" |
Sarah Hu | a93125b | 2017-10-30 19:18:18 | [diff] [blame] | 10 | #include "ash/login/ui/login_button.h" |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 11 | #include "ash/login/ui/non_accessible_view.h" |
James Cook | ab528d0 | 2018-01-09 00:06:08 | [diff] [blame] | 12 | #include "ash/public/cpp/login_constants.h" |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 13 | #include "ash/resources/vector_icons/vector_icons.h" |
Darren Shen | 7db1603 | 2018-03-06 21:02:33 | [diff] [blame] | 14 | #include "ash/shell.h" |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 15 | #include "ash/strings/grit/ash_strings.h" |
Jacob Dufault | 8b152b7 | 2017-06-16 17:52:25 | [diff] [blame] | 16 | #include "base/strings/string_number_conversions.h" |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 17 | #include "base/strings/utf_string_conversions.h" |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 18 | #include "ui/accessibility/ax_node_data.h" |
| 19 | #include "ui/base/l10n/l10n_util.h" |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 20 | #include "ui/base/resource/resource_bundle.h" |
Jacob Dufault | 8b152b7 | 2017-06-16 17:52:25 | [diff] [blame] | 21 | #include "ui/events/event_constants.h" |
| 22 | #include "ui/events/keycodes/dom/dom_code.h" |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 23 | #include "ui/gfx/paint_vector_icon.h" |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 24 | #include "ui/resources/grit/ui_resources.h" |
Sarah Hu | 745c82a | 2017-07-14 23:21:58 | [diff] [blame] | 25 | #include "ui/views/background.h" |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 26 | #include "ui/views/border.h" |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 27 | #include "ui/views/controls/button/image_button.h" |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 28 | #include "ui/views/controls/image_view.h" |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 29 | #include "ui/views/controls/separator.h" |
| 30 | #include "ui/views/controls/textfield/textfield.h" |
| 31 | #include "ui/views/layout/box_layout.h" |
| 32 | #include "ui/views/layout/fill_layout.h" |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 33 | #include "ui/views/widget/widget.h" |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 34 | |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 35 | // TODO(jdufault): On two user view the password prompt is visible to |
| 36 | // accessibility using special navigation keys even though it is invisible. We |
| 37 | // probably need to customize the text box quite a bit, we may want to do |
| 38 | // something similar to SearchBoxView. |
| 39 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 40 | namespace ash { |
| 41 | namespace { |
| 42 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 43 | // How long the user must hover over the easy unlock icon before showing the |
| 44 | // tooltip. |
| 45 | const int kDelayBeforeShowingTooltipMs = 500; |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 46 | |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 47 | // Margin above/below the password view. |
| 48 | constexpr const int kMarginAboveBelowPasswordIconsDp = 8; |
| 49 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 50 | // Total width of the password view. |
| 51 | constexpr int kPasswordTotalWidthDp = 204; |
| 52 | |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 53 | // Size (width/height) of the submit button. |
| 54 | constexpr int kSubmitButtonSizeDp = 20; |
| 55 | |
| 56 | // Size (width/height) of the caps lock hint icon. |
| 57 | constexpr int kCapsLockIconSizeDp = 20; |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 58 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 59 | // Width and height of the easy unlock icon. |
Jacob Dufault | 525a1fd1 | 2017-10-20 17:00:35 | [diff] [blame] | 60 | constexpr const int kEasyUnlockIconSizeDp = 20; |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 61 | |
| 62 | // Horizontal distance/margin between the easy unlock icon and the start of |
| 63 | // the password view. |
| 64 | constexpr const int kHorizontalDistanceBetweenEasyUnlockAndPasswordDp = 12; |
| 65 | |
| 66 | // Non-empty height, useful for debugging/visualization. |
| 67 | constexpr const int kNonEmptyHeight = 1; |
| 68 | |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 69 | constexpr const char kLoginPasswordViewName[] = "LoginPasswordView"; |
| 70 | |
| 71 | class NonAccessibleSeparator : public views::Separator { |
| 72 | public: |
| 73 | NonAccessibleSeparator() = default; |
| 74 | ~NonAccessibleSeparator() override = default; |
| 75 | |
| 76 | // views::Separator: |
| 77 | void GetAccessibleNodeData(ui::AXNodeData* node_data) override { |
| 78 | views::Separator::GetAccessibleNodeData(node_data); |
Dominic Mazzoni | dcef1b73 | 2018-01-26 17:57:04 | [diff] [blame] | 79 | node_data->AddState(ax::mojom::State::kInvisible); |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 80 | } |
| 81 | |
| 82 | private: |
| 83 | DISALLOW_COPY_AND_ASSIGN(NonAccessibleSeparator); |
| 84 | }; |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 85 | |
Jacob Dufault | 9e2a640 | 2017-12-22 05:40:02 | [diff] [blame] | 86 | // A textfield that selects all text on focus. |
| 87 | class LoginTextfield : public views::Textfield { |
| 88 | public: |
| 89 | LoginTextfield() {} |
| 90 | ~LoginTextfield() override {} |
| 91 | |
| 92 | // views::Textfield: |
| 93 | void OnFocus() override { |
| 94 | views::Textfield::OnFocus(); |
| 95 | SelectAll(false /*reverse*/); |
| 96 | } |
| 97 | |
| 98 | private: |
| 99 | DISALLOW_COPY_AND_ASSIGN(LoginTextfield); |
| 100 | }; |
| 101 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 102 | // Set of resources for an easy unlock icon. |
| 103 | struct IconBundle { |
| 104 | // Creates an IconBundle for a static image. |
| 105 | IconBundle(int normal, int hover, int pressed) |
| 106 | : normal(normal), hover(hover), pressed(pressed) {} |
| 107 | // Creates an IconBundle instance for an animation. |
| 108 | IconBundle(int resource, base::TimeDelta duration, int num_frames) |
| 109 | : normal(resource), |
| 110 | hover(resource), |
| 111 | pressed(resource), |
| 112 | duration(duration), |
| 113 | num_frames(num_frames) {} |
| 114 | |
| 115 | // Icons for different button states. |
| 116 | const int normal; |
| 117 | const int hover; |
| 118 | const int pressed; |
| 119 | |
| 120 | // Animation metadata. If these are set then |normal| == |hover| == |pressed|. |
| 121 | const base::TimeDelta duration; |
| 122 | const int num_frames = 0; |
| 123 | }; |
| 124 | |
| 125 | // Construct an IconBundle instance for a given mojom::EasyUnlockIconId value. |
| 126 | IconBundle GetEasyUnlockResources(mojom::EasyUnlockIconId id) { |
| 127 | switch (id) { |
| 128 | case mojom::EasyUnlockIconId::NONE: |
| 129 | break; |
| 130 | case mojom::EasyUnlockIconId::HARDLOCKED: |
| 131 | return IconBundle(IDR_EASY_UNLOCK_HARDLOCKED, |
| 132 | IDR_EASY_UNLOCK_HARDLOCKED_HOVER, |
| 133 | IDR_EASY_UNLOCK_HARDLOCKED_PRESSED); |
| 134 | case mojom::EasyUnlockIconId::LOCKED: |
| 135 | return IconBundle(IDR_EASY_UNLOCK_LOCKED, IDR_EASY_UNLOCK_LOCKED_HOVER, |
| 136 | IDR_EASY_UNLOCK_LOCKED_PRESSED); |
| 137 | case mojom::EasyUnlockIconId::LOCKED_TO_BE_ACTIVATED: |
| 138 | return IconBundle(IDR_EASY_UNLOCK_LOCKED_TO_BE_ACTIVATED, |
| 139 | IDR_EASY_UNLOCK_LOCKED_TO_BE_ACTIVATED_HOVER, |
| 140 | IDR_EASY_UNLOCK_LOCKED_TO_BE_ACTIVATED_PRESSED); |
| 141 | case mojom::EasyUnlockIconId::LOCKED_WITH_PROXIMITY_HINT: |
| 142 | return IconBundle(IDR_EASY_UNLOCK_LOCKED_WITH_PROXIMITY_HINT, |
| 143 | IDR_EASY_UNLOCK_LOCKED_WITH_PROXIMITY_HINT_HOVER, |
| 144 | IDR_EASY_UNLOCK_LOCKED_WITH_PROXIMITY_HINT_PRESSED); |
| 145 | case mojom::EasyUnlockIconId::UNLOCKED: |
| 146 | return IconBundle(IDR_EASY_UNLOCK_UNLOCKED, |
| 147 | IDR_EASY_UNLOCK_UNLOCKED_HOVER, |
| 148 | IDR_EASY_UNLOCK_UNLOCKED_PRESSED); |
| 149 | case mojom::EasyUnlockIconId::SPINNER: |
| 150 | return IconBundle(IDR_EASY_UNLOCK_SPINNER, |
| 151 | base::TimeDelta::FromSeconds(2), 45 /*num_frames*/); |
Lei Zhang | a2c5a9e2 | 2017-10-25 04:13:54 | [diff] [blame] | 152 | } |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 153 | |
| 154 | NOTREACHED(); |
| 155 | return IconBundle(IDR_EASY_UNLOCK_LOCKED, IDR_EASY_UNLOCK_LOCKED_HOVER, |
| 156 | IDR_EASY_UNLOCK_LOCKED_PRESSED); |
| 157 | } |
| 158 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 159 | } // namespace |
| 160 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 161 | class LoginPasswordView::EasyUnlockIcon : public views::Button, |
| 162 | public views::ButtonListener { |
| 163 | public: |
| 164 | EasyUnlockIcon(const gfx::Size& size, int corner_radius) |
| 165 | : views::Button(this) { |
| 166 | SetPreferredSize(size); |
Brett Wilson | 501d664 | 2017-12-14 17:58:18 | [diff] [blame] | 167 | SetLayoutManager(std::make_unique<views::FillLayout>()); |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 168 | icon_ = new AnimatedRoundedImageView(size, corner_radius); |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 169 | AddChildView(icon_); |
| 170 | } |
| 171 | ~EasyUnlockIcon() override = default; |
| 172 | |
| 173 | void Init(const OnEasyUnlockIconHovered& on_hovered, |
| 174 | const OnEasyUnlockIconTapped& on_tapped) { |
| 175 | DCHECK(on_hovered); |
| 176 | DCHECK(on_tapped); |
| 177 | |
| 178 | on_hovered_ = on_hovered; |
| 179 | on_tapped_ = on_tapped; |
| 180 | |
| 181 | hover_notifier_ = std::make_unique<HoverNotifier>( |
| 182 | this, |
| 183 | base::Bind(&LoginPasswordView::EasyUnlockIcon::OnHoverStateChanged, |
| 184 | base::Unretained(this))); |
| 185 | } |
| 186 | |
| 187 | void SetEasyUnlockIcon(mojom::EasyUnlockIconId icon_id, |
| 188 | const base::string16& accessibility_label) { |
| 189 | bool changed_states = icon_id != icon_id_; |
| 190 | icon_id_ = icon_id; |
| 191 | UpdateImage(changed_states); |
| 192 | SetAccessibleName(accessibility_label); |
| 193 | } |
| 194 | |
| 195 | void set_immediately_hover_for_test() { immediately_hover_for_test_ = true; } |
| 196 | |
| 197 | // views::Button: |
| 198 | void StateChanged(ButtonState old_state) override { |
| 199 | // Stop showing tooltip, as we most likely exited hover state. |
| 200 | invoke_hover_.Stop(); |
| 201 | |
| 202 | switch (state()) { |
| 203 | case ButtonState::STATE_NORMAL: |
| 204 | UpdateImage(false /*changed_states*/); |
| 205 | break; |
| 206 | case ButtonState::STATE_HOVERED: |
| 207 | UpdateImage(false /*changed_states*/); |
| 208 | if (immediately_hover_for_test_) { |
| 209 | on_hovered_.Run(); |
| 210 | } else { |
| 211 | invoke_hover_.Start( |
| 212 | FROM_HERE, |
| 213 | base::TimeDelta::FromMilliseconds(kDelayBeforeShowingTooltipMs), |
| 214 | on_hovered_); |
| 215 | } |
| 216 | break; |
| 217 | case ButtonState::STATE_PRESSED: |
| 218 | UpdateImage(false /*changed_states*/); |
| 219 | break; |
| 220 | case ButtonState::STATE_DISABLED: |
| 221 | break; |
| 222 | case ButtonState::STATE_COUNT: |
| 223 | break; |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | // views::ButtonListener: |
| 228 | void ButtonPressed(Button* sender, const ui::Event& event) override { |
| 229 | on_tapped_.Run(); |
| 230 | } |
| 231 | |
| 232 | private: |
| 233 | void OnHoverStateChanged(bool has_hover) { |
| 234 | SetState(has_hover ? ButtonState::STATE_HOVERED |
| 235 | : ButtonState::STATE_NORMAL); |
| 236 | } |
| 237 | |
| 238 | void UpdateImage(bool changed_states) { |
| 239 | // Ignore any calls happening while the view is not attached; |
| 240 | // IsMouseHovered() will CHECK(false) in that scenario. GetWidget() may be |
| 241 | // null during construction and GetWidget()->GetRootView() may be null |
| 242 | // during destruction. Both scenarios only happen in tests. |
| 243 | if (!GetWidget() || !GetWidget()->GetRootView()) |
| 244 | return; |
| 245 | |
| 246 | if (icon_id_ == mojom::EasyUnlockIconId::NONE) |
| 247 | return; |
| 248 | |
| 249 | IconBundle resources = GetEasyUnlockResources(icon_id_); |
| 250 | |
| 251 | int active_resource = resources.normal; |
| 252 | if (IsMouseHovered()) |
| 253 | active_resource = resources.hover; |
| 254 | if (state() == ButtonState::STATE_PRESSED) |
| 255 | active_resource = resources.pressed; |
| 256 | |
| 257 | // Image to show. It may or may not be an animation, depending on |
| 258 | // |resources.duration|. |
| 259 | gfx::ImageSkia* image = |
Lei Zhang | a2c5a9e2 | 2017-10-25 04:13:54 | [diff] [blame] | 260 | ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| 261 | active_resource); |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 262 | |
| 263 | if (!resources.duration.is_zero()) { |
| 264 | // Only change the animation if the state itself has changed, otherwise |
| 265 | // the active animation frame is reset and there is a lot of unecessary |
| 266 | // decoding/image resizing work. This optimization only is valid only if |
| 267 | // all three resource assets are the same. |
| 268 | DCHECK_EQ(resources.normal, resources.hover); |
| 269 | DCHECK_EQ(resources.normal, resources.pressed); |
| 270 | if (changed_states) { |
Sarah Hu | e16b6ef | 2018-08-06 17:50:22 | [diff] [blame] | 271 | icon_->SetAnimationDecoder( |
| 272 | std::make_unique<HorizontalImageSequenceAnimationDecoder>( |
| 273 | *image, resources.duration, resources.num_frames), |
| 274 | AnimatedRoundedImageView::Playback::kRepeat); |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 275 | } |
| 276 | } else { |
| 277 | icon_->SetImage(*image); |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | // Icon we are currently displaying. |
| 282 | mojom::EasyUnlockIconId icon_id_ = mojom::EasyUnlockIconId::NONE; |
| 283 | |
| 284 | // View which renders the icon. |
| 285 | AnimatedRoundedImageView* icon_; |
| 286 | |
| 287 | // Callbacks run when icon is hovered or tapped. |
| 288 | OnEasyUnlockIconHovered on_hovered_; |
| 289 | OnEasyUnlockIconTapped on_tapped_; |
| 290 | |
| 291 | std::unique_ptr<HoverNotifier> hover_notifier_; |
| 292 | |
| 293 | // Timer used to control when we invoke |on_hover_|. |
| 294 | base::OneShotTimer invoke_hover_; |
| 295 | |
| 296 | // If true, the tooltip/hover timer will be skipped and |on_hover_| will be |
| 297 | // run immediately. |
| 298 | bool immediately_hover_for_test_ = false; |
| 299 | |
| 300 | DISALLOW_COPY_AND_ASSIGN(EasyUnlockIcon); |
| 301 | }; |
| 302 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 303 | LoginPasswordView::TestApi::TestApi(LoginPasswordView* view) : view_(view) {} |
| 304 | |
| 305 | LoginPasswordView::TestApi::~TestApi() = default; |
| 306 | |
Jacob Dufault | ec9eead | 2018-11-16 22:24:47 | [diff] [blame] | 307 | void LoginPasswordView::TestApi::SubmitPassword(const std::string& password) { |
| 308 | view_->textfield_->SetText(base::ASCIIToUTF16(password)); |
| 309 | view_->UpdateUiState(); |
| 310 | view_->SubmitPassword(); |
| 311 | } |
| 312 | |
Jacob Dufault | b7a2d84 | 2017-12-01 23:21:15 | [diff] [blame] | 313 | views::Textfield* LoginPasswordView::TestApi::textfield() const { |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 314 | return view_->textfield_; |
| 315 | } |
| 316 | |
| 317 | views::View* LoginPasswordView::TestApi::submit_button() const { |
| 318 | return view_->submit_button_; |
| 319 | } |
| 320 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 321 | views::View* LoginPasswordView::TestApi::easy_unlock_icon() const { |
| 322 | return view_->easy_unlock_icon_; |
| 323 | } |
| 324 | |
| 325 | void LoginPasswordView::TestApi::set_immediately_hover_easy_unlock_icon() { |
| 326 | view_->easy_unlock_icon_->set_immediately_hover_for_test(); |
| 327 | } |
| 328 | |
Darren Shen | 7db1603 | 2018-03-06 21:02:33 | [diff] [blame] | 329 | LoginPasswordView::LoginPasswordView() { |
| 330 | Shell::Get()->ime_controller()->AddObserver(this); |
| 331 | |
Jacob Dufault | 1190491 | 2018-10-01 19:41:35 | [diff] [blame] | 332 | auto* root_layout = SetLayoutManager( |
| 333 | std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical)); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 334 | root_layout->set_main_axis_alignment( |
| 335 | views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 336 | |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 337 | password_row_ = new NonAccessibleView(); |
| 338 | |
Brett Wilson | 501d664 | 2017-12-14 17:58:18 | [diff] [blame] | 339 | auto layout = std::make_unique<views::BoxLayout>( |
| 340 | views::BoxLayout::kHorizontal, |
| 341 | gfx::Insets(kMarginAboveBelowPasswordIconsDp, 0)); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 342 | layout->set_main_axis_alignment(views::BoxLayout::MAIN_AXIS_ALIGNMENT_CENTER); |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 343 | layout->set_cross_axis_alignment( |
| 344 | views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER); |
Brett Wilson | 501d664 | 2017-12-14 17:58:18 | [diff] [blame] | 345 | auto* layout_ptr = password_row_->SetLayoutManager(std::move(layout)); |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 346 | AddChildView(password_row_); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 347 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 348 | // Add easy unlock icon. |
| 349 | easy_unlock_icon_ = new EasyUnlockIcon( |
| 350 | gfx::Size(kEasyUnlockIconSizeDp, kEasyUnlockIconSizeDp), |
| 351 | 0 /*corner_radius*/); |
| 352 | password_row_->AddChildView(easy_unlock_icon_); |
| 353 | |
| 354 | easy_unlock_right_margin_ = new NonAccessibleView(); |
| 355 | easy_unlock_right_margin_->SetPreferredSize(gfx::Size( |
| 356 | kHorizontalDistanceBetweenEasyUnlockAndPasswordDp, kNonEmptyHeight)); |
| 357 | password_row_->AddChildView(easy_unlock_right_margin_); |
| 358 | |
| 359 | // Easy unlock starts invisible. There will be an event later to show it if |
| 360 | // needed. |
| 361 | easy_unlock_icon_->SetVisible(false); |
| 362 | easy_unlock_right_margin_->SetVisible(false); |
| 363 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 364 | // Password textfield. We control the textfield size by sizing the parent |
| 365 | // view, as the textfield will expand to fill it. |
Jacob Dufault | 9e2a640 | 2017-12-22 05:40:02 | [diff] [blame] | 366 | textfield_ = new LoginTextfield(); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 367 | textfield_->set_controller(this); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 368 | textfield_->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD); |
Jacob Dufault | ec14708 | 2018-10-11 20:59:46 | [diff] [blame] | 369 | textfield_->SetTextColor(login_constants::kAuthMethodsTextColor); |
Wenzhao Zang | f141de4 | 2017-11-10 06:20:15 | [diff] [blame] | 370 | textfield_->SetFontList(views::Textfield::GetDefaultFontList().Derive( |
| 371 | 5, gfx::Font::FontStyle::NORMAL, gfx::Font::Weight::NORMAL)); |
| 372 | textfield_->set_placeholder_font_list(views::Textfield::GetDefaultFontList()); |
Jacob Dufault | ec14708 | 2018-10-11 20:59:46 | [diff] [blame] | 373 | textfield_->set_placeholder_text_color( |
| 374 | login_constants::kAuthMethodsTextColor); |
Wenzhao Zang | f141de4 | 2017-11-10 06:20:15 | [diff] [blame] | 375 | textfield_->SetGlyphSpacing(6); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 376 | textfield_->SetBorder(nullptr); |
Sarah Hu | 745c82a | 2017-07-14 23:21:58 | [diff] [blame] | 377 | textfield_->SetBackgroundColor(SK_ColorTRANSPARENT); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 378 | |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 379 | password_row_->AddChildView(textfield_); |
Brett Wilson | 501d664 | 2017-12-14 17:58:18 | [diff] [blame] | 380 | layout_ptr->SetFlexForView(textfield_, 1); |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 381 | |
| 382 | // Caps lock hint icon. |
| 383 | capslock_icon_ = new views::ImageView(); |
| 384 | capslock_icon_->SetImage(gfx::CreateVectorIcon( |
| 385 | kLockScreenCapsLockIcon, kCapsLockIconSizeDp, |
| 386 | SkColorSetA(login_constants::kButtonEnabledColor, |
| 387 | login_constants::kButtonDisabledAlpha))); |
| 388 | password_row_->AddChildView(capslock_icon_); |
| 389 | // Caps lock hint starts invisible. This constructor will call |
| 390 | // OnCapsLockChanged with the actual caps lock state. |
| 391 | capslock_icon_->SetVisible(false); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 392 | |
| 393 | // Submit button. |
Sarah Hu | a93125b | 2017-10-30 19:18:18 | [diff] [blame] | 394 | submit_button_ = new LoginButton(this); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 395 | submit_button_->SetImage( |
| 396 | views::Button::STATE_NORMAL, |
| 397 | gfx::CreateVectorIcon(kLockScreenArrowIcon, kSubmitButtonSizeDp, |
| 398 | login_constants::kButtonEnabledColor)); |
| 399 | submit_button_->SetImage( |
| 400 | views::Button::STATE_DISABLED, |
| 401 | gfx::CreateVectorIcon( |
| 402 | kLockScreenArrowIcon, kSubmitButtonSizeDp, |
| 403 | SkColorSetA(login_constants::kButtonEnabledColor, |
| 404 | login_constants::kButtonDisabledAlpha))); |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 405 | submit_button_->SetAccessibleName(l10n_util::GetStringUTF16( |
| 406 | IDS_ASH_LOGIN_POD_SUBMIT_BUTTON_ACCESSIBLE_NAME)); |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 407 | password_row_->AddChildView(submit_button_); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 408 | |
| 409 | // Separator on bottom. |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 410 | separator_ = new NonAccessibleSeparator(); |
| 411 | AddChildView(separator_); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 412 | |
| 413 | // Make sure the textfield always starts with focus. |
| 414 | textfield_->RequestFocus(); |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 415 | |
Darren Shen | 7db1603 | 2018-03-06 21:02:33 | [diff] [blame] | 416 | // Initialize with the initial state of caps lock. |
| 417 | OnCapsLockChanged(Shell::Get()->ime_controller()->IsCapsLockEnabled()); |
Wenzhao Zang | c61be25 | 2017-12-28 08:34:53 | [diff] [blame] | 418 | |
| 419 | // Make sure the UI start with the correct states. |
| 420 | UpdateUiState(); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 421 | } |
| 422 | |
Darren Shen | 7db1603 | 2018-03-06 21:02:33 | [diff] [blame] | 423 | LoginPasswordView::~LoginPasswordView() { |
| 424 | Shell::Get()->ime_controller()->RemoveObserver(this); |
| 425 | } |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 426 | |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 427 | void LoginPasswordView::Init( |
| 428 | const OnPasswordSubmit& on_submit, |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 429 | const OnPasswordTextChanged& on_password_text_changed, |
| 430 | const OnEasyUnlockIconHovered& on_easy_unlock_icon_hovered, |
| 431 | const OnEasyUnlockIconTapped& on_easy_unlock_icon_tapped) { |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 432 | DCHECK(on_submit); |
| 433 | DCHECK(on_password_text_changed); |
| 434 | on_submit_ = on_submit; |
| 435 | on_password_text_changed_ = on_password_text_changed; |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 436 | easy_unlock_icon_->Init(on_easy_unlock_icon_hovered, |
| 437 | on_easy_unlock_icon_tapped); |
| 438 | } |
| 439 | |
Jacob Dufault | 21cd566 | 2018-08-07 18:04:33 | [diff] [blame] | 440 | void LoginPasswordView::SetEnabledOnEmptyPassword(bool enabled) { |
| 441 | enabled_on_empty_password_ = enabled; |
| 442 | UpdateUiState(); |
| 443 | } |
| 444 | |
Jacob Dufault | a022559 | 2017-10-17 21:53:38 | [diff] [blame] | 445 | void LoginPasswordView::SetEasyUnlockIcon( |
| 446 | mojom::EasyUnlockIconId id, |
| 447 | const base::string16& accessibility_label) { |
| 448 | // Update icon. |
| 449 | easy_unlock_icon_->SetEasyUnlockIcon(id, accessibility_label); |
| 450 | |
| 451 | // Update icon visiblity. |
| 452 | bool has_icon = id != mojom::EasyUnlockIconId::NONE; |
| 453 | easy_unlock_icon_->SetVisible(has_icon); |
| 454 | easy_unlock_right_margin_->SetVisible(has_icon); |
| 455 | password_row_->Layout(); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 456 | } |
| 457 | |
Sarah Hu | f3a99dd0 | 2017-10-03 22:04:11 | [diff] [blame] | 458 | void LoginPasswordView::UpdateForUser(const mojom::LoginUserInfoPtr& user) { |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 459 | textfield_->SetAccessibleName(l10n_util::GetStringFUTF16( |
| 460 | IDS_ASH_LOGIN_POD_PASSWORD_FIELD_ACCESSIBLE_NAME, |
Sarah Hu | f3a99dd0 | 2017-10-03 22:04:11 | [diff] [blame] | 461 | base::UTF8ToUTF16(user->basic_user_info->display_email))); |
Jacob Dufault | 916b6e43 | 2017-09-15 00:15:55 | [diff] [blame] | 462 | } |
| 463 | |
Jacob Dufault | b7795a8 | 2017-09-08 20:42:36 | [diff] [blame] | 464 | void LoginPasswordView::SetFocusEnabledForChildViews(bool enable) { |
| 465 | auto behavior = enable ? FocusBehavior::ALWAYS : FocusBehavior::NEVER; |
| 466 | textfield_->SetFocusBehavior(behavior); |
Jacob Dufault | b7795a8 | 2017-09-08 20:42:36 | [diff] [blame] | 467 | } |
| 468 | |
| 469 | void LoginPasswordView::Clear() { |
| 470 | textfield_->SetText(base::string16()); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 471 | // |ContentsChanged| won't be called by |Textfield| if the text is changed |
| 472 | // by |Textfield::SetText()|. |
| 473 | ContentsChanged(textfield_, textfield_->text()); |
Jacob Dufault | b7795a8 | 2017-09-08 20:42:36 | [diff] [blame] | 474 | } |
| 475 | |
Jacob Dufault | 9ccbec2 | 2017-12-04 21:53:10 | [diff] [blame] | 476 | void LoginPasswordView::InsertNumber(int value) { |
| 477 | textfield_->InsertOrReplaceText(base::IntToString16(value)); |
Jacob Dufault | 8b152b7 | 2017-06-16 17:52:25 | [diff] [blame] | 478 | } |
| 479 | |
| 480 | void LoginPasswordView::Backspace() { |
| 481 | // Instead of just adjusting textfield_ text directly, fire a backspace key |
| 482 | // event as this handles the various edge cases (ie, selected text). |
| 483 | |
| 484 | // views::Textfield::OnKeyPressed is private, so we call it via views::View. |
| 485 | auto* view = static_cast<views::View*>(textfield_); |
| 486 | view->OnKeyPressed(ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_BACK, |
| 487 | ui::DomCode::BACKSPACE, ui::EF_NONE)); |
| 488 | view->OnKeyPressed(ui::KeyEvent(ui::ET_KEY_RELEASED, ui::VKEY_BACK, |
| 489 | ui::DomCode::BACKSPACE, ui::EF_NONE)); |
| 490 | } |
| 491 | |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 492 | void LoginPasswordView::SetPlaceholderText( |
| 493 | const base::string16& placeholder_text) { |
| 494 | textfield_->set_placeholder_text(placeholder_text); |
James Hawkins | a2708ead | 2018-05-24 20:19:11 | [diff] [blame] | 495 | SchedulePaint(); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 496 | } |
| 497 | |
Jacob Dufault | b7a2d84 | 2017-12-01 23:21:15 | [diff] [blame] | 498 | void LoginPasswordView::SetReadOnly(bool read_only) { |
| 499 | textfield_->SetReadOnly(read_only); |
| 500 | textfield_->SetCursorEnabled(!read_only); |
| 501 | UpdateUiState(); |
| 502 | } |
| 503 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 504 | const char* LoginPasswordView::GetClassName() const { |
| 505 | return kLoginPasswordViewName; |
| 506 | } |
| 507 | |
| 508 | gfx::Size LoginPasswordView::CalculatePreferredSize() const { |
| 509 | gfx::Size size = views::View::CalculatePreferredSize(); |
| 510 | size.set_width(kPasswordTotalWidthDp); |
| 511 | return size; |
| 512 | } |
| 513 | |
Jacob Dufault | 81565fa | 2017-08-17 21:45:54 | [diff] [blame] | 514 | void LoginPasswordView::RequestFocus() { |
| 515 | textfield_->RequestFocus(); |
| 516 | } |
| 517 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 518 | bool LoginPasswordView::OnKeyPressed(const ui::KeyEvent& event) { |
Jacob Dufault | 21cd566 | 2018-08-07 18:04:33 | [diff] [blame] | 519 | if (event.key_code() == ui::KeyboardCode::VKEY_RETURN && |
| 520 | submit_button_->enabled()) { |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 521 | SubmitPassword(); |
| 522 | return true; |
| 523 | } |
| 524 | |
| 525 | return false; |
| 526 | } |
| 527 | |
| 528 | void LoginPasswordView::ButtonPressed(views::Button* sender, |
| 529 | const ui::Event& event) { |
| 530 | DCHECK_EQ(sender, submit_button_); |
| 531 | SubmitPassword(); |
| 532 | } |
| 533 | |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 534 | void LoginPasswordView::ContentsChanged(views::Textfield* sender, |
| 535 | const base::string16& new_contents) { |
| 536 | DCHECK_EQ(sender, textfield_); |
Jacob Dufault | b7a2d84 | 2017-12-01 23:21:15 | [diff] [blame] | 537 | UpdateUiState(); |
| 538 | on_password_text_changed_.Run(new_contents.empty() /*is_empty*/); |
| 539 | } |
| 540 | |
Quan Nguyen | 59b4cd57 | 2018-07-10 20:49:23 | [diff] [blame] | 541 | // Implements swapping active user with arrow keys |
| 542 | bool LoginPasswordView::HandleKeyEvent(views::Textfield* sender, |
| 543 | const ui::KeyEvent& key_event) { |
| 544 | // Treat the password field as normal if it has text |
| 545 | if (!textfield_->text().empty()) |
| 546 | return false; |
| 547 | |
| 548 | if (key_event.type() != ui::ET_KEY_PRESSED) |
| 549 | return false; |
| 550 | |
| 551 | if (key_event.is_repeat()) |
| 552 | return false; |
| 553 | |
| 554 | switch (key_event.key_code()) { |
| 555 | case ui::VKEY_LEFT: |
| 556 | LockScreen::Get()->FocusPreviousUser(); |
| 557 | break; |
| 558 | case ui::VKEY_RIGHT: |
| 559 | LockScreen::Get()->FocusNextUser(); |
| 560 | break; |
| 561 | default: |
| 562 | return false; |
| 563 | } |
| 564 | |
| 565 | return true; |
| 566 | } |
| 567 | |
Jacob Dufault | b7a2d84 | 2017-12-01 23:21:15 | [diff] [blame] | 568 | void LoginPasswordView::UpdateUiState() { |
Jacob Dufault | 21cd566 | 2018-08-07 18:04:33 | [diff] [blame] | 569 | bool is_enabled = !textfield_->read_only() && |
| 570 | (enabled_on_empty_password_ || !textfield_->text().empty()); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 571 | submit_button_->SetEnabled(is_enabled); |
Wenzhao Zang | 0b3fce27 | 2017-10-13 04:48:20 | [diff] [blame] | 572 | SkColor color = is_enabled |
| 573 | ? login_constants::kButtonEnabledColor |
| 574 | : SkColorSetA(login_constants::kButtonEnabledColor, |
| 575 | login_constants::kButtonDisabledAlpha); |
| 576 | separator_->SetColor(color); |
| 577 | capslock_icon_->SetImage(gfx::CreateVectorIcon(kLockScreenCapsLockIcon, |
| 578 | kCapsLockIconSizeDp, color)); |
| 579 | } |
| 580 | |
| 581 | void LoginPasswordView::OnCapsLockChanged(bool enabled) { |
| 582 | capslock_icon_->SetVisible(enabled); |
| 583 | password_row_->Layout(); |
Wenzhao Zang | 8c9d690 | 2017-10-01 07:48:43 | [diff] [blame] | 584 | } |
| 585 | |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 586 | void LoginPasswordView::SubmitPassword() { |
Jacob Dufault | 21cd566 | 2018-08-07 18:04:33 | [diff] [blame] | 587 | DCHECK(submit_button_->enabled()); |
Jacob Dufault | b7a2d84 | 2017-12-01 23:21:15 | [diff] [blame] | 588 | if (textfield_->read_only()) |
| 589 | return; |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 590 | on_submit_.Run(textfield_->text()); |
jdufault | d979ce2 | 2017-06-12 21:00:10 | [diff] [blame] | 591 | } |
| 592 | |
Jacob Dufault | 81565fa | 2017-08-17 21:45:54 | [diff] [blame] | 593 | } // namespace ash |