1 // Copyright 2018 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/login/ui/login_menu_view.h"
6
7 #include <algorithm>
8 #include <iterator>
9 #include <memory>
10 #include <utility>
11
12 #include "ash/login/ui/hover_notifier.h"
13 #include "ash/login/ui/non_accessible_view.h"
14 #include "ash/style/ash_color_provider.h"
15 #include "base/bind.h"
16 #include "base/strings/utf_string_conversions.h"
17 #include "ui/views/controls/button/button.h"
18 #include "ui/views/controls/label.h"
19 #include "ui/views/controls/scroll_view.h"
20 #include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
21 #include "ui/views/layout/box_layout.h"
22
23 namespace ash {
24
25 constexpr int kMenuItemWidthDp = 192;
26 constexpr int kMenuItemHeightDp = 28;
27 constexpr int kRegularMenuItemLeftPaddingDp = 2;
28 constexpr int kGroupMenuItemLeftPaddingDp = 10;
29 constexpr int kNonEmptyHeight = 1;
30
31 namespace {
32
33 constexpr SkColor kMenuBackgroundColor = SkColorSetRGB(0x3C, 0x40, 0x43);
34
35 class MenuItemView : public views::Button {
36 public:
MenuItemView(const LoginMenuView::Item & item,const LoginMenuView::OnHighlight & on_highlight)37 MenuItemView(const LoginMenuView::Item& item,
38 const LoginMenuView::OnHighlight& on_highlight)
39 : views::Button(base::BindRepeating(&MenuItemView::ButtonPressed,
40 base::Unretained(this))),
41 item_(item),
42 on_highlight_(on_highlight) {
43 SetFocusBehavior(FocusBehavior::ALWAYS);
44 SetLayoutManager(std::make_unique<views::BoxLayout>(
45 views::BoxLayout::Orientation::kHorizontal));
46 SetPreferredSize(gfx::Size(kMenuItemWidthDp, kMenuItemHeightDp));
47
48 auto spacing = std::make_unique<NonAccessibleView>();
49 spacing->SetPreferredSize(gfx::Size(item.is_group
50 ? kRegularMenuItemLeftPaddingDp
51 : kGroupMenuItemLeftPaddingDp,
52 kNonEmptyHeight));
53 AddChildView(std::move(spacing));
54
55 auto label = std::make_unique<views::Label>(base::UTF8ToUTF16(item.title));
56 label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
57 AshColorProvider::ContentLayerType::kTextColorPrimary));
58 label->SetSubpixelRenderingEnabled(false);
59 label->SetAutoColorReadabilityEnabled(false);
60 label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
61 AddChildView(std::move(label));
62
63 if (item.selected)
64 SetBackground(views::CreateSolidBackground(SK_ColorGRAY));
65
66 hover_notifier_ = std::make_unique<HoverNotifier>(
67 this,
68 base::BindRepeating(&MenuItemView::OnHover, base::Unretained(this)));
69 }
70
71 ~MenuItemView() override = default;
72
73 // views::View:
GetHeightForWidth(int w) const74 int GetHeightForWidth(int w) const override {
75 // Make row height fixed avoiding layout manager adjustments.
76 return GetPreferredSize().height();
77 }
78
OnHover(bool has_hover)79 void OnHover(bool has_hover) {
80 if (has_hover && !item_.is_group)
81 RequestFocus();
82 }
83
OnFocus()84 void OnFocus() override {
85 ScrollViewToVisible();
86 on_highlight_.Run(false /*by_selection*/);
87 }
88
item() const89 const LoginMenuView::Item& item() const { return item_; }
90
91 private:
ButtonPressed()92 void ButtonPressed() {
93 if (!item_.is_group)
94 on_highlight_.Run(true /*by_selection*/);
95 }
96
97 const LoginMenuView::Item item_;
98 const LoginMenuView::OnHighlight on_highlight_;
99 std::unique_ptr<HoverNotifier> hover_notifier_;
100
101 DISALLOW_COPY_AND_ASSIGN(MenuItemView);
102 };
103
104 class LoginScrollBar : public views::OverlayScrollBar {
105 public:
LoginScrollBar()106 LoginScrollBar() : OverlayScrollBar(false) {}
107
108 // OverlayScrollBar:
OnKeyPressed(const ui::KeyEvent & event)109 bool OnKeyPressed(const ui::KeyEvent& event) override {
110 // Let LoginMenuView to handle up/down keypress.
111 return false;
112 }
113
114 private:
115 DISALLOW_COPY_AND_ASSIGN(LoginScrollBar);
116 };
117
118 } // namespace
119
TestApi(LoginMenuView * view)120 LoginMenuView::TestApi::TestApi(LoginMenuView* view) : view_(view) {}
121
122 LoginMenuView::TestApi::~TestApi() = default;
123
contents() const124 views::View* LoginMenuView::TestApi::contents() const {
125 return view_->contents_;
126 }
127
128 LoginMenuView::Item::Item() = default;
129
LoginMenuView(const std::vector<Item> & items,views::View * anchor_view,LoginButton * opener,const OnSelect & on_select)130 LoginMenuView::LoginMenuView(const std::vector<Item>& items,
131 views::View* anchor_view,
132 LoginButton* opener,
133 const OnSelect& on_select)
134 : LoginBaseBubbleView(anchor_view), opener_(opener), on_select_(on_select) {
135 SetBackground(views::CreateSolidBackground(kMenuBackgroundColor));
136 SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
137
138 scroller_ = new views::ScrollView();
139 scroller_->SetBackgroundColor(base::nullopt);
140 scroller_->SetDrawOverflowIndicator(false);
141 scroller_->ClipHeightTo(kMenuItemHeightDp, kMenuItemHeightDp * 5);
142 AddChildView(scroller_);
143
144 views::BoxLayout* box_layout =
145 SetLayoutManager(std::make_unique<views::BoxLayout>(
146 views::BoxLayout::Orientation::kVertical));
147 box_layout->SetFlexForView(scroller_, 1);
148
149 auto contents = std::make_unique<NonAccessibleView>();
150 views::BoxLayout* layout =
151 contents->SetLayoutManager(std::make_unique<views::BoxLayout>(
152 views::BoxLayout::Orientation::kVertical));
153 layout->SetDefaultFlex(1);
154 layout->set_minimum_cross_axis_size(kMenuItemWidthDp);
155 layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
156
157 for (size_t i = 0; i < items.size(); i++) {
158 const Item& item = items[i];
159 contents->AddChildView(new MenuItemView(
160 item, base::BindRepeating(&LoginMenuView::OnHighlightChange,
161 base::Unretained(this), i)));
162
163 if (item.selected)
164 selected_index_ = i;
165 }
166 contents_ = scroller_->SetContents(std::move(contents));
167 scroller_->SetVerticalScrollBar(std::make_unique<LoginScrollBar>());
168 }
169
170 LoginMenuView::~LoginMenuView() = default;
171
OnHighlightChange(size_t item_index,bool by_selection)172 void LoginMenuView::OnHighlightChange(size_t item_index, bool by_selection) {
173 selected_index_ = item_index;
174 views::View* highlight_item = contents_->children()[item_index];
175 for (views::View* child : contents_->GetChildrenInZOrder()) {
176 child->SetBackground(views::CreateSolidBackground(
177 child == highlight_item ? SK_ColorGRAY : SK_ColorTRANSPARENT));
178 }
179
180 if (by_selection) {
181 SetVisible(false);
182 MenuItemView* menu_view = static_cast<MenuItemView*>(highlight_item);
183 on_select_.Run(menu_view->item());
184 }
185 contents_->SchedulePaint();
186 }
187
GetBubbleOpener() const188 LoginButton* LoginMenuView::GetBubbleOpener() const {
189 return opener_;
190 }
191
OnFocus()192 void LoginMenuView::OnFocus() {
193 // Forward the focus to the selected child view.
194 contents_->children()[selected_index_]->RequestFocus();
195 }
196
OnKeyPressed(const ui::KeyEvent & event)197 bool LoginMenuView::OnKeyPressed(const ui::KeyEvent& event) {
198 const ui::KeyboardCode key = event.key_code();
199 if (key == ui::VKEY_UP || key == ui::VKEY_DOWN) {
200 FindNextItem(key == ui::VKEY_UP)->RequestFocus();
201 return true;
202 }
203
204 return false;
205 }
206
VisibilityChanged(View * starting_from,bool is_visible)207 void LoginMenuView::VisibilityChanged(View* starting_from, bool is_visible) {
208 if (is_visible)
209 contents_->children()[selected_index_]->RequestFocus();
210 }
211
FindNextItem(bool reverse)212 views::View* LoginMenuView::FindNextItem(bool reverse) {
213 const auto& children = contents_->children();
214 const auto is_item = [](views::View* v) {
215 return !static_cast<MenuItemView*>(v)->item().is_group;
216 };
217 const auto begin = std::next(children.begin(), selected_index_);
218 if (reverse) {
219 // Subtle: make_reverse_iterator() will result in an iterator that refers to
220 // the element before its argument, which is what we want.
221 const auto i = std::find_if(std::make_reverse_iterator(begin),
222 children.rend(), is_item);
223 return (i == children.rend()) ? *begin : *i;
224 }
225 const auto i = std::find_if(std::next(begin), children.end(), is_item);
226 return (i == children.end()) ? *begin : *i;
227 }
228
229 } // namespace ash
230