blob: d29fbf2422d4b2838035cbad4147df2865cd58a4 [file] [log] [blame]
// Copyright 2018 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/shortcut_viewer/views/keyboard_shortcut_view.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include "ash/display/privacy_screen_controller.h"
#include "ash/public/cpp/app_list/internal_app_id_constants.h"
#include "ash/public/cpp/app_types.h"
#include "ash/public/cpp/ash_typography.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/search_box/search_box_view_base.h"
#include "ash/shell.h"
#include "ash/shortcut_viewer/keyboard_shortcut_viewer_metadata.h"
#include "ash/shortcut_viewer/strings/grit/shortcut_viewer_strings.h"
#include "ash/shortcut_viewer/vector_icons/vector_icons.h"
#include "ash/shortcut_viewer/views/keyboard_shortcut_item_list_view.h"
#include "ash/shortcut_viewer/views/keyboard_shortcut_item_view.h"
#include "ash/shortcut_viewer/views/ksv_search_box_view.h"
#include "base/bind.h"
#include "base/i18n/string_search.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "chromeos/ui/base/window_properties.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/default_style.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/events/keyboard_layout_util.h"
#include "ui/events/event_constants.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/presentation_feedback.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/tabbed_pane/tabbed_pane.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/widget/widget.h"
namespace keyboard_shortcut_viewer {
namespace {
KeyboardShortcutView* g_ksv_view = nullptr;
constexpr base::nullopt_t kAllCategories = base::nullopt;
// Creates the no search result view.
std::unique_ptr<views::View> CreateNoSearchResultView() {
constexpr int kSearchIllustrationIconSize = 150;
constexpr SkColor kSearchIllustrationIconColor =
SkColorSetARGB(0xFF, 0xDA, 0xDC, 0xE0);
auto illustration_view = std::make_unique<views::View>();
constexpr int kTopPadding = 98;
views::BoxLayout* layout =
illustration_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical,
gfx::Insets(kTopPadding, 0, 0, 0)));
layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
auto image_view = std::make_unique<views::ImageView>();
image_view->SetImage(gfx::CreateVectorIcon(kKsvSearchNoResultIcon,
kSearchIllustrationIconColor));
image_view->SetImageSize(
gfx::Size(kSearchIllustrationIconSize, kSearchIllustrationIconSize));
illustration_view->AddChildView(std::move(image_view));
constexpr SkColor kSearchIllustrationTextColor =
SkColorSetARGB(0xFF, 0x20, 0x21, 0x24);
auto text = std::make_unique<views::Label>(
l10n_util::GetStringUTF16(IDS_KSV_SEARCH_NO_RESULT));
text->SetEnabledColor(kSearchIllustrationTextColor);
constexpr int kLabelFontSizeDelta = 1;
ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
text->SetFontList(rb.GetFontListWithDelta(kLabelFontSizeDelta));
illustration_view->AddChildView(std::move(text));
return illustration_view;
}
class ShortcutsListScrollView : public views::ScrollView {
public:
ShortcutsListScrollView() {
GetViewAccessibility().OverrideRole(ax::mojom::Role::kScrollView);
}
~ShortcutsListScrollView() override = default;
// views::View:
void OnFocus() override {
SetHasFocusIndicator(true);
NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true);
}
void OnBlur() override { SetHasFocusIndicator(false); }
private:
DISALLOW_COPY_AND_ASSIGN(ShortcutsListScrollView);
};
std::unique_ptr<ShortcutsListScrollView> CreateScrollView(
std::unique_ptr<views::View> content_view) {
auto scroller = std::make_unique<ShortcutsListScrollView>();
scroller->SetDrawOverflowIndicator(false);
scroller->ClipHeightTo(0, 0);
scroller->SetContents(std::move(content_view));
scroller->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
return scroller;
}
void UpdateAXNodeDataPosition(
std::vector<KeyboardShortcutItemView*>& shortcut_items) {
// Update list item AXNodeData position for assistive tool.
const int number_shortcut_items = shortcut_items.size();
for (int i = 0; i < number_shortcut_items; ++i) {
shortcut_items.at(i)->GetViewAccessibility().OverridePosInSet(
i + 1, number_shortcut_items);
}
}
// Returns true if the given |item| should be excluded from the view, since
// certain shortcuts can be associated with a disabled feature behind a flag,
// or specific device property, e.g. keyboard layout.
bool ShouldExcludeItem(const KeyboardShortcutItem& item) {
switch (item.description_message_id) {
case IDS_KSV_DESCRIPTION_OPEN_GOOGLE_ASSISTANT:
return ui::DeviceKeyboardHasAssistantKey();
case IDS_KSV_DESCRIPTION_PRIVACY_SCREEN_TOGGLE:
return !ash::Shell::Get()->privacy_screen_controller()->IsSupported();
}
return false;
}
} // namespace
KeyboardShortcutView::~KeyboardShortcutView() {
DCHECK_EQ(g_ksv_view, this);
g_ksv_view = nullptr;
}
// static
views::Widget* KeyboardShortcutView::Toggle(aura::Window* context) {
if (g_ksv_view) {
if (g_ksv_view->GetWidget()->IsActive())
g_ksv_view->GetWidget()->Close();
else
g_ksv_view->GetWidget()->Activate();
} else {
const base::TimeTicks start_time = base::TimeTicks::Now();
TRACE_EVENT0("shortcut_viewer", "CreateWidget");
base::RecordAction(
base::UserMetricsAction("KeyboardShortcutViewer.CreateWindow"));
views::Widget::InitParams params;
params.delegate = new KeyboardShortcutView;
params.name = "KeyboardShortcutWidget";
// Intentionally don't set bounds. The window will be sized and centered
// based on CalculatePreferredSize().
views::Widget* widget = new views::Widget;
params.context = context;
params.init_properties_container.SetProperty(
aura::client::kAppType, static_cast<int>(ash::AppType::SYSTEM_APP));
widget->Init(std::move(params));
// Set frame view Active and Inactive colors, both are SK_ColorWHITE.
aura::Window* window = g_ksv_view->GetWidget()->GetNativeWindow();
window->SetProperty(chromeos::kFrameActiveColorKey, SK_ColorWHITE);
window->SetProperty(chromeos::kFrameInactiveColorKey, SK_ColorWHITE);
// Set shelf icon.
const ash::ShelfID shelf_id(ash::kInternalAppIdKeyboardShortcutViewer);
window->SetProperty(
ash::kAppIDKey,
new std::string(ash::kInternalAppIdKeyboardShortcutViewer));
window->SetProperty(ash::kShelfIDKey,
new std::string(shelf_id.Serialize()));
window->SetProperty<int>(ash::kShelfItemTypeKey, ash::TYPE_APP);
// We don't want the KSV window to have a title (per design), however the
// shelf uses the window title to set the shelf item's tooltip text. The
// shelf observes changes to the |kWindowIconKey| property and handles that
// by initializing the shelf item including its tooltip text.
// TODO(wutao): we can remove resource id IDS_KSV_TITLE after implementing
// internal app shelf launcher.
window->SetTitle(l10n_util::GetStringUTF16(IDS_KSV_TITLE));
gfx::ImageSkia* icon =
ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
IDR_SHORTCUT_VIEWER_LOGO_192);
// The new gfx::ImageSkia instance is owned by the window itself.
window->SetProperty(aura::client::kWindowIconKey,
new gfx::ImageSkia(*icon));
g_ksv_view->AddAccelerator(
ui::Accelerator(ui::VKEY_W, ui::EF_CONTROL_DOWN));
g_ksv_view->needs_init_all_categories_ = false;
g_ksv_view->did_first_paint_ = false;
g_ksv_view->GetWidget()->Show();
g_ksv_view->search_box_view_->search_box()->RequestFocus();
widget->GetCompositor()->RequestPresentationTimeForNextFrame(base::BindOnce(
[](base::TimeTicks start_time,
const gfx::PresentationFeedback& feedback) {
UMA_HISTOGRAM_TIMES("Keyboard.ShortcutViewer.StartupTime",
feedback.timestamp - start_time);
},
start_time));
}
return g_ksv_view->GetWidget();
}
const char* KeyboardShortcutView::GetClassName() const {
return "KeyboardShortcutView";
}
ax::mojom::Role KeyboardShortcutView::GetAccessibleWindowRole() {
return ax::mojom::Role::kWindow;
}
base::string16 KeyboardShortcutView::GetAccessibleWindowTitle() const {
return l10n_util::GetStringUTF16(IDS_KSV_TITLE);
}
bool KeyboardShortcutView::AcceleratorPressed(
const ui::Accelerator& accelerator) {
DCHECK_EQ(ui::VKEY_W, accelerator.key_code());
DCHECK_EQ(ui::EF_CONTROL_DOWN, accelerator.modifiers());
GetWidget()->Close();
return true;
}
void KeyboardShortcutView::Layout() {
gfx::Rect content_bounds(GetContentsBounds());
if (content_bounds.IsEmpty())
return;
constexpr int kSearchBoxTopPadding = 8;
constexpr int kSearchBoxBottomPadding = 16;
constexpr int kSearchBoxHorizontalPadding = 30;
const int left = content_bounds.x();
const int top = content_bounds.y();
gfx::Rect search_box_bounds(search_box_view_->GetPreferredSize());
search_box_bounds.set_width(
std::min(search_box_bounds.width(),
content_bounds.width() - 2 * kSearchBoxHorizontalPadding));
search_box_bounds.set_x(
left + (content_bounds.width() - search_box_bounds.width()) / 2);
search_box_bounds.set_y(top + kSearchBoxTopPadding);
search_box_view_->SetBoundsRect(search_box_bounds);
views::View* content_view = categories_tabbed_pane_->GetVisible()
? categories_tabbed_pane_
: search_results_container_;
const int search_box_used_height = search_box_bounds.height() +
kSearchBoxTopPadding +
kSearchBoxBottomPadding;
content_view->SetBounds(left, top + search_box_used_height,
content_bounds.width(),
content_bounds.height() - search_box_used_height);
}
gfx::Size KeyboardShortcutView::CalculatePreferredSize() const {
return gfx::Size(800, 512);
}
void KeyboardShortcutView::OnPaint(gfx::Canvas* canvas) {
views::View::OnPaint(canvas);
// Skip if it is the first OnPaint event.
if (!did_first_paint_) {
did_first_paint_ = true;
needs_init_all_categories_ = true;
return;
}
if (!needs_init_all_categories_)
return;
needs_init_all_categories_ = false;
// Cannot post a task right after initializing the first category, it will
// have a chance to end up in the same group of drawing commands sent to
// compositor. We can wait for the second OnPaint, which means previous
// drawing commands have been sent to compositor for the next frame and new
// coming commands will be sent for the next-next frame.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&KeyboardShortcutView::InitCategoriesTabbedPane,
weak_factory_.GetWeakPtr(), kAllCategories));
}
void KeyboardShortcutView::QueryChanged(ash::SearchBoxViewBase* sender) {
const bool query_empty = sender->IsSearchBoxTrimmedQueryEmpty();
if (is_search_box_empty_ != query_empty) {
is_search_box_empty_ = query_empty;
UpdateViewsLayout(/*is_search_box_active=*/true);
}
debounce_timer_.Stop();
// If search box is empty, do not show |search_results_container_|.
if (query_empty)
return;
// TODO(wutao): This timeout value is chosen based on subjective search
// latency tests on Minnie. Objective method or UMA is desired.
constexpr base::TimeDelta kTimeOut(base::TimeDelta::FromMilliseconds(250));
debounce_timer_.Start(
FROM_HERE, kTimeOut,
base::BindOnce(&KeyboardShortcutView::ShowSearchResults,
base::Unretained(this), sender->search_box()->GetText()));
}
void KeyboardShortcutView::BackButtonPressed() {
search_box_view_->ClearSearch();
search_box_view_->SetSearchBoxActive(false, ui::ET_UNKNOWN);
}
void KeyboardShortcutView::ActiveChanged(ash::SearchBoxViewBase* sender) {
const bool is_search_box_active = sender->is_search_box_active();
is_search_box_empty_ = sender->IsSearchBoxTrimmedQueryEmpty();
if (is_search_box_active) {
base::RecordAction(
base::UserMetricsAction("KeyboardShortcutViewer.Search"));
}
UpdateViewsLayout(is_search_box_active);
}
KeyboardShortcutView::KeyboardShortcutView() {
DCHECK_EQ(g_ksv_view, nullptr);
g_ksv_view = this;
SetCanMinimize(true);
SetShowTitle(false);
// Default background is transparent.
SetBackground(views::CreateSolidBackground(SK_ColorWHITE));
InitViews();
}
void KeyboardShortcutView::InitViews() {
TRACE_EVENT0("shortcut_viewer", "InitViews");
// Init search box view.
search_box_view_ = std::make_unique<KSVSearchBoxView>(this);
search_box_view_->Init();
AddChildView(search_box_view_.get());
// Init no search result illustration view.
search_no_result_view_ = CreateNoSearchResultView();
search_no_result_view_->set_owned_by_client();
// Init search results container view.
search_results_container_ = AddChildView(std::make_unique<views::View>());
search_results_container_->SetLayoutManager(
std::make_unique<views::FillLayout>());
search_results_container_->SetVisible(false);
// Init views of KeyboardShortcutItemView.
// TODO(https://crbug.com/843394): Observe changes in keyboard layout and
// clear the cache.
KeyboardShortcutItemView::ClearKeycodeToString16Cache();
for (const auto& item : GetKeyboardShortcutItemList()) {
if (ShouldExcludeItem(item))
continue;
for (auto category : item.categories) {
shortcut_views_.push_back(
std::make_unique<KeyboardShortcutItemView>(item, category));
shortcut_views_.back()->set_owned_by_client();
}
}
std::sort(shortcut_views_.begin(), shortcut_views_.end(),
[](const auto& lhs, const auto& rhs) {
if (lhs->category() != rhs->category())
return lhs->category() < rhs->category();
return lhs->description_label_view()->GetText() <
rhs->description_label_view()->GetText();
});
// Init views of |categories_tabbed_pane_| and KeyboardShortcutItemListViews.
categories_tabbed_pane_ = AddChildView(std::make_unique<views::TabbedPane>(
views::TabbedPane::Orientation::kVertical,
views::TabbedPane::TabStripStyle::kHighlight));
// Initial Layout of KeyboardShortcutItemView is time consuming. To speed up
// the startup time, we only initialize the first category pane, which is
// visible to user, and defer initialization of other categories in the
// background.
InitCategoriesTabbedPane(ShortcutCategory::kPopular);
}
void KeyboardShortcutView::InitCategoriesTabbedPane(
base::Optional<ShortcutCategory> initial_category) {
active_tab_index_ = categories_tabbed_pane_->GetSelectedTabIndex();
// If the tab count is 0, GetSelectedTabIndex() will return kNoSelectedTab,
// which we do not want to cache.
if (active_tab_index_ == views::TabStrip::kNoSelectedTab)
active_tab_index_ = 0;
ShortcutCategory current_category = ShortcutCategory::kUnknown;
KeyboardShortcutItemListView* item_list_view = nullptr;
std::vector<KeyboardShortcutItemView*> shortcut_items;
const bool already_has_tabs = categories_tabbed_pane_->GetTabCount() > 0;
size_t tab_index = 0;
views::View* const tab_contents = categories_tabbed_pane_->children()[1];
for (const auto& item_view : shortcut_views_) {
const ShortcutCategory category = item_view->category();
DCHECK_NE(ShortcutCategory::kUnknown, category);
if (current_category != category) {
current_category = category;
std::unique_ptr<views::View> content_view;
// Delay constructing a KeyboardShortcutItemListView until it is needed.
if (initial_category.value_or(category) == category) {
auto list_view = std::make_unique<KeyboardShortcutItemListView>();
item_list_view = list_view.get();
// When in a new category, update the node data of the shortcut items in
// previous category and clear the vector in order to store items in
// current category.
UpdateAXNodeDataPosition(shortcut_items);
shortcut_items.clear();
content_view = std::move(list_view);
} else {
content_view = std::make_unique<views::View>();
}
// Create new tabs or update the existing tabs' contents.
if (already_has_tabs) {
auto* scroll_view = static_cast<views::ScrollView*>(
tab_contents->children()[tab_index]);
scroll_view->SetContents(std::move(content_view));
} else {
categories_tabbed_pane_->AddTab(
GetStringForCategory(current_category),
CreateScrollView(std::move(content_view)));
}
++tab_index;
}
// If |initial_category| has a value, we only initialize the pane with the
// KeyboardShortcutItemView in the specific category in |initial_category|.
// Otherwise, we will initialize all the panes.
if (initial_category.value_or(category) != category)
continue;
// Add the item to the category contents container.
if (!item_list_view->children().empty())
item_list_view->AddHorizontalSeparator();
views::StyledLabel* description_label_view =
item_view->description_label_view();
// Clear any styles used to highlight matched search query in search mode.
description_label_view->ClearStyleRanges();
item_list_view->AddChildView(item_view.get());
shortcut_items.push_back(item_view.get());
// Remove the search query highlight.
description_label_view->InvalidateLayout();
}
// Update node data for the last category.
UpdateAXNodeDataPosition(shortcut_items);
tab_contents->InvalidateLayout();
}
void KeyboardShortcutView::UpdateViewsLayout(bool is_search_box_active) {
// 1. Search box is not active: show |categories_tabbed_pane_| and focus on
// active tab.
// 2. Search box is active and empty: show |categories_tabbed_pane_| but focus
// on search box.
// 3. Search box is not empty, show |search_results_container_|. Focus is on
// search box.
const bool should_show_search_results =
is_search_box_active && !is_search_box_empty_;
if (!should_show_search_results) {
// Remove all child views, including horizontal separator lines, to prepare
// for showing search results next time.
search_results_container_->RemoveAllChildViews(true);
if (!categories_tabbed_pane_->GetVisible()) {
// Repopulate |categories_tabbed_pane_| child views, which were removed
// when they were added to |search_results_container_|.
InitCategoriesTabbedPane(kAllCategories);
// Select the category that was active before entering search mode.
categories_tabbed_pane_->SelectTabAt(active_tab_index_);
}
}
categories_tabbed_pane_->SetVisible(!should_show_search_results);
search_results_container_->SetVisible(should_show_search_results);
InvalidateLayout();
}
void KeyboardShortcutView::ShowSearchResults(
const base::string16& search_query) {
search_results_container_->RemoveAllChildViews(true);
auto* search_container_content_view = search_no_result_view_.get();
auto found_items_list_view = std::make_unique<KeyboardShortcutItemListView>();
base::i18n::FixedPatternStringSearchIgnoringCaseAndAccents finder(
search_query);
ShortcutCategory current_category = ShortcutCategory::kUnknown;
bool has_category_item = false;
found_shortcut_items_.clear();
for (const auto& item_view : shortcut_views_) {
base::string16 description_text =
item_view->description_label_view()->GetText();
base::string16 shortcut_text = item_view->shortcut_label_view()->GetText();
size_t match_index = -1;
size_t match_length = 0;
// Only highlight |description_label_view_| in KeyboardShortcutItemView.
// |shortcut_label_view_| has customized style ranges for bubble views
// so it may have overlappings with the searched ranges. The highlighted
// behaviors are not defined so we don't highlight
// |shortcut_label_view_|.
if (finder.Search(description_text, &match_index, &match_length) ||
finder.Search(shortcut_text, nullptr, nullptr)) {
const ShortcutCategory category = item_view->category();
if (current_category != category) {
current_category = category;
has_category_item = false;
found_items_list_view->AddCategoryLabel(GetStringForCategory(category));
}
if (has_category_item)
found_items_list_view->AddHorizontalSeparator();
else
has_category_item = true;
// Highlight matched query in |description_label_view_|.
if (match_length > 0) {
views::StyledLabel::RangeStyleInfo style;
views::StyledLabel* description_label_view =
item_view->description_label_view();
// Clear previous styles.
description_label_view->ClearStyleRanges();
style.text_style = ash::AshTextStyle::STYLE_EMPHASIZED;
description_label_view->AddStyleRange(
gfx::Range(match_index, match_index + match_length), style);
// Apply new styles to highlight matched search query.
description_label_view->InvalidateLayout();
}
found_items_list_view->AddChildView(item_view.get());
found_shortcut_items_.push_back(item_view.get());
}
}
std::vector<base::string16> replacement_strings;
const int number_search_results = found_shortcut_items_.size();
if (!found_items_list_view->children().empty()) {
UpdateAXNodeDataPosition(found_shortcut_items_);
replacement_strings.push_back(
base::NumberToString16(number_search_results));
// To offset the padding between the bottom of the |search_box_view_| and
// the top of the |search_results_container_|.
constexpr int kTopPadding = -16;
constexpr int kHorizontalPadding = 128;
found_items_list_view->SetBorder(views::CreateEmptyBorder(
gfx::Insets(kTopPadding, kHorizontalPadding, 0, kHorizontalPadding)));
search_container_content_view =
CreateScrollView(std::move(found_items_list_view)).release();
}
replacement_strings.push_back(search_query);
search_box_view_->SetAccessibleValue(l10n_util::GetStringFUTF16(
number_search_results == 0
? IDS_KSV_SEARCH_BOX_ACCESSIBILITY_VALUE_WITHOUT_RESULTS
: IDS_KSV_SEARCH_BOX_ACCESSIBILITY_VALUE_WITH_RESULTS,
replacement_strings, nullptr));
search_results_container_->AddChildView(search_container_content_view);
InvalidateLayout();
}
views::ClientView* KeyboardShortcutView::CreateClientView(
views::Widget* widget) {
return new views::ClientView(widget, this);
}
KeyboardShortcutView* KeyboardShortcutView::GetInstanceForTesting() {
return g_ksv_view;
}
size_t KeyboardShortcutView::GetTabCountForTesting() const {
return categories_tabbed_pane_->GetTabCount();
}
const std::vector<std::unique_ptr<KeyboardShortcutItemView>>&
KeyboardShortcutView::GetShortcutViewsForTesting() const {
return shortcut_views_;
}
KSVSearchBoxView* KeyboardShortcutView::GetSearchBoxViewForTesting() {
return search_box_view_.get();
}
const std::vector<KeyboardShortcutItemView*>&
KeyboardShortcutView::GetFoundShortcutItemsForTesting() const {
return found_shortcut_items_;
}
} // namespace keyboard_shortcut_viewer
namespace ash {
void ToggleKeyboardShortcutViewer() {
keyboard_shortcut_viewer::KeyboardShortcutView::Toggle(nullptr);
}
} // namespace ash