Home | History | Annotate | Download | only in user
      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:
     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     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 
    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