Commit 255cb689 authored by Sammie Quon's avatar Sammie Quon Committed by Commit Bot

system: Allow keyboard navigation on user drop down menu on tray.

Allows user drop down menu to activatble so that it can be cycled
through.
Add a custom focus search so the drop down menu closes after all items
have been cycled through.
Migrate Closure to OnceClosure.
Make drop down menu a transient child of system bubble.

Test: manual
Bug: 799401
Change-Id: I67af2bb3e58c88f6926ab65ecf565d69325d00b8
Reviewed-on: https://chromium-review.googlesource.com/1038656Reviewed-by: default avatarSteven Bennetts <stevenjb@chromium.org>
Commit-Queue: Sammie Quon <sammiequon@chromium.org>
Cr-Commit-Position: refs/heads/master@{#555756}
parent 210545ea
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
#include "ui/aura/window.h" #include "ui/aura/window.h"
#include "ui/views/bubble/tray_bubble_view.h" #include "ui/views/bubble/tray_bubble_view.h"
#include "ui/views/widget/widget.h" #include "ui/views/widget/widget.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/core/window_util.h" #include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h" #include "ui/wm/public/activation_client.h"
...@@ -43,6 +44,12 @@ TrayBubbleWrapper::~TrayBubbleWrapper() { ...@@ -43,6 +44,12 @@ TrayBubbleWrapper::~TrayBubbleWrapper() {
tray_->tray_event_filter()->RemoveWrapper(this); tray_->tray_event_filter()->RemoveWrapper(this);
if (bubble_widget_) { if (bubble_widget_) {
auto* transient_manager = ::wm::TransientWindowManager::GetOrCreate(
bubble_widget_->GetNativeWindow());
if (transient_manager) {
for (auto* window : transient_manager->transient_children())
transient_manager->RemoveTransientChild(window);
}
bubble_widget_->GetNativeWindow()->GetRootWindow()->RemoveObserver(this); bubble_widget_->GetNativeWindow()->GetRootWindow()->RemoveObserver(this);
bubble_widget_->RemoveObserver(this); bubble_widget_->RemoveObserver(this);
bubble_widget_->Close(); bubble_widget_->Close();
......
...@@ -36,8 +36,11 @@ ...@@ -36,8 +36,11 @@
#include "ui/views/controls/button/label_button.h" #include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h" #include "ui/views/controls/label.h"
#include "ui/views/controls/separator.h" #include "ui/views/controls/separator.h"
#include "ui/views/focus/focus_search.h"
#include "ui/views/layout/fill_layout.h" #include "ui/views/layout/fill_layout.h"
#include "ui/views/painter.h" #include "ui/views/painter.h"
#include "ui/views/view_model.h"
#include "ui/wm/core/transient_window_manager.h"
namespace ash { namespace ash {
namespace tray { namespace tray {
...@@ -85,15 +88,14 @@ bool IsUserDropdownEnabled() { ...@@ -85,15 +88,14 @@ bool IsUserDropdownEnabled() {
// Creates the view shown in the user switcher popup ("AddUserMenuOption"). // Creates the view shown in the user switcher popup ("AddUserMenuOption").
views::View* CreateAddUserView(AddUserSessionPolicy policy) { views::View* CreateAddUserView(AddUserSessionPolicy policy) {
auto* view = new views::View; auto* view = new views::View();
const int icon_padding = (kMenuButtonSize - kMenuIconSize) / 2; const int icon_padding = (kMenuButtonSize - kMenuIconSize) / 2;
auto layout = std::make_unique<views::BoxLayout>( auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, gfx::Insets(), views::BoxLayout::kHorizontal, gfx::Insets(),
kTrayPopupLabelHorizontalPadding + icon_padding); kTrayPopupLabelHorizontalPadding + icon_padding);
layout->set_minimum_cross_axis_size(kTrayPopupItemMinHeight); layout->set_minimum_cross_axis_size(kTrayPopupItemMinHeight);
view->SetLayoutManager(std::move(layout)); view->SetLayoutManager(std::move(layout));
view->SetBackground(views::CreateThemedSolidBackground( view->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
view, ui::NativeTheme::kColorId_BubbleBackground));
base::string16 message; base::string16 message;
switch (policy) { switch (policy) {
...@@ -146,27 +148,85 @@ views::View* CreateAddUserView(AddUserSessionPolicy policy) { ...@@ -146,27 +148,85 @@ views::View* CreateAddUserView(AddUserSessionPolicy policy) {
// A view that acts as the contents of the widget that appears when clicking // A view that acts as the contents of the widget that appears when clicking
// the active user. If the mouse exits this view or an otherwise unhandled // the active user. If the mouse exits this view or an otherwise unhandled
// click is detected, it will invoke a closure passed at construction time. // click is detected, it will invoke a closure passed at construction time.
class UserDropdownWidgetContents : public views::View { class UserDropdownWidgetContents : public views::View,
public views::FocusTraversable {
public: public:
explicit UserDropdownWidgetContents(const base::Closure& close_widget) explicit UserDropdownWidgetContents(base::OnceClosure close_widget_callback)
: close_widget_(close_widget) { : close_widget_callback_(std::move(close_widget_callback)),
view_model_(std::make_unique<views::ViewModel>()),
focus_search_(
std::make_unique<DropDownFocusSearch>(this, view_model_.get())) {
// Don't want to receive a mouse exit event when the cursor enters a child. // Don't want to receive a mouse exit event when the cursor enters a child.
set_notify_enter_exit_on_child(true); set_notify_enter_exit_on_child(true);
} }
~UserDropdownWidgetContents() override = default; ~UserDropdownWidgetContents() override = default;
void CloseWidget() { std::move(close_widget_callback_).Run(); }
views::ViewModel* view_model() { return view_model_.get(); }
// views::View:
FocusTraversable* GetPaneFocusTraversable() override { return this; }
bool OnMousePressed(const ui::MouseEvent& event) override { return true; } bool OnMousePressed(const ui::MouseEvent& event) override { return true; }
void OnMouseReleased(const ui::MouseEvent& event) override { void OnMouseReleased(const ui::MouseEvent& event) override { CloseWidget(); }
close_widget_.Run(); void OnMouseExited(const ui::MouseEvent& event) override { CloseWidget(); }
void OnGestureEvent(ui::GestureEvent* event) override { CloseWidget(); }
// views::FocusTraversable:
views::FocusSearch* GetFocusSearch() override { return focus_search_.get(); }
FocusTraversable* GetFocusTraversableParent() override { return nullptr; }
View* GetFocusTraversableParentView() override { return nullptr; }
private:
// Custom FocusSearch used to navigate the drop down widget. The navigation is
// according to the view model, which should be populated the same order the
// views are added, but this search also closes the widget when tabbing past
// the boudaries.
class DropDownFocusSearch : public views::FocusSearch {
public:
DropDownFocusSearch(UserDropdownWidgetContents* owner,
views::ViewModel* view_model)
: views::FocusSearch(nullptr, true, true),
owner_(owner),
view_model_(view_model) {}
~DropDownFocusSearch() override = default;
// views::FocusSearch:
views::View* FindNextFocusableView(
views::View* starting_view,
SearchDirection search_direction,
TraversalDirection traversal_direction,
StartingViewPolicy check_starting_view,
views::FocusTraversable** focus_traversable,
views::View** focus_traversable_view) override {
int index = view_model_->GetIndexOfView(starting_view);
index += search_direction == SearchDirection::kForwards ? 1 : -1;
if (index == -1 || index == view_model_->view_size()) {
// |close_widget_| will delete the widget which has the view which owns
// this custom search, so do not run callback immediately.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&DropDownFocusSearch::CloseWidget,
base::Unretained(this)));
return nullptr;
} }
void OnMouseExited(const ui::MouseEvent& event) override { return view_model_->view_at(index);
close_widget_.Run();
} }
void OnGestureEvent(ui::GestureEvent* event) override { close_widget_.Run(); }
private: private:
base::Closure close_widget_; void CloseWidget() { owner_->CloseWidget(); }
UserDropdownWidgetContents* owner_;
views::ViewModel* view_model_;
DISALLOW_COPY_AND_ASSIGN(DropDownFocusSearch);
};
base::OnceClosure close_widget_callback_;
// Used to manage the focusable items in the drop down widget. There is a
// view per item in |view_model_|.
std::unique_ptr<views::ViewModel> view_model_;
std::unique_ptr<DropDownFocusSearch> focus_search_;
DISALLOW_COPY_AND_ASSIGN(UserDropdownWidgetContents); DISALLOW_COPY_AND_ASSIGN(UserDropdownWidgetContents);
}; };
...@@ -250,7 +310,7 @@ void UserView::ButtonPressed(views::Button* sender, const ui::Event& event) { ...@@ -250,7 +310,7 @@ void UserView::ButtonPressed(views::Button* sender, const ui::Event& event) {
HideUserDropdownWidget(); HideUserDropdownWidget();
Shell::Get()->session_controller()->RequestSignOut(); Shell::Get()->session_controller()->RequestSignOut();
} else if (sender == user_card_container_ && IsUserDropdownEnabled()) { } else if (sender == user_card_container_ && IsUserDropdownEnabled()) {
ToggleUserDropdownWidget(); ToggleUserDropdownWidget(event.IsKeyEvent());
} else if (user_dropdown_widget_ && } else if (user_dropdown_widget_ &&
sender->GetWidget() == user_dropdown_widget_.get()) { sender->GetWidget() == user_dropdown_widget_.get()) {
DCHECK_EQ(Shell::Get()->session_controller()->NumberOfLoggedInUsers(), DCHECK_EQ(Shell::Get()->session_controller()->NumberOfLoggedInUsers(),
...@@ -301,7 +361,8 @@ void UserView::AddUserCard(LoginStatus login) { ...@@ -301,7 +361,8 @@ void UserView::AddUserCard(LoginStatus login) {
AddChildViewAt(user_card_container_, 0); AddChildViewAt(user_card_container_, 0);
} }
void UserView::ToggleUserDropdownWidget() { void UserView::ToggleUserDropdownWidget(bool toggled_by_key_event) {
user_dropdown_widget_toggled_by_key_event_ = toggled_by_key_event;
if (user_dropdown_widget_) { if (user_dropdown_widget_) {
HideUserDropdownWidget(); HideUserDropdownWidget();
return; return;
...@@ -309,17 +370,20 @@ void UserView::ToggleUserDropdownWidget() { ...@@ -309,17 +370,20 @@ void UserView::ToggleUserDropdownWidget() {
// Note: We do not need to install a global event handler to delete this // 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 // item since it will destroyed automatically before the menu / user menu item
// gets destroyed.. // gets destroyed.
user_dropdown_widget_.reset(new views::Widget); user_dropdown_widget_.reset(new views::Widget);
views::Widget::InitParams params; views::Widget::InitParams params;
params.type = views::Widget::InitParams::TYPE_MENU; params.type = views::Widget::InitParams::TYPE_MENU;
params.keep_on_top = true; params.keep_on_top = true;
params.accept_events = true; params.accept_events = true;
params.activatable = views::Widget::InitParams::ACTIVATABLE_YES;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
params.name = "AddUserMenuOption"; params.name = "AddUserMenuOption";
// Use |kShellWindowId_SettingBubbleContainer| as the widget has to be
// activatable.
params.parent = GetWidget()->GetNativeWindow()->GetRootWindow()->GetChildById( params.parent = GetWidget()->GetNativeWindow()->GetRootWindow()->GetChildById(
kShellWindowId_DragImageAndTooltipContainer); kShellWindowId_SettingBubbleContainer);
user_dropdown_widget_->Init(params); user_dropdown_widget_->Init(params);
const SessionController* const session_controller = const SessionController* const session_controller =
...@@ -335,8 +399,9 @@ void UserView::ToggleUserDropdownWidget() { ...@@ -335,8 +399,9 @@ void UserView::ToggleUserDropdownWidget() {
bounds.set_width(bounds.width() + kSeparatorWidth); bounds.set_width(bounds.width() + kSeparatorWidth);
int row_height = bounds.height(); int row_height = bounds.height();
views::View* container = new UserDropdownWidgetContents( UserDropdownWidgetContents* container =
base::Bind(&UserView::HideUserDropdownWidget, base::Unretained(this))); new UserDropdownWidgetContents(base::BindOnce(
&UserView::HideUserDropdownWidget, base::Unretained(this)));
views::View* add_user_view = CreateAddUserView(add_user_policy); views::View* add_user_view = CreateAddUserView(add_user_policy);
const SkColor bg_color = add_user_view->background()->get_color(); const SkColor bg_color = add_user_view->background()->get_color();
container->SetBorder(views::CreatePaddedBorder( container->SetBorder(views::CreatePaddedBorder(
...@@ -359,11 +424,26 @@ void UserView::ToggleUserDropdownWidget() { ...@@ -359,11 +424,26 @@ void UserView::ToggleUserDropdownWidget() {
separator->SetBorder(views::CreateEmptyBorder( separator->SetBorder(views::CreateEmptyBorder(
0, separator_horizontal_padding, 0, separator_horizontal_padding)); 0, separator_horizontal_padding, 0, separator_horizontal_padding));
user_dropdown_padding->AddChildView(separator); user_dropdown_padding->AddChildView(separator);
user_dropdown_padding->SetBackground(views::CreateThemedSolidBackground(
user_dropdown_padding, ui::NativeTheme::kColorId_BubbleBackground));
// Add other logged in users. // Add other logged in users.
// Helper function to add a descendant view of |widget_content_view| which
// will need to grab focus with the custom search.
auto add_focusable_child = [](views::View* parent, views::View* child,
UserDropdownWidgetContents* widget_content_view,
int* out_model_index) {
DCHECK(out_model_index);
parent->AddChildView(child);
widget_content_view->view_model()->Add(child, *out_model_index);
++(*out_model_index);
};
int model_index = 0;
for (int i = 1; i < session_controller->NumberOfLoggedInUsers(); ++i) { for (int i = 1; i < session_controller->NumberOfLoggedInUsers(); ++i) {
user_dropdown_padding->AddChildView(new ButtonFromView( auto* button = new ButtonFromView(new UserCardView(i), this,
new UserCardView(i), this, TrayPopupInkDropStyle::INSET_BOUNDS)); TrayPopupInkDropStyle::INSET_BOUNDS);
add_focusable_child(user_dropdown_padding, button, container, &model_index);
} }
// Add the "add user" option or the "can't add another user" message. // Add the "add user" option or the "can't add another user" message.
...@@ -372,7 +452,7 @@ void UserView::ToggleUserDropdownWidget() { ...@@ -372,7 +452,7 @@ void UserView::ToggleUserDropdownWidget() {
TrayPopupInkDropStyle::INSET_BOUNDS); TrayPopupInkDropStyle::INSET_BOUNDS);
button->SetAccessibleName( button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT)); l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_SIGN_IN_ANOTHER_ACCOUNT));
user_dropdown_padding->AddChildView(button); add_focusable_child(user_dropdown_padding, button, container, &model_index);
} else { } else {
user_dropdown_padding->AddChildView(add_user_view); user_dropdown_padding->AddChildView(add_user_view);
} }
...@@ -388,9 +468,19 @@ void UserView::ToggleUserDropdownWidget() { ...@@ -388,9 +468,19 @@ void UserView::ToggleUserDropdownWidget() {
// is open (the icon will appear in the specific user rows). // is open (the icon will appear in the specific user rows).
user_card_view_->SetSuppressCaptureIcon(true); user_card_view_->SetSuppressCaptureIcon(true);
// Make |user_dropdown_widget_| a transient child of the tray bubble, so that
// the bubble does not close when |user_dropdown_widget_| loses activation.
::wm::TransientWindowManager::GetOrCreate(
GetBubbleWidget()->GetNativeWindow())
->AddTransientChild(user_dropdown_widget_->GetNativeWindow());
// Show the content. // Show the content.
user_dropdown_widget_->SetAlwaysOnTop(true); user_dropdown_widget_->SetAlwaysOnTop(true);
user_dropdown_widget_->Show(); user_dropdown_widget_->Show();
if (toggled_by_key_event && container->view_model()->view_size() > 0) {
user_dropdown_widget_->GetFocusManager()->SetFocusedView(
container->view_model()->view_at(0));
}
// Install a listener to focus changes so that we can remove the card when // Install a listener to focus changes so that we can remove the card when
// the focus gets changed. When called through the destruction of the bubble, // the focus gets changed. When called through the destruction of the bubble,
...@@ -404,11 +494,27 @@ void UserView::HideUserDropdownWidget() { ...@@ -404,11 +494,27 @@ void UserView::HideUserDropdownWidget() {
return; return;
focus_manager_->RemoveFocusChangeListener(this); focus_manager_->RemoveFocusChangeListener(this);
focus_manager_ = nullptr; focus_manager_ = nullptr;
if (user_card_container_->GetFocusManager()) if (user_card_container_->GetFocusManager()) {
// Return activation to bubble before destroying |user_dropdown_widget_|,
// otherwise tray_bubble_wrapper will not know |user_dropdown_widget_| is
// a transient child of the bubble.
if (GetBubbleWidget())
GetBubbleWidget()->Activate();
user_card_container_->GetFocusManager()->ClearFocus(); user_card_container_->GetFocusManager()->ClearFocus();
if (user_dropdown_widget_toggled_by_key_event_) {
user_card_container_->GetFocusManager()->SetFocusedView(
user_card_container_);
}
}
user_card_view_->SetSuppressCaptureIcon(false); user_card_view_->SetSuppressCaptureIcon(false);
user_dropdown_widget_.reset(); user_dropdown_widget_.reset();
} }
views::Widget* UserView::GetBubbleWidget() {
if (!owner_->system_tray()->GetSystemBubble())
return nullptr;
return owner_->system_tray()->GetSystemBubble()->bubble_view()->GetWidget();
}
} // namespace tray } // namespace tray
} // namespace ash } // namespace ash
...@@ -60,13 +60,14 @@ class UserView : public views::View, ...@@ -60,13 +60,14 @@ class UserView : public views::View,
void AddLogoutButton(LoginStatus login); void AddLogoutButton(LoginStatus login);
void AddUserCard(LoginStatus login); void AddUserCard(LoginStatus login);
// Create the menu option to add another user. If |disabled| is set the user // Create the menu option to add another user.
// cannot actively click on the item. void ToggleUserDropdownWidget(bool toggled_by_key_event);
void ToggleUserDropdownWidget();
// Removes the add user menu option. // Removes the add user menu option.
void HideUserDropdownWidget(); void HideUserDropdownWidget();
views::Widget* GetBubbleWidget();
// If |user_card_view_| is clickable, this is a ButtonFromView that wraps it. // If |user_card_view_| is clickable, this is a ButtonFromView that wraps it.
// If |user_card_view_| is not clickable, this will be equal to // If |user_card_view_| is not clickable, this will be equal to
// |user_card_view_|. // |user_card_view_|.
...@@ -80,6 +81,9 @@ class UserView : public views::View, ...@@ -80,6 +81,9 @@ class UserView : public views::View,
views::View* logout_button_ = nullptr; views::View* logout_button_ = nullptr;
std::unique_ptr<views::Widget> user_dropdown_widget_; std::unique_ptr<views::Widget> user_dropdown_widget_;
// Tracks whether |user_dropdown_widget_| was opened with a key event. If
// true, HideUserDropdownWidget() will return focus to |user_card_container_|.
bool user_dropdown_widget_toggled_by_key_event_ = false;
// False when the add user panel is visible but not activatable. // False when the add user panel is visible but not activatable.
bool add_user_enabled_ = true; bool add_user_enabled_ = true;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment