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