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