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 @@
#include "ui/aura/window.h"
#include "ui/views/bubble/tray_bubble_view.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/public/activation_client.h"
......@@ -43,6 +44,12 @@ TrayBubbleWrapper::~TrayBubbleWrapper() {
tray_->tray_event_filter()->RemoveWrapper(this);
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_->RemoveObserver(this);
bubble_widget_->Close();
......
......@@ -36,8 +36,11 @@
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/separator.h"
#include "ui/views/focus/focus_search.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/painter.h"
#include "ui/views/view_model.h"
#include "ui/wm/core/transient_window_manager.h"
namespace ash {
namespace tray {
......@@ -85,15 +88,14 @@ bool IsUserDropdownEnabled() {
// Creates the view shown in the user switcher popup ("AddUserMenuOption").
views::View* CreateAddUserView(AddUserSessionPolicy policy) {
auto* view = new views::View;
auto* view = new views::View();
const int icon_padding = (kMenuButtonSize - kMenuIconSize) / 2;
auto layout = std::make_unique<views::BoxLayout>(
views::BoxLayout::kHorizontal, gfx::Insets(),
kTrayPopupLabelHorizontalPadding + icon_padding);
layout->set_minimum_cross_axis_size(kTrayPopupItemMinHeight);
view->SetLayoutManager(std::move(layout));
view->SetBackground(views::CreateThemedSolidBackground(
view, ui::NativeTheme::kColorId_BubbleBackground));
view->SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT));
base::string16 message;
switch (policy) {
......@@ -146,27 +148,85 @@ views::View* CreateAddUserView(AddUserSessionPolicy policy) {
// 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
// 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:
explicit UserDropdownWidgetContents(const base::Closure& close_widget)
: close_widget_(close_widget) {
explicit UserDropdownWidgetContents(base::OnceClosure close_widget_callback)
: 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.
set_notify_enter_exit_on_child(true);
}
~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; }
void OnMouseReleased(const ui::MouseEvent& event) override {
close_widget_.Run();
}
void OnMouseExited(const ui::MouseEvent& event) override {
close_widget_.Run();
}
void OnGestureEvent(ui::GestureEvent* event) override { close_widget_.Run(); }
void OnMouseReleased(const ui::MouseEvent& event) override { CloseWidget(); }
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:
base::Closure close_widget_;
// 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;
}
return view_model_->view_at(index);
}
private:
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);
};
......@@ -250,7 +310,7 @@ void UserView::ButtonPressed(views::Button* sender, const ui::Event& event) {
HideUserDropdownWidget();
Shell::Get()->session_controller()->RequestSignOut();
} else if (sender == user_card_container_ && IsUserDropdownEnabled()) {
ToggleUserDropdownWidget();
ToggleUserDropdownWidget(event.IsKeyEvent());
} else if (user_dropdown_widget_ &&
sender->GetWidget() == user_dropdown_widget_.get()) {
DCHECK_EQ(Shell::Get()->session_controller()->NumberOfLoggedInUsers(),
......@@ -301,7 +361,8 @@ void UserView::AddUserCard(LoginStatus login) {
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_) {
HideUserDropdownWidget();
return;
......@@ -309,17 +370,20 @@ void UserView::ToggleUserDropdownWidget() {
// 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..
// gets destroyed.
user_dropdown_widget_.reset(new views::Widget);
views::Widget::InitParams params;
params.type = views::Widget::InitParams::TYPE_MENU;
params.keep_on_top = true;
params.accept_events = true;
params.activatable = views::Widget::InitParams::ACTIVATABLE_YES;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
params.name = "AddUserMenuOption";
// Use |kShellWindowId_SettingBubbleContainer| as the widget has to be
// activatable.
params.parent = GetWidget()->GetNativeWindow()->GetRootWindow()->GetChildById(
kShellWindowId_DragImageAndTooltipContainer);
kShellWindowId_SettingBubbleContainer);
user_dropdown_widget_->Init(params);
const SessionController* const session_controller =
......@@ -335,8 +399,9 @@ void UserView::ToggleUserDropdownWidget() {
bounds.set_width(bounds.width() + kSeparatorWidth);
int row_height = bounds.height();
views::View* container = new UserDropdownWidgetContents(
base::Bind(&UserView::HideUserDropdownWidget, base::Unretained(this)));
UserDropdownWidgetContents* container =
new UserDropdownWidgetContents(base::BindOnce(
&UserView::HideUserDropdownWidget, base::Unretained(this)));
views::View* add_user_view = CreateAddUserView(add_user_policy);
const SkColor bg_color = add_user_view->background()->get_color();
container->SetBorder(views::CreatePaddedBorder(
......@@ -359,11 +424,26 @@ void UserView::ToggleUserDropdownWidget() {
separator->SetBorder(views::CreateEmptyBorder(
0, separator_horizontal_padding, 0, separator_horizontal_padding));
user_dropdown_padding->AddChildView(separator);
user_dropdown_padding->SetBackground(views::CreateThemedSolidBackground(
user_dropdown_padding, ui::NativeTheme::kColorId_BubbleBackground));
// 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) {
user_dropdown_padding->AddChildView(new ButtonFromView(
new UserCardView(i), this, TrayPopupInkDropStyle::INSET_BOUNDS));
auto* button = new ButtonFromView(new UserCardView(i), this,
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.
......@@ -372,7 +452,7 @@ void UserView::ToggleUserDropdownWidget() {
TrayPopupInkDropStyle::INSET_BOUNDS);
button->SetAccessibleName(
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 {
user_dropdown_padding->AddChildView(add_user_view);
}
......@@ -388,9 +468,19 @@ void UserView::ToggleUserDropdownWidget() {
// is open (the icon will appear in the specific user rows).
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.
user_dropdown_widget_->SetAlwaysOnTop(true);
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
// the focus gets changed. When called through the destruction of the bubble,
......@@ -404,11 +494,27 @@ void UserView::HideUserDropdownWidget() {
return;
focus_manager_->RemoveFocusChangeListener(this);
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();
if (user_dropdown_widget_toggled_by_key_event_) {
user_card_container_->GetFocusManager()->SetFocusedView(
user_card_container_);
}
}
user_card_view_->SetSuppressCaptureIcon(false);
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 ash
......@@ -60,13 +60,14 @@ class UserView : public views::View,
void AddLogoutButton(LoginStatus login);
void AddUserCard(LoginStatus login);
// Create the menu option to add another user. If |disabled| is set the user
// cannot actively click on the item.
void ToggleUserDropdownWidget();
// Create the menu option to add another user.
void ToggleUserDropdownWidget(bool toggled_by_key_event);
// Removes the add user menu option.
void HideUserDropdownWidget();
views::Widget* GetBubbleWidget();
// 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
// |user_card_view_|.
......@@ -80,6 +81,9 @@ class UserView : public views::View,
views::View* logout_button_ = nullptr;
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.
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