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_view.h" 6 7 #include <algorithm> 8 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" 30 31 namespace ash { 32 namespace tray { 33 34 namespace { 35 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, 46 }; 47 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, 58 }; 59 60 // When a hover border is used, it is starting this many pixels before the icon 61 // position. 62 const int kTrayUserTileHoverBorderInset = 10; 63 64 // Offsetting the popup message relative to the tray menu. 65 const int kPopupMessageOffset = 25; 66 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()) 73 return; 74 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()); 81 } 82 83 class LogoutButton : public TrayPopupLabelButton { 84 public: 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, 90 bool placeholder) 91 : TrayPopupLabelButton(listener, text), placeholder_(placeholder) { 92 SetEnabled(!placeholder_); 93 } 94 95 virtual ~LogoutButton() {} 96 97 private: 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. 101 if (!placeholder_) 102 TrayPopupLabelButton::Paint(canvas, cull_set); 103 } 104 105 bool placeholder_; 106 DISALLOW_COPY_AND_ASSIGN(LogoutButton); 107 }; 108 109 class UserViewMouseWatcherHost : public views::MouseWatcherHost { 110 public: 111 explicit UserViewMouseWatcherHost(const gfx::Rect& screen_area) 112 : screen_area_(screen_area) {} 113 virtual ~UserViewMouseWatcherHost() {} 114 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); 119 } 120 121 private: 122 gfx::Rect screen_area_; 123 124 DISALLOW_COPY_AND_ASSIGN(UserViewMouseWatcherHost); 125 }; 126 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 { 130 public: 131 // The |owner| is the view for which this view gets created. 132 AddUserView(ButtonFromView* owner); 133 virtual ~AddUserView(); 134 135 // Get the anchor view for a message. 136 views::View* anchor() { return anchor_; } 137 138 private: 139 // Overridden from views::View. 140 virtual gfx::Size GetPreferredSize() const OVERRIDE; 141 142 // Create the additional client content for this item. 143 void AddContent(); 144 145 // This is the content we create and show. 146 views::View* add_user_; 147 148 // This is the owner view of this item. 149 ButtonFromView* owner_; 150 151 // The anchor view for targetted bubble messages. 152 views::View* anchor_; 153 154 DISALLOW_COPY_AND_ASSIGN(AddUserView); 155 }; 156 157 AddUserView::AddUserView(ButtonFromView* owner) 158 : add_user_(NULL), owner_(owner), anchor_(NULL) { 159 AddContent(); 160 owner_->ForceBorderVisible(true); 161 } 162 163 AddUserView::~AddUserView() { 164 owner_->ForceBorderVisible(false); 165 } 166 167 gfx::Size AddUserView::GetPreferredSize() const { 168 return owner_->bounds().size(); 169 } 170 171 void AddUserView::AddContent() { 172 SetLayoutManager(new views::FillLayout()); 173 set_background(views::Background::CreateSolidBackground(kBackgroundColor)); 174 175 add_user_ = new views::View; 176 add_user_->SetBorder(views::Border::CreateEmptyBorder( 177 0, kTrayUserTileHoverBorderInset, 0, 0)); 178 179 add_user_->SetLayoutManager(new views::BoxLayout( 180 views::BoxLayout::kHorizontal, 0, 0, kTrayPopupPaddingBetweenItems)); 181 AddChildViewAt(add_user_, 0); 182 183 // Add the [+] icon which is also the anchor for messages. 184 RoundedImageView* icon = new RoundedImageView(kTrayAvatarCornerRadius, true); 185 anchor_ = icon; 186 icon->SetImage(*ui::ResourceBundle::GetSharedInstance() 187 .GetImageNamed(IDR_AURA_UBER_TRAY_ADD_MULTIPROFILE_USER) 188 .ToImageSkia(), 189 gfx::Size(kTrayAvatarSize, kTrayAvatarSize)); 190 add_user_->AddChildView(icon); 191 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); 197 } 198 199 } // namespace 200 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), 207 owner_(owner), 208 is_user_card_button_(false), 209 logout_button_(NULL), 210 add_user_enabled_(true), 211 for_detailed_view_(for_detailed_view), 212 focus_manager_(NULL) { 213 CHECK_NE(user::LOGGED_IN_NONE, login); 214 if (!index) { 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)); 220 } 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); 228 AddUserCard(login); 229 } 230 231 UserView::~UserView() { 232 RemoveAddUserMenuOption(); 233 } 234 235 void UserView::MouseMovedOutOfHost() { 236 RemoveAddUserMenuOption(); 237 } 238 239 TrayUser::TestState UserView::GetStateForTest() const { 240 if (add_menu_option_.get()) { 241 return add_user_enabled_ ? TrayUser::ACTIVE : TrayUser::ACTIVE_BUT_DISABLED; 242 } 243 244 if (!is_user_card_button_) 245 return TrayUser::SHOWN; 246 247 return static_cast<ButtonFromView*>(user_card_view_)->is_hovered_for_test() 248 ? TrayUser::HOVERED 249 : TrayUser::SHOWN; 250 } 251 252 gfx::Rect UserView::GetBoundsInScreenOfUserButtonForTest() { 253 DCHECK(user_card_view_); 254 return user_card_view_->GetBoundsInScreen(); 255 } 256 257 gfx::Size UserView::GetPreferredSize() const { 258 gfx::Size size = views::View::GetPreferredSize(); 259 // Only the active user panel will be forced to a certain height. 260 if (!multiprofile_index_) { 261 size.set_height( 262 std::max(size.height(), kTrayPopupItemHeight + GetInsets().height())); 263 } 264 return size; 265 } 266 267 int UserView::GetHeightForWidth(int width) const { 268 return GetPreferredSize().height(); 269 } 270 271 void UserView::Layout() { 272 gfx::Rect contents_area(GetContentsBounds()); 273 if (user_card_view_ && logout_button_) { 274 // Give the logout button the space it requests. 275 gfx::Rect logout_area = contents_area; 276 logout_area.ClampToCenteredSize(logout_button_->GetPreferredSize()); 277 logout_area.set_x(contents_area.right() - logout_area.width()); 278 279 // Give the remaining space to the user card. 280 gfx::Rect user_card_area = contents_area; 281 int remaining_width = contents_area.width() - logout_area.width(); 282 if (IsMultiProfileSupportedAndUserActive() || 283 IsMultiAccountSupportedAndUserActive()) { 284 // In multiprofile/multiaccount case |user_card_view_| and 285 // |logout_button_| have to have the same height. 286 int y = std::min(user_card_area.y(), logout_area.y()); 287 int height = std::max(user_card_area.height(), logout_area.height()); 288 logout_area.set_y(y); 289 logout_area.set_height(height); 290 user_card_area.set_y(y); 291 user_card_area.set_height(height); 292 293 // In multiprofile mode we have also to increase the size of the card by 294 // the size of the border to make it overlap with the logout button. 295 user_card_area.set_width(std::max(0, remaining_width + 1)); 296 297 // To make the logout button symmetrical with the user card we also make 298 // the button longer by the same size the hover area in front of the icon 299 // got inset. 300 logout_area.set_width(logout_area.width() + 301 kTrayUserTileHoverBorderInset); 302 } else { 303 // In all other modes we have to make sure that there is enough spacing 304 // between the two. 305 remaining_width -= kTrayPopupPaddingBetweenItems; 306 } 307 user_card_area.set_width(remaining_width); 308 user_card_view_->SetBoundsRect(user_card_area); 309 logout_button_->SetBoundsRect(logout_area); 310 } else if (user_card_view_) { 311 user_card_view_->SetBoundsRect(contents_area); 312 } else if (logout_button_) { 313 logout_button_->SetBoundsRect(contents_area); 314 } 315 } 316 317 void UserView::ButtonPressed(views::Button* sender, const ui::Event& event) { 318 if (sender == logout_button_) { 319 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 320 ash::UMA_STATUS_AREA_SIGN_OUT); 321 RemoveAddUserMenuOption(); 322 Shell::GetInstance()->system_tray_delegate()->SignOut(); 323 } else if (sender == user_card_view_ && !multiprofile_index_ && 324 IsMultiAccountSupportedAndUserActive()) { 325 owner_->TransitionDetailedView(); 326 } else if (sender == user_card_view_ && 327 IsMultiProfileSupportedAndUserActive()) { 328 if (!multiprofile_index_) { 329 ToggleAddUserMenuOption(); 330 } else { 331 RemoveAddUserMenuOption(); 332 SwitchUser(multiprofile_index_); 333 // Since the user list is about to change the system menu should get 334 // closed. 335 owner_->system_tray()->CloseSystemBubble(); 336 } 337 } else if (add_menu_option_.get() && 338 sender == add_menu_option_->GetContentsView()) { 339 RemoveAddUserMenuOption(); 340 // Let the user add another account to the session. 341 MultiProfileUMA::RecordSigninUser(MultiProfileUMA::SIGNIN_USER_BY_TRAY); 342 Shell::GetInstance()->system_tray_delegate()->ShowUserLogin(); 343 owner_->system_tray()->CloseSystemBubble(); 344 } else { 345 NOTREACHED(); 346 } 347 } 348 349 void UserView::OnWillChangeFocus(View* focused_before, View* focused_now) { 350 if (focused_now) 351 RemoveAddUserMenuOption(); 352 } 353 354 void UserView::OnDidChangeFocus(View* focused_before, View* focused_now) { 355 // Nothing to do here. 356 } 357 358 void UserView::AddLogoutButton(user::LoginStatus login) { 359 const base::string16 title = 360 user::GetLocalizedSignOutStringForStatus(login, true); 361 TrayPopupLabelButton* logout_button = 362 new LogoutButton(this, title, for_detailed_view_); 363 logout_button->SetAccessibleName(title); 364 logout_button_ = logout_button; 365 // In public account mode, the logout button border has a custom color. 366 if (login == user::LOGGED_IN_PUBLIC) { 367 scoped_ptr<TrayPopupLabelButtonBorder> border( 368 new TrayPopupLabelButtonBorder()); 369 border->SetPainter(false, 370 views::Button::STATE_NORMAL, 371 views::Painter::CreateImageGridPainter( 372 kPublicAccountLogoutButtonBorderImagesNormal)); 373 border->SetPainter(false, 374 views::Button::STATE_HOVERED, 375 views::Painter::CreateImageGridPainter( 376 kPublicAccountLogoutButtonBorderImagesHovered)); 377 border->SetPainter(false, 378 views::Button::STATE_PRESSED, 379 views::Painter::CreateImageGridPainter( 380 kPublicAccountLogoutButtonBorderImagesHovered)); 381 logout_button_->SetBorder(border.PassAs<views::Border>()); 382 } 383 AddChildView(logout_button_); 384 } 385 386 void UserView::AddUserCard(user::LoginStatus login) { 387 // Add padding around the panel. 388 SetBorder(views::Border::CreateEmptyBorder(kTrayPopupUserCardVerticalPadding, 389 kTrayPopupPaddingHorizontal, 390 kTrayPopupUserCardVerticalPadding, 391 kTrayPopupPaddingHorizontal)); 392 393 views::TrayBubbleView* bubble_view = 394 owner_->system_tray()->GetSystemBubble()->bubble_view(); 395 int max_card_width = 396 bubble_view->GetMaximumSize().width() - 397 (2 * kTrayPopupPaddingHorizontal + kTrayPopupPaddingBetweenItems); 398 if (logout_button_) 399 max_card_width -= logout_button_->GetPreferredSize().width(); 400 user_card_view_ = 401 new UserCardView(login, max_card_width, multiprofile_index_); 402 // The entry is clickable when no system modal dialog is open and one of the 403 // multi user options is active. 404 bool clickable = !Shell::GetInstance()->IsSystemModalWindowOpen() && 405 (IsMultiProfileSupportedAndUserActive() || 406 IsMultiAccountSupportedAndUserActive()); 407 if (clickable) { 408 // To allow the border to start before the icon, reduce the size before and 409 // add an inset to the icon to get the spacing. 410 if (!multiprofile_index_) { 411 SetBorder(views::Border::CreateEmptyBorder( 412 kTrayPopupUserCardVerticalPadding, 413 kTrayPopupPaddingHorizontal - kTrayUserTileHoverBorderInset, 414 kTrayPopupUserCardVerticalPadding, 415 kTrayPopupPaddingHorizontal)); 416 user_card_view_->SetBorder(views::Border::CreateEmptyBorder( 417 0, kTrayUserTileHoverBorderInset, 0, 0)); 418 } 419 gfx::Insets insets = gfx::Insets(1, 1, 1, 1); 420 views::View* contents_view = user_card_view_; 421 ButtonFromView* button = NULL; 422 if (!for_detailed_view_) { 423 if (multiprofile_index_) { 424 // Since the activation border needs to be drawn around the tile, we 425 // have to put the tile into another view which fills the menu panel, 426 // but keeping the offsets of the content. 427 contents_view = new views::View(); 428 contents_view->SetBorder(views::Border::CreateEmptyBorder( 429 kTrayPopupUserCardVerticalPadding, 430 kTrayPopupPaddingHorizontal, 431 kTrayPopupUserCardVerticalPadding, 432 kTrayPopupPaddingHorizontal)); 433 contents_view->SetLayoutManager(new views::FillLayout()); 434 SetBorder(views::Border::CreateEmptyBorder(0, 0, 0, 0)); 435 contents_view->AddChildView(user_card_view_); 436 insets = gfx::Insets(1, 1, 1, 3); 437 } 438 button = new ButtonFromView(contents_view, 439 this, 440 !multiprofile_index_, 441 insets); 442 // TODO(skuhne): For accessibility we need to call |SetAccessibleName| 443 // with a useful name (string freeze for M37 has passed). 444 } else { 445 // We want user card for detailed view to have exactly the same look 446 // as user card for default view. That's why we wrap it in a button 447 // without click listener and special hover behavior. 448 button = new ButtonFromView(contents_view, NULL, false, insets); 449 } 450 // A click on the button should not trigger a focus change. 451 button->set_request_focus_on_press(false); 452 user_card_view_ = button; 453 is_user_card_button_ = true; 454 } 455 AddChildViewAt(user_card_view_, 0); 456 // Card for supervised user can consume more space than currently 457 // available. In that case we should increase system bubble's width. 458 if (login == user::LOGGED_IN_PUBLIC) 459 bubble_view->SetWidth(GetPreferredSize().width()); 460 } 461 462 void UserView::ToggleAddUserMenuOption() { 463 if (add_menu_option_.get()) { 464 RemoveAddUserMenuOption(); 465 return; 466 } 467 468 // Note: We do not need to install a global event handler to delete this 469 // item since it will destroyed automatically before the menu / user menu item 470 // gets destroyed.. 471 add_menu_option_.reset(new views::Widget); 472 views::Widget::InitParams params; 473 params.type = views::Widget::InitParams::TYPE_TOOLTIP; 474 params.keep_on_top = true; 475 params.context = this->GetWidget()->GetNativeWindow(); 476 params.accept_events = true; 477 params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 478 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 479 add_menu_option_->Init(params); 480 add_menu_option_->SetOpacity(0xFF); 481 add_menu_option_->GetNativeWindow()->set_owned_by_parent(false); 482 SetShadowType(add_menu_option_->GetNativeView(), wm::SHADOW_TYPE_NONE); 483 484 // Position it below our user card. 485 gfx::Rect bounds = user_card_view_->GetBoundsInScreen(); 486 bounds.set_y(bounds.y() + bounds.height()); 487 add_menu_option_->SetBounds(bounds); 488 489 // Show the content. 490 add_menu_option_->SetAlwaysOnTop(true); 491 add_menu_option_->Show(); 492 493 AddUserView* add_user_view = 494 new AddUserView(static_cast<ButtonFromView*>(user_card_view_)); 495 496 const SessionStateDelegate* delegate = 497 Shell::GetInstance()->session_state_delegate(); 498 499 SessionStateDelegate::AddUserError add_user_error; 500 add_user_enabled_ = delegate->CanAddUserToMultiProfile(&add_user_error); 501 502 ButtonFromView* button = new ButtonFromView(add_user_view, 503 add_user_enabled_ ? this : NULL, 504 add_user_enabled_, 505 gfx::Insets(1, 1, 1, 1)); 506 button->set_request_focus_on_press(false); 507 button->SetAccessibleName( 508 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT)); 509 button->ForceBorderVisible(true); 510 add_menu_option_->SetContentsView(button); 511 512 if (add_user_enabled_) { 513 // We activate the entry automatically if invoked with focus. 514 if (user_card_view_->HasFocus()) { 515 button->GetFocusManager()->SetFocusedView(button); 516 user_card_view_->GetFocusManager()->SetFocusedView(button); 517 } 518 } else { 519 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 520 int message_id = 0; 521 switch (add_user_error) { 522 case SessionStateDelegate::ADD_USER_ERROR_NOT_ALLOWED_PRIMARY_USER: 523 message_id = IDS_ASH_STATUS_TRAY_MESSAGE_NOT_ALLOWED_PRIMARY_USER; 524 break; 525 case SessionStateDelegate::ADD_USER_ERROR_MAXIMUM_USERS_REACHED: 526 message_id = IDS_ASH_STATUS_TRAY_MESSAGE_CANNOT_ADD_USER; 527 break; 528 case SessionStateDelegate::ADD_USER_ERROR_OUT_OF_USERS: 529 message_id = IDS_ASH_STATUS_TRAY_MESSAGE_OUT_OF_USERS; 530 break; 531 default: 532 NOTREACHED() << "Unknown adding user error " << add_user_error; 533 } 534 535 popup_message_.reset(new PopupMessage( 536 bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAPTION_CANNOT_ADD_USER), 537 bundle.GetLocalizedString(message_id), 538 PopupMessage::ICON_WARNING, 539 add_user_view->anchor(), 540 views::BubbleBorder::TOP_LEFT, 541 gfx::Size(parent()->bounds().width() - kPopupMessageOffset, 0), 542 2 * kPopupMessageOffset)); 543 } 544 // Find the screen area which encloses both elements and sets then a mouse 545 // watcher which will close the "menu". 546 gfx::Rect area = user_card_view_->GetBoundsInScreen(); 547 area.set_height(2 * area.height()); 548 mouse_watcher_.reset( 549 new views::MouseWatcher(new UserViewMouseWatcherHost(area), this)); 550 mouse_watcher_->Start(); 551 // Install a listener to focus changes so that we can remove the card when 552 // the focus gets changed. When called through the destruction of the bubble, 553 // the FocusManager cannot be determined anymore and we remember it here. 554 focus_manager_ = user_card_view_->GetFocusManager(); 555 focus_manager_->AddFocusChangeListener(this); 556 } 557 558 void UserView::RemoveAddUserMenuOption() { 559 if (!add_menu_option_.get()) 560 return; 561 focus_manager_->RemoveFocusChangeListener(this); 562 focus_manager_ = NULL; 563 if (user_card_view_->GetFocusManager()) 564 user_card_view_->GetFocusManager()->ClearFocus(); 565 popup_message_.reset(); 566 mouse_watcher_.reset(); 567 add_menu_option_.reset(); 568 } 569 570 } // namespace tray 571 } // namespace ash 572