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 virtual ~LogoutButton() {}
98 virtual void Paint(gfx::Canvas
* canvas
,
99 const views::CullSet
& cull_set
) OVERRIDE
{
100 // Just skip paint if this button used as a placeholder.
102 TrayPopupLabelButton::Paint(canvas
, cull_set
);
106 DISALLOW_COPY_AND_ASSIGN(LogoutButton
);
109 class UserViewMouseWatcherHost
: public views::MouseWatcherHost
{
111 explicit UserViewMouseWatcherHost(const gfx::Rect
& screen_area
)
112 : screen_area_(screen_area
) {}
113 virtual ~UserViewMouseWatcherHost() {}
115 // Implementation of MouseWatcherHost.
116 virtual bool Contains(const gfx::Point
& screen_point
,
117 views::MouseWatcherHost::MouseEventType type
) OVERRIDE
{
118 return screen_area_
.Contains(screen_point
);
122 gfx::Rect screen_area_
;
124 DISALLOW_COPY_AND_ASSIGN(UserViewMouseWatcherHost
);
127 // The menu item view which gets shown when the user clicks in multi profile
128 // mode onto the user item.
129 class AddUserView
: public views::View
{
131 // The |owner| is the view for which this view gets created.
132 AddUserView(ButtonFromView
* owner
);
133 virtual ~AddUserView();
135 // Get the anchor view for a message.
136 views::View
* anchor() { return anchor_
; }
139 // Overridden from views::View.
140 virtual gfx::Size
GetPreferredSize() const OVERRIDE
;
142 // Create the additional client content for this item.
145 // This is the content we create and show.
146 views::View
* add_user_
;
148 // This is the owner view of this item.
149 ButtonFromView
* owner_
;
151 // The anchor view for targetted bubble messages.
152 views::View
* anchor_
;
154 DISALLOW_COPY_AND_ASSIGN(AddUserView
);
157 AddUserView::AddUserView(ButtonFromView
* owner
)
158 : add_user_(NULL
), owner_(owner
), anchor_(NULL
) {
160 owner_
->ForceBorderVisible(true);
163 AddUserView::~AddUserView() {
164 owner_
->ForceBorderVisible(false);
167 gfx::Size
AddUserView::GetPreferredSize() const {
168 return owner_
->bounds().size();
171 void AddUserView::AddContent() {
172 SetLayoutManager(new views::FillLayout());
173 set_background(views::Background::CreateSolidBackground(kBackgroundColor
));
175 add_user_
= new views::View
;
176 add_user_
->SetBorder(views::Border::CreateEmptyBorder(
177 0, kTrayUserTileHoverBorderInset
, 0, 0));
179 add_user_
->SetLayoutManager(new views::BoxLayout(
180 views::BoxLayout::kHorizontal
, 0, 0, kTrayPopupPaddingBetweenItems
));
181 AddChildViewAt(add_user_
, 0);
183 // Add the [+] icon which is also the anchor for messages.
184 RoundedImageView
* icon
= new RoundedImageView(kTrayAvatarCornerRadius
, true);
186 icon
->SetImage(*ui::ResourceBundle::GetSharedInstance()
187 .GetImageNamed(IDR_AURA_UBER_TRAY_ADD_MULTIPROFILE_USER
)
189 gfx::Size(kTrayAvatarSize
, kTrayAvatarSize
));
190 add_user_
->AddChildView(icon
);
192 // Add the command text.
193 views::Label
* command_label
= new views::Label(
194 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT
));
195 command_label
->SetHorizontalAlignment(gfx::ALIGN_LEFT
);
196 add_user_
->AddChildView(command_label
);
201 UserView::UserView(SystemTrayItem
* owner
,
202 user::LoginStatus login
,
203 MultiProfileIndex index
,
204 bool for_detailed_view
)
205 : multiprofile_index_(index
),
206 user_card_view_(NULL
),
208 is_user_card_button_(false),
209 logout_button_(NULL
),
210 add_user_disabled_(false),
211 for_detailed_view_(for_detailed_view
),
212 focus_manager_(NULL
) {
213 CHECK_NE(user::LOGGED_IN_NONE
, login
);
215 // Only the logged in user will have a background. All other users will have
216 // to allow the TrayPopupContainer highlighting the menu line.
217 set_background(views::Background::CreateSolidBackground(
218 login
== user::LOGGED_IN_PUBLIC
? kPublicAccountBackgroundColor
219 : kBackgroundColor
));
221 SetLayoutManager(new views::BoxLayout(
222 views::BoxLayout::kHorizontal
, 0, 0, kTrayPopupPaddingBetweenItems
));
223 // The logout button must be added before the user card so that the user card
224 // can correctly calculate the remaining available width.
225 // Note that only the current multiprofile user gets a button.
226 if (!multiprofile_index_
)
227 AddLogoutButton(login
);
231 UserView::~UserView() {
232 RemoveAddUserMenuOption();
235 void UserView::MouseMovedOutOfHost() {
236 RemoveAddUserMenuOption();
239 TrayUser::TestState
UserView::GetStateForTest() const {
240 if (add_menu_option_
.get()) {
241 return add_user_disabled_
? TrayUser::ACTIVE_BUT_DISABLED
245 if (!is_user_card_button_
)
246 return TrayUser::SHOWN
;
248 return static_cast<ButtonFromView
*>(user_card_view_
)->is_hovered_for_test()
253 gfx::Rect
UserView::GetBoundsInScreenOfUserButtonForTest() {
254 DCHECK(user_card_view_
);
255 return user_card_view_
->GetBoundsInScreen();
258 gfx::Size
UserView::GetPreferredSize() const {
259 gfx::Size size
= views::View::GetPreferredSize();
260 // Only the active user panel will be forced to a certain height.
261 if (!multiprofile_index_
) {
263 std::max(size
.height(), kTrayPopupItemHeight
+ GetInsets().height()));
268 int UserView::GetHeightForWidth(int width
) const {
269 return GetPreferredSize().height();
272 void UserView::Layout() {
273 gfx::Rect
contents_area(GetContentsBounds());
274 if (user_card_view_
&& logout_button_
) {
275 // Give the logout button the space it requests.
276 gfx::Rect logout_area
= contents_area
;
277 logout_area
.ClampToCenteredSize(logout_button_
->GetPreferredSize());
278 logout_area
.set_x(contents_area
.right() - logout_area
.width());
280 // Give the remaining space to the user card.
281 gfx::Rect user_card_area
= contents_area
;
282 int remaining_width
= contents_area
.width() - logout_area
.width();
283 if (IsMultiProfileSupportedAndUserActive() ||
284 IsMultiAccountSupportedAndUserActive()) {
285 // In multiprofile/multiaccount case |user_card_view_| and
286 // |logout_button_| have to have the same height.
287 int y
= std::min(user_card_area
.y(), logout_area
.y());
288 int height
= std::max(user_card_area
.height(), logout_area
.height());
289 logout_area
.set_y(y
);
290 logout_area
.set_height(height
);
291 user_card_area
.set_y(y
);
292 user_card_area
.set_height(height
);
294 // In multiprofile mode we have also to increase the size of the card by
295 // the size of the border to make it overlap with the logout button.
296 user_card_area
.set_width(std::max(0, remaining_width
+ 1));
298 // To make the logout button symmetrical with the user card we also make
299 // the button longer by the same size the hover area in front of the icon
301 logout_area
.set_width(logout_area
.width() +
302 kTrayUserTileHoverBorderInset
);
304 // In all other modes we have to make sure that there is enough spacing
306 remaining_width
-= kTrayPopupPaddingBetweenItems
;
308 user_card_area
.set_width(remaining_width
);
309 user_card_view_
->SetBoundsRect(user_card_area
);
310 logout_button_
->SetBoundsRect(logout_area
);
311 } else if (user_card_view_
) {
312 user_card_view_
->SetBoundsRect(contents_area
);
313 } else if (logout_button_
) {
314 logout_button_
->SetBoundsRect(contents_area
);
318 void UserView::ButtonPressed(views::Button
* sender
, const ui::Event
& event
) {
319 if (sender
== logout_button_
) {
320 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
321 ash::UMA_STATUS_AREA_SIGN_OUT
);
322 RemoveAddUserMenuOption();
323 Shell::GetInstance()->system_tray_delegate()->SignOut();
324 } else if (sender
== user_card_view_
&& !multiprofile_index_
&&
325 IsMultiAccountSupportedAndUserActive()) {
326 owner_
->TransitionDetailedView();
327 } else if (sender
== user_card_view_
&&
328 IsMultiProfileSupportedAndUserActive()) {
329 if (!multiprofile_index_
) {
330 ToggleAddUserMenuOption();
332 RemoveAddUserMenuOption();
333 SwitchUser(multiprofile_index_
);
334 // Since the user list is about to change the system menu should get
336 owner_
->system_tray()->CloseSystemBubble();
338 } else if (add_menu_option_
.get() &&
339 sender
== add_menu_option_
->GetContentsView()) {
340 RemoveAddUserMenuOption();
341 // Let the user add another account to the session.
342 MultiProfileUMA::RecordSigninUser(MultiProfileUMA::SIGNIN_USER_BY_TRAY
);
343 Shell::GetInstance()->system_tray_delegate()->ShowUserLogin();
344 owner_
->system_tray()->CloseSystemBubble();
350 void UserView::OnWillChangeFocus(View
* focused_before
, View
* focused_now
) {
352 RemoveAddUserMenuOption();
355 void UserView::OnDidChangeFocus(View
* focused_before
, View
* focused_now
) {
356 // Nothing to do here.
359 void UserView::AddLogoutButton(user::LoginStatus login
) {
360 const base::string16 title
=
361 user::GetLocalizedSignOutStringForStatus(login
, true);
362 TrayPopupLabelButton
* logout_button
=
363 new LogoutButton(this, title
, for_detailed_view_
);
364 logout_button
->SetAccessibleName(title
);
365 logout_button_
= logout_button
;
366 // In public account mode, the logout button border has a custom color.
367 if (login
== user::LOGGED_IN_PUBLIC
) {
368 scoped_ptr
<TrayPopupLabelButtonBorder
> border(
369 new TrayPopupLabelButtonBorder());
370 border
->SetPainter(false,
371 views::Button::STATE_NORMAL
,
372 views::Painter::CreateImageGridPainter(
373 kPublicAccountLogoutButtonBorderImagesNormal
));
374 border
->SetPainter(false,
375 views::Button::STATE_HOVERED
,
376 views::Painter::CreateImageGridPainter(
377 kPublicAccountLogoutButtonBorderImagesHovered
));
378 border
->SetPainter(false,
379 views::Button::STATE_PRESSED
,
380 views::Painter::CreateImageGridPainter(
381 kPublicAccountLogoutButtonBorderImagesHovered
));
382 logout_button_
->SetBorder(border
.PassAs
<views::Border
>());
384 AddChildView(logout_button_
);
387 void UserView::AddUserCard(user::LoginStatus login
) {
388 // Add padding around the panel.
389 SetBorder(views::Border::CreateEmptyBorder(kTrayPopupUserCardVerticalPadding
,
390 kTrayPopupPaddingHorizontal
,
391 kTrayPopupUserCardVerticalPadding
,
392 kTrayPopupPaddingHorizontal
));
394 views::TrayBubbleView
* bubble_view
=
395 owner_
->system_tray()->GetSystemBubble()->bubble_view();
397 bubble_view
->GetMaximumSize().width() -
398 (2 * kTrayPopupPaddingHorizontal
+ kTrayPopupPaddingBetweenItems
);
400 max_card_width
-= logout_button_
->GetPreferredSize().width();
402 new UserCardView(login
, max_card_width
, multiprofile_index_
);
403 // The entry is clickable when no system modal dialog is open and one of the
404 // multi user options is active.
405 bool clickable
= !Shell::GetInstance()->IsSystemModalWindowOpen() &&
406 (IsMultiProfileSupportedAndUserActive() ||
407 IsMultiAccountSupportedAndUserActive());
409 // To allow the border to start before the icon, reduce the size before and
410 // add an inset to the icon to get the spacing.
411 if (!multiprofile_index_
) {
412 SetBorder(views::Border::CreateEmptyBorder(
413 kTrayPopupUserCardVerticalPadding
,
414 kTrayPopupPaddingHorizontal
- kTrayUserTileHoverBorderInset
,
415 kTrayPopupUserCardVerticalPadding
,
416 kTrayPopupPaddingHorizontal
));
417 user_card_view_
->SetBorder(views::Border::CreateEmptyBorder(
418 0, kTrayUserTileHoverBorderInset
, 0, 0));
420 gfx::Insets insets
= gfx::Insets(1, 1, 1, 1);
421 views::View
* contents_view
= user_card_view_
;
422 ButtonFromView
* button
= NULL
;
423 if (!for_detailed_view_
) {
424 if (multiprofile_index_
) {
425 // Since the activation border needs to be drawn around the tile, we
426 // have to put the tile into another view which fills the menu panel,
427 // but keeping the offsets of the content.
428 contents_view
= new views::View();
429 contents_view
->SetBorder(views::Border::CreateEmptyBorder(
430 kTrayPopupUserCardVerticalPadding
,
431 kTrayPopupPaddingHorizontal
,
432 kTrayPopupUserCardVerticalPadding
,
433 kTrayPopupPaddingHorizontal
));
434 contents_view
->SetLayoutManager(new views::FillLayout());
435 SetBorder(views::Border::CreateEmptyBorder(0, 0, 0, 0));
436 contents_view
->AddChildView(user_card_view_
);
437 insets
= gfx::Insets(1, 1, 1, 3);
439 button
= new ButtonFromView(contents_view
,
441 !multiprofile_index_
,
443 // TODO(skuhne): For accessibility we need to call |SetAccessibleName|
444 // with a useful name (string freeze for M37 has passed).
446 // We want user card for detailed view to have exactly the same look
447 // as user card for default view. That's why we wrap it in a button
448 // without click listener and special hover behavior.
449 button
= new ButtonFromView(contents_view
, NULL
, false, insets
);
451 // A click on the button should not trigger a focus change.
452 button
->set_request_focus_on_press(false);
453 user_card_view_
= button
;
454 is_user_card_button_
= true;
456 AddChildViewAt(user_card_view_
, 0);
457 // Card for supervised user can consume more space than currently
458 // available. In that case we should increase system bubble's width.
459 if (login
== user::LOGGED_IN_PUBLIC
)
460 bubble_view
->SetWidth(GetPreferredSize().width());
463 void UserView::ToggleAddUserMenuOption() {
464 if (add_menu_option_
.get()) {
465 RemoveAddUserMenuOption();
469 // Note: We do not need to install a global event handler to delete this
470 // item since it will destroyed automatically before the menu / user menu item
472 add_menu_option_
.reset(new views::Widget
);
473 views::Widget::InitParams params
;
474 params
.type
= views::Widget::InitParams::TYPE_TOOLTIP
;
475 params
.keep_on_top
= true;
476 params
.context
= this->GetWidget()->GetNativeWindow();
477 params
.accept_events
= true;
478 params
.ownership
= views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET
;
479 params
.opacity
= views::Widget::InitParams::TRANSLUCENT_WINDOW
;
480 add_menu_option_
->Init(params
);
481 add_menu_option_
->SetOpacity(0xFF);
482 add_menu_option_
->GetNativeWindow()->set_owned_by_parent(false);
483 SetShadowType(add_menu_option_
->GetNativeView(), wm::SHADOW_TYPE_NONE
);
485 // Position it below our user card.
486 gfx::Rect bounds
= user_card_view_
->GetBoundsInScreen();
487 bounds
.set_y(bounds
.y() + bounds
.height());
488 add_menu_option_
->SetBounds(bounds
);
491 add_menu_option_
->SetAlwaysOnTop(true);
492 add_menu_option_
->Show();
494 AddUserView
* add_user_view
=
495 new AddUserView(static_cast<ButtonFromView
*>(user_card_view_
));
497 const SessionStateDelegate
* delegate
=
498 Shell::GetInstance()->session_state_delegate();
500 bool multi_profile_allowed
=
501 delegate
->IsMultiProfileAllowedByPrimaryUserPolicy();
502 add_user_disabled_
= (delegate
->NumberOfLoggedInUsers() >=
503 delegate
->GetMaximumNumberOfLoggedInUsers()) ||
504 !multi_profile_allowed
;
506 ButtonFromView
* button
= new ButtonFromView(
508 add_user_disabled_
? NULL
: this,
510 gfx::Insets(1, 1, 1, 1));
511 button
->set_request_focus_on_press(false);
512 button
->SetAccessibleName(
513 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT
));
514 button
->ForceBorderVisible(true);
515 add_menu_option_
->SetContentsView(button
);
517 if (add_user_disabled_
) {
518 ui::ResourceBundle
& bundle
= ui::ResourceBundle::GetSharedInstance();
520 if (!multi_profile_allowed
)
521 message_id
= IDS_ASH_STATUS_TRAY_MESSAGE_NOT_ALLOWED_PRIMARY_USER
;
523 message_id
= IDS_ASH_STATUS_TRAY_MESSAGE_CANNOT_ADD_USER
;
525 popup_message_
.reset(new PopupMessage(
526 bundle
.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAPTION_CANNOT_ADD_USER
),
527 bundle
.GetLocalizedString(message_id
),
528 PopupMessage::ICON_WARNING
,
529 add_user_view
->anchor(),
530 views::BubbleBorder::TOP_LEFT
,
531 gfx::Size(parent()->bounds().width() - kPopupMessageOffset
, 0),
532 2 * kPopupMessageOffset
));
534 // We activate the entry automatically if invoked with focus.
535 if (user_card_view_
->HasFocus()) {
536 button
->GetFocusManager()->SetFocusedView(button
);
537 user_card_view_
->GetFocusManager()->SetFocusedView(button
);
540 // Find the screen area which encloses both elements and sets then a mouse
541 // watcher which will close the "menu".
542 gfx::Rect area
= user_card_view_
->GetBoundsInScreen();
543 area
.set_height(2 * area
.height());
544 mouse_watcher_
.reset(
545 new views::MouseWatcher(new UserViewMouseWatcherHost(area
), this));
546 mouse_watcher_
->Start();
547 // Install a listener to focus changes so that we can remove the card when
548 // the focus gets changed. When called through the destruction of the bubble,
549 // the FocusManager cannot be determined anymore and we remember it here.
550 focus_manager_
= user_card_view_
->GetFocusManager();
551 focus_manager_
->AddFocusChangeListener(this);
554 void UserView::RemoveAddUserMenuOption() {
555 if (!add_menu_option_
.get())
557 focus_manager_
->RemoveFocusChangeListener(this);
558 focus_manager_
= NULL
;
559 if (user_card_view_
->GetFocusManager())
560 user_card_view_
->GetFocusManager()->ClearFocus();
561 popup_message_
.reset();
562 mouse_watcher_
.reset();
563 add_menu_option_
.reset();