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/shortcut_viewer/views/keyboard_shortcut_view.h"
6 
7 #include <algorithm>
8 #include <memory>
9 #include <string>
10 #include <utility>
11 
12 #include "ash/display/privacy_screen_controller.h"
13 #include "ash/public/cpp/app_list/internal_app_id_constants.h"
14 #include "ash/public/cpp/app_types.h"
15 #include "ash/public/cpp/ash_typography.h"
16 #include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
17 #include "ash/public/cpp/shelf_item.h"
18 #include "ash/public/cpp/window_properties.h"
19 #include "ash/search_box/search_box_view_base.h"
20 #include "ash/shell.h"
21 #include "ash/shortcut_viewer/keyboard_shortcut_viewer_metadata.h"
22 #include "ash/shortcut_viewer/strings/grit/shortcut_viewer_strings.h"
23 #include "ash/shortcut_viewer/vector_icons/vector_icons.h"
24 #include "ash/shortcut_viewer/views/keyboard_shortcut_item_list_view.h"
25 #include "ash/shortcut_viewer/views/keyboard_shortcut_item_view.h"
26 #include "ash/shortcut_viewer/views/ksv_search_box_view.h"
27 #include "base/bind.h"
28 #include "base/i18n/string_search.h"
29 #include "base/metrics/histogram_macros.h"
30 #include "base/metrics/user_metrics.h"
31 #include "base/metrics/user_metrics_action.h"
32 #include "base/strings/string_number_conversions.h"
33 #include "base/time/time.h"
34 #include "base/trace_event/trace_event.h"
35 #include "chromeos/ui/base/window_properties.h"
36 #include "ui/aura/client/aura_constants.h"
37 #include "ui/aura/window.h"
38 #include "ui/base/accelerators/accelerator.h"
39 #include "ui/base/default_style.h"
40 #include "ui/base/l10n/l10n_util.h"
41 #include "ui/base/resource/resource_bundle.h"
42 #include "ui/chromeos/events/keyboard_layout_util.h"
43 #include "ui/events/event_constants.h"
44 #include "ui/events/types/event_type.h"
45 #include "ui/gfx/paint_vector_icon.h"
46 #include "ui/gfx/presentation_feedback.h"
47 #include "ui/views/accessibility/view_accessibility.h"
48 #include "ui/views/background.h"
49 #include "ui/views/border.h"
50 #include "ui/views/controls/image_view.h"
51 #include "ui/views/controls/label.h"
52 #include "ui/views/controls/scroll_view.h"
53 #include "ui/views/controls/styled_label.h"
54 #include "ui/views/controls/tabbed_pane/tabbed_pane.h"
55 #include "ui/views/controls/textfield/textfield.h"
56 #include "ui/views/layout/box_layout.h"
57 #include "ui/views/layout/fill_layout.h"
58 #include "ui/views/widget/widget.h"
59 
60 namespace keyboard_shortcut_viewer {
61 
62 namespace {
63 
64 KeyboardShortcutView* g_ksv_view = nullptr;
65 
66 constexpr base::nullopt_t kAllCategories = base::nullopt;
67 
68 // Creates the no search result view.
CreateNoSearchResultView()69 std::unique_ptr<views::View> CreateNoSearchResultView() {
70   constexpr int kSearchIllustrationIconSize = 150;
71   constexpr SkColor kSearchIllustrationIconColor =
72       SkColorSetARGB(0xFF, 0xDA, 0xDC, 0xE0);
73 
74   auto illustration_view = std::make_unique<views::View>();
75   constexpr int kTopPadding = 98;
76   views::BoxLayout* layout =
77       illustration_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
78           views::BoxLayout::Orientation::kVertical,
79           gfx::Insets(kTopPadding, 0, 0, 0)));
80   layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
81   auto image_view = std::make_unique<views::ImageView>();
82   image_view->SetImage(gfx::CreateVectorIcon(kKsvSearchNoResultIcon,
83                                              kSearchIllustrationIconColor));
84   image_view->SetImageSize(
85       gfx::Size(kSearchIllustrationIconSize, kSearchIllustrationIconSize));
86   illustration_view->AddChildView(std::move(image_view));
87 
88   constexpr SkColor kSearchIllustrationTextColor =
89       SkColorSetARGB(0xFF, 0x20, 0x21, 0x24);
90   auto text = std::make_unique<views::Label>(
91       l10n_util::GetStringUTF16(IDS_KSV_SEARCH_NO_RESULT));
92   text->SetEnabledColor(kSearchIllustrationTextColor);
93   constexpr int kLabelFontSizeDelta = 1;
94   ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
95   text->SetFontList(rb.GetFontListWithDelta(
96       kLabelFontSizeDelta, gfx::Font::NORMAL, gfx::Font::Weight::NORMAL));
97   illustration_view->AddChildView(std::move(text));
98   return illustration_view;
99 }
100 
101 class ShortcutsListScrollView : public views::ScrollView {
102  public:
ShortcutsListScrollView()103   ShortcutsListScrollView() {
104     GetViewAccessibility().OverrideRole(ax::mojom::Role::kScrollView);
105   }
106 
107   ~ShortcutsListScrollView() override = default;
108 
109   // views::View:
OnFocus()110   void OnFocus() override {
111     SetHasFocusIndicator(true);
112     NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true);
113   }
114 
OnBlur()115   void OnBlur() override { SetHasFocusIndicator(false); }
116 
117  private:
118   DISALLOW_COPY_AND_ASSIGN(ShortcutsListScrollView);
119 };
120 
CreateScrollView(std::unique_ptr<views::View> content_view)121 std::unique_ptr<ShortcutsListScrollView> CreateScrollView(
122     std::unique_ptr<views::View> content_view) {
123   auto scroller = std::make_unique<ShortcutsListScrollView>();
124   scroller->SetDrawOverflowIndicator(false);
125   scroller->ClipHeightTo(0, 0);
126   scroller->SetContents(std::move(content_view));
127   scroller->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
128   return scroller;
129 }
130 
UpdateAXNodeDataPosition(std::vector<KeyboardShortcutItemView * > & shortcut_items)131 void UpdateAXNodeDataPosition(
132     std::vector<KeyboardShortcutItemView*>& shortcut_items) {
133   // Update list item AXNodeData position for assistive tool.
134   const int number_shortcut_items = shortcut_items.size();
135   for (int i = 0; i < number_shortcut_items; ++i) {
136     shortcut_items.at(i)->GetViewAccessibility().OverridePosInSet(
137         i + 1, number_shortcut_items);
138   }
139 }
140 
141 // Returns true if the given |item| should be excluded from the view, since
142 // certain shortcuts can be associated with a disabled feature behind a flag,
143 // or specific device property, e.g. keyboard layout.
ShouldExcludeItem(const KeyboardShortcutItem & item)144 bool ShouldExcludeItem(const KeyboardShortcutItem& item) {
145   switch (item.description_message_id) {
146     case IDS_KSV_DESCRIPTION_OPEN_GOOGLE_ASSISTANT:
147       return ui::DeviceKeyboardHasAssistantKey();
148     case IDS_KSV_DESCRIPTION_PRIVACY_SCREEN_TOGGLE:
149       return !ash::Shell::Get()->privacy_screen_controller()->IsSupported();
150   }
151 
152   return false;
153 }
154 
155 }  // namespace
156 
~KeyboardShortcutView()157 KeyboardShortcutView::~KeyboardShortcutView() {
158   DCHECK_EQ(g_ksv_view, this);
159   g_ksv_view = nullptr;
160 }
161 
162 // static
Toggle(aura::Window * context)163 views::Widget* KeyboardShortcutView::Toggle(aura::Window* context) {
164   if (g_ksv_view) {
165     if (g_ksv_view->GetWidget()->IsActive())
166       g_ksv_view->GetWidget()->Close();
167     else
168       g_ksv_view->GetWidget()->Activate();
169   } else {
170     const base::TimeTicks start_time = base::TimeTicks::Now();
171     TRACE_EVENT0("shortcut_viewer", "CreateWidget");
172     base::RecordAction(
173         base::UserMetricsAction("KeyboardShortcutViewer.CreateWindow"));
174 
175     views::Widget::InitParams params;
176     params.delegate = new KeyboardShortcutView;
177     params.name = "KeyboardShortcutWidget";
178     // Intentionally don't set bounds. The window will be sized and centered
179     // based on CalculatePreferredSize().
180     views::Widget* widget = new views::Widget;
181     params.context = context;
182     params.init_properties_container.SetProperty(
183         aura::client::kAppType, static_cast<int>(ash::AppType::SYSTEM_APP));
184     widget->Init(std::move(params));
185 
186     // Set frame view Active and Inactive colors, both are SK_ColorWHITE.
187     aura::Window* window = g_ksv_view->GetWidget()->GetNativeWindow();
188     window->SetProperty(chromeos::kFrameActiveColorKey, SK_ColorWHITE);
189     window->SetProperty(chromeos::kFrameInactiveColorKey, SK_ColorWHITE);
190 
191     // Set shelf icon.
192     const ash::ShelfID shelf_id(ash::kInternalAppIdKeyboardShortcutViewer);
193     window->SetProperty(
194         ash::kAppIDKey,
195         new std::string(ash::kInternalAppIdKeyboardShortcutViewer));
196     window->SetProperty(ash::kShelfIDKey,
197                         new std::string(shelf_id.Serialize()));
198     window->SetProperty<int>(ash::kShelfItemTypeKey, ash::TYPE_APP);
199 
200     // We don't want the KSV window to have a title (per design), however the
201     // shelf uses the window title to set the shelf item's tooltip text. The
202     // shelf observes changes to the |kWindowIconKey| property and handles that
203     // by initializing the shelf item including its tooltip text.
204     // TODO(wutao): we can remove resource id IDS_KSV_TITLE after implementing
205     // internal app shelf launcher.
206     window->SetTitle(l10n_util::GetStringUTF16(IDS_KSV_TITLE));
207     gfx::ImageSkia* icon =
208         ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
209             IDR_SHORTCUT_VIEWER_LOGO_192);
210     // The new gfx::ImageSkia instance is owned by the window itself.
211     window->SetProperty(aura::client::kWindowIconKey,
212                         new gfx::ImageSkia(*icon));
213 
214     g_ksv_view->AddAccelerator(
215         ui::Accelerator(ui::VKEY_W, ui::EF_CONTROL_DOWN));
216 
217     g_ksv_view->needs_init_all_categories_ = false;
218     g_ksv_view->did_first_paint_ = false;
219     g_ksv_view->GetWidget()->Show();
220     g_ksv_view->search_box_view_->search_box()->RequestFocus();
221 
222     widget->GetCompositor()->RequestPresentationTimeForNextFrame(base::BindOnce(
223         [](base::TimeTicks start_time,
224            const gfx::PresentationFeedback& feedback) {
225           UMA_HISTOGRAM_TIMES("Keyboard.ShortcutViewer.StartupTime",
226                               feedback.timestamp - start_time);
227         },
228         start_time));
229   }
230   return g_ksv_view->GetWidget();
231 }
232 
GetClassName() const233 const char* KeyboardShortcutView::GetClassName() const {
234   return "KeyboardShortcutView";
235 }
236 
GetAccessibleWindowRole()237 ax::mojom::Role KeyboardShortcutView::GetAccessibleWindowRole() {
238   return ax::mojom::Role::kWindow;
239 }
240 
GetAccessibleWindowTitle() const241 base::string16 KeyboardShortcutView::GetAccessibleWindowTitle() const {
242   return l10n_util::GetStringUTF16(IDS_KSV_TITLE);
243 }
244 
AcceleratorPressed(const ui::Accelerator & accelerator)245 bool KeyboardShortcutView::AcceleratorPressed(
246     const ui::Accelerator& accelerator) {
247   DCHECK_EQ(ui::VKEY_W, accelerator.key_code());
248   DCHECK_EQ(ui::EF_CONTROL_DOWN, accelerator.modifiers());
249   GetWidget()->Close();
250   return true;
251 }
252 
Layout()253 void KeyboardShortcutView::Layout() {
254   gfx::Rect content_bounds(GetContentsBounds());
255   if (content_bounds.IsEmpty())
256     return;
257 
258   constexpr int kSearchBoxTopPadding = 8;
259   constexpr int kSearchBoxBottomPadding = 16;
260   constexpr int kSearchBoxHorizontalPadding = 30;
261   const int left = content_bounds.x();
262   const int top = content_bounds.y();
263   gfx::Rect search_box_bounds(search_box_view_->GetPreferredSize());
264   search_box_bounds.set_width(
265       std::min(search_box_bounds.width(),
266                content_bounds.width() - 2 * kSearchBoxHorizontalPadding));
267   search_box_bounds.set_x(
268       left + (content_bounds.width() - search_box_bounds.width()) / 2);
269   search_box_bounds.set_y(top + kSearchBoxTopPadding);
270   search_box_view_->SetBoundsRect(search_box_bounds);
271 
272   views::View* content_view = categories_tabbed_pane_->GetVisible()
273                                   ? categories_tabbed_pane_
274                                   : search_results_container_;
275   const int search_box_used_height = search_box_bounds.height() +
276                                      kSearchBoxTopPadding +
277                                      kSearchBoxBottomPadding;
278   content_view->SetBounds(left, top + search_box_used_height,
279                           content_bounds.width(),
280                           content_bounds.height() - search_box_used_height);
281 }
282 
CalculatePreferredSize() const283 gfx::Size KeyboardShortcutView::CalculatePreferredSize() const {
284   return gfx::Size(800, 512);
285 }
286 
OnPaint(gfx::Canvas * canvas)287 void KeyboardShortcutView::OnPaint(gfx::Canvas* canvas) {
288   views::View::OnPaint(canvas);
289 
290   // Skip if it is the first OnPaint event.
291   if (!did_first_paint_) {
292     did_first_paint_ = true;
293     needs_init_all_categories_ = true;
294     return;
295   }
296 
297   if (!needs_init_all_categories_)
298     return;
299 
300   needs_init_all_categories_ = false;
301   // Cannot post a task right after initializing the first category, it will
302   // have a chance to end up in the same group of drawing commands sent to
303   // compositor. We can wait for the second OnPaint, which means previous
304   // drawing commands have been sent to compositor for the next frame and new
305   // coming commands will be sent for the next-next frame.
306   base::ThreadTaskRunnerHandle::Get()->PostTask(
307       FROM_HERE, base::BindOnce(&KeyboardShortcutView::InitCategoriesTabbedPane,
308                                 weak_factory_.GetWeakPtr(), kAllCategories));
309 }
310 
QueryChanged(ash::SearchBoxViewBase * sender)311 void KeyboardShortcutView::QueryChanged(ash::SearchBoxViewBase* sender) {
312   const bool query_empty = sender->IsSearchBoxTrimmedQueryEmpty();
313   if (is_search_box_empty_ != query_empty) {
314     is_search_box_empty_ = query_empty;
315     UpdateViewsLayout(/*is_search_box_active=*/true);
316   }
317 
318   debounce_timer_.Stop();
319   // If search box is empty, do not show |search_results_container_|.
320   if (query_empty)
321     return;
322 
323   // TODO(wutao): This timeout value is chosen based on subjective search
324   // latency tests on Minnie. Objective method or UMA is desired.
325   constexpr base::TimeDelta kTimeOut(base::TimeDelta::FromMilliseconds(250));
326   debounce_timer_.Start(
327       FROM_HERE, kTimeOut,
328       base::BindOnce(&KeyboardShortcutView::ShowSearchResults,
329                      base::Unretained(this), sender->search_box()->GetText()));
330 }
331 
BackButtonPressed()332 void KeyboardShortcutView::BackButtonPressed() {
333   search_box_view_->ClearSearch();
334   search_box_view_->SetSearchBoxActive(false, ui::ET_UNKNOWN);
335 }
336 
ActiveChanged(ash::SearchBoxViewBase * sender)337 void KeyboardShortcutView::ActiveChanged(ash::SearchBoxViewBase* sender) {
338   const bool is_search_box_active = sender->is_search_box_active();
339   is_search_box_empty_ = sender->IsSearchBoxTrimmedQueryEmpty();
340   if (is_search_box_active) {
341     base::RecordAction(
342         base::UserMetricsAction("KeyboardShortcutViewer.Search"));
343   }
344   UpdateViewsLayout(is_search_box_active);
345 }
346 
KeyboardShortcutView()347 KeyboardShortcutView::KeyboardShortcutView() {
348   DCHECK_EQ(g_ksv_view, nullptr);
349   g_ksv_view = this;
350 
351   SetCanMinimize(true);
352   SetShowTitle(false);
353 
354   // Default background is transparent.
355   SetBackground(views::CreateSolidBackground(SK_ColorWHITE));
356   InitViews();
357 }
358 
InitViews()359 void KeyboardShortcutView::InitViews() {
360   TRACE_EVENT0("shortcut_viewer", "InitViews");
361   // Init search box view.
362   search_box_view_ = std::make_unique<KSVSearchBoxView>(this);
363   search_box_view_->Init();
364   AddChildView(search_box_view_.get());
365 
366   // Init no search result illustration view.
367   search_no_result_view_ = CreateNoSearchResultView();
368   search_no_result_view_->set_owned_by_client();
369 
370   // Init search results container view.
371   search_results_container_ = AddChildView(std::make_unique<views::View>());
372   search_results_container_->SetLayoutManager(
373       std::make_unique<views::FillLayout>());
374   search_results_container_->SetVisible(false);
375 
376   // Init views of KeyboardShortcutItemView.
377   // TODO(https://crbug.com/843394): Observe changes in keyboard layout and
378   // clear the cache.
379   KeyboardShortcutItemView::ClearKeycodeToString16Cache();
380   for (const auto& item : GetKeyboardShortcutItemList()) {
381     if (ShouldExcludeItem(item))
382       continue;
383 
384     for (auto category : item.categories) {
385       shortcut_views_.push_back(
386           std::make_unique<KeyboardShortcutItemView>(item, category));
387       shortcut_views_.back()->set_owned_by_client();
388     }
389   }
390   std::sort(shortcut_views_.begin(), shortcut_views_.end(),
391             [](const auto& lhs, const auto& rhs) {
392               if (lhs->category() != rhs->category())
393                 return lhs->category() < rhs->category();
394               return lhs->description_label_view()->GetText() <
395                      rhs->description_label_view()->GetText();
396             });
397 
398   // Init views of |categories_tabbed_pane_| and KeyboardShortcutItemListViews.
399   categories_tabbed_pane_ = AddChildView(std::make_unique<views::TabbedPane>(
400       views::TabbedPane::Orientation::kVertical,
401       views::TabbedPane::TabStripStyle::kHighlight));
402 
403   // Initial Layout of KeyboardShortcutItemView is time consuming. To speed up
404   // the startup time, we only initialize the first category pane, which is
405   // visible to user, and defer initialization of other categories in the
406   // background.
407   InitCategoriesTabbedPane(ShortcutCategory::kPopular);
408 }
409 
InitCategoriesTabbedPane(base::Optional<ShortcutCategory> initial_category)410 void KeyboardShortcutView::InitCategoriesTabbedPane(
411     base::Optional<ShortcutCategory> initial_category) {
412   active_tab_index_ = categories_tabbed_pane_->GetSelectedTabIndex();
413   // If the tab count is 0, GetSelectedTabIndex() will return kNoSelectedTab,
414   // which we do not want to cache.
415   if (active_tab_index_ == views::TabStrip::kNoSelectedTab)
416     active_tab_index_ = 0;
417 
418   ShortcutCategory current_category = ShortcutCategory::kUnknown;
419   KeyboardShortcutItemListView* item_list_view = nullptr;
420   std::vector<KeyboardShortcutItemView*> shortcut_items;
421   const bool already_has_tabs = categories_tabbed_pane_->GetTabCount() > 0;
422   size_t tab_index = 0;
423   views::View* const tab_contents = categories_tabbed_pane_->children()[1];
424   for (const auto& item_view : shortcut_views_) {
425     const ShortcutCategory category = item_view->category();
426     DCHECK_NE(ShortcutCategory::kUnknown, category);
427     if (current_category != category) {
428       current_category = category;
429       std::unique_ptr<views::View> content_view;
430       // Delay constructing a KeyboardShortcutItemListView until it is needed.
431       if (initial_category.value_or(category) == category) {
432         auto list_view = std::make_unique<KeyboardShortcutItemListView>();
433         item_list_view = list_view.get();
434 
435         // When in a new category, update the node data of the shortcut items in
436         // previous category and clear the vector in order to store items in
437         // current category.
438         UpdateAXNodeDataPosition(shortcut_items);
439         shortcut_items.clear();
440 
441         content_view = std::move(list_view);
442       } else {
443         content_view = std::make_unique<views::View>();
444       }
445 
446       // Create new tabs or update the existing tabs' contents.
447       if (already_has_tabs) {
448         auto* scroll_view = static_cast<views::ScrollView*>(
449             tab_contents->children()[tab_index]);
450         scroll_view->SetContents(std::move(content_view));
451       } else {
452         categories_tabbed_pane_->AddTab(
453             GetStringForCategory(current_category),
454             CreateScrollView(std::move(content_view)));
455       }
456 
457       ++tab_index;
458     }
459 
460     // If |initial_category| has a value, we only initialize the pane with the
461     // KeyboardShortcutItemView in the specific category in |initial_category|.
462     // Otherwise, we will initialize all the panes.
463     if (initial_category.value_or(category) != category)
464       continue;
465 
466     // Add the item to the category contents container.
467     if (!item_list_view->children().empty())
468       item_list_view->AddHorizontalSeparator();
469     views::StyledLabel* description_label_view =
470         item_view->description_label_view();
471     // Clear any styles used to highlight matched search query in search mode.
472     description_label_view->ClearStyleRanges();
473     item_list_view->AddChildView(item_view.get());
474     shortcut_items.push_back(item_view.get());
475     // Remove the search query highlight.
476     description_label_view->InvalidateLayout();
477   }
478   // Update node data for the last category.
479   UpdateAXNodeDataPosition(shortcut_items);
480 
481   tab_contents->InvalidateLayout();
482 }
483 
UpdateViewsLayout(bool is_search_box_active)484 void KeyboardShortcutView::UpdateViewsLayout(bool is_search_box_active) {
485   // 1. Search box is not active: show |categories_tabbed_pane_| and focus on
486   //    active tab.
487   // 2. Search box is active and empty: show |categories_tabbed_pane_| but focus
488   //    on search box.
489   // 3. Search box is not empty, show |search_results_container_|. Focus is on
490   //    search box.
491   const bool should_show_search_results =
492       is_search_box_active && !is_search_box_empty_;
493   if (!should_show_search_results) {
494     // Remove all child views, including horizontal separator lines, to prepare
495     // for showing search results next time.
496     search_results_container_->RemoveAllChildViews(true);
497     if (!categories_tabbed_pane_->GetVisible()) {
498       // Repopulate |categories_tabbed_pane_| child views, which were removed
499       // when they were added to |search_results_container_|.
500       InitCategoriesTabbedPane(kAllCategories);
501       // Select the category that was active before entering search mode.
502       categories_tabbed_pane_->SelectTabAt(active_tab_index_);
503     }
504   }
505   categories_tabbed_pane_->SetVisible(!should_show_search_results);
506   search_results_container_->SetVisible(should_show_search_results);
507   InvalidateLayout();
508 }
509 
ShowSearchResults(const base::string16 & search_query)510 void KeyboardShortcutView::ShowSearchResults(
511     const base::string16& search_query) {
512   search_results_container_->RemoveAllChildViews(true);
513   auto* search_container_content_view = search_no_result_view_.get();
514   auto found_items_list_view = std::make_unique<KeyboardShortcutItemListView>();
515   base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents finder(
516       search_query);
517   ShortcutCategory current_category = ShortcutCategory::kUnknown;
518   bool has_category_item = false;
519   found_shortcut_items_.clear();
520 
521   for (const auto& item_view : shortcut_views_) {
522     base::string16 description_text =
523         item_view->description_label_view()->GetText();
524     base::string16 shortcut_text = item_view->shortcut_label_view()->GetText();
525     size_t match_index = -1;
526     size_t match_length = 0;
527     // Only highlight |description_label_view_| in KeyboardShortcutItemView.
528     // |shortcut_label_view_| has customized style ranges for bubble views
529     // so it may have overlappings with the searched ranges. The highlighted
530     // behaviors are not defined so we don't highlight
531     // |shortcut_label_view_|.
532     if (finder.Search(description_text, &match_index, &match_length) ||
533         finder.Search(shortcut_text, nullptr, nullptr)) {
534       const ShortcutCategory category = item_view->category();
535       if (current_category != category) {
536         current_category = category;
537         has_category_item = false;
538         found_items_list_view->AddCategoryLabel(GetStringForCategory(category));
539       }
540       if (has_category_item)
541         found_items_list_view->AddHorizontalSeparator();
542       else
543         has_category_item = true;
544       // Highlight matched query in |description_label_view_|.
545       if (match_length > 0) {
546         views::StyledLabel::RangeStyleInfo style;
547         views::StyledLabel* description_label_view =
548             item_view->description_label_view();
549         // Clear previous styles.
550         description_label_view->ClearStyleRanges();
551         style.text_style = ash::AshTextStyle::STYLE_EMPHASIZED;
552         description_label_view->AddStyleRange(
553             gfx::Range(match_index, match_index + match_length), style);
554         // Apply new styles to highlight matched search query.
555         description_label_view->InvalidateLayout();
556       }
557 
558       found_items_list_view->AddChildView(item_view.get());
559       found_shortcut_items_.push_back(item_view.get());
560     }
561   }
562 
563   std::vector<base::string16> replacement_strings;
564   const int number_search_results = found_shortcut_items_.size();
565   if (!found_items_list_view->children().empty()) {
566     UpdateAXNodeDataPosition(found_shortcut_items_);
567     replacement_strings.push_back(
568         base::NumberToString16(number_search_results));
569 
570     // To offset the padding between the bottom of the |search_box_view_| and
571     // the top of the |search_results_container_|.
572     constexpr int kTopPadding = -16;
573     constexpr int kHorizontalPadding = 128;
574     found_items_list_view->SetBorder(views::CreateEmptyBorder(
575         gfx::Insets(kTopPadding, kHorizontalPadding, 0, kHorizontalPadding)));
576     search_container_content_view =
577         CreateScrollView(std::move(found_items_list_view)).release();
578   }
579   replacement_strings.push_back(search_query);
580   search_box_view_->SetAccessibleValue(l10n_util::GetStringFUTF16(
581       number_search_results == 0
582           ? IDS_KSV_SEARCH_BOX_ACCESSIBILITY_VALUE_WITHOUT_RESULTS
583           : IDS_KSV_SEARCH_BOX_ACCESSIBILITY_VALUE_WITH_RESULTS,
584       replacement_strings, nullptr));
585   search_results_container_->AddChildView(search_container_content_view);
586   InvalidateLayout();
587 }
588 
CreateClientView(views::Widget * widget)589 views::ClientView* KeyboardShortcutView::CreateClientView(
590     views::Widget* widget) {
591   return new views::ClientView(widget, this);
592 }
593 
GetInstanceForTesting()594 KeyboardShortcutView* KeyboardShortcutView::GetInstanceForTesting() {
595   return g_ksv_view;
596 }
597 
GetTabCountForTesting() const598 size_t KeyboardShortcutView::GetTabCountForTesting() const {
599   return categories_tabbed_pane_->GetTabCount();
600 }
601 
602 const std::vector<std::unique_ptr<KeyboardShortcutItemView>>&
GetShortcutViewsForTesting() const603 KeyboardShortcutView::GetShortcutViewsForTesting() const {
604   return shortcut_views_;
605 }
606 
GetSearchBoxViewForTesting()607 KSVSearchBoxView* KeyboardShortcutView::GetSearchBoxViewForTesting() {
608   return search_box_view_.get();
609 }
610 
611 const std::vector<KeyboardShortcutItemView*>&
GetFoundShortcutItemsForTesting() const612 KeyboardShortcutView::GetFoundShortcutItemsForTesting() const {
613   return found_shortcut_items_;
614 }
615 
616 }  // namespace keyboard_shortcut_viewer
617 
618 namespace ash {
619 
ToggleKeyboardShortcutViewer()620 void ToggleKeyboardShortcutViewer() {
621   keyboard_shortcut_viewer::KeyboardShortcutView::Toggle(nullptr);
622 }
623 
624 }  // namespace ash
625