Commit df417eaa authored by Weidong Guo's avatar Weidong Guo Committed by Commit Bot

applist-focus: Make SearchBoxTextfield listen to key event any time

Changes:
1. Override OnKeyEvent in AppListView and SearchBoxView to redirect key
   event to SearchBoxTextfield through Textfield::InsertChar() and
   Textfield::OnKeyEvent().
2. Make SearchBoxTextfield focused when query changes in
   SearchBoxView::ContentsChanged or key event is handled by
   Textfield::OnKeyEvent().
3. Add a SetFirstResultSelected() in SearchResultContainerView to set a
   fake focus (highlight background) on first search result whenever
   query is updated. Hitting enter when SearchBoxTextfield is focued
   opens the first search result.
4. Add test coverage: a. Type query when focus is on close button,
   suggestion app, expand arrow. b. First result is selected when query
   updated. c. Hitting Enter when search box is focused opens the first
   result.

Design doc: go/applist-focus

BUG=767996
TEST=AppListViewFocusTest

Change-Id: I5e523c4d2d392b888d8029043dab843010031080
Reviewed-on: https://chromium-review.googlesource.com/679157Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Commit-Queue: Weidong Guo <weidongg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#504159}
parent 3229a83e
......@@ -72,6 +72,8 @@ class AppListFolderView : public views::View,
AppsGridView* items_grid_view() { return items_grid_view_; }
FolderHeaderView* folder_header_view() { return folder_header_view_; }
private:
void CalculateIdealBounds();
......
......@@ -230,6 +230,7 @@ AppListView::AppListView(AppListViewDelegate* delegate)
short_animations_for_testing_(false),
is_fullscreen_app_list_enabled_(features::IsFullscreenAppListEnabled()),
is_background_blur_enabled_(features::IsBackgroundBlurEnabled()),
is_app_list_focus_enabled_(features::IsAppListFocusEnabled()),
display_observer_(this),
animation_observer_(new HideViewAnimationObserver()) {
CHECK(delegate);
......@@ -1031,6 +1032,32 @@ void AppListView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
node_data->role = ui::AX_ROLE_DESKTOP;
}
void AppListView::OnKeyEvent(ui::KeyEvent* event) {
views::Textfield* search_box = search_box_view_->search_box();
bool is_search_box_focused = search_box->HasFocus();
bool is_folder_header_view_focused = app_list_main_view_->contents_view()
->apps_container_view()
->app_list_folder_view()
->folder_header_view()
->HasTextFocus();
if (is_app_list_focus_enabled_ && !is_search_box_focused &&
!is_folder_header_view_focused) {
views::Textfield* search_box = search_box_view_->search_box();
// Redirect key event to |search_box_|.
search_box->OnKeyEvent(event);
if (event->handled()) {
// Set search box focused if the key event is consumed.
search_box->RequestFocus();
return;
}
if (event->type() == ui::ET_KEY_PRESSED) {
// Insert it into search box if the key event is a character. Released
// key should not be handled to prevent inserting duplicate character.
search_box->InsertChar(*event);
}
}
}
void AppListView::OnTabletModeChanged(bool started) {
is_tablet_mode_ = started;
search_box_view_->OnTabletModeChanged(started);
......
......@@ -138,6 +138,7 @@ class APP_LIST_EXPORT AppListView : public views::BubbleDialogDelegateView,
void Layout() override;
void SchedulePaintInRect(const gfx::Rect& rect) override;
void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
void OnKeyEvent(ui::KeyEvent* event) override;
// Overridden from ui::EventHandler:
void OnScrollEvent(ui::ScrollEvent* event) override;
......@@ -343,6 +344,8 @@ class APP_LIST_EXPORT AppListView : public views::BubbleDialogDelegateView,
const bool is_fullscreen_app_list_enabled_;
// Whether the background blur is enabled.
const bool is_background_blur_enabled_;
// Whether the app list focus is enabled.
const bool is_app_list_focus_enabled_;
// The state of the app list, controlled via SetState().
AppListState app_list_state_ = PEEKING;
// An observer that notifies AppListView when the display has changed.
......
......@@ -373,7 +373,7 @@ class AppListViewFocusTest : public views::ViewsTestBase {
for (const auto& data : result_types) {
// Set the relevance of the results in each group in decreasing order (so
// the earlier groups have higher relevance, and therefore appear first).
relevance -= 1.0;
relevance -= 0.5;
for (int i = 0; i < data.second; ++i) {
std::unique_ptr<TestSearchResult> result =
base::MakeUnique<TestSearchResult>();
......@@ -387,6 +387,17 @@ class AppListViewFocusTest : public views::ViewsTestBase {
RunPendingMessages();
}
int GetOpenFirstSearchResultCount() {
std::map<size_t, int>& counts = delegate_->open_search_result_counts();
if (counts.size() == 0)
return 0;
return counts[0];
}
int GetTotalOpenSearchResultCount() {
return delegate_->open_search_result_count();
}
AppListView* app_list_view() { return view_; }
AppListMainView* main_view() { return view_->app_list_main_view(); }
......@@ -548,6 +559,100 @@ TEST_F(AppListViewFocusTest, FocusResetAfterStateTransition) {
EXPECT_EQ(search_box_view()->search_box(), focused_view());
}
// Tests that key event which is not handled by focused view will be redirected
// to search box.
TEST_F(AppListViewFocusTest, RedirectFocusToSearchBox) {
Show();
// Set focus to first suggestion app and type a character.
suggestions_container_view()->tile_views()[0]->RequestFocus();
SimulateKeyPress(ui::VKEY_SPACE, false);
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(search_box_view()->search_box()->text(), base::UTF8ToUTF16(" "));
// Set focus to expand arrow and type a character.
apps_grid_view()->expand_arrow_view_for_test()->RequestFocus();
SimulateKeyPress(ui::VKEY_A, false);
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(search_box_view()->search_box()->text(), base::UTF8ToUTF16(" a"));
// Set focus to close button and type a character.
search_box_view()->close_button()->RequestFocus();
SimulateKeyPress(ui::VKEY_B, false);
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(search_box_view()->search_box()->text(), base::UTF8ToUTF16(" ab"));
// Set focus to close button and hitting backspace.
search_box_view()->close_button()->RequestFocus();
SimulateKeyPress(ui::VKEY_BACK, false);
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(search_box_view()->search_box()->text(), base::UTF8ToUTF16(" a"));
}
// Tests that the first search result's view is always selected after search
// results are updated, but the focus is always on search box.
TEST_F(AppListViewFocusTest, FirstResultSelectedAfterSearchResultsUpdated) {
Show();
// Type something in search box to transition to HALF state and populate
// fake list results.
search_box_view()->search_box()->InsertText(base::UTF8ToUTF16("test"));
const int kListResults = 2;
SetUpSearchResults(0, kListResults);
const views::View* results_container =
contents_view()
->search_result_list_view_for_test()
->results_container_for_test();
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(results_container->child_at(0),
contents_view()->search_results_page_view()->first_result_view());
// Populate both fake list results and tile results.
const int kTileResults = 3;
SetUpSearchResults(kTileResults, kListResults);
const std::vector<SearchResultTileItemView*>& tile_views =
contents_view()
->search_result_tile_item_list_view_for_test()
->tile_views_for_test();
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(tile_views[0],
contents_view()->search_results_page_view()->first_result_view());
// Clear up all search results.
SetUpSearchResults(0, 0);
EXPECT_EQ(search_box_view()->search_box(), focused_view());
EXPECT_EQ(nullptr,
contents_view()->search_results_page_view()->first_result_view());
}
// Tests that hitting Enter key when focus is on search box opens the first
// result when it exists.
TEST_F(AppListViewFocusTest, HittingEnterWhenFocusOnSearchBox) {
Show();
// Type something in search box to transition to HALF state and populate
// fake list results. Then hit Enter key.
search_box_view()->search_box()->InsertText(base::UTF8ToUTF16("test"));
const int kListResults = 2;
SetUpSearchResults(0, kListResults);
SimulateKeyPress(ui::VKEY_RETURN, false);
EXPECT_EQ(1, GetOpenFirstSearchResultCount());
EXPECT_EQ(1, GetTotalOpenSearchResultCount());
// Populate both fake list results and tile results. Then hit Enter key.
const int kTileResults = 3;
SetUpSearchResults(kTileResults, kListResults);
SimulateKeyPress(ui::VKEY_RETURN, false);
EXPECT_EQ(2, GetOpenFirstSearchResultCount());
EXPECT_EQ(2, GetTotalOpenSearchResultCount());
// Clear up all search results. Then hit Enter key.
SetUpSearchResults(0, 0);
SimulateKeyPress(ui::VKEY_RETURN, false);
EXPECT_EQ(2, GetOpenFirstSearchResultCount());
EXPECT_EQ(2, GetTotalOpenSearchResultCount());
}
// Tests that opening the app list opens in peeking mode by default.
TEST_F(AppListViewFullscreenTest, ShowPeekingByDefault) {
Initialize(0, false, false);
......
......@@ -657,6 +657,10 @@ void SearchBoxView::OnMouseEvent(ui::MouseEvent* event) {
HandleSearchBoxEvent(event);
}
void SearchBoxView::OnKeyEvent(ui::KeyEvent* event) {
app_list_view_->OnKeyEvent(event);
}
void SearchBoxView::ButtonPressed(views::Button* sender,
const ui::Event& event) {
if (back_button_ && sender == back_button_) {
......@@ -773,9 +777,10 @@ void SearchBoxView::SetSelected(bool selected) {
kSearchBoxBorderWidth, kSearchBoxFocusBorderCornerRadius,
kSearchBoxBorderColor));
}
if (!search_box_->text().empty()) {
if (!is_app_list_focus_enabled_ && !search_box_->text().empty()) {
// If query is not empty (including a string of spaces), we need to select
// the entire text range.
// the entire text range. When app list focus flag is enabled, query is
// automatically selected all when search box is focused.
search_box_->SelectAll(false);
}
} else {
......@@ -800,6 +805,10 @@ void SearchBoxView::NotifyQueryChanged() {
void SearchBoxView::ContentsChanged(views::Textfield* sender,
const base::string16& new_contents) {
if (is_app_list_focus_enabled_) {
// Set search box focused when query changes.
search_box_->RequestFocus();
}
UpdateModel();
view_delegate_->AutoLaunchCanceled();
NotifyQueryChanged();
......@@ -819,6 +828,18 @@ void SearchBoxView::ContentsChanged(views::Textfield* sender,
bool SearchBoxView::HandleKeyEvent(views::Textfield* sender,
const ui::KeyEvent& key_event) {
if (is_app_list_focus_enabled_) {
if (key_event.type() == ui::ET_KEY_PRESSED &&
key_event.key_code() == ui::VKEY_RETURN &&
!IsSearchBoxTrimmedQueryEmpty()) {
// Hitting Enter when focus is on search box opens the first result.
ui::KeyEvent event(key_event);
views::View* first_result_view =
static_cast<ContentsView*>(contents_view_)
->search_results_page_view()
->first_result_view();
if (first_result_view)
first_result_view->OnKeyEvent(&event);
}
// TODO(weidongg/766807) Remove this function when the flag is enabled by
// default.
return false;
......
......@@ -117,6 +117,7 @@ class APP_LIST_EXPORT SearchBoxView : public views::View,
const char* GetClassName() const override;
void OnGestureEvent(ui::GestureEvent* event) override;
void OnMouseEvent(ui::MouseEvent* event) override;
void OnKeyEvent(ui::KeyEvent* evetn) override;
// Overridden from views::ButtonListener:
void ButtonPressed(views::Button* sender, const ui::Event& event) override;
......
......@@ -199,4 +199,9 @@ views::View* SearchResultAnswerCardView::GetSelectedView() const {
: nullptr;
}
views::View* SearchResultAnswerCardView::SetFirstResultSelected(bool selected) {
search_answer_container_view_->SetSelected(selected);
return search_answer_container_view_;
}
} // namespace app_list
......@@ -30,6 +30,7 @@ class APP_LIST_EXPORT SearchResultAnswerCardView
void UpdateSelectedIndex(int old_selected, int new_selected) override;
bool OnKeyPressed(const ui::KeyEvent& event) override;
views::View* GetSelectedView() const override;
views::View* SetFirstResultSelected(bool selected) override;
private:
class SearchAnswerContainerView;
......
......@@ -87,6 +87,10 @@ class APP_LIST_EXPORT SearchResultContainerView : public views::View,
// Returns selected view in this container view.
virtual views::View* GetSelectedView() const = 0;
// Sets the first result in this container view selected/unselected. Returns
// the result's view.
virtual views::View* SetFirstResultSelected(bool selected) = 0;
private:
// Schedules an Update call using |update_factory_|. Do nothing if there is a
// pending call.
......
......@@ -177,6 +177,16 @@ views::View* SearchResultListView::GetSelectedView() const {
: nullptr;
}
views::View* SearchResultListView::SetFirstResultSelected(bool selected) {
DCHECK(results_container_->has_children());
if (num_results() <= 0)
return nullptr;
SearchResultView* search_result_view =
static_cast<SearchResultView*>(results_container_->child_at(0));
search_result_view->SetSelected(selected);
return search_result_view;
}
int SearchResultListView::DoUpdate() {
std::vector<SearchResult*> display_results =
AppListModel::FilterSearchResultsByDisplayType(
......
......@@ -60,6 +60,7 @@ class APP_LIST_EXPORT SearchResultListView : public gfx::AnimationDelegate,
void NotifyFirstResultYIndex(int y_index) override;
int GetYSize() override;
views::View* GetSelectedView() const override;
views::View* SetFirstResultSelected(bool selected) override;
views::View* results_container_for_test() const { return results_container_; }
......
......@@ -310,28 +310,7 @@ bool SearchResultPageView::IsValidSelectionIndex(int index) {
return index >= 0 && index < static_cast<int>(result_container_views_.size());
}
void SearchResultPageView::OnSearchResultContainerResultsChanged() {
DCHECK(!result_container_views_.empty());
if (is_fullscreen_app_list_enabled_)
DCHECK(result_container_views_.size() == separators_.size() + 1);
// Only sort and layout the containers when they have all updated.
for (SearchResultContainerView* view : result_container_views_) {
if (view->UpdateScheduled()) {
return;
}
}
SearchResultContainerView* old_selection =
HasSelection() ? result_container_views_[selected_index_] : nullptr;
// Truncate the currently selected container's selection if necessary. If
// there are no results, the selection will be cleared below.
if (old_selection && old_selection->num_results() > 0 &&
old_selection->selected_index() >= old_selection->num_results()) {
old_selection->SetSelectedIndex(old_selection->num_results() - 1);
}
void SearchResultPageView::ReorderSearchResultContainers() {
// Sort the result container views by their score.
std::sort(result_container_views_.begin(), result_container_views_.end(),
[](const SearchResultContainerView* a,
......@@ -365,6 +344,47 @@ void SearchResultPageView::OnSearchResultContainerResultsChanged() {
}
Layout();
}
void SearchResultPageView::OnSearchResultContainerResultsChanged() {
DCHECK(!result_container_views_.empty());
if (is_fullscreen_app_list_enabled_)
DCHECK(result_container_views_.size() == separators_.size() + 1);
// Only sort and layout the containers when they have all updated.
for (SearchResultContainerView* view : result_container_views_) {
if (view->UpdateScheduled()) {
return;
}
}
if (is_app_list_focus_enabled_) {
if (result_container_views_.empty())
return;
// Set the first result (if it exists) selected when search results are
// updated. Note that the focus is not set on the first result to prevent
// frequent focus switch between search box and first result during typing
// query.
SearchResultContainerView* old_first_container_view =
result_container_views_[0];
ReorderSearchResultContainers();
old_first_container_view->SetFirstResultSelected(false);
first_result_view_ =
result_container_views_[0]->SetFirstResultSelected(true);
return;
}
SearchResultContainerView* old_selection =
HasSelection() ? result_container_views_[selected_index_] : nullptr;
// Truncate the currently selected container's selection if necessary. If
// there are no results, the selection will be cleared below.
if (old_selection && old_selection->num_results() > 0 &&
old_selection->selected_index() >= old_selection->num_results()) {
old_selection->SetSelectedIndex(old_selection->num_results() - 1);
}
ReorderSearchResultContainers();
SearchResultContainerView* new_selection = nullptr;
if (HasSelection() &&
......
......@@ -58,6 +58,8 @@ class APP_LIST_EXPORT SearchResultPageView
views::View* contents_view() { return contents_view_; }
views::View* first_result_view() const { return first_result_view_; }
private:
// Separator between SearchResultContainerView.
class HorizontalSeparator;
......@@ -67,6 +69,9 @@ class APP_LIST_EXPORT SearchResultPageView
void SetSelectedIndex(int index, bool directional_movement);
bool IsValidSelectionIndex(int index);
// Sort the result container views.
void ReorderSearchResultContainers();
// The SearchResultContainerViews that compose the search page. All owned by
// the views hierarchy.
std::vector<SearchResultContainerView*> result_container_views_;
......@@ -84,6 +89,9 @@ class APP_LIST_EXPORT SearchResultPageView
// View containing SearchCardView instances. Owned by view hierarchy.
views::View* const contents_view_;
// The first search result's view or nullptr if there's no search result.
views::View* first_result_view_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(SearchResultPageView);
};
......
......@@ -119,6 +119,15 @@ views::View* SearchResultTileItemListView::GetSelectedView() const {
: nullptr;
}
views::View* SearchResultTileItemListView::SetFirstResultSelected(
bool selected) {
DCHECK(!tile_views_.empty());
if (num_results() <= 0)
return nullptr;
tile_views_[0]->SetSelected(selected);
return tile_views_[0];
}
int SearchResultTileItemListView::DoUpdate() {
std::vector<SearchResult*> display_results =
AppListModel::FilterSearchResultsByDisplayType(
......
......@@ -34,6 +34,7 @@ class APP_LIST_EXPORT SearchResultTileItemListView
void NotifyFirstResultYIndex(int y_index) override;
int GetYSize() override;
views::View* GetSelectedView() const override;
views::View* SetFirstResultSelected(bool selected) override;
// Overridden from views::View:
bool OnKeyPressed(const ui::KeyEvent& event) override;
......
......@@ -182,6 +182,13 @@ base::string16 SearchResultView::ComputeAccessibleName() const {
return accessible_name;
}
void SearchResultView::SetSelected(bool selected) {
if (selected_ == selected)
return;
selected_ = selected;
SchedulePaint();
}
void SearchResultView::UpdateAccessibleName() {
SetAccessibleName(ComputeAccessibleName());
}
......@@ -507,13 +514,6 @@ void SearchResultView::OnSearchResultActionActivated(size_t index,
list_view_->SearchResultActionActivated(this, index, event_flags);
}
void SearchResultView::SetSelected(bool selected) {
if (selected_ == selected)
return;
selected_ = selected;
SchedulePaint();
}
void SearchResultView::ShowContextMenuForView(views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
......
......@@ -67,6 +67,9 @@ class APP_LIST_EXPORT SearchResultView
void set_is_last_result(bool is_last) { is_last_result_ = is_last; }
void SetSelected(bool selected);
bool selected() const { return selected_; }
private:
friend class app_list::test::SearchResultListViewTest;
......@@ -111,9 +114,6 @@ class APP_LIST_EXPORT SearchResultView
// SearchResultActionsViewDelegate overrides:
void OnSearchResultActionActivated(size_t index, int event_flags) override;
void SetSelected(bool selected);
bool selected() const { return selected_; }
SearchResult* result_ = nullptr; // Owned by AppListModel::SearchResults.
bool is_last_result_ = false;
......
......@@ -138,6 +138,10 @@ views::View* SuggestionsContainerView::GetSelectedView() const {
: nullptr;
}
views::View* SuggestionsContainerView::SetFirstResultSelected(bool selected) {
return nullptr;
}
void SuggestionsContainerView::CreateAppsGrid(int apps_num) {
DCHECK(search_result_tile_views_.empty());
views::GridLayout* tiles_layout_manager =
......
......@@ -43,6 +43,7 @@ class SuggestionsContainerView : public SearchResultContainerView {
void NotifyFirstResultYIndex(int y_index) override;
int GetYSize() override;
views::View* GetSelectedView() const override;
views::View* SetFirstResultSelected(bool selected) override;
private:
void CreateAppsGrid(int apps_num);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment