| // Copyright 2019 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/app_list/views/result_selection_controller.h" |
| |
| #include "ash/app_list/app_list_util.h" |
| #include "ash/app_list/views/search_result_container_view.h" |
| |
| namespace app_list { |
| |
| ResultLocationDetails::ResultLocationDetails(int container_index, |
| int container_count, |
| int result_index, |
| int result_count, |
| bool container_is_horizontal) |
| : container_index(container_index), |
| container_count(container_count), |
| result_index(result_index), |
| result_count(result_count), |
| container_is_horizontal(container_is_horizontal) {} |
| |
| bool ResultLocationDetails::operator==( |
| const ResultLocationDetails& other) const { |
| return container_index == other.container_index && |
| container_count == other.container_count && |
| result_index == other.result_index && |
| result_count == other.result_count && |
| container_is_horizontal == other.container_is_horizontal; |
| } |
| |
| bool ResultLocationDetails::operator!=( |
| const ResultLocationDetails& other) const { |
| return !(*this == other); |
| } |
| |
| ResultSelectionController::ResultSelectionController( |
| const ResultSelectionModel* result_container_views) |
| : result_selection_model_(result_container_views) {} |
| |
| ResultSelectionController::~ResultSelectionController() = default; |
| |
| bool ResultSelectionController::MoveSelection(const ui::KeyEvent& event) { |
| ResultLocationDetails next_location = GetNextResultLocation(event); |
| bool selection_changed = !(next_location == *selected_location_details_); |
| SetSelection(next_location); |
| return selection_changed; |
| } |
| |
| void ResultSelectionController::ResetSelection() { |
| // Prevents crash on start up |
| if (result_selection_model_->size() == 0) |
| return; |
| ClearSelection(); |
| selected_location_details_ = std::make_unique<ResultLocationDetails>( |
| 0 /* container_index */, |
| result_selection_model_->size() /* container_count */, |
| 0 /* result_index */, |
| result_selection_model_->at(0)->num_results() /* result_count */, |
| result_selection_model_->at(0) |
| ->horizontally_traversable() /* container_is_horizontal */); |
| |
| selected_result_ = result_selection_model_->at(0)->GetFirstResultView(); |
| if (selected_result_) |
| selected_result_->SetBackgroundHighlighted(true); |
| } |
| |
| void ResultSelectionController::ClearSelection() { |
| selected_location_details_ = nullptr; |
| if (selected_result_) |
| selected_result_->SetBackgroundHighlighted(false); |
| selected_result_ = nullptr; |
| } |
| |
| ResultLocationDetails ResultSelectionController::GetNextResultLocation( |
| const ui::KeyEvent& event) { |
| return GetNextResultLocationForLocation(event, *selected_location_details_); |
| } |
| |
| ResultLocationDetails |
| ResultSelectionController::GetNextResultLocationForLocation( |
| const ui::KeyEvent& event, |
| const ResultLocationDetails& location) { |
| ResultLocationDetails new_location(location); |
| |
| // Only arrow keys (unhandled and unmodified) or the tab key will change our |
| // selection. |
| if (!(IsUnhandledArrowKeyEvent(event) || event.key_code() == ui::VKEY_TAB)) |
| return new_location; |
| |
| switch (event.key_code()) { |
| case ui::VKEY_TAB: |
| if (event.IsShiftDown()) { |
| // Reverse tab traversal always goes to the 'previous' result. |
| if (location.is_first_result()) |
| ChangeContainer(&new_location, location.container_index - 1); |
| else |
| --new_location.result_index; |
| } else { |
| // Forward tab traversal always goes to the 'next' result. |
| if (location.is_last_result()) |
| ChangeContainer(&new_location, location.container_index + 1); |
| else |
| ++new_location.result_index; |
| } |
| |
| break; |
| case ui::VKEY_UP: |
| if (location.container_is_horizontal || location.is_first_result()) { |
| // Traversing 'up' from the top of a container changes containers. |
| ChangeContainer(&new_location, location.container_index - 1); |
| } else { |
| // Traversing 'up' moves up one result. |
| --new_location.result_index; |
| } |
| break; |
| case ui::VKEY_DOWN: |
| if (location.container_is_horizontal || location.is_last_result()) { |
| // Traversing 'down' from the bottom of a container changes containers. |
| ChangeContainer(&new_location, location.container_index + 1); |
| } else { |
| // Traversing 'down' moves down one result. |
| ++new_location.result_index; |
| } |
| break; |
| case ui::VKEY_RIGHT: |
| case ui::VKEY_LEFT: { |
| // Containers are stacked, so left/right does not traverse vertical |
| // containers. |
| if (!location.container_is_horizontal) |
| break; |
| |
| ui::KeyboardCode forward = ui::VKEY_RIGHT; |
| |
| // If RTL is active, 'forward' is left instead. |
| if (base::i18n::IsRTL()) |
| forward = ui::VKEY_LEFT; |
| |
| // Traversing should move one result, but loop within the |
| // container. |
| if (event.key_code() == forward) { |
| // If not at the last result, increment forward. |
| if (location.result_index != location.result_count - 1) |
| ++new_location.result_index; |
| else |
| // Loop back to the first result. |
| new_location.result_index = 0; |
| } else { |
| // If not at the first result, increment backward. |
| if (location.result_index != 0) |
| --new_location.result_index; |
| else |
| // Loop around to the last result. |
| new_location.result_index = location.result_count - 1; |
| } |
| } break; |
| |
| default: |
| NOTREACHED(); |
| } |
| return new_location; |
| } |
| |
| void ResultSelectionController::SetSelection( |
| const ResultLocationDetails& location) { |
| ClearSelection(); |
| |
| selected_result_ = GetResultAtLocation(location); |
| selected_location_details_ = |
| std::make_unique<ResultLocationDetails>(location); |
| selected_result_->SetBackgroundHighlighted(true); |
| } |
| |
| SearchResultBaseView* ResultSelectionController::GetResultAtLocation( |
| const ResultLocationDetails& location) { |
| SearchResultContainerView* located_container = |
| result_selection_model_->at(location.container_index); |
| return located_container->GetResultViewAt(location.result_index); |
| } |
| |
| void ResultSelectionController::ChangeContainer( |
| ResultLocationDetails* location_details, |
| int new_container_index) { |
| if (new_container_index == location_details->container_index) { |
| return; |
| } |
| |
| // If the index is advancing |
| bool container_advancing = |
| new_container_index > location_details->container_index; |
| |
| // This handles 'looping', so if the selection goes off the end of the |
| // container, it will come back to the beginning. |
| int new_container = new_container_index; |
| if (new_container < 0) { |
| new_container = location_details->container_count - 1; |
| } |
| if (new_container >= location_details->container_count) |
| new_container = 0; |
| |
| // Because all containers always exist, we need to make sure there are results |
| // in the next container. |
| while (result_selection_model_->at(new_container)->num_results() <= 0) { |
| if (container_advancing) { |
| ++new_container; |
| } else { |
| --new_container; |
| } |
| |
| // Prevent any potential infinite looping by resetting to '0', a container |
| // that should never be empty. |
| if (new_container <= 0 || |
| new_container >= location_details->container_count) { |
| new_container = 0; |
| break; |
| } |
| } |
| |
| // Updates |result_count| and |container_is_horizontal| based on |
| // |new_container|. |
| location_details->result_count = |
| result_selection_model_->at(new_container)->num_results(); |
| location_details->container_is_horizontal = |
| result_selection_model_->at(new_container)->horizontally_traversable(); |
| |
| // Updates |result_index| to the first or the last result in the container |
| // based on whether the |container_index| is increasing or decreasing. |
| if (container_advancing) { |
| location_details->result_index = 0; |
| } else { |
| location_details->result_index = location_details->result_count - 1; |
| } |
| |
| // Finally, we update |container_index| to the new index. |container_count| |
| // doesn't change in this function. |
| location_details->container_index = new_container; |
| } |
| |
| } // namespace app_list |