result_selection_controller.cc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. // Copyright 2019 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "ash/app_list/views/result_selection_controller.h"
  5. #include <utility>
  6. #include "ash/app_list/app_list_util.h"
  7. #include "ash/app_list/views/search_result_container_view.h"
  8. #include "base/i18n/rtl.h"
  9. namespace ash {
  10. ResultLocationDetails::ResultLocationDetails() = default;
  11. ResultLocationDetails::ResultLocationDetails(int container_index,
  12. int container_count,
  13. int result_index,
  14. int result_count,
  15. bool container_is_horizontal)
  16. : container_index(container_index),
  17. container_count(container_count),
  18. result_index(result_index),
  19. result_count(result_count),
  20. container_is_horizontal(container_is_horizontal) {}
  21. bool ResultLocationDetails::operator==(
  22. const ResultLocationDetails& other) const {
  23. return container_index == other.container_index &&
  24. container_count == other.container_count &&
  25. result_index == other.result_index &&
  26. result_count == other.result_count &&
  27. container_is_horizontal == other.container_is_horizontal;
  28. }
  29. bool ResultLocationDetails::operator!=(
  30. const ResultLocationDetails& other) const {
  31. return !(*this == other);
  32. }
  33. ResultSelectionController::ResultSelectionController(
  34. const ResultSelectionModel* result_container_views,
  35. const base::RepeatingClosure& selection_change_callback)
  36. : result_selection_model_(result_container_views),
  37. selection_change_callback_(selection_change_callback) {}
  38. ResultSelectionController::~ResultSelectionController() = default;
  39. ResultSelectionController::MoveResult ResultSelectionController::MoveSelection(
  40. const ui::KeyEvent& event) {
  41. if (block_selection_changes_)
  42. return MoveResult::kNone;
  43. ResultLocationDetails next_location;
  44. if (!selected_location_details_) {
  45. ResetSelection(&event, false /* default_selection */);
  46. return MoveResult::kResultChanged;
  47. }
  48. MoveResult result = GetNextResultLocation(event, &next_location);
  49. if (result == MoveResult::kResultChanged)
  50. SetSelection(next_location, event.IsShiftDown());
  51. return result;
  52. }
  53. void ResultSelectionController::ResetSelection(const ui::KeyEvent* key_event,
  54. bool default_selection) {
  55. // Prevents crash on start up
  56. if (result_selection_model_->size() == 0)
  57. return;
  58. if (block_selection_changes_)
  59. return;
  60. std::unique_ptr<ResultLocationDetails> default_location =
  61. GetFirstAvailableResultLocation();
  62. // Clear selection if no search results exist.
  63. if (!default_location) {
  64. ClearSelection();
  65. return;
  66. }
  67. // If a non-default result is selected, i.e. if the current selection was a
  68. // result of a user action, keep the selection at the same result (identified
  69. // by the result ID). Note that this method gets called whenever the set of
  70. // search results changes.
  71. selected_location_details_ =
  72. selected_result_ && !selected_result_->is_default_result()
  73. ? FindResultWithId(selected_result_id_)
  74. : nullptr;
  75. const bool selected_id_preserved = selected_location_details_.get();
  76. const bool is_up_key = key_event && key_event->key_code() == ui::VKEY_UP;
  77. const bool is_shift_tab = key_event &&
  78. key_event->key_code() == ui::VKEY_TAB &&
  79. key_event->IsShiftDown();
  80. if (!selected_location_details_) {
  81. selected_location_details_ = std::move(default_location);
  82. // Note: left and right arrows are used primarily for traversal in
  83. // horizontal containers, so treat "back" arrow as other non-traversal keys
  84. // when deciding whether to reverse selection direction.
  85. if (is_up_key || is_shift_tab)
  86. ChangeContainer(selected_location_details_.get(),
  87. selected_location_details_->container_index - 1);
  88. }
  89. SearchResultBaseView* new_selection =
  90. GetResultAtLocation(*selected_location_details_);
  91. if (new_selection && new_selection->selected())
  92. return;
  93. if (selected_result_)
  94. selected_result_->SetSelected(false, absl::nullopt);
  95. selected_result_ = new_selection;
  96. // Set the state of the new selected result.
  97. if (selected_result_) {
  98. selected_result_->set_is_default_result(default_selection &&
  99. !selected_id_preserved);
  100. selected_result_->SetSelected(true, is_shift_tab);
  101. selected_result_id_ = new_selection->result()->id();
  102. } else {
  103. selected_result_id_ = std::string();
  104. }
  105. selection_change_callback_.Run();
  106. }
  107. void ResultSelectionController::ClearSelection() {
  108. selected_location_details_ = nullptr;
  109. if (selected_result_) {
  110. // Reset the state of the previous selected result.
  111. selected_result_->SetSelected(false, absl::nullopt);
  112. selected_result_id_ = std::string();
  113. selected_result_->set_is_default_result(false);
  114. }
  115. selected_result_ = nullptr;
  116. }
  117. ResultSelectionController::MoveResult
  118. ResultSelectionController::GetNextResultLocation(
  119. const ui::KeyEvent& event,
  120. ResultLocationDetails* next_location) {
  121. return GetNextResultLocationForLocation(event, *selected_location_details_,
  122. next_location);
  123. }
  124. ResultSelectionController::MoveResult
  125. ResultSelectionController::GetNextResultLocationForLocation(
  126. const ui::KeyEvent& event,
  127. const ResultLocationDetails& location,
  128. ResultLocationDetails* next_location) {
  129. *next_location = location;
  130. // Only arrow keys (unhandled and unmodified) or the tab key will change our
  131. // selection.
  132. if (!(IsUnhandledArrowKeyEvent(event) || event.key_code() == ui::VKEY_TAB))
  133. return MoveResult::kNone;
  134. if (selected_result_ && event.key_code() == ui::VKEY_TAB &&
  135. selected_result_->SelectNextResultAction(event.IsShiftDown())) {
  136. selection_change_callback_.Run();
  137. return MoveResult::kNone;
  138. }
  139. switch (event.key_code()) {
  140. case ui::VKEY_TAB:
  141. if (event.IsShiftDown()) {
  142. // Reverse tab traversal always goes to the 'previous' result.
  143. if (location.is_first_result()) {
  144. ChangeContainer(next_location, location.container_index - 1);
  145. if (next_location->container_index >= location.container_index)
  146. return MoveResult::kSelectionCycleRejected;
  147. } else {
  148. --next_location->result_index;
  149. }
  150. } else {
  151. // Forward tab traversal always goes to the 'next' result.
  152. if (location.is_last_result()) {
  153. ChangeContainer(next_location, location.container_index + 1);
  154. if (next_location->container_index <= location.container_index)
  155. return MoveResult::kSelectionCycleRejected;
  156. } else {
  157. ++next_location->result_index;
  158. }
  159. }
  160. break;
  161. case ui::VKEY_UP:
  162. if (location.container_is_horizontal || location.is_first_result()) {
  163. // Traversing 'up' from the top of a container changes containers.
  164. ChangeContainer(next_location, location.container_index - 1);
  165. if (next_location->container_index >= location.container_index)
  166. return MoveResult::kSelectionCycleRejected;
  167. } else {
  168. // Traversing 'up' moves up one result.
  169. --next_location->result_index;
  170. }
  171. break;
  172. case ui::VKEY_DOWN:
  173. if (location.container_is_horizontal || location.is_last_result()) {
  174. // Traversing 'down' from the bottom of a container changes containers.
  175. ChangeContainer(next_location, location.container_index + 1);
  176. if (next_location->container_index <= location.container_index)
  177. return MoveResult::kSelectionCycleRejected;
  178. } else {
  179. // Traversing 'down' moves down one result.
  180. ++next_location->result_index;
  181. }
  182. break;
  183. case ui::VKEY_RIGHT:
  184. case ui::VKEY_LEFT: {
  185. // Containers are stacked, so left/right does not traverse vertical
  186. // containers.
  187. if (!location.container_is_horizontal)
  188. break;
  189. ui::KeyboardCode forward = ui::VKEY_RIGHT;
  190. // If RTL is active, 'forward' is left instead.
  191. if (base::i18n::IsRTL())
  192. forward = ui::VKEY_LEFT;
  193. // Traversing should move one result, but loop within the
  194. // container.
  195. if (event.key_code() == forward) {
  196. // If not at the last result, increment forward.
  197. if (location.result_index != location.result_count - 1)
  198. ++next_location->result_index;
  199. else
  200. // Loop back to the first result.
  201. next_location->result_index = 0;
  202. } else {
  203. // If not at the first result, increment backward.
  204. if (location.result_index != 0)
  205. --next_location->result_index;
  206. else
  207. // Loop around to the last result.
  208. next_location->result_index = location.result_count - 1;
  209. }
  210. } break;
  211. default:
  212. NOTREACHED();
  213. }
  214. return *next_location == location ? MoveResult::kNone
  215. : MoveResult::kResultChanged;
  216. }
  217. void ResultSelectionController::SetSelection(
  218. const ResultLocationDetails& location,
  219. bool reverse_tab_order) {
  220. ClearSelection();
  221. selected_result_ = GetResultAtLocation(location);
  222. if (selected_result_->result())
  223. selected_result_id_ = selected_result_->result()->id();
  224. // SetSelection is only called by MoveSelection when user changes
  225. // selected result, therefore, the result selected by user is not
  226. // a default result.
  227. selected_result_->set_is_default_result(false);
  228. selected_location_details_ =
  229. std::make_unique<ResultLocationDetails>(location);
  230. selected_result_->SetSelected(true, reverse_tab_order);
  231. selection_change_callback_.Run();
  232. }
  233. std::unique_ptr<ResultLocationDetails>
  234. ResultSelectionController::GetFirstAvailableResultLocation() const {
  235. for (size_t container_index = 0;
  236. container_index < result_selection_model_->size(); ++container_index) {
  237. SearchResultContainerView* container =
  238. result_selection_model_->at(container_index);
  239. if (!container->num_results())
  240. continue;
  241. return std::make_unique<ResultLocationDetails>(
  242. container_index, result_selection_model_->size() /* container_count */,
  243. 0 /* result_index */, container->num_results() /* result_count */,
  244. container->horizontally_traversable() /* container_is_horizontal */);
  245. }
  246. return nullptr;
  247. }
  248. SearchResultBaseView* ResultSelectionController::GetResultAtLocation(
  249. const ResultLocationDetails& location) {
  250. SearchResultContainerView* located_container =
  251. result_selection_model_->at(location.container_index);
  252. return located_container->GetResultViewAt(location.result_index);
  253. }
  254. std::unique_ptr<ResultLocationDetails>
  255. ResultSelectionController::FindResultWithId(const std::string& id) {
  256. for (size_t container_index = 0;
  257. container_index < result_selection_model_->size(); ++container_index) {
  258. SearchResultContainerView* const container =
  259. result_selection_model_->at(container_index);
  260. for (int result_index = 0; result_index < container->num_results();
  261. ++result_index) {
  262. if (container->GetResultViewAt(result_index)->result()->id() == id) {
  263. return std::make_unique<ResultLocationDetails>(
  264. container_index, result_selection_model_->size(), result_index,
  265. container->num_results(), container->horizontally_traversable());
  266. }
  267. }
  268. }
  269. return nullptr;
  270. }
  271. void ResultSelectionController::ChangeContainer(
  272. ResultLocationDetails* location_details,
  273. int new_container_index) {
  274. if (new_container_index == location_details->container_index)
  275. return;
  276. // If the index is advancing
  277. bool container_advancing =
  278. new_container_index > location_details->container_index;
  279. // This handles 'looping', so if the selection goes off the end of the
  280. // container, it will come back to the beginning.
  281. auto ensure_valid_index = [&location_details](int index) -> int {
  282. if (index < 0)
  283. return location_details->container_count - 1;
  284. if (index >= location_details->container_count)
  285. return 0;
  286. return index;
  287. };
  288. int new_container = ensure_valid_index(new_container_index);
  289. // Because all containers always exist, we need to make sure there are results
  290. // in the next container.
  291. while (result_selection_model_->at(new_container)->num_results() <= 0) {
  292. if (container_advancing) {
  293. ++new_container;
  294. } else {
  295. --new_container;
  296. }
  297. new_container = ensure_valid_index(new_container);
  298. // Prevent any potential infinite looping by resetting to currently selected
  299. // container.
  300. if (new_container == location_details->container_index)
  301. break;
  302. }
  303. // Updates |result_count| and |container_is_horizontal| based on
  304. // |new_container|.
  305. location_details->result_count =
  306. result_selection_model_->at(new_container)->num_results();
  307. location_details->container_is_horizontal =
  308. result_selection_model_->at(new_container)->horizontally_traversable();
  309. // Updates |result_index| to the first or the last result in the container
  310. // based on whether the |container_index| is increasing or decreasing.
  311. if (container_advancing) {
  312. location_details->result_index = 0;
  313. } else {
  314. location_details->result_index = location_details->result_count - 1;
  315. }
  316. // Finally, we update |container_index| to the new index. |container_count|
  317. // doesn't change in this function.
  318. location_details->container_index = new_container;
  319. }
  320. } // namespace ash