| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/system/user/user_card_view.h" |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "ash/session/session_state_delegate.h" |
| #include "ash/shell.h" |
| #include "ash/system/tray/system_tray_delegate.h" |
| #include "ash/system/tray/system_tray_notifier.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/user/config.h" |
| #include "ash/system/user/rounded_image_view.h" |
| #include "base/i18n/rtl.h" |
| #include "base/memory/scoped_vector.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/user_manager/user_info.h" |
| #include "grit/ash_resources.h" |
| #include "grit/ash_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/insets.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/gfx/rect.h" |
| #include "ui/gfx/render_text.h" |
| #include "ui/gfx/size.h" |
| #include "ui/gfx/text_elider.h" |
| #include "ui/gfx/text_utils.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/link.h" |
| #include "ui/views/controls/link_listener.h" |
| #include "ui/views/layout/box_layout.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "ash/ash_view_ids.h" |
| #include "ash/media_delegate.h" |
| #include "ash/system/tray/media_security/media_capture_observer.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/layout/fill_layout.h" |
| #endif |
| |
| namespace ash { |
| namespace tray { |
| |
| namespace { |
| |
| const int kUserDetailsVerticalPadding = 5; |
| |
| // The invisible word joiner character, used as a marker to indicate the start |
| // and end of the user's display name in the public account user card's text. |
| const base::char16 kDisplayNameMark[] = {0x2060, 0}; |
| |
| #if defined(OS_CHROMEOS) |
| class MediaIndicator : public views::View, public MediaCaptureObserver { |
| public: |
| explicit MediaIndicator(MultiProfileIndex index) |
| : index_(index), label_(new views::Label) { |
| SetLayoutManager(new views::FillLayout); |
| views::ImageView* icon = new views::ImageView; |
| icon->SetImage(ui::ResourceBundle::GetSharedInstance() |
| .GetImageNamed(IDR_AURA_UBER_TRAY_RECORDING_RED) |
| .ToImageSkia()); |
| AddChildView(icon); |
| label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| label_->SetFontList(ui::ResourceBundle::GetSharedInstance().GetFontList( |
| ui::ResourceBundle::SmallFont)); |
| OnMediaCaptureChanged(); |
| Shell::GetInstance()->system_tray_notifier()->AddMediaCaptureObserver(this); |
| set_id(VIEW_ID_USER_VIEW_MEDIA_INDICATOR); |
| } |
| |
| virtual ~MediaIndicator() { |
| Shell::GetInstance()->system_tray_notifier()->RemoveMediaCaptureObserver( |
| this); |
| } |
| |
| // MediaCaptureObserver: |
| virtual void OnMediaCaptureChanged() OVERRIDE { |
| Shell* shell = Shell::GetInstance(); |
| content::BrowserContext* context = |
| shell->session_state_delegate()->GetBrowserContextByIndex(index_); |
| MediaCaptureState state = |
| Shell::GetInstance()->media_delegate()->GetMediaCaptureState(context); |
| int res_id = 0; |
| switch (state) { |
| case MEDIA_CAPTURE_AUDIO_VIDEO: |
| res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO_VIDEO; |
| break; |
| case MEDIA_CAPTURE_AUDIO: |
| res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_AUDIO; |
| break; |
| case MEDIA_CAPTURE_VIDEO: |
| res_id = IDS_ASH_STATUS_TRAY_MEDIA_RECORDING_VIDEO; |
| break; |
| case MEDIA_CAPTURE_NONE: |
| break; |
| } |
| SetMessage(res_id ? l10n_util::GetStringUTF16(res_id) : base::string16()); |
| } |
| |
| views::View* GetMessageView() { return label_; } |
| |
| void SetMessage(const base::string16& message) { |
| SetVisible(!message.empty()); |
| label_->SetText(message); |
| label_->SetVisible(!message.empty()); |
| } |
| |
| private: |
| MultiProfileIndex index_; |
| views::Label* label_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MediaIndicator); |
| }; |
| #endif |
| |
| // The user details shown in public account mode. This is essentially a label |
| // but with custom painting code as the text is styled with multiple colors and |
| // contains a link. |
| class PublicAccountUserDetails : public views::View, |
| public views::LinkListener { |
| public: |
| PublicAccountUserDetails(int max_width); |
| virtual ~PublicAccountUserDetails(); |
| |
| private: |
| // Overridden from views::View. |
| virtual void Layout() OVERRIDE; |
| virtual gfx::Size GetPreferredSize() const OVERRIDE; |
| virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; |
| |
| // Overridden from views::LinkListener. |
| virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; |
| |
| // Calculate a preferred size that ensures the label text and the following |
| // link do not wrap over more than three lines in total for aesthetic reasons |
| // if possible. |
| void CalculatePreferredSize(int max_allowed_width); |
| |
| base::string16 text_; |
| views::Link* learn_more_; |
| gfx::Size preferred_size_; |
| ScopedVector<gfx::RenderText> lines_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PublicAccountUserDetails); |
| }; |
| |
| PublicAccountUserDetails::PublicAccountUserDetails(int max_width) |
| : learn_more_(NULL) { |
| const int inner_padding = |
| kTrayPopupPaddingHorizontal - kTrayPopupPaddingBetweenItems; |
| const bool rtl = base::i18n::IsRTL(); |
| SetBorder(views::Border::CreateEmptyBorder(kUserDetailsVerticalPadding, |
| rtl ? 0 : inner_padding, |
| kUserDetailsVerticalPadding, |
| rtl ? inner_padding : 0)); |
| |
| // Retrieve the user's display name and wrap it with markers. |
| // Note that since this is a public account it always has to be the primary |
| // user. |
| base::string16 display_name = Shell::GetInstance() |
| ->session_state_delegate() |
| ->GetUserInfo(0) |
| ->GetDisplayName(); |
| base::RemoveChars(display_name, kDisplayNameMark, &display_name); |
| display_name = kDisplayNameMark[0] + display_name + kDisplayNameMark[0]; |
| // Retrieve the domain managing the device and wrap it with markers. |
| base::string16 domain = base::UTF8ToUTF16( |
| Shell::GetInstance()->system_tray_delegate()->GetEnterpriseDomain()); |
| base::RemoveChars(domain, kDisplayNameMark, &domain); |
| base::i18n::WrapStringWithLTRFormatting(&domain); |
| // Retrieve the label text, inserting the display name and domain. |
| text_ = l10n_util::GetStringFUTF16( |
| IDS_ASH_STATUS_TRAY_PUBLIC_LABEL, display_name, domain); |
| |
| learn_more_ = new views::Link(l10n_util::GetStringUTF16(IDS_ASH_LEARN_MORE)); |
| learn_more_->SetUnderline(false); |
| learn_more_->set_listener(this); |
| AddChildView(learn_more_); |
| |
| CalculatePreferredSize(max_width); |
| } |
| |
| PublicAccountUserDetails::~PublicAccountUserDetails() {} |
| |
| void PublicAccountUserDetails::Layout() { |
| lines_.clear(); |
| const gfx::Rect contents_area = GetContentsBounds(); |
| if (contents_area.IsEmpty()) |
| return; |
| |
| // Word-wrap the label text. |
| const gfx::FontList font_list; |
| std::vector<base::string16> lines; |
| gfx::ElideRectangleText(text_, |
| font_list, |
| contents_area.width(), |
| contents_area.height(), |
| gfx::ELIDE_LONG_WORDS, |
| &lines); |
| // Loop through the lines, creating a renderer for each. |
| gfx::Point position = contents_area.origin(); |
| gfx::Range display_name(gfx::Range::InvalidRange()); |
| for (std::vector<base::string16>::const_iterator it = lines.begin(); |
| it != lines.end(); |
| ++it) { |
| gfx::RenderText* line = gfx::RenderText::CreateInstance(); |
| line->SetDirectionalityMode(gfx::DIRECTIONALITY_FROM_UI); |
| line->SetText(*it); |
| const gfx::Size size(contents_area.width(), line->GetStringSize().height()); |
| line->SetDisplayRect(gfx::Rect(position, size)); |
| position.set_y(position.y() + size.height()); |
| |
| // Set the default text color for the line. |
| line->SetColor(kPublicAccountUserCardTextColor); |
| |
| // If a range of the line contains the user's display name, apply a custom |
| // text color to it. |
| if (display_name.is_empty()) |
| display_name.set_start(it->find(kDisplayNameMark)); |
| if (!display_name.is_empty()) { |
| display_name.set_end( |
| it->find(kDisplayNameMark, display_name.start() + 1)); |
| gfx::Range line_range(0, it->size()); |
| line->ApplyColor(kPublicAccountUserCardNameColor, |
| display_name.Intersect(line_range)); |
| // Update the range for the next line. |
| if (display_name.end() >= line_range.end()) |
| display_name.set_start(0); |
| else |
| display_name = gfx::Range::InvalidRange(); |
| } |
| |
| lines_.push_back(line); |
| } |
| |
| // Position the link after the label text, separated by a space. If it does |
| // not fit onto the last line of the text, wrap the link onto its own line. |
| const gfx::Size last_line_size = lines_.back()->GetStringSize(); |
| const int space_width = |
| gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list); |
| const gfx::Size link_size = learn_more_->GetPreferredSize(); |
| if (contents_area.width() - last_line_size.width() >= |
| space_width + link_size.width()) { |
| position.set_x(position.x() + last_line_size.width() + space_width); |
| position.set_y(position.y() - last_line_size.height()); |
| } |
| position.set_y(position.y() - learn_more_->GetInsets().top()); |
| gfx::Rect learn_more_bounds(position, link_size); |
| learn_more_bounds.Intersect(contents_area); |
| if (base::i18n::IsRTL()) { |
| const gfx::Insets insets = GetInsets(); |
| learn_more_bounds.Offset(insets.right() - insets.left(), 0); |
| } |
| learn_more_->SetBoundsRect(learn_more_bounds); |
| } |
| |
| gfx::Size PublicAccountUserDetails::GetPreferredSize() const { |
| return preferred_size_; |
| } |
| |
| void PublicAccountUserDetails::OnPaint(gfx::Canvas* canvas) { |
| for (ScopedVector<gfx::RenderText>::const_iterator it = lines_.begin(); |
| it != lines_.end(); |
| ++it) { |
| (*it)->Draw(canvas); |
| } |
| views::View::OnPaint(canvas); |
| } |
| |
| void PublicAccountUserDetails::LinkClicked(views::Link* source, |
| int event_flags) { |
| DCHECK_EQ(source, learn_more_); |
| Shell::GetInstance()->system_tray_delegate()->ShowPublicAccountInfo(); |
| } |
| |
| void PublicAccountUserDetails::CalculatePreferredSize(int max_allowed_width) { |
| const gfx::FontList font_list; |
| const gfx::Size link_size = learn_more_->GetPreferredSize(); |
| const int space_width = |
| gfx::GetStringWidth(base::ASCIIToUTF16(" "), font_list); |
| const gfx::Insets insets = GetInsets(); |
| int min_width = link_size.width(); |
| int max_width = std::min( |
| gfx::GetStringWidth(text_, font_list) + space_width + link_size.width(), |
| max_allowed_width - insets.width()); |
| // Do a binary search for the minimum width that ensures no more than three |
| // lines are needed. The lower bound is the minimum of the current bubble |
| // width and the width of the link (as no wrapping is permitted inside the |
| // link). The upper bound is the maximum of the largest allowed bubble width |
| // and the sum of the label text and link widths when put on a single line. |
| std::vector<base::string16> lines; |
| while (min_width < max_width) { |
| lines.clear(); |
| const int width = (min_width + max_width) / 2; |
| const bool too_narrow = gfx::ElideRectangleText(text_, |
| font_list, |
| width, |
| INT_MAX, |
| gfx::TRUNCATE_LONG_WORDS, |
| &lines) != 0; |
| int line_count = lines.size(); |
| if (!too_narrow && line_count == 3 && |
| width - gfx::GetStringWidth(lines.back(), font_list) <= |
| space_width + link_size.width()) |
| ++line_count; |
| if (too_narrow || line_count > 3) |
| min_width = width + 1; |
| else |
| max_width = width; |
| } |
| |
| // Calculate the corresponding height and set the preferred size. |
| lines.clear(); |
| gfx::ElideRectangleText( |
| text_, font_list, min_width, INT_MAX, gfx::TRUNCATE_LONG_WORDS, &lines); |
| int line_count = lines.size(); |
| if (min_width - gfx::GetStringWidth(lines.back(), font_list) <= |
| space_width + link_size.width()) { |
| ++line_count; |
| } |
| const int line_height = font_list.GetHeight(); |
| const int link_extra_height = std::max( |
| link_size.height() - learn_more_->GetInsets().top() - line_height, 0); |
| preferred_size_ = |
| gfx::Size(min_width + insets.width(), |
| line_count * line_height + link_extra_height + insets.height()); |
| } |
| |
| } // namespace |
| |
| UserCardView::UserCardView(user::LoginStatus login_status, |
| int max_width, |
| int multiprofile_index) { |
| SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems)); |
| switch (login_status) { |
| case user::LOGGED_IN_RETAIL_MODE: |
| AddRetailModeUserContent(); |
| break; |
| case user::LOGGED_IN_PUBLIC: |
| AddPublicModeUserContent(max_width); |
| break; |
| default: |
| AddUserContent(login_status, multiprofile_index); |
| break; |
| } |
| } |
| |
| UserCardView::~UserCardView() {} |
| |
| void UserCardView::AddRetailModeUserContent() { |
| views::Label* details = new views::Label; |
| details->SetText(l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_KIOSK_LABEL)); |
| details->SetBorder(views::Border::CreateEmptyBorder(0, 4, 0, 1)); |
| details->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| AddChildView(details); |
| } |
| |
| void UserCardView::AddPublicModeUserContent(int max_width) { |
| views::View* icon = CreateIcon(user::LOGGED_IN_PUBLIC, 0); |
| AddChildView(icon); |
| int details_max_width = max_width - icon->GetPreferredSize().width() - |
| kTrayPopupPaddingBetweenItems; |
| AddChildView(new PublicAccountUserDetails(details_max_width)); |
| } |
| |
| void UserCardView::AddUserContent(user::LoginStatus login_status, |
| int multiprofile_index) { |
| views::View* icon = CreateIcon(login_status, multiprofile_index); |
| AddChildView(icon); |
| views::Label* user_name = NULL; |
| SessionStateDelegate* delegate = |
| Shell::GetInstance()->session_state_delegate(); |
| if (!multiprofile_index) { |
| base::string16 user_name_string = |
| login_status == user::LOGGED_IN_GUEST |
| ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_GUEST_LABEL) |
| : delegate->GetUserInfo(multiprofile_index)->GetDisplayName(); |
| if (user_name_string.empty() && IsMultiAccountSupportedAndUserActive()) |
| user_name_string = base::ASCIIToUTF16( |
| delegate->GetUserInfo(multiprofile_index)->GetEmail()); |
| if (!user_name_string.empty()) { |
| user_name = new views::Label(user_name_string); |
| user_name->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| } |
| } |
| |
| views::Label* user_email = NULL; |
| if (login_status != user::LOGGED_IN_GUEST && |
| (multiprofile_index || !IsMultiAccountSupportedAndUserActive())) { |
| base::string16 user_email_string = |
| login_status == user::LOGGED_IN_LOCALLY_MANAGED |
| ? l10n_util::GetStringUTF16( |
| IDS_ASH_STATUS_TRAY_LOCALLY_MANAGED_LABEL) |
| : base::UTF8ToUTF16( |
| delegate->GetUserInfo(multiprofile_index)->GetEmail()); |
| if (!user_email_string.empty()) { |
| user_email = new views::Label(user_email_string); |
| user_email->SetFontList( |
| ui::ResourceBundle::GetSharedInstance().GetFontList( |
| ui::ResourceBundle::SmallFont)); |
| user_email->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| } |
| } |
| |
| // Adjust text properties dependent on if it is an active or inactive user. |
| if (multiprofile_index) { |
| // Fade the text of non active users to 50%. |
| SkColor text_color = user_email->enabled_color(); |
| text_color = SkColorSetA(text_color, SkColorGetA(text_color) / 2); |
| if (user_email) |
| user_email->SetDisabledColor(text_color); |
| if (user_name) |
| user_name->SetDisabledColor(text_color); |
| } |
| |
| if (user_email && user_name) { |
| views::View* details = new views::View; |
| details->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0)); |
| details->AddChildView(user_name); |
| details->AddChildView(user_email); |
| AddChildView(details); |
| } else { |
| if (user_name) |
| AddChildView(user_name); |
| if (user_email) { |
| #if defined(OS_CHROMEOS) |
| // Only non active user can have a media indicator. |
| MediaIndicator* media_indicator = new MediaIndicator(multiprofile_index); |
| views::View* email_indicator_view = new views::View; |
| email_indicator_view->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems)); |
| email_indicator_view->AddChildView(user_email); |
| email_indicator_view->AddChildView(media_indicator); |
| |
| views::View* details = new views::View; |
| details->SetLayoutManager(new views::BoxLayout( |
| views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0)); |
| details->AddChildView(email_indicator_view); |
| details->AddChildView(media_indicator->GetMessageView()); |
| AddChildView(details); |
| #else |
| AddChildView(user_email); |
| #endif |
| } |
| } |
| } |
| |
| views::View* UserCardView::CreateIcon(user::LoginStatus login_status, |
| int multiprofile_index) { |
| RoundedImageView* icon = |
| new RoundedImageView(kTrayAvatarCornerRadius, multiprofile_index == 0); |
| if (login_status == user::LOGGED_IN_GUEST) { |
| icon->SetImage(*ui::ResourceBundle::GetSharedInstance() |
| .GetImageNamed(IDR_AURA_UBER_TRAY_GUEST_ICON) |
| .ToImageSkia(), |
| gfx::Size(kTrayAvatarSize, kTrayAvatarSize)); |
| } else { |
| SessionStateDelegate* delegate = |
| Shell::GetInstance()->session_state_delegate(); |
| content::BrowserContext* context = |
| delegate->GetBrowserContextByIndex(multiprofile_index); |
| icon->SetImage(delegate->GetUserInfo(context)->GetImage(), |
| gfx::Size(kTrayAvatarSize, kTrayAvatarSize)); |
| } |
| return icon; |
| } |
| |
| } // namespace tray |
| } // namespace ash |