123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- // 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 <utility>
- #include "ash/app_list/app_list_util.h"
- #include "ash/app_list/views/search_result_container_view.h"
- #include "base/i18n/rtl.h"
- namespace ash {
- ResultLocationDetails::ResultLocationDetails() = default;
- 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,
- const base::RepeatingClosure& selection_change_callback)
- : result_selection_model_(result_container_views),
- selection_change_callback_(selection_change_callback) {}
- ResultSelectionController::~ResultSelectionController() = default;
- ResultSelectionController::MoveResult ResultSelectionController::MoveSelection(
- const ui::KeyEvent& event) {
- if (block_selection_changes_)
- return MoveResult::kNone;
- ResultLocationDetails next_location;
- if (!selected_location_details_) {
- ResetSelection(&event, false /* default_selection */);
- return MoveResult::kResultChanged;
- }
- MoveResult result = GetNextResultLocation(event, &next_location);
- if (result == MoveResult::kResultChanged)
- SetSelection(next_location, event.IsShiftDown());
- return result;
- }
- void ResultSelectionController::ResetSelection(const ui::KeyEvent* key_event,
- bool default_selection) {
- // Prevents crash on start up
- if (result_selection_model_->size() == 0)
- return;
- if (block_selection_changes_)
- return;
- std::unique_ptr<ResultLocationDetails> default_location =
- GetFirstAvailableResultLocation();
- // Clear selection if no search results exist.
- if (!default_location) {
- ClearSelection();
- return;
- }
- // If a non-default result is selected, i.e. if the current selection was a
- // result of a user action, keep the selection at the same result (identified
- // by the result ID). Note that this method gets called whenever the set of
- // search results changes.
- selected_location_details_ =
- selected_result_ && !selected_result_->is_default_result()
- ? FindResultWithId(selected_result_id_)
- : nullptr;
- const bool selected_id_preserved = selected_location_details_.get();
- const bool is_up_key = key_event && key_event->key_code() == ui::VKEY_UP;
- const bool is_shift_tab = key_event &&
- key_event->key_code() == ui::VKEY_TAB &&
- key_event->IsShiftDown();
- if (!selected_location_details_) {
- selected_location_details_ = std::move(default_location);
- // Note: left and right arrows are used primarily for traversal in
- // horizontal containers, so treat "back" arrow as other non-traversal keys
- // when deciding whether to reverse selection direction.
- if (is_up_key || is_shift_tab)
- ChangeContainer(selected_location_details_.get(),
- selected_location_details_->container_index - 1);
- }
- SearchResultBaseView* new_selection =
- GetResultAtLocation(*selected_location_details_);
- if (new_selection && new_selection->selected())
- return;
- if (selected_result_)
- selected_result_->SetSelected(false, absl::nullopt);
- selected_result_ = new_selection;
- // Set the state of the new selected result.
- if (selected_result_) {
- selected_result_->set_is_default_result(default_selection &&
- !selected_id_preserved);
- selected_result_->SetSelected(true, is_shift_tab);
- selected_result_id_ = new_selection->result()->id();
- } else {
- selected_result_id_ = std::string();
- }
- selection_change_callback_.Run();
- }
- void ResultSelectionController::ClearSelection() {
- selected_location_details_ = nullptr;
- if (selected_result_) {
- // Reset the state of the previous selected result.
- selected_result_->SetSelected(false, absl::nullopt);
- selected_result_id_ = std::string();
- selected_result_->set_is_default_result(false);
- }
- selected_result_ = nullptr;
- }
- ResultSelectionController::MoveResult
- ResultSelectionController::GetNextResultLocation(
- const ui::KeyEvent& event,
- ResultLocationDetails* next_location) {
- return GetNextResultLocationForLocation(event, *selected_location_details_,
- next_location);
- }
- ResultSelectionController::MoveResult
- ResultSelectionController::GetNextResultLocationForLocation(
- const ui::KeyEvent& event,
- const ResultLocationDetails& location,
- ResultLocationDetails* next_location) {
- *next_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 MoveResult::kNone;
- if (selected_result_ && event.key_code() == ui::VKEY_TAB &&
- selected_result_->SelectNextResultAction(event.IsShiftDown())) {
- selection_change_callback_.Run();
- return MoveResult::kNone;
- }
- 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(next_location, location.container_index - 1);
- if (next_location->container_index >= location.container_index)
- return MoveResult::kSelectionCycleRejected;
- } else {
- --next_location->result_index;
- }
- } else {
- // Forward tab traversal always goes to the 'next' result.
- if (location.is_last_result()) {
- ChangeContainer(next_location, location.container_index + 1);
- if (next_location->container_index <= location.container_index)
- return MoveResult::kSelectionCycleRejected;
- } else {
- ++next_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(next_location, location.container_index - 1);
- if (next_location->container_index >= location.container_index)
- return MoveResult::kSelectionCycleRejected;
- } else {
- // Traversing 'up' moves up one result.
- --next_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(next_location, location.container_index + 1);
- if (next_location->container_index <= location.container_index)
- return MoveResult::kSelectionCycleRejected;
- } else {
- // Traversing 'down' moves down one result.
- ++next_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)
- ++next_location->result_index;
- else
- // Loop back to the first result.
- next_location->result_index = 0;
- } else {
- // If not at the first result, increment backward.
- if (location.result_index != 0)
- --next_location->result_index;
- else
- // Loop around to the last result.
- next_location->result_index = location.result_count - 1;
- }
- } break;
- default:
- NOTREACHED();
- }
- return *next_location == location ? MoveResult::kNone
- : MoveResult::kResultChanged;
- }
- void ResultSelectionController::SetSelection(
- const ResultLocationDetails& location,
- bool reverse_tab_order) {
- ClearSelection();
- selected_result_ = GetResultAtLocation(location);
- if (selected_result_->result())
- selected_result_id_ = selected_result_->result()->id();
- // SetSelection is only called by MoveSelection when user changes
- // selected result, therefore, the result selected by user is not
- // a default result.
- selected_result_->set_is_default_result(false);
- selected_location_details_ =
- std::make_unique<ResultLocationDetails>(location);
- selected_result_->SetSelected(true, reverse_tab_order);
- selection_change_callback_.Run();
- }
- std::unique_ptr<ResultLocationDetails>
- ResultSelectionController::GetFirstAvailableResultLocation() const {
- for (size_t container_index = 0;
- container_index < result_selection_model_->size(); ++container_index) {
- SearchResultContainerView* container =
- result_selection_model_->at(container_index);
- if (!container->num_results())
- continue;
- return std::make_unique<ResultLocationDetails>(
- container_index, result_selection_model_->size() /* container_count */,
- 0 /* result_index */, container->num_results() /* result_count */,
- container->horizontally_traversable() /* container_is_horizontal */);
- }
- return nullptr;
- }
- SearchResultBaseView* ResultSelectionController::GetResultAtLocation(
- const ResultLocationDetails& location) {
- SearchResultContainerView* located_container =
- result_selection_model_->at(location.container_index);
- return located_container->GetResultViewAt(location.result_index);
- }
- std::unique_ptr<ResultLocationDetails>
- ResultSelectionController::FindResultWithId(const std::string& id) {
- for (size_t container_index = 0;
- container_index < result_selection_model_->size(); ++container_index) {
- SearchResultContainerView* const container =
- result_selection_model_->at(container_index);
- for (int result_index = 0; result_index < container->num_results();
- ++result_index) {
- if (container->GetResultViewAt(result_index)->result()->id() == id) {
- return std::make_unique<ResultLocationDetails>(
- container_index, result_selection_model_->size(), result_index,
- container->num_results(), container->horizontally_traversable());
- }
- }
- }
- return nullptr;
- }
- 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.
- auto ensure_valid_index = [&location_details](int index) -> int {
- if (index < 0)
- return location_details->container_count - 1;
- if (index >= location_details->container_count)
- return 0;
- return index;
- };
- int new_container = ensure_valid_index(new_container_index);
- // 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;
- }
- new_container = ensure_valid_index(new_container);
- // Prevent any potential infinite looping by resetting to currently selected
- // container.
- if (new_container == location_details->container_index)
- 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 ash
|