blob: 2e030f5bdbe30cdd5e16824cb6a16db3393065f1 [file] [log] [blame]
// Copyright (c) 2012 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/tray_user.h"
#include <algorithm>
#include <climits>
#include <vector>
#include "ash/ash_switches.h"
#include "ash/popup_message.h"
#include "ash/root_window_controller.h"
#include "ash/session_state_delegate.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include "ash/system/tray/system_tray.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/tray/tray_item_view.h"
#include "ash/system/tray/tray_popup_label_button.h"
#include "ash/system/tray/tray_popup_label_button_border.h"
#include "ash/system/tray/tray_utils.h"
#include "base/i18n/rtl.h"
#include "base/logging.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 "grit/ash_resources.h"
#include "grit/ash_strings.h"
#include "skia/ext/image_operations.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/font.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.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/skia_util.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/border.h"
#include "ui/views/bubble/tray_bubble_view.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/custom_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/link.h"
#include "ui/views/controls/link_listener.h"
#include "ui/views/corewm/shadow_types.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/mouse_watcher.h"
#include "ui/views/painter.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace {
const int kUserDetailsVerticalPadding = 5;
const int kUserCardVerticalPadding = 10;
const int kInactiveUserCardVerticalPadding = 4;
const int kProfileRoundedCornerRadius = 2;
const int kUserIconSize = 27;
const int kUserIconLargeSize = 32;
const int kUserIconLargeCornerRadius = 2;
const int kUserLabelToIconPadding = 5;
// When a hover border is used, it is starting this many pixels before the icon
// position.
const int kTrayUserTileHoverBorderInset = 10;
// The border color of the user button.
const SkColor kBorderColor = 0xffdcdcdc;
// 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 char16 kDisplayNameMark[] = { 0x2060, 0 };
const int kPublicAccountLogoutButtonBorderImagesNormal[] = {
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
};
const int kPublicAccountLogoutButtonBorderImagesHovered[] = {
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_LABEL_BUTTON_HOVER_BACKGROUND,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
};
// Offsetting the popup message relative to the tray menu.
const int kPopupMessageOffset = 25;
} // namespace
namespace ash {
namespace internal {
namespace tray {
// A custom image view with rounded edges.
class RoundedImageView : public views::View {
public:
// Constructs a new rounded image view with rounded corners of radius
// |corner_radius|. If |active_user| is set, the icon will be drawn in
// full colors - otherwise it will fade into the background.
RoundedImageView(int corner_radius, bool active_user);
virtual ~RoundedImageView();
// Set the image that should be displayed. The image contents is copied to the
// receiver's image.
void SetImage(const gfx::ImageSkia& img, const gfx::Size& size);
// Set the radii of the corners independantly.
void SetCornerRadii(int top_left,
int top_right,
int bottom_right,
int bottom_left);
private:
// Overridden from views::View.
virtual gfx::Size GetPreferredSize() OVERRIDE;
virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE;
gfx::ImageSkia image_;
gfx::ImageSkia resized_;
gfx::Size image_size_;
int corner_radius_[4];
// True if the given user is the active user and the icon should get
// painted as active.
bool active_user_;
DISALLOW_COPY_AND_ASSIGN(RoundedImageView);
};
// 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(SystemTrayItem* owner, int used_width);
virtual ~PublicAccountUserDetails();
private:
// Overridden from views::View.
virtual void Layout() OVERRIDE;
virtual gfx::Size GetPreferredSize() 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(SystemTrayItem* owner, int used_width);
base::string16 text_;
views::Link* learn_more_;
gfx::Size preferred_size_;
ScopedVector<gfx::RenderText> lines_;
DISALLOW_COPY_AND_ASSIGN(PublicAccountUserDetails);
};
// The button which holds the user card in case of multi profile.
class UserCard : public views::CustomButton {
public:
UserCard(views::ButtonListener* listener, bool active_user);
virtual ~UserCard();
// Called when the border should remain even in the non highlighted state.
void ForceBorderVisible(bool show);
// Overridden from views::View
virtual void OnMouseEntered(const ui::MouseEvent& event) OVERRIDE;
virtual void OnMouseExited(const ui::MouseEvent& event) OVERRIDE;
// Check if the item is hovered.
bool is_hovered_for_test() {return button_hovered_; }
private:
// Change the hover/active state of the "button" when the status changes.
void ShowActive();
// True if this is the active user.
bool is_active_user_;
// True if button is hovered.
bool button_hovered_;
// True if the border should be visible.
bool show_border_;
DISALLOW_COPY_AND_ASSIGN(UserCard);
};
class UserViewMouseWatcherHost : public views::MouseWatcherHost {
public:
explicit UserViewMouseWatcherHost(const gfx::Rect& screen_area)
: screen_area_(screen_area) {}
virtual ~UserViewMouseWatcherHost() {}
// Implementation of MouseWatcherHost.
virtual bool Contains(const gfx::Point& screen_point,
views::MouseWatcherHost::MouseEventType type) OVERRIDE {
return screen_area_.Contains(screen_point);
}
private:
gfx::Rect screen_area_;
DISALLOW_COPY_AND_ASSIGN(UserViewMouseWatcherHost);
};
// The view of a user item.
class UserView : public views::View,
public views::ButtonListener,
public views::MouseWatcherListener {
public:
UserView(SystemTrayItem* owner,
ash::user::LoginStatus login,
MultiProfileIndex index);
virtual ~UserView();
// Overridden from MouseWatcherListener:
virtual void MouseMovedOutOfHost() OVERRIDE;
TrayUser::TestState GetStateForTest() const;
gfx::Rect GetBoundsInScreenOfUserButtonForTest();
private:
// Overridden from views::View.
virtual gfx::Size GetPreferredSize() OVERRIDE;
virtual int GetHeightForWidth(int width) OVERRIDE;
virtual void Layout() OVERRIDE;
// Overridden from views::ButtonListener.
virtual void ButtonPressed(views::Button* sender,
const ui::Event& event) OVERRIDE;
void AddLogoutButton(ash::user::LoginStatus login);
void AddUserCard(SystemTrayItem* owner, ash::user::LoginStatus login);
// Create a user icon representation for the user card.
views::View* CreateIconForUserCard(ash::user::LoginStatus login);
// Create the additional user card content for the retail logged in mode.
void AddLoggedInRetailModeUserCardContent();
// Create the additional user card content for the public mode.
void AddLoggedInPublicModeUserCardContent(SystemTrayItem* owner);
// Create the menu option to add another user. If |disabled| is set the user
// cannot actively click on the item.
void ToggleAddUserMenuOption();
MultiProfileIndex multiprofile_index_;
// The view of the user card.
views::View* user_card_view_;
// This is the owner system tray item of this view.
SystemTrayItem* owner_;
// True if |user_card_view_| is a |UserView| - otherwise it is only a
// |views::View|.
bool is_user_card_;
views::View* logout_button_;
scoped_ptr<ash::PopupMessage> popup_message_;
scoped_ptr<views::Widget> add_menu_option_;
// True when the add user panel is visible but not activatable.
bool add_user_visible_but_disabled_;
// The mouse watcher which takes care of out of window hover events.
scoped_ptr<views::MouseWatcher> mouse_watcher_;
DISALLOW_COPY_AND_ASSIGN(UserView);
};
// The menu item view which gets shown when the user clicks in multi profile
// mode onto the user item.
class AddUserView : public views::CustomButton,
public views::ButtonListener {
public:
// The |owner| is the view for which this view gets created. The |listener|
// will get notified when this item gets clicked.
AddUserView(UserCard* owner, views::ButtonListener* listener);
virtual ~AddUserView();
// Get the anchor view for a message.
views::View* anchor() { return anchor_; }
// Overridden from views::ButtonListener.
virtual void ButtonPressed(views::Button* sender,
const ui::Event& event) OVERRIDE;
private:
// Overridden from views::View.
virtual gfx::Size GetPreferredSize() OVERRIDE;
virtual int GetHeightForWidth(int width) OVERRIDE;
virtual void Layout() OVERRIDE;
// Create the additional client content for this item.
void AddContent();
// This is the content we create and show.
views::View* add_user_;
// This listener will get informed when someone clicks on this button.
views::ButtonListener* listener_;
// This is the owner view of this item.
UserCard* owner_;
// The anchor view for targetted bubble messages.
views::View* anchor_;
DISALLOW_COPY_AND_ASSIGN(AddUserView);
};
RoundedImageView::RoundedImageView(int corner_radius, bool active_user)
: active_user_(active_user) {
for (int i = 0; i < 4; ++i)
corner_radius_[i] = corner_radius;
}
RoundedImageView::~RoundedImageView() {}
void RoundedImageView::SetImage(const gfx::ImageSkia& img,
const gfx::Size& size) {
image_ = img;
image_size_ = size;
// Try to get the best image quality for the avatar.
resized_ = gfx::ImageSkiaOperations::CreateResizedImage(image_,
skia::ImageOperations::RESIZE_BEST, size);
if (GetWidget() && visible()) {
PreferredSizeChanged();
SchedulePaint();
}
}
void RoundedImageView::SetCornerRadii(int top_left,
int top_right,
int bottom_right,
int bottom_left) {
corner_radius_[0] = top_left;
corner_radius_[1] = top_right;
corner_radius_[2] = bottom_right;
corner_radius_[3] = bottom_left;
}
gfx::Size RoundedImageView::GetPreferredSize() {
return gfx::Size(image_size_.width() + GetInsets().width(),
image_size_.height() + GetInsets().height());
}
void RoundedImageView::OnPaint(gfx::Canvas* canvas) {
View::OnPaint(canvas);
gfx::Rect image_bounds(size());
image_bounds.ClampToCenteredSize(GetPreferredSize());
image_bounds.Inset(GetInsets());
const SkScalar kRadius[8] = {
SkIntToScalar(corner_radius_[0]),
SkIntToScalar(corner_radius_[0]),
SkIntToScalar(corner_radius_[1]),
SkIntToScalar(corner_radius_[1]),
SkIntToScalar(corner_radius_[2]),
SkIntToScalar(corner_radius_[2]),
SkIntToScalar(corner_radius_[3]),
SkIntToScalar(corner_radius_[3])
};
SkPath path;
path.addRoundRect(gfx::RectToSkRect(image_bounds), kRadius);
SkPaint paint;
paint.setAntiAlias(true);
paint.setXfermodeMode(active_user_ ? SkXfermode::kSrcOver_Mode :
SkXfermode::kLuminosity_Mode);
canvas->DrawImageInPath(resized_, image_bounds.x(), image_bounds.y(),
path, paint);
}
PublicAccountUserDetails::PublicAccountUserDetails(SystemTrayItem* owner,
int used_width)
: learn_more_(NULL) {
const int inner_padding =
kTrayPopupPaddingHorizontal - kTrayPopupPaddingBetweenItems;
const bool rtl = base::i18n::IsRTL();
set_border(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 =
ash::Shell::GetInstance()->session_state_delegate()->
GetUserDisplayName(0);
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 = UTF8ToUTF16(
ash::Shell::GetInstance()->system_tray_delegate()->GetEnterpriseDomain());
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(owner, used_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::Font font;
std::vector<base::string16> lines;
gfx::ElideRectangleText(text_, font, 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 = font.GetStringWidth(ASCIIToUTF16(" "));
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() {
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_);
ash::Shell::GetInstance()->system_tray_delegate()->ShowPublicAccountInfo();
}
void PublicAccountUserDetails::CalculatePreferredSize(SystemTrayItem* owner,
int used_width) {
const gfx::Font font;
const gfx::Size link_size = learn_more_->GetPreferredSize();
const int space_width = font.GetStringWidth(ASCIIToUTF16(" "));
const gfx::Insets insets = GetInsets();
views::TrayBubbleView* bubble_view =
owner->system_tray()->GetSystemBubble()->bubble_view();
int min_width = std::max(
link_size.width(),
bubble_view->GetPreferredSize().width() - (used_width + insets.width()));
int max_width = std::min(
font.GetStringWidth(text_) + space_width + link_size.width(),
bubble_view->GetMaximumSize().width() - (used_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, width, INT_MAX, gfx::TRUNCATE_LONG_WORDS, &lines) != 0;
int line_count = lines.size();
if (!too_narrow && line_count == 3 &&
width - font.GetStringWidth(lines.back()) <=
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, min_width, INT_MAX, gfx::TRUNCATE_LONG_WORDS, &lines);
int line_count = lines.size();
if (min_width - font.GetStringWidth(lines.back()) <=
space_width + link_size.width()) {
++line_count;
}
const int line_height = font.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());
bubble_view->SetWidth(preferred_size_.width() + used_width);
}
UserCard::UserCard(views::ButtonListener* listener, bool active_user)
: CustomButton(listener),
is_active_user_(active_user),
button_hovered_(false),
show_border_(false) {
if (is_active_user_) {
set_background(
views::Background::CreateSolidBackground(kBackgroundColor));
ShowActive();
}
}
UserCard::~UserCard() {}
void UserCard::ForceBorderVisible(bool show) {
show_border_ = show;
ShowActive();
}
void UserCard::OnMouseEntered(const ui::MouseEvent& event) {
if (is_active_user_) {
button_hovered_ = true;
background()->SetNativeControlColor(kHoverBackgroundColor);
ShowActive();
}
}
void UserCard::OnMouseExited(const ui::MouseEvent& event) {
if (is_active_user_) {
button_hovered_ = false;
background()->SetNativeControlColor(kBackgroundColor);
ShowActive();
}
}
void UserCard::ShowActive() {
int width = button_hovered_ || show_border_ ? 1 : 0;
set_border(views::Border::CreateSolidSidedBorder(width, width, width, 1,
kBorderColor));
SchedulePaint();
}
UserView::UserView(SystemTrayItem* owner,
ash::user::LoginStatus login,
MultiProfileIndex index)
: multiprofile_index_(index),
user_card_view_(NULL),
owner_(owner),
is_user_card_(false),
logout_button_(NULL),
add_user_visible_but_disabled_(false) {
CHECK_NE(ash::user::LOGGED_IN_NONE, login);
if (!index) {
// Only the logged in user will have a background. All other users will have
// to allow the TrayPopupContainer highlighting the menu line.
set_background(views::Background::CreateSolidBackground(
login == ash::user::LOGGED_IN_PUBLIC ? kPublicAccountBackgroundColor :
kBackgroundColor));
}
SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0,
kTrayPopupPaddingBetweenItems));
// The logout button must be added before the user card so that the user card
// can correctly calculate the remaining available width.
// Note that only the current multiprofile user gets a button.
if (!multiprofile_index_)
AddLogoutButton(login);
AddUserCard(owner, login);
}
UserView::~UserView() {}
void UserView::MouseMovedOutOfHost() {
popup_message_.reset();
mouse_watcher_.reset();
add_menu_option_.reset();
}
TrayUser::TestState UserView::GetStateForTest() const {
if (add_menu_option_.get()) {
return add_user_visible_but_disabled_ ? TrayUser::ACTIVE_BUT_DISABLED :
TrayUser::ACTIVE;
}
if (!is_user_card_)
return TrayUser::SHOWN;
return static_cast<UserCard*>(user_card_view_)->is_hovered_for_test() ?
TrayUser::HOVERED : TrayUser::SHOWN;
}
gfx::Rect UserView::GetBoundsInScreenOfUserButtonForTest() {
DCHECK(user_card_view_);
return user_card_view_->GetBoundsInScreen();
}
gfx::Size UserView::GetPreferredSize() {
gfx::Size size = views::View::GetPreferredSize();
// Only the active user panel will be forced to a certain height.
if (!multiprofile_index_) {
size.set_height(std::max(size.height(),
kTrayPopupItemHeight + GetInsets().height()));
}
return size;
}
int UserView::GetHeightForWidth(int width) {
return GetPreferredSize().height();
}
void UserView::Layout() {
gfx::Rect contents_area(GetContentsBounds());
if (user_card_view_ && logout_button_) {
// Give the logout button the space it requests.
gfx::Rect logout_area = contents_area;
logout_area.ClampToCenteredSize(logout_button_->GetPreferredSize());
logout_area.set_x(contents_area.right() - logout_area.width());
// Give the remaining space to the user card.
gfx::Rect user_card_area = contents_area;
int remaining_width = contents_area.width() - logout_area.width();
if (ash::Shell::GetInstance()->delegate()->IsMultiProfilesEnabled()) {
// In multiprofile case |user_card_view_| and |logout_button_| have to
// have the same height.
int y = std::min(user_card_area.y(), logout_area.y());
int height = std::max(user_card_area.height(), logout_area.height());
logout_area.set_y(y);
logout_area.set_height(height);
user_card_area.set_y(y);
user_card_area.set_height(height);
// In multiprofile mode we have also to increase the size of the card by
// the size of the border to make it overlap with the logout button.
user_card_area.set_width(std::max(0, remaining_width + 1));
// To make the logout button symmetrical with the user card we also make
// the button longer by the same size the hover area in front of the icon
// got inset.
logout_area.set_width(logout_area.width() +
kTrayUserTileHoverBorderInset);
} else {
// In all other modes we have to make sure that there is enough spacing
// between the two.
remaining_width -= kTrayPopupPaddingBetweenItems;
}
user_card_area.set_width(remaining_width);
user_card_view_->SetBoundsRect(user_card_area);
logout_button_->SetBoundsRect(logout_area);
} else if (user_card_view_) {
user_card_view_->SetBoundsRect(contents_area);
} else if (logout_button_) {
logout_button_->SetBoundsRect(contents_area);
}
}
void UserView::ButtonPressed(views::Button* sender, const ui::Event& event) {
if (sender == logout_button_) {
ash::Shell::GetInstance()->system_tray_delegate()->SignOut();
} else if (sender == user_card_view_ &&
ash::Shell::GetInstance()->delegate()->IsMultiProfilesEnabled()) {
if (!multiprofile_index_) {
ToggleAddUserMenuOption();
} else {
ash::SessionStateDelegate* delegate =
ash::Shell::GetInstance()->session_state_delegate();
delegate->SwitchActiveUser(delegate->GetUserEmail(multiprofile_index_));
// Since the user list is about to change the system menu should get
// closed.
owner_->system_tray()->CloseSystemBubble();
}
} else if (add_menu_option_.get() &&
sender == add_menu_option_->GetContentsView()) {
// Let the user add another account to the session.
ash::Shell::GetInstance()->system_tray_delegate()->ShowUserLogin();
} else {
NOTREACHED();
}
}
void UserView::AddLogoutButton(ash::user::LoginStatus login) {
const base::string16 title = ash::user::GetLocalizedSignOutStringForStatus(
login, true);
TrayPopupLabelButton* logout_button = new TrayPopupLabelButton(this, title);
logout_button->SetAccessibleName(title);
logout_button_ = logout_button;
// In public account mode, the logout button border has a custom color.
if (login == ash::user::LOGGED_IN_PUBLIC) {
TrayPopupLabelButtonBorder* border =
static_cast<TrayPopupLabelButtonBorder*>(logout_button_->border());
border->SetPainter(false, views::Button::STATE_NORMAL,
views::Painter::CreateImageGridPainter(
kPublicAccountLogoutButtonBorderImagesNormal));
border->SetPainter(false, views::Button::STATE_HOVERED,
views::Painter::CreateImageGridPainter(
kPublicAccountLogoutButtonBorderImagesHovered));
border->SetPainter(false, views::Button::STATE_PRESSED,
views::Painter::CreateImageGridPainter(
kPublicAccountLogoutButtonBorderImagesHovered));
}
AddChildView(logout_button_);
}
void UserView::AddUserCard(SystemTrayItem* owner,
ash::user::LoginStatus login) {
// Add padding around the panel.
set_border(views::Border::CreateEmptyBorder(
kUserCardVerticalPadding, kTrayPopupPaddingHorizontal,
kUserCardVerticalPadding, kTrayPopupPaddingHorizontal));
if (ash::Shell::GetInstance()->delegate()->IsMultiProfilesEnabled() &&
login != ash::user::LOGGED_IN_RETAIL_MODE) {
user_card_view_ = new UserCard(this, multiprofile_index_ == 0);
is_user_card_ = true;
} else {
user_card_view_ = new views::View();
is_user_card_ = false;
}
user_card_view_->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kHorizontal, 0, 0 , kTrayPopupPaddingBetweenItems));
AddChildViewAt(user_card_view_, 0);
if (login == ash::user::LOGGED_IN_RETAIL_MODE) {
AddLoggedInRetailModeUserCardContent();
return;
}
// The entire user card should trigger hover (the inner items get disabled).
user_card_view_->SetEnabled(true);
user_card_view_->set_notify_enter_exit_on_child(true);
if (login == ash::user::LOGGED_IN_PUBLIC) {
AddLoggedInPublicModeUserCardContent(owner);
return;
}
views::View* icon = CreateIconForUserCard(login);
user_card_view_->AddChildView(icon);
// To allow the border to start before the icon, reduce the size before and
// add an inset to the icon to get the spacing.
if (multiprofile_index_ == 0 &&
ash::Shell::GetInstance()->delegate()->IsMultiProfilesEnabled()) {
icon->set_border(views::Border::CreateEmptyBorder(
0, kTrayUserTileHoverBorderInset, 0, 0));
set_border(views::Border::CreateEmptyBorder(
kUserCardVerticalPadding,
kTrayPopupPaddingHorizontal - kTrayUserTileHoverBorderInset,
kUserCardVerticalPadding,
kTrayPopupPaddingHorizontal));
}
ash::SessionStateDelegate* delegate =
ash::Shell::GetInstance()->session_state_delegate();
views::Label* username = NULL;
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
if (!multiprofile_index_) {
base::string16 user_name_string =
login == ash::user::LOGGED_IN_GUEST ?
bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_GUEST_LABEL) :
delegate->GetUserDisplayName(multiprofile_index_);
if (!user_name_string.empty()) {
username = new views::Label(user_name_string);
username->SetHorizontalAlignment(gfx::ALIGN_LEFT);
}
}
views::Label* additional = NULL;
if (login != ash::user::LOGGED_IN_GUEST) {
base::string16 user_email_string =
login == ash::user::LOGGED_IN_LOCALLY_MANAGED ?
bundle.GetLocalizedString(
IDS_ASH_STATUS_TRAY_LOCALLY_MANAGED_LABEL) :
UTF8ToUTF16(delegate->GetUserEmail(multiprofile_index_));
if (!user_email_string.empty()) {
additional = new views::Label(user_email_string);
additional->SetFont(bundle.GetFont(ui::ResourceBundle::SmallFont));
additional->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 = additional->enabled_color();
text_color = SkColorSetA(text_color, SkColorGetA(text_color) / 2);
if (additional)
additional->SetDisabledColor(text_color);
if (username)
username->SetDisabledColor(text_color);
}
if (additional && username) {
views::View* details = new views::View;
details->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kVertical, 0, kUserDetailsVerticalPadding, 0));
details->AddChildView(username);
details->AddChildView(additional);
user_card_view_->AddChildView(details);
} else {
if (username)
user_card_view_->AddChildView(username);
if (additional)
user_card_view_->AddChildView(additional);
}
}
views::View* UserView::CreateIconForUserCard(ash::user::LoginStatus login) {
RoundedImageView* icon = new RoundedImageView(kProfileRoundedCornerRadius,
multiprofile_index_ == 0);
icon->SetEnabled(false);
if (login == ash::user::LOGGED_IN_GUEST) {
icon->SetImage(*ui::ResourceBundle::GetSharedInstance().
GetImageNamed(IDR_AURA_UBER_TRAY_GUEST_ICON).ToImageSkia(),
gfx::Size(kUserIconSize, kUserIconSize));
} else {
icon->SetImage(
ash::Shell::GetInstance()->session_state_delegate()->
GetUserImage(multiprofile_index_),
gfx::Size(kUserIconSize, kUserIconSize));
}
return icon;
}
void UserView::AddLoggedInRetailModeUserCardContent() {
views::Label* details = new views::Label;
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
details->SetText(
bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_KIOSK_LABEL));
details->set_border(views::Border::CreateEmptyBorder(0, 4, 0, 1));
details->SetHorizontalAlignment(gfx::ALIGN_LEFT);
user_card_view_->AddChildView(details);
}
void UserView::AddLoggedInPublicModeUserCardContent(SystemTrayItem* owner) {
user_card_view_->AddChildView(
CreateIconForUserCard(ash::user::LOGGED_IN_PUBLIC));
user_card_view_->AddChildView(new PublicAccountUserDetails(
owner, GetPreferredSize().width() + kTrayPopupPaddingBetweenItems));
}
void UserView::ToggleAddUserMenuOption() {
if (add_menu_option_.get()) {
popup_message_.reset();
mouse_watcher_.reset();
add_menu_option_.reset();
return;
}
// Note: We do not need to install a global event handler to delete this
// item since it will destroyed automatically before the menu / user menu item
// gets destroyed..
const SessionStateDelegate* session_state_delegate =
ash::Shell::GetInstance()->session_state_delegate();
add_user_visible_but_disabled_ =
session_state_delegate->NumberOfLoggedInUsers() >=
session_state_delegate->GetMaximumNumberOfLoggedInUsers();
add_menu_option_.reset(new views::Widget);
views::Widget::InitParams params;
params.type = views::Widget::InitParams::TYPE_TOOLTIP;
params.keep_on_top = true;
params.context = this->GetWidget()->GetNativeWindow();
params.accept_events = true;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
add_menu_option_->Init(params);
add_menu_option_->SetOpacity(0xFF);
add_menu_option_->GetNativeWindow()->set_owned_by_parent(false);
SetShadowType(add_menu_option_->GetNativeView(),
views::corewm::SHADOW_TYPE_NONE);
// Position it below our user card.
gfx::Rect bounds = user_card_view_->GetBoundsInScreen();
bounds.set_y(bounds.y() + bounds.height());
add_menu_option_->SetBounds(bounds);
// Show the content.
AddUserView* add_user_view = new AddUserView(
static_cast<UserCard*>(user_card_view_), this);
add_menu_option_->SetContentsView(add_user_view);
add_menu_option_->SetAlwaysOnTop(true);
add_menu_option_->Show();
if (add_user_visible_but_disabled_) {
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
popup_message_.reset(new PopupMessage(
bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAPTION_CANNOT_ADD_USER),
bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_MESSAGE_CANNOT_ADD_USER),
PopupMessage::ICON_WARNING,
add_user_view->anchor(),
views::BubbleBorder::TOP_LEFT,
gfx::Size(parent()->bounds().width() - kPopupMessageOffset, 0),
2 * kPopupMessageOffset));
}
// Find the screen area which encloses both elements and sets then a mouse
// watcher which will close the "menu".
gfx::Rect area = user_card_view_->GetBoundsInScreen();
area.set_height(2 * area.height());
mouse_watcher_.reset(new views::MouseWatcher(
new UserViewMouseWatcherHost(area),
this));
mouse_watcher_->Start();
}
AddUserView::AddUserView(UserCard* owner, views::ButtonListener* listener)
: CustomButton(listener_),
add_user_(NULL),
listener_(listener),
owner_(owner),
anchor_(NULL) {
AddContent();
owner_->ForceBorderVisible(true);
}
AddUserView::~AddUserView() {
owner_->ForceBorderVisible(false);
}
gfx::Size AddUserView::GetPreferredSize() {
return owner_->bounds().size();
}
int AddUserView::GetHeightForWidth(int width) {
return owner_->bounds().size().height();
}
void AddUserView::Layout() {
gfx::Rect contents_area(GetContentsBounds());
add_user_->SetBoundsRect(contents_area);
}
void AddUserView::ButtonPressed(views::Button* sender, const ui::Event& event) {
if (add_user_ == sender)
listener_->ButtonPressed(this, event);
else
NOTREACHED();
}
void AddUserView::AddContent() {
set_notify_enter_exit_on_child(true);
const SessionStateDelegate* delegate =
ash::Shell::GetInstance()->session_state_delegate();
bool enable = delegate->NumberOfLoggedInUsers() <
delegate->GetMaximumNumberOfLoggedInUsers();
SetLayoutManager(new views::FillLayout());
set_background(views::Background::CreateSolidBackground(kBackgroundColor));
// Add padding around the panel.
set_border(views::Border::CreateSolidBorder(1, kBorderColor));
add_user_ = new UserCard(this, enable);
add_user_->set_border(views::Border::CreateEmptyBorder(
kUserCardVerticalPadding,
kTrayPopupPaddingHorizontal- kTrayUserTileHoverBorderInset,
kUserCardVerticalPadding,
kTrayPopupPaddingHorizontal- kTrayUserTileHoverBorderInset));
add_user_->SetLayoutManager(new views::BoxLayout(
views::BoxLayout::kHorizontal, 0, 0 , kTrayPopupPaddingBetweenItems));
AddChildViewAt(add_user_, 0);
// Add the [+] icon which is also the anchor for messages.
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
RoundedImageView* icon = new RoundedImageView(kProfileRoundedCornerRadius,
true);
anchor_ = icon;
icon->SetImage(*ui::ResourceBundle::GetSharedInstance().
GetImageNamed(IDR_AURA_UBER_TRAY_ADD_MULTIPROFILE_USER).ToImageSkia(),
gfx::Size(kUserIconSize, kUserIconSize));
add_user_->AddChildView(icon);
// Add the command text.
views::Label* command_label = new views::Label(
bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT));
command_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
add_user_->AddChildView(command_label);
}
} // namespace tray
TrayUser::TrayUser(SystemTray* system_tray, MultiProfileIndex index)
: SystemTrayItem(system_tray),
multiprofile_index_(index),
user_(NULL),
layout_view_(NULL),
avatar_(NULL),
label_(NULL),
separator_shown_(false) {
Shell::GetInstance()->system_tray_notifier()->AddUserObserver(this);
}
TrayUser::~TrayUser() {
Shell::GetInstance()->system_tray_notifier()->RemoveUserObserver(this);
}
TrayUser::TestState TrayUser::GetStateForTest() const {
if (separator_shown_)
return SEPARATOR;
if (!user_)
return HIDDEN;
return user_->GetStateForTest();
}
gfx::Rect TrayUser::GetUserPanelBoundsInScreenForTest() const {
DCHECK(user_);
return user_->GetBoundsInScreenOfUserButtonForTest();
}
views::View* TrayUser::CreateTrayView(user::LoginStatus status) {
CHECK(layout_view_ == NULL);
// Only the current user gets an icon. All other users will only be
// represented in the tray menu.
if (multiprofile_index_)
return NULL;
layout_view_ = new views::View();
layout_view_->SetLayoutManager(
new views::BoxLayout(views::BoxLayout::kHorizontal,
0, 0, kUserLabelToIconPadding));
UpdateAfterLoginStatusChange(status);
return layout_view_;
}
views::View* TrayUser::CreateDefaultView(user::LoginStatus status) {
if (status == user::LOGGED_IN_NONE)
return NULL;
CHECK(user_ == NULL);
const SessionStateDelegate* session_state_delegate =
ash::Shell::GetInstance()->session_state_delegate();
int logged_in_users = session_state_delegate->NumberOfLoggedInUsers();
// If there are multiple users logged in, the users will be separated from the
// rest of the menu by a separator.
if (multiprofile_index_ ==
session_state_delegate->GetMaximumNumberOfLoggedInUsers() &&
logged_in_users > 1) {
separator_shown_ = true;
return new views::View();
}
// Do not show more UserView's then there are logged in users.
if (multiprofile_index_ >= logged_in_users)
return NULL;
user_ = new tray::UserView(this, status, multiprofile_index_);
return user_;
}
views::View* TrayUser::CreateDetailedView(user::LoginStatus status) {
return NULL;
}
void TrayUser::DestroyTrayView() {
layout_view_ = NULL;
avatar_ = NULL;
label_ = NULL;
separator_shown_ = false;
}
void TrayUser::DestroyDefaultView() {
user_ = NULL;
}
void TrayUser::DestroyDetailedView() {
}
void TrayUser::UpdateAfterLoginStatusChange(user::LoginStatus status) {
// Only the active user is represented in the tray.
if (!layout_view_)
return;
bool need_label = false;
bool need_avatar = false;
switch (status) {
case user::LOGGED_IN_LOCKED:
case user::LOGGED_IN_USER:
case user::LOGGED_IN_OWNER:
case user::LOGGED_IN_PUBLIC:
need_avatar = true;
break;
case user::LOGGED_IN_LOCALLY_MANAGED:
need_avatar = true;
need_label = true;
break;
case user::LOGGED_IN_GUEST:
need_label = true;
break;
case user::LOGGED_IN_RETAIL_MODE:
case user::LOGGED_IN_KIOSK_APP:
case user::LOGGED_IN_NONE:
break;
}
if ((need_avatar != (avatar_ != NULL)) ||
(need_label != (label_ != NULL))) {
layout_view_->RemoveAllChildViews(true);
if (need_label) {
label_ = new views::Label;
SetupLabelForTray(label_);
layout_view_->AddChildView(label_);
} else {
label_ = NULL;
}
if (need_avatar) {
avatar_ = new tray::RoundedImageView(kProfileRoundedCornerRadius, true);
layout_view_->AddChildView(avatar_);
} else {
avatar_ = NULL;
}
}
ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
if (status == user::LOGGED_IN_LOCALLY_MANAGED) {
label_->SetText(
bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_LOCALLY_MANAGED_LABEL));
} else if (status == user::LOGGED_IN_GUEST) {
label_->SetText(bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_GUEST_LABEL));
}
if (avatar_ && ash::switches::UseAlternateShelfLayout()) {
avatar_->SetCornerRadii(0,
kUserIconLargeCornerRadius,
kUserIconLargeCornerRadius,
0);
avatar_->set_border(NULL);
}
UpdateAvatarImage(status);
// Update layout after setting label_ and avatar_ with new login status.
if (Shell::GetPrimaryRootWindowController()->shelf())
UpdateAfterShelfAlignmentChange(
Shell::GetPrimaryRootWindowController()->GetShelfLayoutManager()->
GetAlignment());
}
void TrayUser::UpdateAfterShelfAlignmentChange(ShelfAlignment alignment) {
// Inactive users won't have a layout.
if (!layout_view_)
return;
if (alignment == SHELF_ALIGNMENT_BOTTOM ||
alignment == SHELF_ALIGNMENT_TOP) {
if (avatar_) {
if (ash::switches::UseAlternateShelfLayout()) {
avatar_->set_border(NULL);
avatar_->SetCornerRadii(0,
kUserIconLargeCornerRadius,
kUserIconLargeCornerRadius,
0);
} else {
avatar_->set_border(views::Border::CreateEmptyBorder(
0, kTrayImageItemHorizontalPaddingBottomAlignment + 2,
0, kTrayImageItemHorizontalPaddingBottomAlignment));
}
}
if (label_) {
label_->set_border(views::Border::CreateEmptyBorder(
0, kTrayLabelItemHorizontalPaddingBottomAlignment,
0, kTrayLabelItemHorizontalPaddingBottomAlignment));
}
layout_view_->SetLayoutManager(
new views::BoxLayout(views::BoxLayout::kHorizontal,
0, 0, kUserLabelToIconPadding));
} else {
if (avatar_) {
if (ash::switches::UseAlternateShelfLayout()) {
avatar_->set_border(NULL);
avatar_->SetCornerRadii(0,
0,
kUserIconLargeCornerRadius,
kUserIconLargeCornerRadius);
} else {
SetTrayImageItemBorder(avatar_, alignment);
}
}
if (label_) {
label_->set_border(views::Border::CreateEmptyBorder(
kTrayLabelItemVerticalPaddingVerticalAlignment,
kTrayLabelItemHorizontalPaddingBottomAlignment,
kTrayLabelItemVerticalPaddingVerticalAlignment,
kTrayLabelItemHorizontalPaddingBottomAlignment));
}
layout_view_->SetLayoutManager(
new views::BoxLayout(views::BoxLayout::kVertical,
0, 0, kUserLabelToIconPadding));
}
}
void TrayUser::OnUserUpdate() {
UpdateAvatarImage(Shell::GetInstance()->system_tray_delegate()->
GetUserLoginStatus());
}
void TrayUser::UpdateAvatarImage(user::LoginStatus status) {
if (!avatar_)
return;
int icon_size = ash::switches::UseAlternateShelfLayout() ?
kUserIconLargeSize : kUserIconSize;
avatar_->SetImage(
ash::Shell::GetInstance()->session_state_delegate()->GetUserImage(
multiprofile_index_),
gfx::Size(icon_size, icon_size));
}
} // namespace internal
} // namespace ash