1 // Copyright 2013 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/ui/views/passwords/password_items_view.h"
6
7 #include <algorithm>
8 #include <memory>
9 #include <numeric>
10 #include <utility>
11
12 #include "base/macros.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/util/type_safety/strong_alias.h"
15 #include "build/branding_buildflags.h"
16 #include "chrome/app/vector_icons/vector_icons.h"
17 #include "chrome/browser/ui/passwords/bubble_controllers/items_bubble_controller.h"
18 #include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
19 #include "chrome/browser/ui/passwords/passwords_model_delegate.h"
20 #include "chrome/browser/ui/views/chrome_layout_provider.h"
21 #include "chrome/grit/generated_resources.h"
22 #include "components/password_manager/core/browser/password_form.h"
23 #include "components/password_manager/core/common/password_manager_ui.h"
24 #include "components/vector_icons/vector_icons.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/models/simple_combobox_model.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "ui/gfx/favicon_size.h"
29 #include "ui/gfx/image/image_skia.h"
30 #include "ui/gfx/paint_vector_icon.h"
31 #include "ui/gfx/range/range.h"
32 #include "ui/gfx/vector_icon_types.h"
33 #include "ui/resources/grit/ui_resources.h"
34 #include "ui/views/controls/button/button.h"
35 #include "ui/views/controls/button/image_button.h"
36 #include "ui/views/controls/button/image_button_factory.h"
37 #include "ui/views/controls/button/md_text_button.h"
38 #include "ui/views/controls/color_tracking_icon_view.h"
39 #include "ui/views/controls/image_view.h"
40 #include "ui/views/controls/label.h"
41 #include "ui/views/controls/link.h"
42 #include "ui/views/controls/separator.h"
43 #include "ui/views/layout/fill_layout.h"
44 #include "ui/views/layout/grid_layout.h"
45 #include "ui/views/style/typography.h"
46
47 namespace {
48
49 // Column set identifiers for displaying or undoing removal of credentials.
50 // All of them allocate space differently.
51 enum PasswordItemsViewColumnSetType {
52 // Contains three columns for credential pair and a delete button.
53 PASSWORD_COLUMN_SET,
54 // Like PASSWORD_COLUMN_SET plus a column for an icon indicating the store,
55 // and a vertical bar before the delete button.
56 MULTI_STORE_PASSWORD_COLUMN_SET,
57 // Contains two columns for text and an undo button.
58 UNDO_COLUMN_SET
59 };
60
InferColumnSetTypeFromCredentials(const std::vector<password_manager::PasswordForm> & credentials)61 PasswordItemsViewColumnSetType InferColumnSetTypeFromCredentials(
62 const std::vector<password_manager::PasswordForm>& credentials) {
63 if (std::any_of(credentials.begin(), credentials.end(),
64 [](const password_manager::PasswordForm& form) {
65 return form.in_store ==
66 password_manager::PasswordForm::Store::kAccountStore;
67 })) {
68 return MULTI_STORE_PASSWORD_COLUMN_SET;
69 }
70 return PASSWORD_COLUMN_SET;
71 }
72
BuildColumnSet(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)73 void BuildColumnSet(views::GridLayout* layout,
74 PasswordItemsViewColumnSetType type_id) {
75 DCHECK(!layout->GetColumnSet(type_id));
76 views::ColumnSet* column_set = layout->AddColumnSet(type_id);
77 // Passwords are split 60/40 (6:4) as the username is more important
78 // than obscured password digits. Otherwise two columns are 50/50 (1:1).
79 constexpr float kFirstColumnWeight = 60.0f;
80 constexpr float kSecondColumnWeight = 40.0f;
81 const int between_column_padding =
82 ChromeLayoutProvider::Get()->GetDistanceMetric(
83 views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
84 // Add favicon column
85 if (type_id == PASSWORD_COLUMN_SET ||
86 type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
87 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
88 views::GridLayout::kFixedSize,
89 views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
90 column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
91 between_column_padding);
92 }
93
94 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
95 kFirstColumnWeight,
96 views::GridLayout::ColumnSize::kFixed, 0, 0);
97
98 if (type_id == PASSWORD_COLUMN_SET ||
99 type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
100 column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
101 between_column_padding);
102 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
103 kSecondColumnWeight,
104 views::GridLayout::ColumnSize::kFixed, 0, 0);
105 }
106 if (type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
107 // All rows show a store indicator or leave the space blank.
108 column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
109 between_column_padding);
110 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::FILL,
111 views::GridLayout::kFixedSize,
112 views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
113 // Add a column for the vertical bar.
114 column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
115 between_column_padding);
116 column_set->AddColumn(views::GridLayout::FILL, views::GridLayout::CENTER,
117 views::GridLayout::kFixedSize,
118 views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
119 }
120 // All rows end with a trailing column for the undo/trash button.
121 column_set->AddPaddingColumn(views::GridLayout::kFixedSize,
122 between_column_padding);
123 column_set->AddColumn(views::GridLayout::TRAILING, views::GridLayout::FILL,
124 views::GridLayout::kFixedSize,
125 views::GridLayout::ColumnSize::kUsePreferred, 0, 0);
126 }
127
StartRow(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)128 void StartRow(views::GridLayout* layout,
129 PasswordItemsViewColumnSetType type_id) {
130 if (!layout->GetColumnSet(type_id))
131 BuildColumnSet(layout, type_id);
132 layout->StartRow(views::GridLayout::kFixedSize, type_id);
133 }
134
135 } // namespace
136
137 // An entry for each credential. Relays delete/undo actions associated with
138 // this password row to parent dialog.
139 class PasswordItemsView::PasswordRow {
140 public:
141 PasswordRow(PasswordItemsView* parent,
142 const password_manager::PasswordForm* password_form);
143
144 void AddToLayout(views::GridLayout* layout,
145 PasswordItemsViewColumnSetType type_id);
146
147 private:
148 void AddUndoRow(views::GridLayout* layout);
149 void AddPasswordRow(views::GridLayout* layout,
150 PasswordItemsViewColumnSetType type_id);
151
152 void DeleteButtonPressed();
153 void UndoButtonPressed();
154
155 PasswordItemsView* const parent_;
156 const password_manager::PasswordForm* const password_form_;
157 bool deleted_ = false;
158
159 DISALLOW_COPY_AND_ASSIGN(PasswordRow);
160 };
161
PasswordRow(PasswordItemsView * parent,const password_manager::PasswordForm * password_form)162 PasswordItemsView::PasswordRow::PasswordRow(
163 PasswordItemsView* parent,
164 const password_manager::PasswordForm* password_form)
165 : parent_(parent), password_form_(password_form) {}
166
AddToLayout(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)167 void PasswordItemsView::PasswordRow::AddToLayout(
168 views::GridLayout* layout,
169 PasswordItemsViewColumnSetType type_id) {
170 if (deleted_)
171 AddUndoRow(layout);
172 else
173 AddPasswordRow(layout, type_id);
174 }
175
AddUndoRow(views::GridLayout * layout)176 void PasswordItemsView::PasswordRow::AddUndoRow(views::GridLayout* layout) {
177 StartRow(layout, UNDO_COLUMN_SET);
178 layout
179 ->AddView(std::make_unique<views::Label>(
180 l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_DELETED),
181 views::style::CONTEXT_DIALOG_BODY_TEXT))
182 ->SetHorizontalAlignment(gfx::ALIGN_LEFT);
183 auto* undo_button = layout->AddView(std::make_unique<views::MdTextButton>(
184 base::BindRepeating(&PasswordRow::UndoButtonPressed,
185 base::Unretained(this)),
186 l10n_util::GetStringUTF16(IDS_MANAGE_PASSWORDS_UNDO)));
187 undo_button->SetTooltipText(l10n_util::GetStringFUTF16(
188 IDS_MANAGE_PASSWORDS_UNDO_TOOLTIP, GetDisplayUsername(*password_form_)));
189 }
190
AddPasswordRow(views::GridLayout * layout,PasswordItemsViewColumnSetType type_id)191 void PasswordItemsView::PasswordRow::AddPasswordRow(
192 views::GridLayout* layout,
193 PasswordItemsViewColumnSetType type_id) {
194 StartRow(layout, type_id);
195
196 if (parent_->favicon_.IsEmpty()) {
197 // Use a globe fallback until the actual favicon is loaded.
198 layout->AddView(std::make_unique<views::ColorTrackingIconView>(
199 kGlobeIcon, gfx::kFaviconSize));
200 } else {
201 layout->AddView(std::make_unique<views::ImageView>())
202 ->SetImage(parent_->favicon_.AsImageSkia());
203 }
204
205 layout->AddView(CreateUsernameLabel(*password_form_));
206 layout->AddView(CreatePasswordLabel(*password_form_));
207
208 if (type_id == MULTI_STORE_PASSWORD_COLUMN_SET) {
209 if (password_form_->in_store ==
210 password_manager::PasswordForm::Store::kAccountStore) {
211 auto* image_view = layout->AddView(std::make_unique<views::ImageView>());
212 image_view->SetImage(gfx::CreateVectorIcon(
213 #if BUILDFLAG(GOOGLE_CHROME_BRANDING)
214 kGoogleGLogoIcon,
215 #else
216 vector_icons::kSyncIcon,
217 #endif // !BUILDFLAG(GOOGLE_CHROME_BRANDING)
218 gfx::kFaviconSize, gfx::kPlaceholderColor));
219 image_view->SetAccessibleName(l10n_util::GetStringUTF16(
220 IDS_MANAGE_PASSWORDS_ACCOUNT_STORE_ICON_DESCRIPTION));
221 } else {
222 layout->SkipColumns(1);
223 }
224
225 auto* separator = layout->AddView(std::make_unique<views::Separator>());
226 separator->SetFocusBehavior(
227 LocationBarBubbleDelegateView::FocusBehavior::NEVER);
228 separator->SetPreferredHeight(views::style::GetLineHeight(
229 views::style::CONTEXT_MENU, views::style::STYLE_SECONDARY));
230 separator->SetCanProcessEventsWithinSubtree(false);
231 }
232
233 auto* delete_button =
234 layout->AddView(views::CreateVectorImageButtonWithNativeTheme(
235 base::BindRepeating(&PasswordRow::DeleteButtonPressed,
236 base::Unretained(this)),
237 kTrashCanIcon));
238 delete_button->SetTooltipText(l10n_util::GetStringFUTF16(
239 IDS_MANAGE_PASSWORDS_DELETE, GetDisplayUsername(*password_form_)));
240 }
241
DeleteButtonPressed()242 void PasswordItemsView::PasswordRow::DeleteButtonPressed() {
243 deleted_ = true;
244 parent_->NotifyPasswordFormAction(
245 *password_form_,
246 PasswordBubbleControllerBase::PasswordAction::kRemovePassword);
247 }
248
UndoButtonPressed()249 void PasswordItemsView::PasswordRow::UndoButtonPressed() {
250 deleted_ = false;
251 parent_->NotifyPasswordFormAction(
252 *password_form_,
253 PasswordBubbleControllerBase::PasswordAction::kAddPassword);
254 }
255
PasswordItemsView(content::WebContents * web_contents,views::View * anchor_view)256 PasswordItemsView::PasswordItemsView(content::WebContents* web_contents,
257 views::View* anchor_view)
258 : PasswordBubbleViewBase(web_contents,
259 anchor_view,
260 /*easily_dismissable=*/true),
261 controller_(PasswordsModelDelegateFromWebContents(web_contents)) {
262 SetButtons(ui::DIALOG_BUTTON_OK);
263 SetExtraView(std::make_unique<views::MdTextButton>(
264 base::BindRepeating(
265 [](PasswordItemsView* items) {
266 items->controller_.OnManageClicked(
267 password_manager::ManagePasswordsReferrer::
268 kManagePasswordsBubble);
269 items->CloseBubble();
270 },
271 base::Unretained(this)),
272 l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_MANAGE_PASSWORDS_BUTTON)));
273
274 if (controller_.local_credentials().empty()) {
275 // A LayoutManager is required for GetHeightForWidth() even without
276 // content.
277 SetLayoutManager(std::make_unique<views::FillLayout>());
278 } else {
279 // The request is cancelled when the |controller_| is destructed.
280 // |controller_| has the same life time as |this| and hence it's safe to use
281 // base::Unretained(this).
282 controller_.RequestFavicon(base::BindOnce(
283 &PasswordItemsView::OnFaviconReady, base::Unretained(this)));
284 for (auto& password_form : controller_.local_credentials()) {
285 password_rows_.push_back(
286 std::make_unique<PasswordRow>(this, &password_form));
287 }
288
289 RecreateLayout();
290 }
291 }
292
293 PasswordItemsView::~PasswordItemsView() = default;
294
GetController()295 PasswordBubbleControllerBase* PasswordItemsView::GetController() {
296 return &controller_;
297 }
298
GetController() const299 const PasswordBubbleControllerBase* PasswordItemsView::GetController() const {
300 return &controller_;
301 }
302
RecreateLayout()303 void PasswordItemsView::RecreateLayout() {
304 // This method should only be used when we have password rows, otherwise the
305 // dialog should only show the no-passwords title and doesn't need to be
306 // recreated.
307 DCHECK(!controller_.local_credentials().empty());
308
309 RemoveAllChildViews(true);
310
311 views::GridLayout* grid_layout =
312 SetLayoutManager(std::make_unique<views::GridLayout>());
313
314 const int vertical_padding = ChromeLayoutProvider::Get()->GetDistanceMetric(
315 DISTANCE_CONTROL_LIST_VERTICAL);
316 bool first_row = true;
317 PasswordItemsViewColumnSetType row_column_set_type =
318 InferColumnSetTypeFromCredentials(controller_.local_credentials());
319 for (auto& row : password_rows_) {
320 if (!first_row)
321 grid_layout->AddPaddingRow(views::GridLayout::kFixedSize,
322 vertical_padding);
323
324 row->AddToLayout(grid_layout, row_column_set_type);
325 first_row = false;
326 }
327
328 PreferredSizeChanged();
329 if (GetBubbleFrameView())
330 SizeToContents();
331 }
332
NotifyPasswordFormAction(const password_manager::PasswordForm & password_form,PasswordBubbleControllerBase::PasswordAction action)333 void PasswordItemsView::NotifyPasswordFormAction(
334 const password_manager::PasswordForm& password_form,
335 PasswordBubbleControllerBase::PasswordAction action) {
336 RecreateLayout();
337 // After the view is consistent, notify the model that the password needs to
338 // be updated (either removed or put back into the store, as appropriate.
339 controller_.OnPasswordAction(password_form, action);
340 }
341
OnFaviconReady(const gfx::Image & favicon)342 void PasswordItemsView::OnFaviconReady(const gfx::Image& favicon) {
343 if (!favicon.IsEmpty()) {
344 favicon_ = favicon;
345 RecreateLayout();
346 }
347 }
348