productivity_launcher_search_view.cc 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. // Copyright 2021 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/productivity_launcher_search_view.h"
  5. #include <algorithm>
  6. #include <limits>
  7. #include <memory>
  8. #include <string>
  9. #include <utility>
  10. #include "ash/app_list/app_list_model_provider.h"
  11. #include "ash/app_list/app_list_util.h"
  12. #include "ash/app_list/app_list_view_delegate.h"
  13. #include "ash/app_list/views/result_selection_controller.h"
  14. #include "ash/app_list/views/search_box_view.h"
  15. #include "ash/app_list/views/search_result_list_view.h"
  16. #include "ash/app_list/views/search_result_view.h"
  17. #include "ash/constants/ash_features.h"
  18. #include "ash/controls/rounded_scroll_bar.h"
  19. #include "ash/public/cpp/app_list/app_list_color_provider.h"
  20. #include "ash/public/cpp/app_list/app_list_features.h"
  21. #include "ash/strings/grit/ash_strings.h"
  22. #include "base/bind.h"
  23. #include "base/check.h"
  24. #include "base/metrics/histogram_macros.h"
  25. #include "base/notreached.h"
  26. #include "base/strings/string_number_conversions.h"
  27. #include "third_party/abseil-cpp/absl/types/optional.h"
  28. #include "ui/accessibility/platform/ax_unique_id.h"
  29. #include "ui/base/l10n/l10n_util.h"
  30. #include "ui/base/metadata/metadata_impl_macros.h"
  31. #include "ui/gfx/geometry/rect.h"
  32. #include "ui/views/accessibility/view_accessibility.h"
  33. #include "ui/views/background.h"
  34. #include "ui/views/controls/scroll_view.h"
  35. #include "ui/views/controls/textfield/textfield.h"
  36. #include "ui/views/layout/box_layout.h"
  37. using views::BoxLayout;
  38. namespace ash {
  39. namespace {
  40. // The amount of time by which notifications to accessibility framework about
  41. // result page changes are delayed.
  42. constexpr base::TimeDelta kNotifyA11yDelay = base::Milliseconds(1500);
  43. // Insets for the vertical scroll bar.
  44. constexpr auto kVerticalScrollInsets = gfx::Insets::TLBR(1, 0, 1, 1);
  45. // The amount of time after search result animations are preempted during which
  46. // result animations should be sped up.
  47. constexpr base::TimeDelta kForcedFastAnimationInterval =
  48. base::Milliseconds(500);
  49. } // namespace
  50. ProductivityLauncherSearchView::ProductivityLauncherSearchView(
  51. AppListViewDelegate* view_delegate,
  52. SearchResultPageDialogController* dialog_controller,
  53. SearchBoxView* search_box_view)
  54. : dialog_controller_(dialog_controller), search_box_view_(search_box_view) {
  55. DCHECK(view_delegate);
  56. DCHECK(search_box_view_);
  57. SetUseDefaultFillLayout(true);
  58. // The entire page scrolls. Use layer scrolling so that the contents will
  59. // paint on top of the parent, which uses SetPaintToLayer().
  60. scroll_view_ = AddChildView(std::make_unique<views::ScrollView>(
  61. views::ScrollView::ScrollWithLayers::kEnabled));
  62. scroll_view_->ClipHeightTo(0, std::numeric_limits<int>::max());
  63. scroll_view_->SetDrawOverflowIndicator(false);
  64. // Don't paint a background. The bubble already has one.
  65. scroll_view_->SetBackgroundColor(absl::nullopt);
  66. // Set up scroll bars.
  67. scroll_view_->SetHorizontalScrollBarMode(
  68. views::ScrollView::ScrollBarMode::kDisabled);
  69. auto vertical_scroll =
  70. std::make_unique<RoundedScrollBar>(/*horizontal=*/false);
  71. vertical_scroll->SetInsets(kVerticalScrollInsets);
  72. scroll_view_->SetVerticalScrollBar(std::move(vertical_scroll));
  73. auto scroll_contents = std::make_unique<views::View>();
  74. scroll_contents->SetLayoutManager(
  75. std::make_unique<BoxLayout>(BoxLayout::Orientation::kVertical));
  76. result_selection_controller_ = std::make_unique<ResultSelectionController>(
  77. &result_container_views_,
  78. base::BindRepeating(
  79. &ProductivityLauncherSearchView::OnSelectedResultChanged,
  80. base::Unretained(this)));
  81. search_box_view_->SetResultSelectionController(
  82. result_selection_controller_.get());
  83. auto add_result_container = [&](SearchResultListView* new_container) {
  84. new_container->SetResults(
  85. AppListModelProvider::Get()->search_model()->results());
  86. new_container->set_delegate(this);
  87. result_container_views_.push_back(new_container);
  88. };
  89. // kAnswerCard is always the first list view shown.
  90. auto* answer_card_container =
  91. scroll_contents->AddChildView(std::make_unique<SearchResultListView>(
  92. /*main_view=*/nullptr, view_delegate, dialog_controller_,
  93. SearchResultView::SearchResultViewType::kAnswerCard,
  94. /*animates_result_updates=*/true, absl::nullopt));
  95. answer_card_container->SetListType(
  96. SearchResultListView::SearchResultListType::kAnswerCard);
  97. add_result_container(answer_card_container);
  98. // kBestMatch is always the second list view shown.
  99. auto* best_match_container =
  100. scroll_contents->AddChildView(std::make_unique<SearchResultListView>(
  101. /*main_view=*/nullptr, view_delegate, dialog_controller_,
  102. SearchResultView::SearchResultViewType::kDefault,
  103. /*animated_result_updates=*/true, absl::nullopt));
  104. best_match_container->SetListType(
  105. SearchResultListView::SearchResultListType::kBestMatch);
  106. add_result_container(best_match_container);
  107. // SearchResultListViews are aware of their relative position in the
  108. // Productivity launcher search view. SearchResultListViews with mutable
  109. // positions are passed their productivity_launcher_search_view_position to
  110. // update their own category type. kAnswerCard and kBestMatch have already
  111. // been constructed.
  112. const size_t category_count =
  113. SearchResultListView::GetAllListTypesForCategoricalSearch().size() -
  114. result_container_views_.size();
  115. for (size_t i = 0; i < category_count; ++i) {
  116. auto* result_container =
  117. scroll_contents->AddChildView(std::make_unique<SearchResultListView>(
  118. /*main_view=*/nullptr, view_delegate, dialog_controller_,
  119. SearchResultView::SearchResultViewType::kDefault,
  120. /*animates_result_updates=*/true, i));
  121. add_result_container(result_container);
  122. }
  123. scroll_view_->SetContents(std::move(scroll_contents));
  124. AppListModelProvider* const model_provider = AppListModelProvider::Get();
  125. model_provider->AddObserver(this);
  126. }
  127. ProductivityLauncherSearchView::~ProductivityLauncherSearchView() {
  128. AppListModelProvider::Get()->RemoveObserver(this);
  129. }
  130. void ProductivityLauncherSearchView::OnSearchResultContainerResultsChanging() {
  131. // Block any result selection changes while result updates are in flight.
  132. // The selection will be reset once the results are all updated.
  133. result_selection_controller_->set_block_selection_changes(true);
  134. notify_a11y_results_changed_timer_.Stop();
  135. SetIgnoreResultChangesForA11y(true);
  136. }
  137. void ProductivityLauncherSearchView::OnSearchResultContainerResultsChanged() {
  138. DCHECK(!result_container_views_.empty());
  139. int result_count = 0;
  140. // Only sort and layout the containers when they have all updated.
  141. for (SearchResultContainerView* view : result_container_views_) {
  142. if (view->UpdateScheduled())
  143. return;
  144. result_count += view->num_results();
  145. }
  146. SearchResultBaseView* first_result_view = nullptr;
  147. std::vector<SearchResultContainerView::SearchResultAimationMetadata>
  148. search_result_metadata;
  149. // If the user cleared the search box text, skip animating the views. The
  150. // visible views will animate out and the whole search page will be hidden.
  151. // See AppListBubbleSearchPage::AnimateHidePage().
  152. if (search_box_view_->HasSearch()) {
  153. using AnimationInfo = SearchResultContainerView::ResultsAnimationInfo;
  154. AnimationInfo aggregate_animation_info;
  155. // Search result changes within `kForcedFastAnimationInterval` of
  156. // `search_result_fast_update_time_` should also use fast animations and
  157. // refresh the timestamp.
  158. if (search_result_fast_update_time_.has_value() &&
  159. app_list_features::IsDynamicSearchUpdateAnimationEnabled()) {
  160. const base::TimeDelta time_since_last_update =
  161. base::TimeTicks::Now() - search_result_fast_update_time_.value();
  162. if (time_since_last_update < kForcedFastAnimationInterval) {
  163. aggregate_animation_info.use_short_animations = true;
  164. }
  165. }
  166. for (SearchResultContainerView* view : result_container_views_) {
  167. view->AppendShownResultMetadata(&search_result_metadata);
  168. }
  169. int first_animated_result_view_index = 0;
  170. for (size_t i = 0; i < std::min(search_result_metadata.size(),
  171. last_result_metadata_.size());
  172. ++i) {
  173. const bool matching_result_id = search_result_metadata[i].result_id ==
  174. last_result_metadata_[i].result_id;
  175. const bool skip_animations = search_result_metadata[i].skip_animations &&
  176. last_result_metadata_[i].skip_animations;
  177. if (!skip_animations && !matching_result_id)
  178. break;
  179. first_animated_result_view_index += 1;
  180. }
  181. aggregate_animation_info.first_animated_result_view_index =
  182. first_animated_result_view_index;
  183. for (SearchResultContainerView* view : result_container_views_) {
  184. absl::optional<AnimationInfo> container_animation_info =
  185. view->ScheduleResultAnimations(aggregate_animation_info);
  186. DCHECK(container_animation_info);
  187. aggregate_animation_info.total_views +=
  188. container_animation_info->total_views;
  189. aggregate_animation_info.total_result_views +=
  190. container_animation_info->total_result_views;
  191. aggregate_animation_info.animating_views +=
  192. container_animation_info->animating_views;
  193. // Fetch the first visible search result view for search box autocomplete.
  194. if (!first_result_view && view->GetFirstResultView()) {
  195. first_result_view = view->GetFirstResultView();
  196. }
  197. }
  198. // Update the `search_result_fast_update_time_` if fast animations were
  199. // used.
  200. if (aggregate_animation_info.use_short_animations)
  201. search_result_fast_update_time_ = base::TimeTicks::Now();
  202. // Records metrics on whether shortened search animations were used.
  203. base::UmaHistogramBoolean("Ash.SearchResultUpdateAnimationShortened",
  204. aggregate_animation_info.use_short_animations);
  205. }
  206. Layout();
  207. last_search_result_count_ = result_count;
  208. last_result_metadata_.swap(search_result_metadata);
  209. ScheduleResultsChangedA11yNotification();
  210. // Reset selection to first when things change. The first result is set as
  211. // as the default result.
  212. result_selection_controller_->set_block_selection_changes(false);
  213. result_selection_controller_->ResetSelection(/*key_event=*/nullptr,
  214. /*default_selection=*/true);
  215. // Update SearchBoxView search box autocomplete as necessary based on new
  216. // first result view.
  217. if (first_result_view) {
  218. search_box_view_->ProcessAutocomplete(first_result_view);
  219. } else {
  220. search_box_view_->ClearAutocompleteText();
  221. }
  222. }
  223. void ProductivityLauncherSearchView::GetAccessibleNodeData(
  224. ui::AXNodeData* node_data) {
  225. if (!GetVisible())
  226. return;
  227. node_data->role = ax::mojom::Role::kListBox;
  228. std::u16string value;
  229. const std::u16string& query = search_box_view_->current_query();
  230. if (!query.empty()) {
  231. if (last_search_result_count_ == 1) {
  232. value = l10n_util::GetStringFUTF16(
  233. IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT_SINGLE_RESULT,
  234. query);
  235. } else {
  236. value = l10n_util::GetStringFUTF16(
  237. IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT,
  238. base::NumberToString16(last_search_result_count_), query);
  239. }
  240. } else {
  241. // TODO(crbug.com/1204551): New(?) accessibility announcement. We used to
  242. // have a zero state A11Y announcement but zero state is removed for the
  243. // bubble launcher.
  244. value = std::u16string();
  245. }
  246. node_data->SetValue(value);
  247. }
  248. void ProductivityLauncherSearchView::OnActiveAppListModelsChanged(
  249. AppListModel* model,
  250. SearchModel* search_model) {
  251. for (auto* container : result_container_views_)
  252. container->SetResults(search_model->results());
  253. }
  254. void ProductivityLauncherSearchView::UpdateForNewSearch(bool search_active) {
  255. if (app_list_features::IsDynamicSearchUpdateAnimationEnabled()) {
  256. if (search_active) {
  257. // Scan result_container_views_ to see if there are any in progress
  258. // animations when the search model is updated.
  259. for (SearchResultContainerView* view : result_container_views_) {
  260. if (view->HasAnimatingChildView()) {
  261. search_result_fast_update_time_ = base::TimeTicks::Now();
  262. }
  263. }
  264. } else {
  265. search_result_fast_update_time_.reset();
  266. }
  267. }
  268. }
  269. void ProductivityLauncherSearchView::OnSelectedResultChanged() {
  270. if (!result_selection_controller_->selected_result()) {
  271. return;
  272. }
  273. views::View* selected_row = result_selection_controller_->selected_result();
  274. selected_row->ScrollViewToVisible();
  275. for (SearchResultContainerView* view : result_container_views_)
  276. view->OnSelectedResultChanged();
  277. MaybeNotifySelectedResultChanged();
  278. }
  279. void ProductivityLauncherSearchView::SetIgnoreResultChangesForA11y(
  280. bool ignore) {
  281. if (ignore_result_changes_for_a11y_ == ignore)
  282. return;
  283. ignore_result_changes_for_a11y_ = ignore;
  284. SetViewIgnoredForAccessibility(this, ignore);
  285. }
  286. void ProductivityLauncherSearchView::ScheduleResultsChangedA11yNotification() {
  287. if (!ignore_result_changes_for_a11y_) {
  288. NotifyA11yResultsChanged();
  289. return;
  290. }
  291. notify_a11y_results_changed_timer_.Start(
  292. FROM_HERE, kNotifyA11yDelay,
  293. base::BindOnce(&ProductivityLauncherSearchView::NotifyA11yResultsChanged,
  294. base::Unretained(this)));
  295. }
  296. void ProductivityLauncherSearchView::NotifyA11yResultsChanged() {
  297. SetIgnoreResultChangesForA11y(false);
  298. NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
  299. MaybeNotifySelectedResultChanged();
  300. }
  301. void ProductivityLauncherSearchView::MaybeNotifySelectedResultChanged() {
  302. if (ignore_result_changes_for_a11y_)
  303. return;
  304. if (!result_selection_controller_->selected_result()) {
  305. search_box_view_->SetA11yActiveDescendant(absl::nullopt);
  306. return;
  307. }
  308. views::View* selected_view =
  309. result_selection_controller_->selected_result()->GetSelectedView();
  310. if (!selected_view) {
  311. search_box_view_->SetA11yActiveDescendant(absl::nullopt);
  312. return;
  313. }
  314. search_box_view_->SetA11yActiveDescendant(
  315. selected_view->GetViewAccessibility().GetUniqueId().Get());
  316. }
  317. bool ProductivityLauncherSearchView::CanSelectSearchResults() {
  318. DCHECK(!result_container_views_.empty());
  319. return last_search_result_count_ > 0;
  320. }
  321. int ProductivityLauncherSearchView::TabletModePreferredHeight() {
  322. int max_height = 0;
  323. for (SearchResultContainerView* view : result_container_views_) {
  324. if (view->GetVisible()) {
  325. max_height += view->GetPreferredSize().height();
  326. }
  327. }
  328. return max_height;
  329. }
  330. ui::Layer* ProductivityLauncherSearchView::GetPageAnimationLayer() const {
  331. // The scroll view has a layer containing all the visible contents, so use
  332. // that for "whole page" animations.
  333. return scroll_view_->contents()->layer();
  334. }
  335. BEGIN_METADATA(ProductivityLauncherSearchView, views::View)
  336. END_METADATA
  337. } // namespace ash