blob: aae7cb397644706624c0f1515dc86ae90ed900e7 [file] [log] [blame]
// 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