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.
5 #include "ash/system/user/user_view.h"
9 #include "ash/multi_profile_uma.h"
10 #include "ash/popup_message.h"
11 #include "ash/session/session_state_delegate.h"
12 #include "ash/shell.h"
13 #include "ash/shell_delegate.h"
14 #include "ash/system/tray/system_tray.h"
15 #include "ash/system/tray/system_tray_delegate.h"
16 #include "ash/system/tray/tray_popup_label_button.h"
17 #include "ash/system/tray/tray_popup_label_button_border.h"
18 #include "ash/system/user/button_from_view.h"
19 #include "ash/system/user/config.h"
20 #include "ash/system/user/rounded_image_view.h"
21 #include "ash/system/user/user_card_view.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/views/layout/fill_layout.h"
28 #include "ui/views/painter.h"
29 #include "ui/wm/core/shadow_types.h"
36 const int kPublicAccountLogoutButtonBorderImagesNormal[] = {
37 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
38 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
39 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
40 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
41 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
42 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
43 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
44 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
45 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_NORMAL_BACKGROUND,
48 const int kPublicAccountLogoutButtonBorderImagesHovered[] = {
49 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
50 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
51 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
52 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
53 IDR_AURA_TRAY_POPUP_LABEL_BUTTON_HOVER_BACKGROUND,
54 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
55 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
56 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
57 IDR_AURA_TRAY_POPUP_PUBLIC_ACCOUNT_LOGOUT_BUTTON_BORDER,
60 // When a hover border is used, it is starting this many pixels before the icon
62 const int kTrayUserTileHoverBorderInset = 10;
64 // Offsetting the popup message relative to the tray menu.
65 const int kPopupMessageOffset = 25;
67 // Switch to a user with the given |user_index|.
68 void SwitchUser(ash::MultiProfileIndex user_index) {
69 // Do not switch users when the log screen is presented.
70 if (ash::Shell::GetInstance()
71 ->session_state_delegate()
72 ->IsUserSessionBlocked())
75 DCHECK(user_index > 0);
76 ash::SessionStateDelegate* delegate =
77 ash::Shell::GetInstance()->session_state_delegate();
78 ash::MultiProfileUMA::RecordSwitchActiveUser(
79 ash::MultiProfileUMA::SWITCH_ACTIVE_USER_BY_TRAY);
80 delegate->SwitchActiveUser(delegate->GetUserInfo(user_index)->GetUserID());
83 class LogoutButton : public TrayPopupLabelButton {
85 // If |placeholder| is true, button is used as placeholder. That means that
86 // button is inactive and is not painted, but consume the same ammount of
87 // space, as if it was painted.
88 LogoutButton(views::ButtonListener* listener,
89 const base::string16& text,
91 : TrayPopupLabelButton(listener, text), placeholder_(placeholder) {
92 SetEnabled(!placeholder_);
95 ~LogoutButton() override {}
98 void Paint(gfx::Canvas* canvas, const views::CullSet& cull_set) override {
99 // Just skip paint if this button used as a placeholder.
101 TrayPopupLabelButton::Paint(canvas, cull_set);
105 DISALLOW_COPY_AND_ASSIGN(LogoutButton);
108 class UserViewMouseWatcherHost : public views::MouseWatcherHost {
110 explicit UserViewMouseWatcherHost(const gfx::Rect& screen_area)
111 : screen_area_(screen_area) {}
112 ~UserViewMouseWatcherHost() override {}
114 // Implementation of MouseWatcherHost.
115 bool Contains(const gfx::Point& screen_point,
116 views::MouseWatcherHost::MouseEventType type) override {
117 return screen_area_.Contains(screen_point);
121 gfx::Rect screen_area_;
123 DISALLOW_COPY_AND_ASSIGN(UserViewMouseWatcherHost);
126 // The menu item view which gets shown when the user clicks in multi profile
127 // mode onto the user item.
128 class AddUserView : public views::View {
130 // The |owner| is the view for which this view gets created.
131 AddUserView(ButtonFromView* owner);
132 ~AddUserView() override;
134 // Get the anchor view for a message.
135 views::View* anchor() { return anchor_; }
138 // Overridden from views::View.
139 gfx::Size GetPreferredSize() const override;
141 // Create the additional client content for this item.
144 // This is the content we create and show.
145 views::View* add_user_;
147 // This is the owner view of this item.
148 ButtonFromView* owner_;
150 // The anchor view for targetted bubble messages.
151 views::View* anchor_;
153 DISALLOW_COPY_AND_ASSIGN(AddUserView);
156 AddUserView::AddUserView(ButtonFromView* owner)
157 : add_user_(NULL), owner_(owner), anchor_(NULL) {
159 owner_->ForceBorderVisible(true);
162 AddUserView::~AddUserView() {
163 owner_->ForceBorderVisible(false);
166 gfx::Size AddUserView::GetPreferredSize() const {
167 return owner_->bounds().size();
170 void AddUserView::AddContent() {
171 SetLayoutManager(new views::FillLayout());
172 set_background(views::Background::CreateSolidBackground(kBackgroundColor));
174 add_user_ = new views::View;
175 add_user_->SetBorder(views::Border::CreateEmptyBorder(
176 0, kTrayUserTileHoverBorderInset, 0, 0));
178 add_user_->SetLayoutManager(new views::BoxLayout(
179 views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems));
180 AddChildViewAt(add_user_, 0);
182 // Add the [+] icon which is also the anchor for messages.
183 RoundedImageView* icon = new RoundedImageView(kTrayAvatarCornerRadius, true);
185 icon->SetImage(*ui::ResourceBundle::GetSharedInstance()
186 .GetImageNamed(IDR_AURA_UBER_TRAY_ADD_MULTIPROFILE_USER)
188 gfx::Size(kTrayAvatarSize, kTrayAvatarSize));
189 add_user_->AddChildView(icon);
191 // Add the command text.
192 views::Label* command_label = new views::Label(
193 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT));
194 command_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
195 add_user_->AddChildView(command_label);
200 UserView::UserView(SystemTrayItem* owner,
201 user::LoginStatus login,
202 MultiProfileIndex index,
203 bool for_detailed_view)
204 : multiprofile_index_(index),
205 user_card_view_(NULL),
207 is_user_card_button_(false),
208 logout_button_(NULL),
209 add_user_enabled_(true),
210 for_detailed_view_(for_detailed_view),
211 focus_manager_(NULL) {
212 CHECK_NE(user::LOGGED_IN_NONE, login);
214 // Only the logged in user will have a background. All other users will have
215 // to allow the TrayPopupContainer highlighting the menu line.
216 set_background(views::Background::CreateSolidBackground(
217 login == user::LOGGED_IN_PUBLIC ? kPublicAccountBackgroundColor
218 : kBackgroundColor));
220 SetLayoutManager(new views::BoxLayout(
221 views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems));
222 // The logout button must be added before the user card so that the user card
223 // can correctly calculate the remaining available width.
224 // Note that only the current multiprofile user gets a button.
225 if (!multiprofile_index_)
226 AddLogoutButton(login);
230 UserView::~UserView() {
231 RemoveAddUserMenuOption();
234 void UserView::MouseMovedOutOfHost() {
235 RemoveAddUserMenuOption();
238 TrayUser::TestState UserView::GetStateForTest() const {
239 if (add_menu_option_.get()) {
240 return add_user_enabled_ ? TrayUser::ACTIVE : TrayUser::ACTIVE_BUT_DISABLED;
243 if (!is_user_card_button_)
244 return TrayUser::SHOWN;
246 return static_cast<ButtonFromView*>(user_card_view_)->is_hovered_for_test()
251 gfx::Rect UserView::GetBoundsInScreenOfUserButtonForTest() {
252 DCHECK(user_card_view_);
253 return user_card_view_->GetBoundsInScreen();
256 gfx::Size UserView::GetPreferredSize() const {
257 gfx::Size size = views::View::GetPreferredSize();
258 // Only the active user panel will be forced to a certain height.
259 if (!multiprofile_index_) {
261 std::max(size.height(), kTrayPopupItemHeight + GetInsets().height()));
266 int UserView::GetHeightForWidth(int width) const {
267 return GetPreferredSize().height();
270 void UserView::Layout() {
271 gfx::Rect contents_area(GetContentsBounds());
272 if (user_card_view_ && logout_button_) {
273 // Give the logout button the space it requests.
274 gfx::Rect logout_area = contents_area;
275 logout_area.ClampToCenteredSize(logout_button_->GetPreferredSize());
276 logout_area.set_x(contents_area.right() - logout_area.width());
278 // Give the remaining space to the user card.
279 gfx::Rect user_card_area = contents_area;
280 int remaining_width = contents_area.width() - logout_area.width();
281 if (IsMultiProfileSupportedAndUserActive() ||
282 IsMultiAccountSupportedAndUserActive()) {
283 // In multiprofile/multiaccount case |user_card_view_| and
284 // |logout_button_| have to have the same height.
285 int y = std::min(user_card_area.y(), logout_area.y());
286 int height = std::max(user_card_area.height(), logout_area.height());
287 logout_area.set_y(y);
288 logout_area.set_height(height);
289 user_card_area.set_y(y);
290 user_card_area.set_height(height);
292 // In multiprofile mode we have also to increase the size of the card by
293 // the size of the border to make it overlap with the logout button.
294 user_card_area.set_width(std::max(0, remaining_width + 1));
296 // To make the logout button symmetrical with the user card we also make
297 // the button longer by the same size the hover area in front of the icon
299 logout_area.set_width(logout_area.width() +
300 kTrayUserTileHoverBorderInset);
302 // In all other modes we have to make sure that there is enough spacing
304 remaining_width -= kTrayPopupPaddingBetweenItems;
306 user_card_area.set_width(remaining_width);
307 user_card_view_->SetBoundsRect(user_card_area);
308 logout_button_->SetBoundsRect(logout_area);
309 } else if (user_card_view_) {
310 user_card_view_->SetBoundsRect(contents_area);
311 } else if (logout_button_) {
312 logout_button_->SetBoundsRect(contents_area);
316 void UserView::ButtonPressed(views::Button* sender, const ui::Event& event) {
317 if (sender == logout_button_) {
318 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
319 ash::UMA_STATUS_AREA_SIGN_OUT);
320 RemoveAddUserMenuOption();
321 Shell::GetInstance()->system_tray_delegate()->SignOut();
322 } else if (sender == user_card_view_ && !multiprofile_index_ &&
323 IsMultiAccountSupportedAndUserActive()) {
324 owner_->TransitionDetailedView();
325 } else if (sender == user_card_view_ &&
326 IsMultiProfileSupportedAndUserActive()) {
327 if (!multiprofile_index_) {
328 ToggleAddUserMenuOption();
330 RemoveAddUserMenuOption();
331 SwitchUser(multiprofile_index_);
332 // Since the user list is about to change the system menu should get
334 owner_->system_tray()->CloseSystemBubble();
336 } else if (add_menu_option_.get() &&
337 sender == add_menu_option_->GetContentsView()) {
338 RemoveAddUserMenuOption();
339 // Let the user add another account to the session.
340 MultiProfileUMA::RecordSigninUser(MultiProfileUMA::SIGNIN_USER_BY_TRAY);
341 Shell::GetInstance()->system_tray_delegate()->ShowUserLogin();
342 owner_->system_tray()->CloseSystemBubble();
348 void UserView::OnWillChangeFocus(View* focused_before, View* focused_now) {
350 RemoveAddUserMenuOption();
353 void UserView::OnDidChangeFocus(View* focused_before, View* focused_now) {
354 // Nothing to do here.
357 void UserView::AddLogoutButton(user::LoginStatus login) {
358 const base::string16 title =
359 user::GetLocalizedSignOutStringForStatus(login, true);
360 TrayPopupLabelButton* logout_button =
361 new LogoutButton(this, title, for_detailed_view_);
362 logout_button->SetAccessibleName(title);
363 logout_button_ = logout_button;
364 // In public account mode, the logout button border has a custom color.
365 if (login == user::LOGGED_IN_PUBLIC) {
366 scoped_ptr<TrayPopupLabelButtonBorder> border(
367 new TrayPopupLabelButtonBorder());
368 border->SetPainter(false,
369 views::Button::STATE_NORMAL,
370 views::Painter::CreateImageGridPainter(
371 kPublicAccountLogoutButtonBorderImagesNormal));
372 border->SetPainter(false,
373 views::Button::STATE_HOVERED,
374 views::Painter::CreateImageGridPainter(
375 kPublicAccountLogoutButtonBorderImagesHovered));
376 border->SetPainter(false,
377 views::Button::STATE_PRESSED,
378 views::Painter::CreateImageGridPainter(
379 kPublicAccountLogoutButtonBorderImagesHovered));
380 logout_button_->SetBorder(border.Pass());
382 AddChildView(logout_button_);
385 void UserView::AddUserCard(user::LoginStatus login) {
386 // Add padding around the panel.
387 SetBorder(views::Border::CreateEmptyBorder(kTrayPopupUserCardVerticalPadding,
388 kTrayPopupPaddingHorizontal,
389 kTrayPopupUserCardVerticalPadding,
390 kTrayPopupPaddingHorizontal));
392 views::TrayBubbleView* bubble_view =
393 owner_->system_tray()->GetSystemBubble()->bubble_view();
395 bubble_view->GetMaximumSize().width() -
396 (2 * kTrayPopupPaddingHorizontal + kTrayPopupPaddingBetweenItems);
398 max_card_width -= logout_button_->GetPreferredSize().width();
400 new UserCardView(login, max_card_width, multiprofile_index_);
401 // The entry is clickable when no system modal dialog is open and one of the
402 // multi user options is active.
403 bool clickable = !Shell::GetInstance()->IsSystemModalWindowOpen() &&
404 (IsMultiProfileSupportedAndUserActive() ||
405 IsMultiAccountSupportedAndUserActive());
407 // To allow the border to start before the icon, reduce the size before and
408 // add an inset to the icon to get the spacing.
409 if (!multiprofile_index_) {
410 SetBorder(views::Border::CreateEmptyBorder(
411 kTrayPopupUserCardVerticalPadding,
412 kTrayPopupPaddingHorizontal - kTrayUserTileHoverBorderInset,
413 kTrayPopupUserCardVerticalPadding,
414 kTrayPopupPaddingHorizontal));
415 user_card_view_->SetBorder(views::Border::CreateEmptyBorder(
416 0, kTrayUserTileHoverBorderInset, 0, 0));
418 gfx::Insets insets = gfx::Insets(1, 1, 1, 1);
419 views::View* contents_view = user_card_view_;
420 ButtonFromView* button = NULL;
421 if (!for_detailed_view_) {
422 if (multiprofile_index_) {
423 // Since the activation border needs to be drawn around the tile, we
424 // have to put the tile into another view which fills the menu panel,
425 // but keeping the offsets of the content.
426 contents_view = new views::View();
427 contents_view->SetBorder(views::Border::CreateEmptyBorder(
428 kTrayPopupUserCardVerticalPadding,
429 kTrayPopupPaddingHorizontal,
430 kTrayPopupUserCardVerticalPadding,
431 kTrayPopupPaddingHorizontal));
432 contents_view->SetLayoutManager(new views::FillLayout());
433 SetBorder(views::Border::CreateEmptyBorder(0, 0, 0, 0));
434 contents_view->AddChildView(user_card_view_);
435 insets = gfx::Insets(1, 1, 1, 3);
437 button = new ButtonFromView(contents_view,
439 !multiprofile_index_,
441 // TODO(skuhne): For accessibility we need to call |SetAccessibleName|
442 // with a useful name (string freeze for M37 has passed).
444 // We want user card for detailed view to have exactly the same look
445 // as user card for default view. That's why we wrap it in a button
446 // without click listener and special hover behavior.
447 button = new ButtonFromView(contents_view, NULL, false, insets);
449 // A click on the button should not trigger a focus change.
450 button->set_request_focus_on_press(false);
451 user_card_view_ = button;
452 is_user_card_button_ = true;
454 AddChildViewAt(user_card_view_, 0);
455 // Card for supervised user can consume more space than currently
456 // available. In that case we should increase system bubble's width.
457 if (login == user::LOGGED_IN_PUBLIC)
458 bubble_view->SetWidth(GetPreferredSize().width());
461 void UserView::ToggleAddUserMenuOption() {
462 if (add_menu_option_.get()) {
463 RemoveAddUserMenuOption();
467 // Note: We do not need to install a global event handler to delete this
468 // item since it will destroyed automatically before the menu / user menu item
470 add_menu_option_.reset(new views::Widget);
471 views::Widget::InitParams params;
472 params.type = views::Widget::InitParams::TYPE_TOOLTIP;
473 params.keep_on_top = true;
474 params.context = this->GetWidget()->GetNativeWindow();
475 params.accept_events = true;
476 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
477 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
478 add_menu_option_->Init(params);
479 add_menu_option_->SetOpacity(0xFF);
480 add_menu_option_->GetNativeWindow()->set_owned_by_parent(false);
481 SetShadowType(add_menu_option_->GetNativeView(), wm::SHADOW_TYPE_NONE);
483 // Position it below our user card.
484 gfx::Rect bounds = user_card_view_->GetBoundsInScreen();
485 bounds.set_y(bounds.y() + bounds.height());
486 add_menu_option_->SetBounds(bounds);
489 add_menu_option_->SetAlwaysOnTop(true);
490 add_menu_option_->Show();
492 AddUserView* add_user_view =
493 new AddUserView(static_cast<ButtonFromView*>(user_card_view_));
495 const SessionStateDelegate* delegate =
496 Shell::GetInstance()->session_state_delegate();
498 SessionStateDelegate::AddUserError add_user_error;
499 add_user_enabled_ = delegate->CanAddUserToMultiProfile(&add_user_error);
501 ButtonFromView* button = new ButtonFromView(add_user_view,
502 add_user_enabled_ ? this : NULL,
504 gfx::Insets(1, 1, 1, 1));
505 button->set_request_focus_on_press(false);
506 button->SetAccessibleName(
507 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT));
508 button->ForceBorderVisible(true);
509 add_menu_option_->SetContentsView(button);
511 if (add_user_enabled_) {
512 // We activate the entry automatically if invoked with focus.
513 if (user_card_view_->HasFocus()) {
514 button->GetFocusManager()->SetFocusedView(button);
515 user_card_view_->GetFocusManager()->SetFocusedView(button);
518 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
520 switch (add_user_error) {
521 case SessionStateDelegate::ADD_USER_ERROR_NOT_ALLOWED_PRIMARY_USER:
522 message_id = IDS_ASH_STATUS_TRAY_MESSAGE_NOT_ALLOWED_PRIMARY_USER;
524 case SessionStateDelegate::ADD_USER_ERROR_MAXIMUM_USERS_REACHED:
525 message_id = IDS_ASH_STATUS_TRAY_MESSAGE_CANNOT_ADD_USER;
527 case SessionStateDelegate::ADD_USER_ERROR_OUT_OF_USERS:
528 message_id = IDS_ASH_STATUS_TRAY_MESSAGE_OUT_OF_USERS;
531 NOTREACHED() << "Unknown adding user error " << add_user_error;
534 popup_message_.reset(new PopupMessage(
535 bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAPTION_CANNOT_ADD_USER),
536 bundle.GetLocalizedString(message_id),
537 PopupMessage::ICON_WARNING,
538 add_user_view->anchor(),
539 views::BubbleBorder::TOP_LEFT,
540 gfx::Size(parent()->bounds().width() - kPopupMessageOffset, 0),
541 2 * kPopupMessageOffset));
543 // Find the screen area which encloses both elements and sets then a mouse
544 // watcher which will close the "menu".
545 gfx::Rect area = user_card_view_->GetBoundsInScreen();
546 area.set_height(2 * area.height());
547 mouse_watcher_.reset(
548 new views::MouseWatcher(new UserViewMouseWatcherHost(area), this));
549 mouse_watcher_->Start();
550 // Install a listener to focus changes so that we can remove the card when
551 // the focus gets changed. When called through the destruction of the bubble,
552 // the FocusManager cannot be determined anymore and we remember it here.
553 focus_manager_ = user_card_view_->GetFocusManager();
554 focus_manager_->AddFocusChangeListener(this);
557 void UserView::RemoveAddUserMenuOption() {
558 if (!add_menu_option_.get())
560 focus_manager_->RemoveFocusChangeListener(this);
561 focus_manager_ = NULL;
562 if (user_card_view_->GetFocusManager())
563 user_card_view_->GetFocusManager()->ClearFocus();
564 popup_message_.reset();
565 mouse_watcher_.reset();
566 add_menu_option_.reset();