| // Copyright 2016 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/system/ime_menu/ime_list_view.h" |
| |
| #include "ash/ime/ime_controller.h" |
| #include "ash/ime/ime_switch_type.h" |
| #include "ash/keyboard/ash_keyboard_controller.h" |
| #include "ash/keyboard/ui/keyboard_util.h" |
| #include "ash/keyboard/virtual_keyboard_controller.h" |
| #include "ash/public/interfaces/ime_info.mojom.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/tray/actionable_view.h" |
| #include "ash/system/tray/system_menu_button.h" |
| #include "ash/system/tray/tray_constants.h" |
| #include "ash/system/tray/tray_detailed_view.h" |
| #include "ash/system/tray/tray_popup_item_style.h" |
| #include "ash/system/tray/tray_popup_utils.h" |
| #include "ash/system/tray/tri_view.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/toggle_button.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/painter.h" |
| #include "ui/views/view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| namespace { |
| |
| const int kMinFontSizeDelta = -10; |
| |
| // Represents a row in the scrollable IME list; each row is either an IME or |
| // an IME property. A checkmark icon is shown in the row if selected. |
| class ImeListItemView : public ActionableView { |
| public: |
| ImeListItemView(ImeListView* list_view, |
| const base::string16& id, |
| const base::string16& label, |
| bool selected, |
| const SkColor button_color, |
| bool use_unified_theme) |
| : ActionableView(TrayPopupInkDropStyle::FILL_BOUNDS), |
| ime_list_view_(list_view), |
| selected_(selected) { |
| SetInkDropMode(InkDropMode::ON); |
| |
| TriView* tri_view = TrayPopupUtils::CreateDefaultRowView(); |
| AddChildView(tri_view); |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| |
| // |id_label| contains the IME short name (e.g., 'US', 'GB', 'IT'). |
| views::Label* id_label = TrayPopupUtils::CreateDefaultLabel(); |
| if (use_unified_theme) { |
| id_label->SetEnabledColor(kUnifiedMenuTextColor); |
| id_label->SetAutoColorReadabilityEnabled(false); |
| } |
| id_label->SetText(id); |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| const gfx::FontList& base_font_list = |
| rb.GetFontList(ui::ResourceBundle::MediumBoldFont); |
| id_label->SetFontList(base_font_list); |
| |
| // IMEs having a short name of more than two characters (e.g., 'INTL') will |
| // elide if rendered within |kMenuIconSize|. Shrink the font size until the |
| // entire short name fits within the bounds. |
| int size_delta = -1; |
| while ((id_label->GetPreferredSize().width() - |
| id_label->GetInsets().width()) > kMenuIconSize && |
| size_delta >= kMinFontSizeDelta) { |
| id_label->SetFontList(base_font_list.DeriveWithSizeDelta(size_delta)); |
| --size_delta; |
| } |
| tri_view->AddView(TriView::Container::START, id_label); |
| |
| // The label shows the IME full name. |
| auto* label_view = TrayPopupUtils::CreateDefaultLabel(); |
| label_view->SetText(label); |
| TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL, |
| use_unified_theme); |
| style.SetupLabel(label_view); |
| |
| label_view->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| tri_view->AddView(TriView::Container::CENTER, label_view); |
| |
| if (selected) { |
| // The checked button indicates the IME is selected. |
| views::ImageView* checked_image = TrayPopupUtils::CreateMainImageView(); |
| checked_image->SetImage( |
| gfx::CreateVectorIcon(kCheckCircleIcon, kMenuIconSize, button_color)); |
| tri_view->AddView(TriView::Container::END, checked_image); |
| } |
| SetAccessibleName(label_view->text()); |
| } |
| |
| ~ImeListItemView() override = default; |
| |
| // ActionableView: |
| bool PerformAction(const ui::Event& event) override { |
| ime_list_view_->set_last_item_selected_with_keyboard( |
| ime_list_view_->should_focus_ime_after_selection_with_keyboard() && |
| event.type() == ui::EventType::ET_KEY_PRESSED); |
| ime_list_view_->HandleViewClicked(this); |
| return true; |
| } |
| |
| void OnFocus() override { |
| ActionableView::OnFocus(); |
| if (ime_list_view_) |
| ime_list_view_->ScrollItemToVisible(this); |
| } |
| |
| void GetAccessibleNodeData(ui::AXNodeData* node_data) override { |
| ActionableView::GetAccessibleNodeData(node_data); |
| node_data->role = ax::mojom::Role::kCheckBox; |
| node_data->SetCheckedState(selected_ ? ax::mojom::CheckedState::kTrue |
| : ax::mojom::CheckedState::kFalse); |
| } |
| |
| private: |
| ImeListView* ime_list_view_; |
| bool selected_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ImeListItemView); |
| }; |
| |
| } // namespace |
| |
| // Contains a toggle button to let the user enable/disable whether the |
| // on-screen keyboard should be shown when focusing a textfield. This row is |
| // shown only under certain conditions, e.g., when an external keyboard is |
| // attached and the user is in TabletMode mode. |
| class KeyboardStatusRow : public views::View { |
| public: |
| KeyboardStatusRow() = default; |
| ~KeyboardStatusRow() override = default; |
| |
| views::ToggleButton* toggle() const { return toggle_; } |
| |
| void Init(views::ButtonListener* listener, bool use_unified_theme) { |
| TrayPopupUtils::ConfigureAsStickyHeader(this); |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| |
| TriView* tri_view = TrayPopupUtils::CreateDefaultRowView(); |
| AddChildView(tri_view); |
| |
| // The on-screen keyboard image button. |
| views::ImageView* keyboard_image = TrayPopupUtils::CreateMainImageView(); |
| keyboard_image->SetImage(gfx::CreateVectorIcon( |
| kImeMenuOnScreenKeyboardIcon, kMenuIconSize, kMenuIconColor)); |
| tri_view->AddView(TriView::Container::START, keyboard_image); |
| |
| // The on-screen keyboard label ('On-screen keyboard'). |
| auto* label = TrayPopupUtils::CreateDefaultLabel(); |
| label->SetText(ui::ResourceBundle::GetSharedInstance().GetLocalizedString( |
| IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD)); |
| TrayPopupItemStyle style(TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL, |
| use_unified_theme); |
| style.SetupLabel(label); |
| tri_view->AddView(TriView::Container::CENTER, label); |
| |
| // The on-screen keyboard toggle button. |
| toggle_ = TrayPopupUtils::CreateToggleButton( |
| listener, IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD); |
| toggle_->SetIsOn(keyboard::IsKeyboardEnabled()); |
| tri_view->AddView(TriView::Container::END, toggle_); |
| } |
| |
| private: |
| // ToggleButton to toggle keyboard on or off. |
| views::ToggleButton* toggle_ = nullptr; |
| |
| DISALLOW_COPY_AND_ASSIGN(KeyboardStatusRow); |
| }; |
| |
| ImeListView::ImeListView(DetailedViewDelegate* delegate) |
| : ImeListView(delegate, true) {} |
| |
| ImeListView::ImeListView(DetailedViewDelegate* delegate, bool use_unified_theme) |
| : TrayDetailedView(delegate), |
| last_item_selected_with_keyboard_(false), |
| should_focus_ime_after_selection_with_keyboard_(false), |
| current_ime_view_(nullptr), |
| use_unified_theme_(use_unified_theme) {} |
| |
| ImeListView::~ImeListView() = default; |
| |
| void ImeListView::Init(bool show_keyboard_toggle, |
| SingleImeBehavior single_ime_behavior) { |
| ImeController* ime_controller = Shell::Get()->ime_controller(); |
| Update(ime_controller->current_ime().id, ime_controller->available_imes(), |
| ime_controller->current_ime_menu_items(), show_keyboard_toggle, |
| single_ime_behavior); |
| } |
| |
| void ImeListView::Update(const std::string& current_ime_id, |
| const std::vector<mojom::ImeInfo>& list, |
| const std::vector<mojom::ImeMenuItem>& property_items, |
| bool show_keyboard_toggle, |
| SingleImeBehavior single_ime_behavior) { |
| ResetImeListView(); |
| ime_map_.clear(); |
| property_map_.clear(); |
| CreateScrollableList(); |
| |
| if (single_ime_behavior == ImeListView::SHOW_SINGLE_IME || list.size() > 1) |
| AppendImeListAndProperties(current_ime_id, list, property_items); |
| |
| if (show_keyboard_toggle) |
| PrependKeyboardStatusRow(); |
| |
| Layout(); |
| SchedulePaint(); |
| |
| if (should_focus_ime_after_selection_with_keyboard_ && |
| last_item_selected_with_keyboard_) { |
| FocusCurrentImeIfNeeded(); |
| } else if (current_ime_view_) { |
| ScrollItemToVisible(current_ime_view_); |
| } |
| } |
| |
| void ImeListView::ResetImeListView() { |
| // Children are removed from the view hierarchy and deleted in Reset(). |
| Reset(); |
| keyboard_status_row_ = nullptr; |
| current_ime_view_ = nullptr; |
| } |
| |
| void ImeListView::ScrollItemToVisible(views::View* item_view) { |
| if (scroll_content()) |
| scroll_content()->ScrollRectToVisible(item_view->bounds()); |
| } |
| |
| void ImeListView::CloseImeListView() { |
| last_selected_item_id_.clear(); |
| current_ime_view_ = nullptr; |
| last_item_selected_with_keyboard_ = false; |
| GetWidget()->Close(); |
| } |
| |
| void ImeListView::AppendImeListAndProperties( |
| const std::string& current_ime_id, |
| const std::vector<mojom::ImeInfo>& list, |
| const std::vector<mojom::ImeMenuItem>& property_list) { |
| DCHECK(ime_map_.empty()); |
| for (size_t i = 0; i < list.size(); i++) { |
| const bool selected = current_ime_id == list[i].id; |
| views::View* ime_view = |
| new ImeListItemView(this, list[i].short_name, list[i].name, selected, |
| gfx::kGoogleGreen700, use_unified_theme_); |
| scroll_content()->AddChildView(ime_view); |
| ime_map_[ime_view] = list[i].id; |
| |
| if (selected) |
| current_ime_view_ = ime_view; |
| |
| // Add the properties, if any, of the currently-selected IME. |
| if (selected && !property_list.empty()) { |
| // Adds a separator on the top of property items. |
| scroll_content()->AddChildView( |
| TrayPopupUtils::CreateListItemSeparator(true)); |
| |
| // Adds the property items. |
| for (size_t i = 0; i < property_list.size(); i++) { |
| ImeListItemView* property_view = new ImeListItemView( |
| this, base::string16(), property_list[i].label, |
| property_list[i].checked, kMenuIconColor, use_unified_theme_); |
| scroll_content()->AddChildView(property_view); |
| property_map_[property_view] = property_list[i].key; |
| } |
| |
| // Adds a separator on the bottom of property items if there are still |
| // other IMEs under the current one. |
| if (i < list.size() - 1) |
| scroll_content()->AddChildView( |
| TrayPopupUtils::CreateListItemSeparator(true)); |
| } |
| } |
| } |
| |
| void ImeListView::PrependKeyboardStatusRow() { |
| DCHECK(!keyboard_status_row_); |
| keyboard_status_row_ = new KeyboardStatusRow; |
| keyboard_status_row_->Init(this, use_unified_theme_); |
| scroll_content()->AddChildViewAt(keyboard_status_row_, 0); |
| } |
| |
| void ImeListView::HandleViewClicked(views::View* view) { |
| ImeController* ime_controller = Shell::Get()->ime_controller(); |
| std::map<views::View*, std::string>::const_iterator ime = ime_map_.find(view); |
| if (ime != ime_map_.end()) { |
| base::RecordAction(base::UserMetricsAction("StatusArea_IME_SwitchMode")); |
| std::string ime_id = ime->second; |
| last_selected_item_id_ = ime_id; |
| ime_controller->SwitchImeById(ime_id, false /* show_message */); |
| UMA_HISTOGRAM_ENUMERATION("InputMethod.ImeSwitch", ImeSwitchType::kTray, |
| ImeSwitchType::kCount); |
| |
| } else { |
| std::map<views::View*, std::string>::const_iterator property = |
| property_map_.find(view); |
| if (property == property_map_.end()) |
| return; |
| const std::string key = property->second; |
| last_selected_item_id_ = key; |
| ime_controller->ActivateImeMenuItem(key); |
| } |
| |
| if (!should_focus_ime_after_selection_with_keyboard_ || |
| !last_item_selected_with_keyboard_) { |
| CloseImeListView(); |
| } |
| } |
| |
| void ImeListView::HandleButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| DCHECK_EQ(sender, keyboard_status_row_->toggle()); |
| |
| Shell::Get() |
| ->ash_keyboard_controller() |
| ->virtual_keyboard_controller() |
| ->ToggleIgnoreExternalKeyboard(); |
| last_selected_item_id_.clear(); |
| last_item_selected_with_keyboard_ = false; |
| } |
| |
| void ImeListView::VisibilityChanged(View* starting_from, bool is_visible) { |
| if (!is_visible || (should_focus_ime_after_selection_with_keyboard_ && |
| last_item_selected_with_keyboard_) || |
| !current_ime_view_) { |
| return; |
| } |
| |
| ScrollItemToVisible(current_ime_view_); |
| } |
| |
| void ImeListView::FocusCurrentImeIfNeeded() { |
| views::FocusManager* manager = GetFocusManager(); |
| if (!manager || manager->GetFocusedView() || last_selected_item_id_.empty()) |
| return; |
| |
| for (auto ime_map : ime_map_) { |
| if (ime_map.second == last_selected_item_id_) { |
| (ime_map.first)->RequestFocus(); |
| return; |
| } |
| } |
| |
| for (auto property_map : property_map_) { |
| if (property_map.second == last_selected_item_id_) { |
| (property_map.first)->RequestFocus(); |
| return; |
| } |
| } |
| } |
| |
| ImeListViewTestApi::ImeListViewTestApi(ImeListView* ime_list_view) |
| : ime_list_view_(ime_list_view) {} |
| |
| ImeListViewTestApi::~ImeListViewTestApi() = default; |
| |
| views::View* ImeListViewTestApi::GetToggleView() const { |
| return ime_list_view_->keyboard_status_row_->toggle(); |
| } |
| |
| } // namespace ash |