OLD | NEW |
| (Empty) |
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 #include "ash/common/system/user/user_card_view.h" | |
6 | |
7 #include <algorithm> | |
8 #include <memory> | |
9 #include <vector> | |
10 | |
11 #include "ash/common/ash_view_ids.h" | |
12 #include "ash/common/login_status.h" | |
13 #include "ash/common/media_controller.h" | |
14 #include "ash/common/session/session_state_delegate.h" | |
15 #include "ash/common/system/tray/system_tray_controller.h" | |
16 #include "ash/common/system/tray/system_tray_delegate.h" | |
17 #include "ash/common/system/tray/tray_constants.h" | |
18 #include "ash/common/system/tray/tray_popup_item_style.h" | |
19 #include "ash/common/system/user/rounded_image_view.h" | |
20 #include "ash/common/wm_shell.h" | |
21 #include "ash/resources/vector_icons/vector_icons.h" | |
22 #include "ash/strings/grit/ash_strings.h" | |
23 #include "base/i18n/rtl.h" | |
24 #include "base/memory/ptr_util.h" | |
25 #include "base/strings/string16.h" | |
26 #include "base/strings/string_util.h" | |
27 #include "base/strings/utf_string_conversions.h" | |
28 #include "components/user_manager/user_info.h" | |
29 #include "ui/accessibility/ax_node_data.h" | |
30 #include "ui/base/l10n/l10n_util.h" | |
31 #include "ui/compositor/compositing_recorder.h" | |
32 #include "ui/gfx/canvas.h" | |
33 #include "ui/gfx/color_palette.h" | |
34 #include "ui/gfx/geometry/insets.h" | |
35 #include "ui/gfx/geometry/rect.h" | |
36 #include "ui/gfx/geometry/size.h" | |
37 #include "ui/gfx/paint_vector_icon.h" | |
38 #include "ui/gfx/range/range.h" | |
39 #include "ui/gfx/render_text.h" | |
40 #include "ui/gfx/text_elider.h" | |
41 #include "ui/gfx/text_utils.h" | |
42 #include "ui/views/border.h" | |
43 #include "ui/views/controls/image_view.h" | |
44 #include "ui/views/controls/link.h" | |
45 #include "ui/views/controls/link_listener.h" | |
46 #include "ui/views/layout/box_layout.h" | |
47 | |
48 namespace ash { | |
49 namespace tray { | |
50 | |
51 namespace { | |
52 | |
53 const int kUserDetailsVerticalPadding = 5; | |
54 | |
55 // The invisible word joiner character, used as a marker to indicate the start | |
56 // and end of the user's display name in the public account user card's text. | |
57 const base::char16 kDisplayNameMark[] = {0x2060, 0}; | |
58 | |
59 views::View* CreateUserAvatarView(LoginStatus login_status, int user_index) { | |
60 RoundedImageView* image_view = new RoundedImageView(kTrayItemSize / 2); | |
61 if (login_status == LoginStatus::GUEST) { | |
62 gfx::ImageSkia icon = | |
63 gfx::CreateVectorIcon(kSystemMenuGuestIcon, kMenuIconColor); | |
64 image_view->SetImage(icon, icon.size()); | |
65 } else { | |
66 SessionStateDelegate* delegate = WmShell::Get()->GetSessionStateDelegate(); | |
67 image_view->SetImage(delegate->GetUserInfo(user_index)->GetImage(), | |
68 gfx::Size(kTrayItemSize, kTrayItemSize)); | |
69 } | |
70 | |
71 image_view->SetBorder(views::CreateEmptyBorder(gfx::Insets( | |
72 (kTrayPopupItemMinStartWidth - image_view->GetPreferredSize().width()) / | |
73 2))); | |
74 return image_view; | |
75 } | |
76 | |
77 // The user details shown in public account mode. This is essentially a label | |
78 // but with custom painting code as the text is styled with multiple colors and | |
79 // contains a link. | |
80 class PublicAccountUserDetails : public views::View, | |
81 public views::LinkListener { | |
82 public: | |
83 PublicAccountUserDetails(int max_width); | |
84 ~PublicAccountUserDetails() override; | |
85 | |
86 private: | |
87 // Overridden from views::View. | |
88 void Layout() override; | |
89 gfx::Size GetPreferredSize() const override; | |
90 void OnPaint(gfx::Canvas* canvas) override; | |
91 void GetAccessibleNodeData(ui::AXNodeData* node_data) override; | |
92 | |
93 // Overridden from views::LinkListener. | |
94 void LinkClicked(views::Link* source, int event_flags) override; | |
95 | |
96 // Calculate a preferred size that ensures the label text and the following | |
97 // link do not wrap over more than three lines in total for aesthetic reasons | |
98 // if possible. | |
99 void CalculatePreferredSize(); | |
100 | |
101 base::string16 text_; | |
102 views::Link* learn_more_; | |
103 gfx::Size preferred_size_; | |
104 std::vector<std::unique_ptr<gfx::RenderText>> lines_; | |
105 | |
106 DISALLOW_COPY_AND_ASSIGN(PublicAccountUserDetails); | |
107 }; | |
108 | |
109 PublicAccountUserDetails::PublicAccountUserDetails(int max_width) | |
110 : learn_more_(NULL) { | |
111 const int inner_padding = | |
112 kTrayPopupPaddingHorizontal - kTrayPopupPaddingBetweenItems; | |
113 const bool rtl = base::i18n::IsRTL(); | |
114 SetBorder(views::CreateEmptyBorder( | |
115 kUserDetailsVerticalPadding, rtl ? 0 : inner_padding, | |
116 kUserDetailsVerticalPadding, rtl ? inner_padding : 0)); | |
117 | |
118 // Retrieve the user's display name and wrap it with markers. | |
119 // Note that since this is a public account it always has to be the primary | |
120 // user. | |
121 base::string16 display_name = WmShell::Get() | |
122 ->GetSessionStateDelegate() | |
123 ->GetUserInfo(0) | |
124 ->GetDisplayName(); | |
125 base::RemoveChars(display_name, kDisplayNameMark, &display_name); | |
126 display_name = kDisplayNameMark[0] + display_name + kDisplayNameMark[0]; | |
127 // Retrieve the domain managing the device and wrap it with markers. | |
128 base::string16 domain = base::UTF8ToUTF16( | |
129 WmShell::Get()->system_tray_delegate()->GetEnterpriseDomain()); | |
130 base::RemoveChars(domain, kDisplayNameMark, &domain); | |
131 base::i18n::WrapStringWithLTRFormatting(&domain); | |
132 // Retrieve the label text, inserting the display name and domain. | |
133 text_ = l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_PUBLIC_LABEL, | |
134 display_name, domain); | |
135 | |
136 learn_more_ = new views::Link(l10n_util::GetStringUTF16(IDS_ASH_LEARN_MORE)); | |
137 learn_more_->SetUnderline(false); | |
138 learn_more_->set_listener(this); | |
139 AddChildView(learn_more_); | |
140 | |
141 CalculatePreferredSize(); | |
142 } | |
143 | |
144 PublicAccountUserDetails::~PublicAccountUserDetails() {} | |
145 | |
146 void PublicAccountUserDetails::Layout() { | |
147 lines_.clear(); | |
148 const gfx::Rect contents_area = GetContentsBounds(); | |
149 if (contents_area.IsEmpty()) | |
150 return; | |
151 | |
152 // Word-wrap the label text. | |
153 const gfx::FontList font_list; | |
154 std::vector<base::string16> lines; | |
155 gfx::ElideRectangleText(text_, font_list, contents_area.width(), | |
156 contents_area.height(), gfx::ELIDE_LONG_WORDS, | |
157 &lines); | |
158 // Loop through the lines, creating a renderer for each. | |
159 gfx::Point position = contents_area.origin(); | |
160 gfx::Range display_name(gfx::Range::InvalidRange()); | |
161 for (auto it = lines.begin(); it != lines.end(); ++it) { | |
162 auto line = base::WrapUnique(gfx::RenderText::CreateInstance()); | |
163 line->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI); | |
164 line->SetText(*it); | |
165 const gfx::Size size(contents_area.width(), line->GetStringSize().height()); | |
166 line->SetDisplayRect(gfx::Rect(position, size)); | |
167 position.set_y(position.y() + size.height()); | |
168 | |
169 // Set the default text color for the line. | |
170 line->SetColor(kPublicAccountUserCardTextColor); | |
171 | |
172 // If a range of the line contains the user's display name, apply a custom | |
173 // text color to it. | |
174 if (display_name.is_empty()) | |
175 display_name.set_start(it->find(kDisplayNameMark)); | |
176 if (!display_name.is_empty()) { | |
177 display_name.set_end( | |
178 it->find(kDisplayNameMark, display_name.start() + 1)); | |
179 gfx::Range line_range(0, it->size()); | |
180 line->ApplyColor(kPublicAccountUserCardNameColor, | |
181 display_name.Intersect(line_range)); | |
182 // Update the range for the next line. | |
183 if (display_name.end() >= line_range.end()) | |
184 display_name.set_start(0); | |
185 else | |
186 display_name = gfx::Range::InvalidRange(); | |
187 } | |
188 | |
189 lines_.push_back(std::move(line)); | |
190 } | |
191 | |
192 // Position the link after the label text, separated by a space. If it does | |
193 // not fit onto the last line of the text, wrap the link onto its own line. | |
194 const gfx::Size last_line_size = lines_.back()->GetStringSize(); | |
195 const int space_width = | |
196 gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list); | |
197 const gfx::Size link_size = learn_more_->GetPreferredSize(); | |
198 if (contents_area.width() - last_line_size.width() >= | |
199 space_width + link_size.width()) { | |
200 position.set_x(position.x() + last_line_size.width() + space_width); | |
201 position.set_y(position.y() - last_line_size.height()); | |
202 } | |
203 position.set_y(position.y() - learn_more_->GetInsets().top()); | |
204 gfx::Rect learn_more_bounds(position, link_size); | |
205 learn_more_bounds.Intersect(contents_area); | |
206 if (base::i18n::IsRTL()) { | |
207 const gfx::Insets insets = GetInsets(); | |
208 learn_more_bounds.Offset(insets.right() - insets.left(), 0); | |
209 } | |
210 learn_more_->SetBoundsRect(learn_more_bounds); | |
211 } | |
212 | |
213 gfx::Size PublicAccountUserDetails::GetPreferredSize() const { | |
214 return preferred_size_; | |
215 } | |
216 | |
217 void PublicAccountUserDetails::OnPaint(gfx::Canvas* canvas) { | |
218 for (const auto& line : lines_) | |
219 line->Draw(canvas); | |
220 | |
221 views::View::OnPaint(canvas); | |
222 } | |
223 | |
224 void PublicAccountUserDetails::GetAccessibleNodeData( | |
225 ui::AXNodeData* node_data) { | |
226 node_data->role = ui::AX_ROLE_STATIC_TEXT; | |
227 node_data->SetName(text_); | |
228 } | |
229 | |
230 void PublicAccountUserDetails::LinkClicked(views::Link* source, | |
231 int event_flags) { | |
232 DCHECK_EQ(source, learn_more_); | |
233 WmShell::Get()->system_tray_controller()->ShowPublicAccountInfo(); | |
234 } | |
235 | |
236 void PublicAccountUserDetails::CalculatePreferredSize() { | |
237 const gfx::FontList font_list; | |
238 const gfx::Size link_size = learn_more_->GetPreferredSize(); | |
239 const int space_width = | |
240 gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list); | |
241 const gfx::Insets insets = GetInsets(); | |
242 int min_width = link_size.width(); | |
243 int max_width = | |
244 gfx::GetStringWidth(text_, font_list) + space_width + link_size.width(); | |
245 | |
246 // Do a binary search for the minimum width that ensures no more than three | |
247 // lines are needed. The lower bound is the minimum of the current bubble | |
248 // width and the width of the link (as no wrapping is permitted inside the | |
249 // link). The upper bound is the maximum of the largest allowed bubble width | |
250 // and the sum of the label text and link widths when put on a single line. | |
251 std::vector<base::string16> lines; | |
252 while (min_width < max_width) { | |
253 lines.clear(); | |
254 const int width = (min_width + max_width) / 2; | |
255 const bool too_narrow = | |
256 gfx::ElideRectangleText(text_, font_list, width, INT_MAX, | |
257 gfx::TRUNCATE_LONG_WORDS, &lines) != 0; | |
258 int line_count = lines.size(); | |
259 if (!too_narrow && line_count == 3 && | |
260 width - gfx::GetStringWidth(lines.back(), font_list) <= | |
261 space_width + link_size.width()) | |
262 ++line_count; | |
263 if (too_narrow || line_count > 3) | |
264 min_width = width + 1; | |
265 else | |
266 max_width = width; | |
267 } | |
268 | |
269 // Calculate the corresponding height and set the preferred size. | |
270 lines.clear(); | |
271 gfx::ElideRectangleText(text_, font_list, min_width, INT_MAX, | |
272 gfx::TRUNCATE_LONG_WORDS, &lines); | |
273 int line_count = lines.size(); | |
274 if (min_width - gfx::GetStringWidth(lines.back(), font_list) <= | |
275 space_width + link_size.width()) { | |
276 ++line_count; | |
277 } | |
278 const int line_height = font_list.GetHeight(); | |
279 const int link_extra_height = std::max( | |
280 link_size.height() - learn_more_->GetInsets().top() - line_height, 0); | |
281 preferred_size_ = | |
282 gfx::Size(min_width + insets.width(), | |
283 line_count * line_height + link_extra_height + insets.height()); | |
284 } | |
285 | |
286 } // namespace | |
287 | |
288 UserCardView::UserCardView(LoginStatus login_status, | |
289 int max_width, | |
290 int user_index) | |
291 : user_index_(user_index), | |
292 user_name_(nullptr), | |
293 media_capture_label_(nullptr), | |
294 media_capture_icon_(nullptr) { | |
295 auto* layout = new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, | |
296 kTrayPopupLabelHorizontalPadding); | |
297 SetLayoutManager(layout); | |
298 layout->set_minimum_cross_axis_size(kTrayPopupItemMinHeight); | |
299 layout->set_cross_axis_alignment( | |
300 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER); | |
301 // For active users, the left inset is provided by ActiveUserBorder, which | |
302 // is necessary to make sure the ripple does not cover that part of the row. | |
303 // For inactive users, we set the inset here and this causes the ripple to | |
304 // extend all the way to the edges of the menu. | |
305 if (!is_active_user()) | |
306 SetBorder(views::CreateEmptyBorder(0, kMenuExtraMarginFromLeftEdge, 0, 0)); | |
307 | |
308 WmShell::Get()->media_controller()->AddObserver(this); | |
309 | |
310 if (login_status == LoginStatus::PUBLIC) | |
311 AddPublicModeUserContent(max_width); | |
312 else | |
313 AddUserContent(layout, login_status); | |
314 } | |
315 | |
316 UserCardView::~UserCardView() { | |
317 WmShell::Get()->media_controller()->RemoveObserver(this); | |
318 } | |
319 | |
320 void UserCardView::PaintChildren(const ui::PaintContext& context) { | |
321 if (!is_active_user()) { | |
322 ui::CompositingRecorder alpha(context, 0xFF / 2, true); | |
323 View::PaintChildren(context); | |
324 } else { | |
325 View::PaintChildren(context); | |
326 } | |
327 } | |
328 | |
329 void UserCardView::GetAccessibleNodeData(ui::AXNodeData* node_data) { | |
330 node_data->role = ui::AX_ROLE_STATIC_TEXT; | |
331 std::vector<base::string16> labels; | |
332 | |
333 // Construct the name by concatenating descendants' names. | |
334 std::list<views::View*> descendants; | |
335 descendants.push_back(this); | |
336 while (!descendants.empty()) { | |
337 auto* view = descendants.front(); | |
338 descendants.pop_front(); | |
339 if (view != this) { | |
340 ui::AXNodeData descendant_data; | |
341 view->GetAccessibleNodeData(&descendant_data); | |
342 base::string16 label = | |
343 descendant_data.GetString16Attribute(ui::AX_ATTR_NAME); | |
344 // If we find a non-empty name, use that and don't descend further into | |
345 // the tree. | |
346 if (!label.empty()) { | |
347 labels.push_back(label); | |
348 continue; | |
349 } | |
350 } | |
351 | |
352 // This view didn't have its own name, so look over its children. | |
353 for (int i = view->child_count() - 1; i >= 0; --i) | |
354 descendants.push_front(view->child_at(i)); | |
355 } | |
356 node_data->SetName(base::JoinString(labels, base::ASCIIToUTF16(" "))); | |
357 } | |
358 | |
359 void UserCardView::OnMediaCaptureChanged( | |
360 const std::vector<mojom::MediaCaptureState>& capture_states) { | |
361 if (is_active_user()) | |
362 return; | |
363 | |
364 mojom::MediaCaptureState state = capture_states[user_index_]; | |
365 int res_id = 0; | |
366 switch (state) { | |
367 case mojom::MediaCaptureState::AUDIO_VIDEO: | |
368 res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO_VIDEO; | |
369 break; | |
370 case mojom::MediaCaptureState::AUDIO: | |
371 res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO; | |
372 break; | |
373 case mojom::MediaCaptureState::VIDEO: | |
374 res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_VIDEO; | |
375 break; | |
376 case mojom::MediaCaptureState::NONE: | |
377 break; | |
378 } | |
379 if (res_id) | |
380 media_capture_label_->SetText(l10n_util::GetStringUTF16(res_id)); | |
381 media_capture_label_->SetVisible(!!res_id); | |
382 media_capture_icon_->SetVisible(!!res_id); | |
383 user_name_->SetVisible(!res_id); | |
384 Layout(); | |
385 } | |
386 | |
387 void UserCardView::AddPublicModeUserContent(int max_width) { | |
388 views::View* avatar = CreateUserAvatarView(LoginStatus::PUBLIC, 0); | |
389 AddChildView(avatar); | |
390 int details_max_width = max_width - avatar->GetPreferredSize().width() - | |
391 kTrayPopupPaddingBetweenItems; | |
392 AddChildView(new PublicAccountUserDetails(details_max_width)); | |
393 } | |
394 | |
395 void UserCardView::AddUserContent(views::BoxLayout* layout, | |
396 LoginStatus login_status) { | |
397 AddChildView(CreateUserAvatarView(login_status, user_index_)); | |
398 SessionStateDelegate* delegate = WmShell::Get()->GetSessionStateDelegate(); | |
399 base::string16 user_name_string = | |
400 login_status == LoginStatus::GUEST | |
401 ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_GUEST_LABEL) | |
402 : delegate->GetUserInfo(user_index_)->GetDisplayName(); | |
403 user_name_ = new views::Label(user_name_string); | |
404 user_name_->SetHorizontalAlignment(gfx::ALIGN_LEFT); | |
405 TrayPopupItemStyle user_name_style( | |
406 TrayPopupItemStyle::FontStyle::DEFAULT_VIEW_LABEL); | |
407 user_name_style.SetupLabel(user_name_); | |
408 | |
409 TrayPopupItemStyle user_email_style(TrayPopupItemStyle::FontStyle::CAPTION); | |
410 // Only the active user's email label is lightened (for the inactive user, the | |
411 // label starts as black and the entire row is 54% opacity). | |
412 if (is_active_user()) | |
413 user_email_style.set_color_style(TrayPopupItemStyle::ColorStyle::INACTIVE); | |
414 auto* user_email = new views::Label(); | |
415 base::string16 user_email_string; | |
416 if (login_status != LoginStatus::GUEST) { | |
417 user_email_string = | |
418 WmShell::Get()->system_tray_delegate()->IsUserSupervised() | |
419 ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SUPERVISED_LABEL) | |
420 : base::UTF8ToUTF16( | |
421 delegate->GetUserInfo(user_index_)->GetDisplayEmail()); | |
422 } | |
423 user_email->SetText(user_email_string); | |
424 user_email->SetHorizontalAlignment(gfx::ALIGN_LEFT); | |
425 user_email_style.SetupLabel(user_email); | |
426 user_email->SetVisible(!user_email_string.empty()); | |
427 user_email->set_collapse_when_hidden(true); | |
428 | |
429 views::View* stack_of_labels = new views::View; | |
430 AddChildView(stack_of_labels); | |
431 layout->SetFlexForView(stack_of_labels, 1); | |
432 stack_of_labels->SetLayoutManager( | |
433 new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0)); | |
434 stack_of_labels->AddChildView(user_name_); | |
435 stack_of_labels->AddChildView(user_email); | |
436 // The name and email have different font sizes. This border is designed | |
437 // to make both views take up equal space so the whitespace between them | |
438 // is centered on the vertical midpoint. | |
439 int user_email_bottom_pad = user_name_->GetPreferredSize().height() - | |
440 user_email->GetPreferredSize().height(); | |
441 user_email->SetBorder( | |
442 views::CreateEmptyBorder(0, 0, user_email_bottom_pad, 0)); | |
443 | |
444 // Only inactive users need media capture indicators. | |
445 if (!is_active_user()) { | |
446 media_capture_label_ = new views::Label(); | |
447 media_capture_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); | |
448 media_capture_label_->SetBorder( | |
449 views::CreateEmptyBorder(0, 0, user_email_bottom_pad, 0)); | |
450 user_email_style.SetupLabel(media_capture_label_); | |
451 stack_of_labels->AddChildView(media_capture_label_); | |
452 | |
453 media_capture_icon_ = new views::ImageView; | |
454 media_capture_icon_->SetImage( | |
455 gfx::CreateVectorIcon(kSystemTrayRecordingIcon, gfx::kGoogleRed700)); | |
456 const int media_capture_width = kTrayPopupItemMinEndWidth; | |
457 media_capture_icon_->SetBorder(views::CreateEmptyBorder( | |
458 gfx::Insets(0, (media_capture_width - | |
459 media_capture_icon_->GetPreferredSize().width()) / | |
460 2))); | |
461 | |
462 media_capture_icon_->set_id(VIEW_ID_USER_VIEW_MEDIA_INDICATOR); | |
463 AddChildView(media_capture_icon_); | |
464 | |
465 WmShell::Get()->media_controller()->RequestCaptureState(); | |
466 } | |
467 } | |
468 | |
469 } // namespace tray | |
470 } // namespace ash | |
OLD | NEW |