1 // Copyright 2016 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/system/ime_menu/ime_list_view.h"
6 
7 #include "ash/ime/ime_controller_impl.h"
8 #include "ash/ime/ime_switch_type.h"
9 #include "ash/keyboard/keyboard_controller_impl.h"
10 #include "ash/keyboard/ui/keyboard_util.h"
11 #include "ash/keyboard/virtual_keyboard_controller.h"
12 #include "ash/public/cpp/ime_info.h"
13 #include "ash/resources/vector_icons/vector_icons.h"
14 #include "ash/shell.h"
15 #include "ash/strings/grit/ash_strings.h"
16 #include "ash/style/ash_color_provider.h"
17 #include "ash/system/tray/actionable_view.h"
18 #include "ash/system/tray/system_menu_button.h"
19 #include "ash/system/tray/tray_detailed_view.h"
20 #include "ash/system/tray/tray_popup_utils.h"
21 #include "ash/system/tray/tri_view.h"
22 #include "base/metrics/histogram_macros.h"
23 #include "base/metrics/user_metrics.h"
24 #include "ui/accessibility/ax_enums.mojom.h"
25 #include "ui/accessibility/ax_node_data.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "ui/gfx/color_palette.h"
29 #include "ui/gfx/paint_vector_icon.h"
30 #include "ui/views/background.h"
31 #include "ui/views/controls/button/toggle_button.h"
32 #include "ui/views/controls/image_view.h"
33 #include "ui/views/controls/label.h"
34 #include "ui/views/controls/separator.h"
35 #include "ui/views/layout/fill_layout.h"
36 #include "ui/views/painter.h"
37 #include "ui/views/view.h"
38 #include "ui/views/widget/widget.h"
39 
40 namespace ash {
41 namespace {
42 
43 const int kMinFontSizeDelta = -10;
44 
45 // Represents a row in the scrollable IME list; each row is either an IME or
46 // an IME property. A checkmark icon is shown in the row if selected.
47 class ImeListItemView : public ActionableView {
48  public:
ImeListItemView(ImeListView * list_view,const base::string16 & id,const base::string16 & label,bool selected,const SkColor button_color)49   ImeListItemView(ImeListView* list_view,
50                   const base::string16& id,
51                   const base::string16& label,
52                   bool selected,
53                   const SkColor button_color)
54       : ActionableView(TrayPopupInkDropStyle::FILL_BOUNDS),
55         ime_list_view_(list_view),
56         selected_(selected) {
57     SetInkDropMode(InkDropMode::ON);
58 
59     TriView* tri_view = TrayPopupUtils::CreateDefaultRowView();
60     AddChildView(tri_view);
61     SetLayoutManager(std::make_unique<views::FillLayout>());
62 
63     // |id_label| contains the IME short name (e.g., 'US', 'GB', 'IT').
64     views::Label* id_label = TrayPopupUtils::CreateDefaultLabel();
65     id_label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
66         AshColorProvider::ContentLayerType::kTextColorPrimary));
67     id_label->SetAutoColorReadabilityEnabled(false);
68     id_label->SetText(id);
69     ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
70     const gfx::FontList& base_font_list =
71         rb.GetFontList(ui::ResourceBundle::MediumBoldFont);
72     id_label->SetFontList(base_font_list);
73 
74     // IMEs having a short name of more than two characters (e.g., 'INTL') will
75     // elide if rendered within |kMenuIconSize|. Shrink the font size until the
76     // entire short name fits within the bounds.
77     int size_delta = -1;
78     while ((id_label->GetPreferredSize().width() -
79             id_label->GetInsets().width()) > kMenuIconSize &&
80            size_delta >= kMinFontSizeDelta) {
81       id_label->SetFontList(base_font_list.DeriveWithSizeDelta(size_delta));
82       --size_delta;
83     }
84     tri_view->AddView(TriView::Container::START, id_label);
85 
86     // The label shows the IME full name.
87     auto* label_view = TrayPopupUtils::CreateDefaultLabel();
88     label_view->SetText(label);
89     label_view->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
90         AshColorProvider::ContentLayerType::kTextColorPrimary));
91     TrayPopupUtils::SetLabelFontList(
92         label_view, TrayPopupUtils::FontStyle::kDetailedViewLabel);
93     label_view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
94     tri_view->AddView(TriView::Container::CENTER, label_view);
95 
96     if (selected) {
97       // The checked button indicates the IME is selected.
98       views::ImageView* checked_image = TrayPopupUtils::CreateMainImageView();
99       checked_image->SetImage(gfx::CreateVectorIcon(
100           kHollowCheckCircleIcon, kMenuIconSize, button_color));
101       tri_view->AddView(TriView::Container::END, checked_image);
102     }
103     SetAccessibleName(label_view->GetText());
104   }
105 
106   ~ImeListItemView() override = default;
107 
108   // ActionableView:
PerformAction(const ui::Event & event)109   bool PerformAction(const ui::Event& event) override {
110     ime_list_view_->set_last_item_selected_with_keyboard(
111         ime_list_view_->should_focus_ime_after_selection_with_keyboard() &&
112         event.type() == ui::EventType::ET_KEY_PRESSED);
113     ime_list_view_->HandleViewClicked(this);
114     return true;
115   }
116 
OnFocus()117   void OnFocus() override {
118     ActionableView::OnFocus();
119     if (ime_list_view_)
120       ime_list_view_->ScrollItemToVisible(this);
121   }
122 
GetAccessibleNodeData(ui::AXNodeData * node_data)123   void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
124     ActionableView::GetAccessibleNodeData(node_data);
125     node_data->role = ax::mojom::Role::kCheckBox;
126     node_data->SetCheckedState(selected_ ? ax::mojom::CheckedState::kTrue
127                                          : ax::mojom::CheckedState::kFalse);
128   }
129 
130  private:
131   ImeListView* ime_list_view_;
132   bool selected_;
133 
134   DISALLOW_COPY_AND_ASSIGN(ImeListItemView);
135 };
136 
137 }  // namespace
138 
139 // Contains a toggle button to let the user enable/disable whether the
140 // on-screen keyboard should be shown when focusing a textfield. This row is
141 // shown only under certain conditions, e.g., when an external keyboard is
142 // attached and the user is in TabletMode mode.
143 class KeyboardStatusRow : public views::View {
144  public:
145   KeyboardStatusRow() = default;
146   ~KeyboardStatusRow() override = default;
147 
toggle() const148   views::ToggleButton* toggle() const { return toggle_; }
149 
Init(views::Button::PressedCallback callback)150   void Init(views::Button::PressedCallback callback) {
151     TrayPopupUtils::ConfigureAsStickyHeader(this);
152     SetLayoutManager(std::make_unique<views::FillLayout>());
153 
154     TriView* tri_view = TrayPopupUtils::CreateDefaultRowView();
155     AddChildView(tri_view);
156 
157     auto* color_provider = AshColorProvider::Get();
158     // The on-screen keyboard image button.
159     views::ImageView* keyboard_image = TrayPopupUtils::CreateMainImageView();
160     keyboard_image->SetImage(gfx::CreateVectorIcon(
161         kImeMenuOnScreenKeyboardIcon, kMenuIconSize,
162         color_provider->GetContentLayerColor(
163             AshColorProvider::ContentLayerType::kIconColorPrimary)));
164     tri_view->AddView(TriView::Container::START, keyboard_image);
165 
166     // The on-screen keyboard label ('On-screen keyboard').
167     auto* label = TrayPopupUtils::CreateDefaultLabel();
168     label->SetText(ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
169         IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD));
170     label->SetEnabledColor(color_provider->GetContentLayerColor(
171         AshColorProvider::ContentLayerType::kTextColorPrimary));
172     TrayPopupUtils::SetLabelFontList(
173         label, TrayPopupUtils::FontStyle::kDetailedViewLabel);
174     tri_view->AddView(TriView::Container::CENTER, label);
175 
176     // The on-screen keyboard toggle button.
177     toggle_ = TrayPopupUtils::CreateToggleButton(
178         std::move(callback),
179         IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD);
180     toggle_->SetIsOn(keyboard::IsKeyboardEnabled());
181     tri_view->AddView(TriView::Container::END, toggle_);
182   }
183 
184   // views::View:
GetClassName() const185   const char* GetClassName() const override { return "KeyboardStatusRow"; }
186 
187  private:
188   // ToggleButton to toggle keyboard on or off.
189   views::ToggleButton* toggle_ = nullptr;
190 
191   DISALLOW_COPY_AND_ASSIGN(KeyboardStatusRow);
192 };
193 
ImeListView(DetailedViewDelegate * delegate)194 ImeListView::ImeListView(DetailedViewDelegate* delegate)
195     : TrayDetailedView(delegate) {}
196 
197 ImeListView::~ImeListView() = default;
198 
Init(bool show_keyboard_toggle,SingleImeBehavior single_ime_behavior)199 void ImeListView::Init(bool show_keyboard_toggle,
200                        SingleImeBehavior single_ime_behavior) {
201   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
202   Update(ime_controller->current_ime().id, ime_controller->available_imes(),
203          ime_controller->current_ime_menu_items(), show_keyboard_toggle,
204          single_ime_behavior);
205 }
206 
Update(const std::string & current_ime_id,const std::vector<ImeInfo> & list,const std::vector<ImeMenuItem> & property_items,bool show_keyboard_toggle,SingleImeBehavior single_ime_behavior)207 void ImeListView::Update(const std::string& current_ime_id,
208                          const std::vector<ImeInfo>& list,
209                          const std::vector<ImeMenuItem>& property_items,
210                          bool show_keyboard_toggle,
211                          SingleImeBehavior single_ime_behavior) {
212   ResetImeListView();
213   ime_map_.clear();
214   property_map_.clear();
215   CreateScrollableList();
216 
217   if (single_ime_behavior == ImeListView::SHOW_SINGLE_IME || list.size() > 1)
218     AppendImeListAndProperties(current_ime_id, list, property_items);
219 
220   if (show_keyboard_toggle)
221     PrependKeyboardStatusRow();
222 
223   Layout();
224   SchedulePaint();
225 
226   if (should_focus_ime_after_selection_with_keyboard_ &&
227       last_item_selected_with_keyboard_) {
228     FocusCurrentImeIfNeeded();
229   } else if (current_ime_view_) {
230     ScrollItemToVisible(current_ime_view_);
231   }
232 }
233 
ResetImeListView()234 void ImeListView::ResetImeListView() {
235   // Children are removed from the view hierarchy and deleted in Reset().
236   Reset();
237   keyboard_status_row_ = nullptr;
238   current_ime_view_ = nullptr;
239 }
240 
ScrollItemToVisible(views::View * item_view)241 void ImeListView::ScrollItemToVisible(views::View* item_view) {
242   if (scroll_content())
243     scroll_content()->ScrollRectToVisible(item_view->bounds());
244 }
245 
CloseImeListView()246 void ImeListView::CloseImeListView() {
247   last_selected_item_id_.clear();
248   current_ime_view_ = nullptr;
249   last_item_selected_with_keyboard_ = false;
250   GetWidget()->Close();
251 }
252 
AppendImeListAndProperties(const std::string & current_ime_id,const std::vector<ImeInfo> & list,const std::vector<ImeMenuItem> & property_list)253 void ImeListView::AppendImeListAndProperties(
254     const std::string& current_ime_id,
255     const std::vector<ImeInfo>& list,
256     const std::vector<ImeMenuItem>& property_list) {
257   DCHECK(ime_map_.empty());
258   for (size_t i = 0; i < list.size(); i++) {
259     const bool selected = current_ime_id == list[i].id;
260     views::View* ime_view = new ImeListItemView(
261         this, list[i].short_name, list[i].name, selected,
262         AshColorProvider::Get()->GetContentLayerColor(
263             AshColorProvider::ContentLayerType::kIconColorProminent));
264     scroll_content()->AddChildView(ime_view);
265     ime_map_[ime_view] = list[i].id;
266 
267     if (selected)
268       current_ime_view_ = ime_view;
269 
270     // Add the properties, if any, of the currently-selected IME.
271     if (selected && !property_list.empty()) {
272       // Adds a separator on the top of property items.
273       scroll_content()->AddChildView(
274           TrayPopupUtils::CreateListItemSeparator(true));
275 
276       const SkColor icon_color = AshColorProvider::Get()->GetContentLayerColor(
277           AshColorProvider::ContentLayerType::kIconColorPrimary);
278       // Adds the property items.
279       for (size_t i = 0; i < property_list.size(); i++) {
280         ImeListItemView* property_view =
281             new ImeListItemView(this, base::string16(), property_list[i].label,
282                                 property_list[i].checked, icon_color);
283         scroll_content()->AddChildView(property_view);
284         property_map_[property_view] = property_list[i].key;
285       }
286 
287       // Adds a separator on the bottom of property items if there are still
288       // other IMEs under the current one.
289       if (i < list.size() - 1)
290         scroll_content()->AddChildView(
291             TrayPopupUtils::CreateListItemSeparator(true));
292     }
293   }
294 }
295 
PrependKeyboardStatusRow()296 void ImeListView::PrependKeyboardStatusRow() {
297   DCHECK(!keyboard_status_row_);
298   keyboard_status_row_ = new KeyboardStatusRow;
299   keyboard_status_row_->Init(base::BindRepeating(
300       &ImeListView::KeyboardStatusTogglePressed, base::Unretained(this)));
301   scroll_content()->AddChildViewAt(keyboard_status_row_, 0);
302 }
303 
KeyboardStatusTogglePressed()304 void ImeListView::KeyboardStatusTogglePressed() {
305   Shell::Get()
306       ->keyboard_controller()
307       ->virtual_keyboard_controller()
308       ->ToggleIgnoreExternalKeyboard();
309   last_selected_item_id_.clear();
310   last_item_selected_with_keyboard_ = false;
311 }
312 
HandleViewClicked(views::View * view)313 void ImeListView::HandleViewClicked(views::View* view) {
314   ImeControllerImpl* ime_controller = Shell::Get()->ime_controller();
315   std::map<views::View*, std::string>::const_iterator ime = ime_map_.find(view);
316   if (ime != ime_map_.end()) {
317     base::RecordAction(base::UserMetricsAction("StatusArea_IME_SwitchMode"));
318     std::string ime_id = ime->second;
319     last_selected_item_id_ = ime_id;
320     ime_controller->SwitchImeById(ime_id, false /* show_message */);
321     UMA_HISTOGRAM_ENUMERATION("InputMethod.ImeSwitch", ImeSwitchType::kTray);
322 
323   } else {
324     std::map<views::View*, std::string>::const_iterator property =
325         property_map_.find(view);
326     if (property == property_map_.end())
327       return;
328     const std::string key = property->second;
329     last_selected_item_id_ = key;
330     ime_controller->ActivateImeMenuItem(key);
331   }
332 
333   if (!should_focus_ime_after_selection_with_keyboard_ ||
334       !last_item_selected_with_keyboard_) {
335     CloseImeListView();
336   }
337 }
338 
VisibilityChanged(View * starting_from,bool is_visible)339 void ImeListView::VisibilityChanged(View* starting_from, bool is_visible) {
340   if (!is_visible ||
341       (should_focus_ime_after_selection_with_keyboard_ &&
342        last_item_selected_with_keyboard_) ||
343       !current_ime_view_) {
344     return;
345   }
346 
347   ScrollItemToVisible(current_ime_view_);
348 }
349 
GetClassName() const350 const char* ImeListView::GetClassName() const {
351   return "ImeListView";
352 }
353 
FocusCurrentImeIfNeeded()354 void ImeListView::FocusCurrentImeIfNeeded() {
355   views::FocusManager* manager = GetFocusManager();
356   if (!manager || manager->GetFocusedView() || last_selected_item_id_.empty())
357     return;
358 
359   for (auto ime_map : ime_map_) {
360     if (ime_map.second == last_selected_item_id_) {
361       (ime_map.first)->RequestFocus();
362       return;
363     }
364   }
365 
366   for (auto property_map : property_map_) {
367     if (property_map.second == last_selected_item_id_) {
368       (property_map.first)->RequestFocus();
369       return;
370     }
371   }
372 }
373 
ImeListViewTestApi(ImeListView * ime_list_view)374 ImeListViewTestApi::ImeListViewTestApi(ImeListView* ime_list_view)
375     : ime_list_view_(ime_list_view) {}
376 
377 ImeListViewTestApi::~ImeListViewTestApi() = default;
378 
GetToggleView() const379 views::View* ImeListViewTestApi::GetToggleView() const {
380   return ime_list_view_->keyboard_status_row_->toggle();
381 }
382 
383 }  // namespace ash
384