blob: 864b04f3694d1136bf1ef5ef766678a0c1348c4e [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/login/ui/login_menu_view.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <utility>
#include "ash/login/ui/hover_notifier.h"
#include "ash/login/ui/non_accessible_view.h"
#include "base/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
#include "ui/views/layout/box_layout.h"
namespace ash {
constexpr int kMenuItemWidthDp = 178;
constexpr int kMenuItemHeightDp = 28;
constexpr int kRegularMenuItemLeftPaddingDp = 2;
constexpr int kGroupMenuItemLeftPaddingDp = 10;
constexpr int kNonEmptyHeight = 1;
namespace {
constexpr SkColor kMenuBackgroundColor = SkColorSetRGB(0x3C, 0x40, 0x43);
class MenuItemView : public views::Button, public views::ButtonListener {
public:
MenuItemView(const LoginMenuView::Item& item,
const LoginMenuView::OnHighlight& on_highlight)
: views::Button(this), item_(item), on_highlight_(on_highlight) {
SetFocusBehavior(FocusBehavior::ALWAYS);
SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kHorizontal));
SetPreferredSize(gfx::Size(kMenuItemWidthDp, kMenuItemHeightDp));
auto* spacing = new NonAccessibleView();
spacing->SetPreferredSize(gfx::Size(item.is_group
? kRegularMenuItemLeftPaddingDp
: kGroupMenuItemLeftPaddingDp,
kNonEmptyHeight));
AddChildView(spacing);
views::Label* label = new views::Label(base::UTF8ToUTF16(item.title));
label->SetEnabledColor(SK_ColorWHITE);
label->SetSubpixelRenderingEnabled(false);
label->SetAutoColorReadabilityEnabled(false);
label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
AddChildView(label);
if (item.selected)
SetBackground(views::CreateSolidBackground(SK_ColorGRAY));
hover_notifier_ = std::make_unique<HoverNotifier>(
this,
base::BindRepeating(&MenuItemView::OnHover, base::Unretained(this)));
}
~MenuItemView() override = default;
// views::View:
int GetHeightForWidth(int w) const override {
// Make row height fixed avoiding layout manager adjustments.
return GetPreferredSize().height();
}
// views::ButtonListener:
void ButtonPressed(views::Button* sender, const ui::Event& event) override {
DCHECK(sender == this);
if (item_.is_group)
return;
on_highlight_.Run(true /*by_selection*/);
}
void OnHover(bool has_hover) {
if (has_hover && !item_.is_group)
RequestFocus();
}
void OnFocus() override {
ScrollViewToVisible();
on_highlight_.Run(false /*by_selection*/);
}
const LoginMenuView::Item& item() const { return item_; }
private:
const LoginMenuView::Item item_;
const LoginMenuView::OnHighlight on_highlight_;
std::unique_ptr<HoverNotifier> hover_notifier_;
DISALLOW_COPY_AND_ASSIGN(MenuItemView);
};
class LoginScrollBar : public views::OverlayScrollBar {
public:
LoginScrollBar() : OverlayScrollBar(false) {}
// OverlayScrollBar:
bool OnKeyPressed(const ui::KeyEvent& event) override {
// Let LoginMenuView to handle up/down keypress.
return false;
}
private:
DISALLOW_COPY_AND_ASSIGN(LoginScrollBar);
};
} // namespace
LoginMenuView::TestApi::TestApi(LoginMenuView* view) : view_(view) {}
LoginMenuView::TestApi::~TestApi() = default;
views::View* LoginMenuView::TestApi::contents() const {
return view_->contents_;
}
LoginMenuView::Item::Item() = default;
LoginMenuView::LoginMenuView(const std::vector<Item>& items,
views::View* anchor_view,
LoginButton* opener,
const OnSelect& on_select)
: LoginBaseBubbleView(anchor_view), opener_(opener), on_select_(on_select) {
SetBackground(views::CreateSolidBackground(kMenuBackgroundColor));
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
scroller_ = new views::ScrollView();
scroller_->SetBackgroundColor(SK_ColorTRANSPARENT);
scroller_->set_draw_overflow_indicator(false);
scroller_->ClipHeightTo(kMenuItemHeightDp, kMenuItemHeightDp * 5);
AddChildView(scroller_);
views::BoxLayout* box_layout = SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
box_layout->SetFlexForView(scroller_, 1);
auto contents = std::make_unique<NonAccessibleView>();
views::BoxLayout* layout = contents->SetLayoutManager(
std::make_unique<views::BoxLayout>(views::BoxLayout::kVertical));
layout->SetDefaultFlex(1);
layout->set_minimum_cross_axis_size(kMenuItemWidthDp);
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
for (size_t i = 0; i < items.size(); i++) {
const Item& item = items[i];
contents->AddChildView(new MenuItemView(
item, base::BindRepeating(&LoginMenuView::OnHighlightChange,
base::Unretained(this), i)));
if (item.selected)
selected_index_ = i;
}
contents_ = scroller_->SetContents(std::move(contents));
scroller_->SetVerticalScrollBar(new LoginScrollBar());
}
LoginMenuView::~LoginMenuView() = default;
void LoginMenuView::OnHighlightChange(size_t item_index, bool by_selection) {
selected_index_ = item_index;
views::View* highlight_item = contents_->children()[item_index];
for (views::View* child : contents_->GetChildrenInZOrder()) {
child->SetBackground(views::CreateSolidBackground(
child == highlight_item ? SK_ColorGRAY : SK_ColorTRANSPARENT));
}
if (by_selection) {
SetVisible(false);
MenuItemView* menu_view = static_cast<MenuItemView*>(highlight_item);
on_select_.Run(menu_view->item());
}
contents_->SchedulePaint();
}
LoginButton* LoginMenuView::GetBubbleOpener() const {
return opener_;
}
void LoginMenuView::OnFocus() {
// Forward the focus to the selected child view.
contents_->children()[selected_index_]->RequestFocus();
}
bool LoginMenuView::OnKeyPressed(const ui::KeyEvent& event) {
const ui::KeyboardCode key = event.key_code();
if (key == ui::VKEY_UP || key == ui::VKEY_DOWN) {
FindNextItem(key == ui::VKEY_UP)->RequestFocus();
return true;
}
return false;
}
void LoginMenuView::VisibilityChanged(View* starting_from, bool is_visible) {
if (is_visible)
contents_->children()[selected_index_]->RequestFocus();
}
views::View* LoginMenuView::FindNextItem(bool reverse) {
const auto& children = contents_->children();
const auto is_item = [](views::View* v) {
return !static_cast<MenuItemView*>(v)->item().is_group;
};
const auto begin = std::next(children.begin(), selected_index_);
if (reverse) {
// Subtle: make_reverse_iterator() will result in an iterator that refers to
// the element before its argument, which is what we want.
const auto i = std::find_if(std::make_reverse_iterator(begin),
children.rend(), is_item);
return (i == children.rend()) ? *begin : *i;
}
const auto i = std::find_if(std::next(begin), children.end(), is_item);
return (i == children.end()) ? *begin : *i;
}
} // namespace ash