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/shell.h" 12 #include "ash/system/tray/system_tray_delegate.h" 13 #include "ash/system/tray/system_tray_notifier.h" 14 #include "ash/system/tray/tray_constants.h" 15 #include "ash/system/user/config.h" 16 #include "ash/system/user/rounded_image_view.h" 17 #include "base/i18n/rtl.h" 18 #include "base/memory/scoped_vector.h" 19 #include "base/strings/string16.h" 20 #include "base/strings/string_util.h" 21 #include "base/strings/utf_string_conversions.h" 22 #include "components/user_manager/user_info.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: 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 77 virtual ~MediaIndicator() { 78 Shell::GetInstance()->system_tray_notifier()->RemoveMediaCaptureObserver( 79 this); 80 } 81 82 // MediaCaptureObserver: 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 106 views::View* GetMessageView() { return label_; } 107 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 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 189 PublicAccountUserDetails::~PublicAccountUserDetails() {} 190 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 263 gfx::Size PublicAccountUserDetails::GetPreferredSize() const { 264 return preferred_size_; 265 } 266 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 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 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 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 355 UserCardView::~UserCardView() {} 356 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 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 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 SystemTrayDelegate* tray_delegate = 398 Shell::GetInstance()->system_tray_delegate(); 399 base::string16 user_email_string = 400 tray_delegate->IsUserSupervised() 401 ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SUPERVISED_LABEL) 402 : base::UTF8ToUTF16( 403 delegate->GetUserInfo(multiprofile_index)->GetEmail()); 404 if (!user_email_string.empty()) { 405 user_email = new views::Label(user_email_string); 406 user_email->SetFontList( 407 ui::ResourceBundle::GetSharedInstance().GetFontList( 408 ui::ResourceBundle::SmallFont)); 409 user_email->SetHorizontalAlignment(gfx::ALIGN_LEFT); 410 } 411 } 412 413 // Adjust text properties dependent on if it is an active or inactive user. 414 if (multiprofile_index) { 415 // Fade the text of non active users to 50%. 416 SkColor text_color = user_email->enabled_color(); 417 text_color = SkColorSetA(text_color, SkColorGetA(text_color) / 2); 418 if (user_email) 419 user_email->SetDisabledColor(text_color); 420 if (user_name) 421 user_name->SetDisabledColor(text_color); 422 } 423 424 if (user_email && user_name) { 425 views::View* details = new views::View; 426 details->SetLayoutManager(new views::BoxLayout( 427 views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0)); 428 details->AddChildView(user_name); 429 details->AddChildView(user_email); 430 AddChildView(details); 431 } else { 432 if (user_name) 433 AddChildView(user_name); 434 if (user_email) { 435 #if defined(OS_CHROMEOS) 436 // Only non active user can have a media indicator. 437 MediaIndicator* media_indicator = new MediaIndicator(multiprofile_index); 438 views::View* email_indicator_view = new views::View; 439 email_indicator_view->SetLayoutManager(new views::BoxLayout( 440 views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems)); 441 email_indicator_view->AddChildView(user_email); 442 email_indicator_view->AddChildView(media_indicator); 443 444 views::View* details = new views::View; 445 details->SetLayoutManager(new views::BoxLayout( 446 views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0)); 447 details->AddChildView(email_indicator_view); 448 details->AddChildView(media_indicator->GetMessageView()); 449 AddChildView(details); 450 #else 451 AddChildView(user_email); 452 #endif 453 } 454 } 455 } 456 457 views::View* UserCardView::CreateIcon(user::LoginStatus login_status, 458 int multiprofile_index) { 459 RoundedImageView* icon = 460 new RoundedImageView(kTrayAvatarCornerRadius, multiprofile_index == 0); 461 if (login_status == user::LOGGED_IN_GUEST) { 462 icon->SetImage(*ui::ResourceBundle::GetSharedInstance() 463 .GetImageNamed(IDR_AURA_UBER_TRAY_GUEST_ICON) 464 .ToImageSkia(), 465 gfx::Size(kTrayAvatarSize, kTrayAvatarSize)); 466 } else { 467 SessionStateDelegate* delegate = 468 Shell::GetInstance()->session_state_delegate(); 469 content::BrowserContext* context = 470 delegate->GetBrowserContextByIndex(multiprofile_index); 471 icon->SetImage(delegate->GetUserInfo(context)->GetImage(), 472 gfx::Size(kTrayAvatarSize, kTrayAvatarSize)); 473 } 474 return icon; 475 } 476 477 } // namespace tray 478 } // namespace ash 479