| OLD | NEW |
| (Empty) |
| 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/common/system/chromeos/ime_menu/ime_list_view.h" | |
| 6 | |
| 7 #include "ash/common/material_design/material_design_controller.h" | |
| 8 #include "ash/common/system/tray/hover_highlight_view.h" | |
| 9 #include "ash/common/system/tray/ime_info.h" | |
| 10 #include "ash/common/system/tray/system_menu_button.h" | |
| 11 #include "ash/common/system/tray/system_tray_delegate.h" | |
| 12 #include "ash/common/system/tray/tray_constants.h" | |
| 13 #include "ash/common/system/tray/tray_details_view.h" | |
| 14 #include "ash/common/system/tray/tray_popup_header_button.h" | |
| 15 #include "ash/common/system/tray/tray_popup_item_style.h" | |
| 16 #include "ash/common/system/tray/tray_popup_utils.h" | |
| 17 #include "ash/common/system/tray/tri_view.h" | |
| 18 #include "ash/common/wm_shell.h" | |
| 19 #include "ash/resources/grit/ash_resources.h" | |
| 20 #include "ash/strings/grit/ash_strings.h" | |
| 21 #include "ui/accessibility/ax_node_data.h" | |
| 22 #include "ui/base/l10n/l10n_util.h" | |
| 23 #include "ui/base/resource/resource_bundle.h" | |
| 24 #include "ui/gfx/color_palette.h" | |
| 25 #include "ui/gfx/paint_vector_icon.h" | |
| 26 #include "ui/gfx/vector_icons_public.h" | |
| 27 #include "ui/keyboard/keyboard_util.h" | |
| 28 #include "ui/views/background.h" | |
| 29 #include "ui/views/border.h" | |
| 30 #include "ui/views/controls/button/toggle_button.h" | |
| 31 #include "ui/views/controls/image_view.h" | |
| 32 #include "ui/views/controls/label.h" | |
| 33 #include "ui/views/controls/separator.h" | |
| 34 #include "ui/views/layout/fill_layout.h" | |
| 35 #include "ui/views/painter.h" | |
| 36 #include "ui/views/view.h" | |
| 37 #include "ui/views/widget/widget.h" | |
| 38 | |
| 39 namespace ash { | |
| 40 namespace { | |
| 41 | |
| 42 const int kMinFontSizeDelta = -10; | |
| 43 | |
| 44 const SkColor kKeyboardRowSeparatorColor = SkColorSetA(SK_ColorBLACK, 0x1F); | |
| 45 | |
| 46 // A |HoverHighlightView| that uses bold or normal font depending on whether it | |
| 47 // is selected. This view exposes itself as a checkbox to the accessibility | |
| 48 // framework. | |
| 49 class SelectableHoverHighlightView : public HoverHighlightView { | |
| 50 public: | |
| 51 SelectableHoverHighlightView(ViewClickListener* listener, | |
| 52 const base::string16& label, | |
| 53 bool selected) | |
| 54 : HoverHighlightView(listener), selected_(selected) { | |
| 55 AddLabel(label, gfx::ALIGN_LEFT, selected); | |
| 56 } | |
| 57 | |
| 58 ~SelectableHoverHighlightView() override {} | |
| 59 | |
| 60 protected: | |
| 61 // views::View: | |
| 62 void GetAccessibleNodeData(ui::AXNodeData* node_data) override { | |
| 63 HoverHighlightView::GetAccessibleNodeData(node_data); | |
| 64 node_data->role = ui::AX_ROLE_CHECK_BOX; | |
| 65 if (selected_) | |
| 66 node_data->AddStateFlag(ui::AX_STATE_CHECKED); | |
| 67 } | |
| 68 | |
| 69 private: | |
| 70 bool selected_; | |
| 71 | |
| 72 DISALLOW_COPY_AND_ASSIGN(SelectableHoverHighlightView); | |
| 73 }; | |
| 74 | |
| 75 // The IME list item view used in the material design. It contains IME info | |
| 76 // (name and label) and a check button if the item is selected. It's also used | |
| 77 // for IME property item, which has no name but label and a gray checked icon. | |
| 78 class ImeListItemView : public ActionableView { | |
| 79 public: | |
| 80 ImeListItemView(SystemTrayItem* owner, | |
| 81 ImeListView* list_view, | |
| 82 const base::string16& id, | |
| 83 const base::string16& label, | |
| 84 bool selected, | |
| 85 const SkColor button_color) | |
| 86 : ActionableView(owner, TrayPopupInkDropStyle::FILL_BOUNDS), | |
| 87 ime_list_view_(list_view), | |
| 88 selected_(selected) { | |
| 89 if (MaterialDesignController::IsSystemTrayMenuMaterial()) | |
| 90 SetInkDropMode(InkDropHostView::InkDropMode::ON); | |
| 91 | |
| 92 TriView* tri_view = TrayPopupUtils::CreateDefaultRowView(); | |
| 93 AddChildView(tri_view); | |
| 94 SetLayoutManager(new views::FillLayout); | |
| 95 | |
| 96 // The id button shows the IME short name. | |
| 97 views::Label* id_label = TrayPopupUtils::CreateDefaultLabel(); | |
| 98 id_label->SetText(id); | |
| 99 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
| 100 const gfx::FontList& base_font_list = | |
| 101 rb.GetFontList(ui::ResourceBundle::MediumBoldFont); | |
| 102 id_label->SetFontList(base_font_list); | |
| 103 | |
| 104 // For IMEs whose short name are more than 2 characters (INTL, EXTD, etc.), | |
| 105 // |kMenuIconSize| is not enough. The label will trigger eliding as "I..." | |
| 106 // or "...". So we shrink the font size until it fits within the bounds. | |
| 107 int size_delta = -1; | |
| 108 while ((id_label->GetPreferredSize().width() - | |
| 109 id_label->GetInsets().width()) > kMenuIconSize && | |
| 110 size_delta >= kMinFontSizeDelta) { | |
| 111 id_label->SetFontList(base_font_list.DeriveWithSizeDelta(size_delta)); | |
| 112 --size_delta; | |
| 113 } | |
| 114 tri_view->AddView(TriView::Container::START, id_label); | |
| 115 | |
| 116 // The label shows the IME name. | |
| 117 auto* label_view = TrayPopupUtils::CreateDefaultLabel(); | |
| 118 label_view->SetText(label); | |
| 119 TrayPopupItemStyle style( | |
| 120 TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL); | |
| 121 style.SetupLabel(label_view); | |
| 122 | |
| 123 label_view->SetHorizontalAlignment(gfx::ALIGN_LEFT); | |
| 124 tri_view->AddView(TriView::Container::CENTER, label_view); | |
| 125 | |
| 126 if (selected) { | |
| 127 // The checked button indicates the IME is selected. | |
| 128 views::ImageView* checked_image = TrayPopupUtils::CreateMainImageView(); | |
| 129 checked_image->SetImage(gfx::CreateVectorIcon( | |
| 130 gfx::VectorIconId::CHECK_CIRCLE, kMenuIconSize, button_color)); | |
| 131 tri_view->AddView(TriView::Container::END, checked_image); | |
| 132 } | |
| 133 SetAccessibleName(label_view->text()); | |
| 134 } | |
| 135 | |
| 136 ~ImeListItemView() override {} | |
| 137 | |
| 138 // ActionableView: | |
| 139 bool PerformAction(const ui::Event& event) override { | |
| 140 if (ime_list_view_->should_focus_ime_after_selection_with_keyboard() && | |
| 141 event.type() == ui::EventType::ET_KEY_PRESSED) { | |
| 142 ime_list_view_->set_last_item_selected_with_keyboard(true); | |
| 143 } else { | |
| 144 ime_list_view_->set_last_item_selected_with_keyboard(false); | |
| 145 } | |
| 146 | |
| 147 ime_list_view_->HandleViewClicked(this); | |
| 148 return true; | |
| 149 } | |
| 150 | |
| 151 void OnFocus() override { | |
| 152 ActionableView::OnFocus(); | |
| 153 if (ime_list_view_ && ime_list_view_->scroll_content()) | |
| 154 ime_list_view_->scroll_content()->ScrollRectToVisible(bounds()); | |
| 155 } | |
| 156 | |
| 157 void GetAccessibleNodeData(ui::AXNodeData* node_data) override { | |
| 158 ActionableView::GetAccessibleNodeData(node_data); | |
| 159 node_data->role = ui::AX_ROLE_CHECK_BOX; | |
| 160 node_data->AddStateFlag(selected_ ? ui::AX_STATE_CHECKED | |
| 161 : ui::AX_STATE_NONE); | |
| 162 } | |
| 163 | |
| 164 private: | |
| 165 ImeListView* ime_list_view_; | |
| 166 bool selected_; | |
| 167 | |
| 168 DISALLOW_COPY_AND_ASSIGN(ImeListItemView); | |
| 169 }; | |
| 170 | |
| 171 } // namespace | |
| 172 | |
| 173 // The view that contains a |KeyboardButtonView| and a toggle button. | |
| 174 class MaterialKeyboardStatusRowView : public views::View { | |
| 175 public: | |
| 176 MaterialKeyboardStatusRowView(views::ButtonListener* listener, bool enabled) | |
| 177 : listener_(listener), toggle_(nullptr) { | |
| 178 Init(); | |
| 179 toggle_->SetIsOn(enabled, false); | |
| 180 } | |
| 181 | |
| 182 ~MaterialKeyboardStatusRowView() override {} | |
| 183 | |
| 184 views::Button* toggle() const { return toggle_; } | |
| 185 bool is_toggled() const { return toggle_->is_on(); } | |
| 186 | |
| 187 protected: | |
| 188 // views::View: | |
| 189 int GetHeightForWidth(int w) const override { | |
| 190 return GetPreferredSize().height(); | |
| 191 } | |
| 192 | |
| 193 private: | |
| 194 void Init() { | |
| 195 TrayPopupUtils::ConfigureAsStickyHeader(this); | |
| 196 SetLayoutManager(new views::FillLayout); | |
| 197 | |
| 198 TriView* tri_view = TrayPopupUtils::CreateDefaultRowView(); | |
| 199 AddChildView(tri_view); | |
| 200 | |
| 201 // The on-screen keyboard image button. | |
| 202 views::ImageView* keyboard_image = TrayPopupUtils::CreateMainImageView(); | |
| 203 keyboard_image->SetImage(gfx::CreateVectorIcon( | |
| 204 kImeMenuOnScreenKeyboardIcon, kMenuIconSize, kMenuIconColor)); | |
| 205 tri_view->AddView(TriView::Container::START, keyboard_image); | |
| 206 | |
| 207 // The on-screen keyboard label ('On-screen keyboard'). | |
| 208 auto* label = TrayPopupUtils::CreateDefaultLabel(); | |
| 209 label->SetText(ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 210 IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD)); | |
| 211 TrayPopupItemStyle style( | |
| 212 TrayPopupItemStyle::FontStyle::DETAILED_VIEW_LABEL); | |
| 213 style.SetupLabel(label); | |
| 214 tri_view->AddView(TriView::Container::CENTER, label); | |
| 215 | |
| 216 // The on-screen keyboard toggle button. | |
| 217 toggle_ = TrayPopupUtils::CreateToggleButton( | |
| 218 listener_, IDS_ASH_STATUS_TRAY_ACCESSIBILITY_VIRTUAL_KEYBOARD); | |
| 219 tri_view->AddView(TriView::Container::END, toggle_); | |
| 220 } | |
| 221 | |
| 222 // ButtonListener to notify when |toggle_| is clicked. | |
| 223 views::ButtonListener* listener_; | |
| 224 | |
| 225 // ToggleButton to toggle keyboard on or off. | |
| 226 views::ToggleButton* toggle_; | |
| 227 | |
| 228 DISALLOW_COPY_AND_ASSIGN(MaterialKeyboardStatusRowView); | |
| 229 }; | |
| 230 | |
| 231 ImeListView::ImeListView(SystemTrayItem* owner) | |
| 232 : TrayDetailsView(owner), | |
| 233 last_item_selected_with_keyboard_(false), | |
| 234 should_focus_ime_after_selection_with_keyboard_(false), | |
| 235 current_ime_view_(nullptr) {} | |
| 236 | |
| 237 ImeListView::~ImeListView() {} | |
| 238 | |
| 239 void ImeListView::Init(bool show_keyboard_toggle, | |
| 240 SingleImeBehavior single_ime_behavior) { | |
| 241 SystemTrayDelegate* delegate = WmShell::Get()->system_tray_delegate(); | |
| 242 IMEInfoList list; | |
| 243 delegate->GetAvailableIMEList(&list); | |
| 244 IMEPropertyInfoList property_list; | |
| 245 delegate->GetCurrentIMEProperties(&property_list); | |
| 246 Update(list, property_list, show_keyboard_toggle, single_ime_behavior); | |
| 247 } | |
| 248 | |
| 249 void ImeListView::Update(const IMEInfoList& list, | |
| 250 const IMEPropertyInfoList& property_list, | |
| 251 bool show_keyboard_toggle, | |
| 252 SingleImeBehavior single_ime_behavior) { | |
| 253 ResetImeListView(); | |
| 254 ime_map_.clear(); | |
| 255 property_map_.clear(); | |
| 256 CreateScrollableList(); | |
| 257 | |
| 258 // Appends IME list and IME properties. | |
| 259 if (single_ime_behavior == ImeListView::SHOW_SINGLE_IME || list.size() > 1) { | |
| 260 if (MaterialDesignController::IsSystemTrayMenuMaterial()) { | |
| 261 AppendImeListAndProperties(list, property_list); | |
| 262 } else { | |
| 263 AppendIMEList(list); | |
| 264 if (!property_list.empty()) | |
| 265 AppendIMEProperties(property_list); | |
| 266 } | |
| 267 } | |
| 268 | |
| 269 if (show_keyboard_toggle) { | |
| 270 if (MaterialDesignController::IsSystemTrayMenuMaterial()) { | |
| 271 PrependMaterialKeyboardStatus(); | |
| 272 } else { | |
| 273 if (list.size() > 1 || !property_list.empty()) | |
| 274 AddScrollSeparator(); | |
| 275 AppendKeyboardStatus(); | |
| 276 } | |
| 277 } | |
| 278 | |
| 279 Layout(); | |
| 280 SchedulePaint(); | |
| 281 | |
| 282 if (should_focus_ime_after_selection_with_keyboard_ && | |
| 283 last_item_selected_with_keyboard_) { | |
| 284 FocusCurrentImeIfNeeded(); | |
| 285 } else if (current_ime_view_) { | |
| 286 scroll_content()->ScrollRectToVisible(current_ime_view_->bounds()); | |
| 287 } | |
| 288 } | |
| 289 | |
| 290 void ImeListView::ResetImeListView() { | |
| 291 // Children are removed from the view hierarchy and deleted in Reset(). | |
| 292 Reset(); | |
| 293 material_keyboard_status_view_ = nullptr; | |
| 294 keyboard_status_ = nullptr; | |
| 295 current_ime_view_ = nullptr; | |
| 296 } | |
| 297 | |
| 298 void ImeListView::CloseImeListView() { | |
| 299 last_selected_item_id_.clear(); | |
| 300 current_ime_view_ = nullptr; | |
| 301 last_item_selected_with_keyboard_ = false; | |
| 302 GetWidget()->Close(); | |
| 303 } | |
| 304 | |
| 305 void ImeListView::AppendIMEList(const IMEInfoList& list) { | |
| 306 DCHECK(ime_map_.empty()); | |
| 307 for (size_t i = 0; i < list.size(); i++) { | |
| 308 HoverHighlightView* container = | |
| 309 new SelectableHoverHighlightView(this, list[i].name, list[i].selected); | |
| 310 scroll_content()->AddChildView(container); | |
| 311 ime_map_[container] = list[i].id; | |
| 312 } | |
| 313 } | |
| 314 | |
| 315 void ImeListView::AppendIMEProperties( | |
| 316 const IMEPropertyInfoList& property_list) { | |
| 317 DCHECK(property_map_.empty()); | |
| 318 for (size_t i = 0; i < property_list.size(); i++) { | |
| 319 HoverHighlightView* container = new SelectableHoverHighlightView( | |
| 320 this, property_list[i].name, property_list[i].selected); | |
| 321 if (i == 0) | |
| 322 container->SetBorder( | |
| 323 views::CreateSolidSidedBorder(1, 0, 0, 0, kBorderLightColor)); | |
| 324 scroll_content()->AddChildView(container); | |
| 325 property_map_[container] = property_list[i].key; | |
| 326 } | |
| 327 } | |
| 328 | |
| 329 void ImeListView::AppendImeListAndProperties( | |
| 330 const IMEInfoList& list, | |
| 331 const IMEPropertyInfoList& property_list) { | |
| 332 DCHECK(ime_map_.empty()); | |
| 333 for (size_t i = 0; i < list.size(); i++) { | |
| 334 views::View* ime_view = | |
| 335 new ImeListItemView(owner(), this, list[i].short_name, list[i].name, | |
| 336 list[i].selected, gfx::kGoogleGreen700); | |
| 337 scroll_content()->AddChildView(ime_view); | |
| 338 ime_map_[ime_view] = list[i].id; | |
| 339 | |
| 340 if (list[i].selected) | |
| 341 current_ime_view_ = ime_view; | |
| 342 | |
| 343 // In material design, the property items will be added after the current | |
| 344 // selected IME item. | |
| 345 if (list[i].selected && !property_list.empty()) { | |
| 346 // Adds a separator on the top of property items. | |
| 347 scroll_content()->AddChildView( | |
| 348 TrayPopupUtils::CreateListItemSeparator(true)); | |
| 349 | |
| 350 // Adds the property items. | |
| 351 for (size_t i = 0; i < property_list.size(); i++) { | |
| 352 ImeListItemView* property_view = new ImeListItemView( | |
| 353 owner(), this, base::string16(), property_list[i].name, | |
| 354 property_list[i].selected, kMenuIconColor); | |
| 355 scroll_content()->AddChildView(property_view); | |
| 356 property_map_[property_view] = property_list[i].key; | |
| 357 } | |
| 358 | |
| 359 // Adds a separator on the bottom of property items if there are still | |
| 360 // other IMEs under the current one. | |
| 361 if (i < list.size() - 1) | |
| 362 scroll_content()->AddChildView( | |
| 363 TrayPopupUtils::CreateListItemSeparator(true)); | |
| 364 } | |
| 365 } | |
| 366 } | |
| 367 | |
| 368 void ImeListView::AppendKeyboardStatus() { | |
| 369 DCHECK(!MaterialDesignController::IsSystemTrayMenuMaterial()); | |
| 370 HoverHighlightView* container = new HoverHighlightView(this); | |
| 371 int id = keyboard::IsKeyboardEnabled() ? IDS_ASH_STATUS_TRAY_DISABLE_KEYBOARD | |
| 372 : IDS_ASH_STATUS_TRAY_ENABLE_KEYBOARD; | |
| 373 container->AddLabel( | |
| 374 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(id), | |
| 375 gfx::ALIGN_LEFT, false /* highlight */); | |
| 376 scroll_content()->AddChildView(container); | |
| 377 keyboard_status_ = container; | |
| 378 } | |
| 379 | |
| 380 void ImeListView::PrependMaterialKeyboardStatus() { | |
| 381 DCHECK(MaterialDesignController::IsSystemTrayMenuMaterial()); | |
| 382 DCHECK(!material_keyboard_status_view_); | |
| 383 MaterialKeyboardStatusRowView* view = | |
| 384 new MaterialKeyboardStatusRowView(this, keyboard::IsKeyboardEnabled()); | |
| 385 scroll_content()->AddChildViewAt(view, 0); | |
| 386 material_keyboard_status_view_ = view; | |
| 387 } | |
| 388 | |
| 389 void ImeListView::HandleViewClicked(views::View* view) { | |
| 390 if (view == keyboard_status_) { | |
| 391 WmShell::Get()->ToggleIgnoreExternalKeyboard(); | |
| 392 last_selected_item_id_.clear(); | |
| 393 last_item_selected_with_keyboard_ = false; | |
| 394 return; | |
| 395 } | |
| 396 | |
| 397 SystemTrayDelegate* delegate = WmShell::Get()->system_tray_delegate(); | |
| 398 std::map<views::View*, std::string>::const_iterator ime = ime_map_.find(view); | |
| 399 if (ime != ime_map_.end()) { | |
| 400 WmShell::Get()->RecordUserMetricsAction(UMA_STATUS_AREA_IME_SWITCH_MODE); | |
| 401 std::string ime_id = ime->second; | |
| 402 last_selected_item_id_ = ime_id; | |
| 403 delegate->SwitchIME(ime_id); | |
| 404 } else { | |
| 405 std::map<views::View*, std::string>::const_iterator property = | |
| 406 property_map_.find(view); | |
| 407 if (property == property_map_.end()) | |
| 408 return; | |
| 409 const std::string key = property->second; | |
| 410 last_selected_item_id_ = key; | |
| 411 delegate->ActivateIMEProperty(key); | |
| 412 } | |
| 413 | |
| 414 if (!should_focus_ime_after_selection_with_keyboard_ || | |
| 415 !last_item_selected_with_keyboard_) { | |
| 416 CloseImeListView(); | |
| 417 } | |
| 418 } | |
| 419 | |
| 420 void ImeListView::HandleButtonPressed(views::Button* sender, | |
| 421 const ui::Event& event) { | |
| 422 if (material_keyboard_status_view_ && | |
| 423 sender == material_keyboard_status_view_->toggle()) { | |
| 424 WmShell::Get()->ToggleIgnoreExternalKeyboard(); | |
| 425 last_selected_item_id_.clear(); | |
| 426 last_item_selected_with_keyboard_ = false; | |
| 427 } | |
| 428 } | |
| 429 | |
| 430 void ImeListView::VisibilityChanged(View* starting_from, bool is_visible) { | |
| 431 if (!is_visible || (should_focus_ime_after_selection_with_keyboard_ && | |
| 432 last_item_selected_with_keyboard_) || | |
| 433 !current_ime_view_) { | |
| 434 return; | |
| 435 } | |
| 436 | |
| 437 scroll_content()->ScrollRectToVisible(current_ime_view_->bounds()); | |
| 438 } | |
| 439 | |
| 440 void ImeListView::FocusCurrentImeIfNeeded() { | |
| 441 views::FocusManager* manager = GetFocusManager(); | |
| 442 if (!manager || manager->GetFocusedView() || last_selected_item_id_.empty()) | |
| 443 return; | |
| 444 | |
| 445 for (auto ime_map : ime_map_) { | |
| 446 if (ime_map.second == last_selected_item_id_) { | |
| 447 (ime_map.first)->RequestFocus(); | |
| 448 return; | |
| 449 } | |
| 450 } | |
| 451 | |
| 452 for (auto property_map : property_map_) { | |
| 453 if (property_map.second == last_selected_item_id_) { | |
| 454 (property_map.first)->RequestFocus(); | |
| 455 return; | |
| 456 } | |
| 457 } | |
| 458 } | |
| 459 | |
| 460 ImeListViewTestApi::ImeListViewTestApi(ImeListView* ime_list_view) | |
| 461 : ime_list_view_(ime_list_view) {} | |
| 462 | |
| 463 ImeListViewTestApi::~ImeListViewTestApi() {} | |
| 464 | |
| 465 views::View* ImeListViewTestApi::GetToggleView() const { | |
| 466 return ime_list_view_->material_keyboard_status_view_->toggle(); | |
| 467 } | |
| 468 | |
| 469 } // namespace ash | |
| OLD | NEW |