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/system/user/user_card_view.h"
6
7 #include <algorithm>
8 #include <vector>
9
10 #include "ash/session/session_state_delegate.h"
11 #include "ash/session/user_info.h"
12 #include "ash/shell.h"
13 #include "ash/system/tray/system_tray_delegate.h"
14 #include "ash/system/tray/system_tray_notifier.h"
15 #include "ash/system/tray/tray_constants.h"
16 #include "ash/system/user/config.h"
17 #include "ash/system/user/rounded_image_view.h"
18 #include "base/i18n/rtl.h"
19 #include "base/memory/scoped_vector.h"
20 #include "base/strings/string16.h"
21 #include "base/strings/string_util.h"
22 #include "base/strings/utf_string_conversions.h"
23 #include "grit/ash_resources.h"
24 #include "grit/ash_strings.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/resource/resource_bundle.h"
27 #include "ui/gfx/insets.h"
28 #include "ui/gfx/range/range.h"
29 #include "ui/gfx/rect.h"
30 #include "ui/gfx/render_text.h"
31 #include "ui/gfx/size.h"
32 #include "ui/gfx/text_elider.h"
33 #include "ui/gfx/text_utils.h"
34 #include "ui/views/border.h"
35 #include "ui/views/controls/link.h"
36 #include "ui/views/controls/link_listener.h"
37 #include "ui/views/layout/box_layout.h"
38
39 #if defined(OS_CHROMEOS)
40 #include "ash/ash_view_ids.h"
41 #include "ash/media_delegate.h"
42 #include "ash/system/tray/media_security/media_capture_observer.h"
43 #include "ui/views/controls/image_view.h"
44 #include "ui/views/layout/fill_layout.h"
45 #endif
46
47 namespace ash {
48 namespace tray {
49
50 namespace {
51
52 const int kUserDetailsVerticalPadding = 5;
53
54 // The invisible word joiner character, used as a marker to indicate the start
55 // and end of the user's display name in the public account user card's text.
56 const base::char16 kDisplayNameMark[] = {0x2060, 0};
57
58 #if defined(OS_CHROMEOS)
59 class MediaIndicator : public views::View, public MediaCaptureObserver {
60 public:
MediaIndicator(MultiProfileIndex index)61 explicit MediaIndicator(MultiProfileIndex index)
62 : index_(index), label_(new views::Label) {
63 SetLayoutManager(new views::FillLayout);
64 views::ImageView* icon = new views::ImageView;
65 icon->SetImage(ui::ResourceBundle::GetSharedInstance()
66 .GetImageNamed(IDR_AURA_UBER_TRAY_RECORDING_RED)
67 .ToImageSkia());
68 AddChildView(icon);
69 label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
70 label_->SetFontList(ui::ResourceBundle::GetSharedInstance().GetFontList(
71 ui::ResourceBundle::SmallFont));
72 OnMediaCaptureChanged();
73 Shell::GetInstance()->system_tray_notifier()->AddMediaCaptureObserver(this);
74 set_id(VIEW_ID_USER_VIEW_MEDIA_INDICATOR);
75 }
76
~MediaIndicator()77 virtual ~MediaIndicator() {
78 Shell::GetInstance()->system_tray_notifier()->RemoveMediaCaptureObserver(
79 this);
80 }
81
82 // MediaCaptureObserver:
OnMediaCaptureChanged()83 virtual void OnMediaCaptureChanged() OVERRIDE {
84 Shell* shell = Shell::GetInstance();
85 content::BrowserContext* context =
86 shell->session_state_delegate()->GetBrowserContextByIndex(index_);
87 MediaCaptureState state =
88 Shell::GetInstance()->media_delegate()->GetMediaCaptureState(context);
89 int res_id = 0;
90 switch (state) {
91 case MEDIA_CAPTURE_AUDIO_VIDEO:
92 res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO_VIDEO;
93 break;
94 case MEDIA_CAPTURE_AUDIO:
95 res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO;
96 break;
97 case MEDIA_CAPTURE_VIDEO:
98 res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_VIDEO;
99 break;
100 case MEDIA_CAPTURE_NONE:
101 break;
102 }
103 SetMessage(res_id ? l10n_util::GetStringUTF16(res_id) : base::string16());
104 }
105
GetMessageView()106 views::View* GetMessageView() { return label_; }
107
SetMessage(const base::string16 & message)108 void SetMessage(const base::string16& message) {
109 SetVisible(!message.empty());
110 label_->SetText(message);
111 label_->SetVisible(!message.empty());
112 }
113
114 private:
115 MultiProfileIndex index_;
116 views::Label* label_;
117
118 DISALLOW_COPY_AND_ASSIGN(MediaIndicator);
119 };
120 #endif
121
122 // The user details shown in public account mode. This is essentially a label
123 // but with custom painting code as the text is styled with multiple colors and
124 // contains a link.
125 class PublicAccountUserDetails : public views::View,
126 public views::LinkListener {
127 public:
128 PublicAccountUserDetails(int max_width);
129 virtual ~PublicAccountUserDetails();
130
131 private:
132 // Overridden from views::View.
133 virtual void Layout() OVERRIDE;
134 virtual gfx::Size GetPreferredSize() const OVERRIDE;
135 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE;
136
137 // Overridden from views::LinkListener.
138 virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE;
139
140 // Calculate a preferred size that ensures the label text and the following
141 // link do not wrap over more than three lines in total for aesthetic reasons
142 // if possible.
143 void CalculatePreferredSize(int max_allowed_width);
144
145 base::string16 text_;
146 views::Link* learn_more_;
147 gfx::Size preferred_size_;
148 ScopedVector<gfx::RenderText> lines_;
149
150 DISALLOW_COPY_AND_ASSIGN(PublicAccountUserDetails);
151 };
152
PublicAccountUserDetails(int max_width)153 PublicAccountUserDetails::PublicAccountUserDetails(int max_width)
154 : learn_more_(NULL) {
155 const int inner_padding =
156 kTrayPopupPaddingHorizontal - kTrayPopupPaddingBetweenItems;
157 const bool rtl = base::i18n::IsRTL();
158 SetBorder(views::Border::CreateEmptyBorder(kUserDetailsVerticalPadding,
159 rtl ? 0 : inner_padding,
160 kUserDetailsVerticalPadding,
161 rtl ? inner_padding : 0));
162
163 // Retrieve the user's display name and wrap it with markers.
164 // Note that since this is a public account it always has to be the primary
165 // user.
166 base::string16 display_name = Shell::GetInstance()
167 ->session_state_delegate()
168 ->GetUserInfo(0)
169 ->GetDisplayName();
170 base::RemoveChars(display_name, kDisplayNameMark, &display_name);
171 display_name = kDisplayNameMark[0] + display_name + kDisplayNameMark[0];
172 // Retrieve the domain managing the device and wrap it with markers.
173 base::string16 domain = base::UTF8ToUTF16(
174 Shell::GetInstance()->system_tray_delegate()->GetEnterpriseDomain());
175 base::RemoveChars(domain, kDisplayNameMark, &domain);
176 base::i18n::WrapStringWithLTRFormatting(&domain);
177 // Retrieve the label text, inserting the display name and domain.
178 text_ = l10n_util::GetStringFUTF16(
179 IDS_ASH_STATUS_TRAY_PUBLIC_LABEL, display_name, domain);
180
181 learn_more_ = new views::Link(l10n_util::GetStringUTF16(IDS_ASH_LEARN_MORE));
182 learn_more_->SetUnderline(false);
183 learn_more_->set_listener(this);
184 AddChildView(learn_more_);
185
186 CalculatePreferredSize(max_width);
187 }
188
~PublicAccountUserDetails()189 PublicAccountUserDetails::~PublicAccountUserDetails() {}
190
Layout()191 void PublicAccountUserDetails::Layout() {
192 lines_.clear();
193 const gfx::Rect contents_area = GetContentsBounds();
194 if (contents_area.IsEmpty())
195 return;
196
197 // Word-wrap the label text.
198 const gfx::FontList font_list;
199 std::vector<base::string16> lines;
200 gfx::ElideRectangleText(text_,
201 font_list,
202 contents_area.width(),
203 contents_area.height(),
204 gfx::ELIDE_LONG_WORDS,
205 &lines);
206 // Loop through the lines, creating a renderer for each.
207 gfx::Point position = contents_area.origin();
208 gfx::Range display_name(gfx::Range::InvalidRange());
209 for (std::vector<base::string16>::const_iterator it = lines.begin();
210 it != lines.end();
211 ++it) {
212 gfx::RenderText* line = gfx::RenderText::CreateInstance();
213 line->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI);
214 line->SetText(*it);
215 const gfx::Size size(contents_area.width(), line->GetStringSize().height());
216 line->SetDisplayRect(gfx::Rect(position, size));
217 position.set_y(position.y() + size.height());
218
219 // Set the default text color for the line.
220 line->SetColor(kPublicAccountUserCardTextColor);
221
222 // If a range of the line contains the user's display name, apply a custom
223 // text color to it.
224 if (display_name.is_empty())
225 display_name.set_start(it->find(kDisplayNameMark));
226 if (!display_name.is_empty()) {
227 display_name.set_end(
228 it->find(kDisplayNameMark, display_name.start() + 1));
229 gfx::Range line_range(0, it->size());
230 line->ApplyColor(kPublicAccountUserCardNameColor,
231 display_name.Intersect(line_range));
232 // Update the range for the next line.
233 if (display_name.end() >= line_range.end())
234 display_name.set_start(0);
235 else
236 display_name = gfx::Range::InvalidRange();
237 }
238
239 lines_.push_back(line);
240 }
241
242 // Position the link after the label text, separated by a space. If it does
243 // not fit onto the last line of the text, wrap the link onto its own line.
244 const gfx::Size last_line_size = lines_.back()->GetStringSize();
245 const int space_width =
246 gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list);
247 const gfx::Size link_size = learn_more_->GetPreferredSize();
248 if (contents_area.width() - last_line_size.width() >=
249 space_width + link_size.width()) {
250 position.set_x(position.x() + last_line_size.width() + space_width);
251 position.set_y(position.y() - last_line_size.height());
252 }
253 position.set_y(position.y() - learn_more_->GetInsets().top());
254 gfx::Rect learn_more_bounds(position, link_size);
255 learn_more_bounds.Intersect(contents_area);
256 if (base::i18n::IsRTL()) {
257 const gfx::Insets insets = GetInsets();
258 learn_more_bounds.Offset(insets.right() - insets.left(), 0);
259 }
260 learn_more_->SetBoundsRect(learn_more_bounds);
261 }
262
GetPreferredSize() const263 gfx::Size PublicAccountUserDetails::GetPreferredSize() const {
264 return preferred_size_;
265 }
266
OnPaint(gfx::Canvas * canvas)267 void PublicAccountUserDetails::OnPaint(gfx::Canvas* canvas) {
268 for (ScopedVector<gfx::RenderText>::const_iterator it = lines_.begin();
269 it != lines_.end();
270 ++it) {
271 (*it)->Draw(canvas);
272 }
273 views::View::OnPaint(canvas);
274 }
275
LinkClicked(views::Link * source,int event_flags)276 void PublicAccountUserDetails::LinkClicked(views::Link* source,
277 int event_flags) {
278 DCHECK_EQ(source, learn_more_);
279 Shell::GetInstance()->system_tray_delegate()->ShowPublicAccountInfo();
280 }
281
CalculatePreferredSize(int max_allowed_width)282 void PublicAccountUserDetails::CalculatePreferredSize(int max_allowed_width) {
283 const gfx::FontList font_list;
284 const gfx::Size link_size = learn_more_->GetPreferredSize();
285 const int space_width =
286 gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list);
287 const gfx::Insets insets = GetInsets();
288 int min_width = link_size.width();
289 int max_width = std::min(
290 gfx::GetStringWidth(text_, font_list) + space_width + link_size.width(),
291 max_allowed_width - insets.width());
292 // Do a binary search for the minimum width that ensures no more than three
293 // lines are needed. The lower bound is the minimum of the current bubble
294 // width and the width of the link (as no wrapping is permitted inside the
295 // link). The upper bound is the maximum of the largest allowed bubble width
296 // and the sum of the label text and link widths when put on a single line.
297 std::vector<base::string16> lines;
298 while (min_width < max_width) {
299 lines.clear();
300 const int width = (min_width + max_width) / 2;
301 const bool too_narrow = gfx::ElideRectangleText(text_,
302 font_list,
303 width,
304 INT_MAX,
305 gfx::TRUNCATE_LONG_WORDS,
306 &lines) != 0;
307 int line_count = lines.size();
308 if (!too_narrow && line_count == 3 &&
309 width - gfx::GetStringWidth(lines.back(), font_list) <=
310 space_width + link_size.width())
311 ++line_count;
312 if (too_narrow || line_count > 3)
313 min_width = width + 1;
314 else
315 max_width = width;
316 }
317
318 // Calculate the corresponding height and set the preferred size.
319 lines.clear();
320 gfx::ElideRectangleText(
321 text_, font_list, min_width, INT_MAX, gfx::TRUNCATE_LONG_WORDS, &lines);
322 int line_count = lines.size();
323 if (min_width - gfx::GetStringWidth(lines.back(), font_list) <=
324 space_width + link_size.width()) {
325 ++line_count;
326 }
327 const int line_height = font_list.GetHeight();
328 const int link_extra_height = std::max(
329 link_size.height() - learn_more_->GetInsets().top() - line_height, 0);
330 preferred_size_ =
331 gfx::Size(min_width + insets.width(),
332 line_count * line_height + link_extra_height + insets.height());
333 }
334
335 } // namespace
336
UserCardView(user::LoginStatus login_status,int max_width,int multiprofile_index)337 UserCardView::UserCardView(user::LoginStatus login_status,
338 int max_width,
339 int multiprofile_index) {
340 SetLayoutManager(new views::BoxLayout(
341 views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems));
342 switch (login_status) {
343 case user::LOGGED_IN_RETAIL_MODE:
344 AddRetailModeUserContent();
345 break;
346 case user::LOGGED_IN_PUBLIC:
347 AddPublicModeUserContent(max_width);
348 break;
349 default:
350 AddUserContent(login_status, multiprofile_index);
351 break;
352 }
353 }
354
~UserCardView()355 UserCardView::~UserCardView() {}
356
AddRetailModeUserContent()357 void UserCardView::AddRetailModeUserContent() {
358 views::Label* details = new views::Label;
359 details->SetText(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_KIOSK_LABEL));
360 details->SetBorder(views::Border::CreateEmptyBorder(0, 4, 0, 1));
361 details->SetHorizontalAlignment(gfx::ALIGN_LEFT);
362 AddChildView(details);
363 }
364
AddPublicModeUserContent(int max_width)365 void UserCardView::AddPublicModeUserContent(int max_width) {
366 views::View* icon = CreateIcon(user::LOGGED_IN_PUBLIC, 0);
367 AddChildView(icon);
368 int details_max_width = max_width - icon->GetPreferredSize().width() -
369 kTrayPopupPaddingBetweenItems;
370 AddChildView(new PublicAccountUserDetails(details_max_width));
371 }
372
AddUserContent(user::LoginStatus login_status,int multiprofile_index)373 void UserCardView::AddUserContent(user::LoginStatus login_status,
374 int multiprofile_index) {
375 views::View* icon = CreateIcon(login_status, multiprofile_index);
376 AddChildView(icon);
377 views::Label* user_name = NULL;
378 SessionStateDelegate* delegate =
379 Shell::GetInstance()->session_state_delegate();
380 if (!multiprofile_index) {
381 base::string16 user_name_string =
382 login_status == user::LOGGED_IN_GUEST
383 ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_GUEST_LABEL)
384 : delegate->GetUserInfo(multiprofile_index)->GetDisplayName();
385 if (user_name_string.empty() && IsMultiAccountSupportedAndUserActive())
386 user_name_string = base::ASCIIToUTF16(
387 delegate->GetUserInfo(multiprofile_index)->GetEmail());
388 if (!user_name_string.empty()) {
389 user_name = new views::Label(user_name_string);
390 user_name->SetHorizontalAlignment(gfx::ALIGN_LEFT);
391 }
392 }
393
394 views::Label* user_email = NULL;
395 if (login_status != user::LOGGED_IN_GUEST &&
396 (multiprofile_index || !IsMultiAccountSupportedAndUserActive())) {
397 base::string16 user_email_string =
398 login_status == user::LOGGED_IN_LOCALLY_MANAGED
399 ? l10n_util::GetStringUTF16(
400 IDS_ASH_STATUS_TRAY_LOCALLY_MANAGED_LABEL)
401 : base::UTF8ToUTF16(
402 delegate->GetUserInfo(multiprofile_index)->GetEmail());
403 if (!user_email_string.empty()) {
404 user_email = new views::Label(user_email_string);
405 user_email->SetFontList(
406 ui::ResourceBundle::GetSharedInstance().GetFontList(
407 ui::ResourceBundle::SmallFont));
408 user_email->SetHorizontalAlignment(gfx::ALIGN_LEFT);
409 }
410 }
411
412 // Adjust text properties dependent on if it is an active or inactive user.
413 if (multiprofile_index) {
414 // Fade the text of non active users to 50%.
415 SkColor text_color = user_email->enabled_color();
416 text_color = SkColorSetA(text_color, SkColorGetA(text_color) / 2);
417 if (user_email)
418 user_email->SetDisabledColor(text_color);
419 if (user_name)
420 user_name->SetDisabledColor(text_color);
421 }
422
423 if (user_email && user_name) {
424 views::View* details = new views::View;
425 details->SetLayoutManager(new views::BoxLayout(
426 views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0));
427 details->AddChildView(user_name);
428 details->AddChildView(user_email);
429 AddChildView(details);
430 } else {
431 if (user_name)
432 AddChildView(user_name);
433 if (user_email) {
434 #if defined(OS_CHROMEOS)
435 // Only non active user can have a media indicator.
436 MediaIndicator* media_indicator = new MediaIndicator(multiprofile_index);
437 views::View* email_indicator_view = new views::View;
438 email_indicator_view->SetLayoutManager(new views::BoxLayout(
439 views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems));
440 email_indicator_view->AddChildView(user_email);
441 email_indicator_view->AddChildView(media_indicator);
442
443 views::View* details = new views::View;
444 details->SetLayoutManager(new views::BoxLayout(
445 views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0));
446 details->AddChildView(email_indicator_view);
447 details->AddChildView(media_indicator->GetMessageView());
448 AddChildView(details);
449 #else
450 AddChildView(user_email);
451 #endif
452 }
453 }
454 }
455
CreateIcon(user::LoginStatus login_status,int multiprofile_index)456 views::View* UserCardView::CreateIcon(user::LoginStatus login_status,
457 int multiprofile_index) {
458 RoundedImageView* icon =
459 new RoundedImageView(kTrayAvatarCornerRadius, multiprofile_index == 0);
460 if (login_status == user::LOGGED_IN_GUEST) {
461 icon->SetImage(*ui::ResourceBundle::GetSharedInstance()
462 .GetImageNamed(IDR_AURA_UBER_TRAY_GUEST_ICON)
463 .ToImageSkia(),
464 gfx::Size(kTrayAvatarSize, kTrayAvatarSize));
465 } else {
466 SessionStateDelegate* delegate =
467 Shell::GetInstance()->session_state_delegate();
468 content::BrowserContext* context =
469 delegate->GetBrowserContextByIndex(multiprofile_index);
470 icon->SetImage(delegate->GetUserInfo(context)->GetImage(),
471 gfx::Size(kTrayAvatarSize, kTrayAvatarSize));
472 }
473 return icon;
474 }
475
476 } // namespace tray
477 } // namespace ash
478