search_result_page_view.cc 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. // Copyright 2014 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/search_result_page_view.h"
  5. #include <stddef.h>
  6. #include <algorithm>
  7. #include "ash/app_list/app_list_model_provider.h"
  8. #include "ash/app_list/app_list_util.h"
  9. #include "ash/app_list/views/app_list_main_view.h"
  10. #include "ash/app_list/views/contents_view.h"
  11. #include "ash/app_list/views/privacy_container_view.h"
  12. #include "ash/app_list/views/productivity_launcher_search_view.h"
  13. #include "ash/app_list/views/search_box_view.h"
  14. #include "ash/app_list/views/search_result_base_view.h"
  15. #include "ash/app_list/views/search_result_list_view.h"
  16. #include "ash/app_list/views/search_result_page_anchored_dialog.h"
  17. #include "ash/app_list/views/search_result_tile_item_list_view.h"
  18. #include "ash/constants/ash_features.h"
  19. #include "ash/public/cpp/app_list/app_list_color_provider.h"
  20. #include "ash/public/cpp/app_list/app_list_config.h"
  21. #include "ash/public/cpp/app_list/app_list_features.h"
  22. #include "ash/public/cpp/style/color_provider.h"
  23. #include "ash/search_box/search_box_constants.h"
  24. #include "ash/strings/grit/ash_strings.h"
  25. #include "ash/style/system_shadow.h"
  26. #include "base/bind.h"
  27. #include "base/memory/ptr_util.h"
  28. #include "base/strings/string_number_conversions.h"
  29. #include "base/time/time.h"
  30. #include "third_party/abseil-cpp/absl/types/optional.h"
  31. #include "ui/accessibility/platform/ax_unique_id.h"
  32. #include "ui/base/l10n/l10n_util.h"
  33. #include "ui/base/metadata/metadata_header_macros.h"
  34. #include "ui/base/metadata/metadata_impl_macros.h"
  35. #include "ui/color/color_id.h"
  36. #include "ui/color/color_provider.h"
  37. #include "ui/compositor/layer.h"
  38. #include "ui/compositor/scoped_layer_animation_settings.h"
  39. #include "ui/gfx/canvas.h"
  40. #include "ui/gfx/color_palette.h"
  41. #include "ui/gfx/geometry/insets.h"
  42. #include "ui/views/accessibility/view_accessibility.h"
  43. #include "ui/views/animation/animation_builder.h"
  44. #include "ui/views/background.h"
  45. #include "ui/views/border.h"
  46. #include "ui/views/controls/scroll_view.h"
  47. #include "ui/views/controls/scrollbar/overlay_scroll_bar.h"
  48. #include "ui/views/controls/textfield/textfield.h"
  49. #include "ui/views/layout/box_layout.h"
  50. #include "ui/views/layout/fill_layout.h"
  51. #include "ui/views/window/dialog_delegate.h"
  52. namespace ash {
  53. namespace {
  54. constexpr int kMinHeight = 440;
  55. constexpr int kWidth = 640;
  56. // The horizontal padding of the separator.
  57. constexpr int kSeparatorPadding = 12;
  58. constexpr int kSeparatorThickness = 1;
  59. // The height of the active search box in this page.
  60. constexpr int kActiveSearchBoxHeight = 56;
  61. // The spacing between search box bottom and separator line.
  62. // Add 1 pixel spacing so that the search bbox bottom will not paint over
  63. // the separator line drawn by SearchResultPageBackground in some scale factors
  64. // due to the round up.
  65. constexpr int kSearchBoxBottomSpacing = 1;
  66. // Minimum spacing between shelf and bottom of search box.
  67. constexpr int kSearchResultPageMinimumBottomMargin = 24;
  68. // The shadow type for the shadow of the expanded search box.
  69. constexpr SystemShadow::Type kSearchBoxSearchResultShadowType =
  70. SystemShadow::Type::kElevation12;
  71. // The amount of time by which notifications to accessibility framework about
  72. // result page changes are delayed.
  73. constexpr base::TimeDelta kNotifyA11yDelay = base::Milliseconds(1500);
  74. // The duration of the search result page view expanding animation.
  75. constexpr base::TimeDelta kExpandingSearchResultDuration =
  76. base::Milliseconds(200);
  77. // The duration of the search result page view closing animation.
  78. constexpr base::TimeDelta kClosingSearchResultDuration =
  79. base::Milliseconds(100);
  80. // The duration of the search result page view decreasing height animation
  81. // within the kExpanded state.
  82. constexpr base::TimeDelta kDecreasingHeightSearchResultsDuration =
  83. base::Milliseconds(200);
  84. // A container view that ensures the card background and the shadow are painted
  85. // in the correct order.
  86. class SearchCardView : public views::View {
  87. public:
  88. METADATA_HEADER(SearchCardView);
  89. explicit SearchCardView(std::unique_ptr<views::View> content_view) {
  90. SetLayoutManager(std::make_unique<views::FillLayout>());
  91. AddChildView(std::move(content_view));
  92. }
  93. SearchCardView(const SearchCardView&) = delete;
  94. SearchCardView& operator=(const SearchCardView&) = delete;
  95. ~SearchCardView() override = default;
  96. };
  97. BEGIN_METADATA(SearchCardView, views::View)
  98. END_METADATA
  99. class ZeroWidthVerticalScrollBar : public views::OverlayScrollBar {
  100. public:
  101. ZeroWidthVerticalScrollBar() : OverlayScrollBar(false) {}
  102. ZeroWidthVerticalScrollBar(const ZeroWidthVerticalScrollBar&) = delete;
  103. ZeroWidthVerticalScrollBar& operator=(const ZeroWidthVerticalScrollBar&) =
  104. delete;
  105. ~ZeroWidthVerticalScrollBar() override = default;
  106. // OverlayScrollBar overrides:
  107. int GetThickness() const override { return 0; }
  108. bool OnKeyPressed(const ui::KeyEvent& event) override {
  109. // Arrow keys should be handled by FocusManager to move focus. When a search
  110. // result is focused, it will be set visible in scroll view.
  111. return false;
  112. }
  113. };
  114. class SearchResultPageBackground : public views::Background {
  115. public:
  116. explicit SearchResultPageBackground(SkColor color) {
  117. SetNativeControlColor(color);
  118. }
  119. SearchResultPageBackground(const SearchResultPageBackground&) = delete;
  120. SearchResultPageBackground& operator=(const SearchResultPageBackground&) =
  121. delete;
  122. ~SearchResultPageBackground() override = default;
  123. private:
  124. // views::Background overrides:
  125. void Paint(gfx::Canvas* canvas, views::View* view) const override {
  126. canvas->DrawColor(get_color());
  127. gfx::Rect bounds = view->GetContentsBounds();
  128. if (bounds.height() <= kActiveSearchBoxHeight)
  129. return;
  130. // Draw a separator between SearchBoxView and SearchResultPageView.
  131. bounds.set_y(kActiveSearchBoxHeight + kSearchBoxBottomSpacing);
  132. bounds.set_height(kSeparatorThickness);
  133. canvas->FillRect(bounds,
  134. view->GetColorProvider()->GetColor(
  135. AppListColorProvider::Get()->GetSeparatorColorId()));
  136. }
  137. };
  138. } // namespace
  139. class SearchResultPageView::HorizontalSeparator : public views::View {
  140. public:
  141. explicit HorizontalSeparator(int preferred_width)
  142. : preferred_width_(preferred_width) {
  143. SetBorder(views::CreateEmptyBorder(
  144. gfx::Insets::TLBR(0, kSeparatorPadding, 0, kSeparatorPadding)));
  145. }
  146. HorizontalSeparator(const HorizontalSeparator&) = delete;
  147. HorizontalSeparator& operator=(const HorizontalSeparator&) = delete;
  148. ~HorizontalSeparator() override = default;
  149. // views::View overrides:
  150. const char* GetClassName() const override { return "HorizontalSeparator"; }
  151. gfx::Size CalculatePreferredSize() const override {
  152. return gfx::Size(preferred_width_, kSeparatorThickness);
  153. }
  154. void OnPaint(gfx::Canvas* canvas) override {
  155. gfx::Rect rect = GetContentsBounds();
  156. canvas->FillRect(rect,
  157. GetColorProvider()->GetColor(
  158. AppListColorProvider::Get()->GetSeparatorColorId()));
  159. View::OnPaint(canvas);
  160. }
  161. private:
  162. const int preferred_width_;
  163. };
  164. SearchResultPageView::SearchResultPageView() : contents_view_(new views::View) {
  165. SetPaintToLayer();
  166. layer()->SetFillsBoundsOpaquely(false);
  167. contents_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
  168. views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0));
  169. shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView(
  170. this, kSearchBoxSearchResultShadowType);
  171. shadow_->SetRoundedCornerRadius(kSearchBoxBorderCornerRadiusSearchResult);
  172. // Hides this view behind the search box by using the same color and
  173. // background border corner radius. All child views' background should be
  174. // set transparent so that the rounded corner is not overwritten.
  175. SetBackground(std::make_unique<SearchResultPageBackground>(
  176. features::IsProductivityLauncherEnabled()
  177. ? ColorProvider::Get()->GetBaseLayerColor(
  178. ColorProvider::BaseLayerType::kTransparent80)
  179. : AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor()));
  180. if (features::IsProductivityLauncherEnabled()) {
  181. layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
  182. layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
  183. layer()->SetRoundedCornerRadius(gfx::RoundedCornersF(
  184. kExpandedSearchBoxCornerRadiusForProductivityLauncher));
  185. }
  186. SetLayoutManager(std::make_unique<views::FillLayout>());
  187. // App list bubble search page has its own scroller and result selection
  188. // controller so we do not need to construct new ones here.
  189. if (features::IsProductivityLauncherEnabled()) {
  190. contents_view_->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
  191. kActiveSearchBoxHeight + kSearchBoxBottomSpacing + kSeparatorThickness,
  192. 0, 0, 0)));
  193. AddChildView(contents_view_);
  194. } else {
  195. auto scroller = std::make_unique<views::ScrollView>();
  196. // Leaves a placeholder area for the search box and the separator below it.
  197. scroller->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
  198. kActiveSearchBoxHeight + kSearchBoxBottomSpacing + kSeparatorThickness,
  199. 0, 0, 0)));
  200. scroller->SetDrawOverflowIndicator(false);
  201. scroller->SetContents(base::WrapUnique(contents_view_));
  202. // Setting clip height is necessary to make ScrollView take into account its
  203. // contents' size. Using zeroes doesn't prevent it from scrolling and sizing
  204. // correctly.
  205. scroller->ClipHeightTo(0, 0);
  206. scroller->SetVerticalScrollBar(
  207. std::make_unique<ZeroWidthVerticalScrollBar>());
  208. scroller->SetBackgroundColor(absl::nullopt);
  209. AddChildView(std::move(scroller));
  210. result_selection_controller_ = std::make_unique<ResultSelectionController>(
  211. &result_container_views_,
  212. base::BindRepeating(&SearchResultPageView::SelectedResultChanged,
  213. base::Unretained(this)));
  214. }
  215. AppListModelProvider* const model_provider = AppListModelProvider::Get();
  216. model_provider->AddObserver(this);
  217. }
  218. SearchResultPageView::~SearchResultPageView() {
  219. AppListModelProvider::Get()->RemoveObserver(this);
  220. }
  221. void SearchResultPageView::InitializeContainers(
  222. AppListViewDelegate* view_delegate,
  223. AppListMainView* app_list_main_view,
  224. SearchBoxView* search_box_view) {
  225. DCHECK(view_delegate);
  226. view_delegate_ = view_delegate;
  227. if (features::IsProductivityLauncherEnabled()) {
  228. // For productivity launcher, the dialog will be anchored to the search box
  229. // to keep the position of dialogs consistent.
  230. dialog_controller_ =
  231. std::make_unique<SearchResultPageDialogController>(search_box_view);
  232. std::unique_ptr<ProductivityLauncherSearchView> search_view_ptr =
  233. std::make_unique<ProductivityLauncherSearchView>(
  234. view_delegate, dialog_controller_.get(), search_box_view);
  235. productivity_launcher_search_view_ = search_view_ptr.get();
  236. contents_view_->AddChildView(
  237. std::make_unique<SearchCardView>(std::move(search_view_ptr)));
  238. } else {
  239. dialog_controller_ =
  240. std::make_unique<SearchResultPageDialogController>(this);
  241. privacy_container_view_ = AddSearchResultContainerView(
  242. std::make_unique<PrivacyContainerView>(view_delegate));
  243. search_result_tile_item_list_view_ = AddSearchResultContainerView(
  244. std::make_unique<SearchResultTileItemListView>(
  245. search_box_view->search_box(), view_delegate));
  246. result_lists_separator_ = contents_view_->AddChildView(
  247. std::make_unique<HorizontalSeparator>(bounds().width()));
  248. // productivity_launcher_index is not set as the feature is not enabled.
  249. search_result_list_view_ =
  250. AddSearchResultContainerView(std::make_unique<SearchResultListView>(
  251. app_list_main_view, view_delegate, dialog_controller_.get(),
  252. SearchResultView::SearchResultViewType::kClassic,
  253. /*animates_result_updates=*/false, absl::nullopt));
  254. search_box_view->SetResultSelectionController(
  255. result_selection_controller());
  256. }
  257. }
  258. void SearchResultPageView::AddSearchResultContainerViewInternal(
  259. std::unique_ptr<SearchResultContainerView> result_container) {
  260. auto* result_container_ptr = result_container.get();
  261. contents_view_->AddChildView(
  262. std::make_unique<SearchCardView>(std::move(result_container)));
  263. result_container_views_.push_back(result_container_ptr);
  264. result_container_ptr->SetResults(
  265. AppListModelProvider::Get()->search_model()->results());
  266. result_container_ptr->set_delegate(this);
  267. }
  268. bool SearchResultPageView::IsFirstResultTile() const {
  269. // In the event that the result does not exist, it is not a tile.
  270. if (!first_result_view_ || !first_result_view_->result())
  271. return false;
  272. return first_result_view_->result()->display_type() ==
  273. SearchResultDisplayType::kTile;
  274. }
  275. bool SearchResultPageView::IsFirstResultHighlighted() const {
  276. DCHECK(first_result_view_);
  277. return first_result_view_->selected();
  278. }
  279. const char* SearchResultPageView::GetClassName() const {
  280. return "SearchResultPageView";
  281. }
  282. gfx::Size SearchResultPageView::CalculatePreferredSize() const {
  283. // TODO(https://crbug.com/1216097) Update height based on available space.
  284. if (!features::IsProductivityLauncherEnabled())
  285. return gfx::Size(kWidth, kMinHeight);
  286. int adjusted_height = std::min(
  287. std::max(kMinHeight,
  288. productivity_launcher_search_view_->TabletModePreferredHeight() +
  289. kActiveSearchBoxHeight + kSearchBoxBottomSpacing +
  290. kSeparatorThickness +
  291. kExpandedSearchBoxCornerRadiusForProductivityLauncher),
  292. AppListPage::contents_view()->height());
  293. return gfx::Size(kWidth, adjusted_height);
  294. }
  295. void SearchResultPageView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  296. if (previous_bounds.size() != bounds().size()) {
  297. // If the clip rect is currently animating, then animate from the current
  298. // clip rect bounds to the newly set bounds.
  299. if (features::IsProductivityLauncherEnabled() &&
  300. layer()->GetAnimator()->is_animating()) {
  301. AnimateBetweenBounds(layer()->clip_rect(), gfx::Rect(bounds().size()));
  302. return;
  303. }
  304. // The clip rect set for page state animations needs to be reset when the
  305. // bounds change because page size change invalidates the previous bounds.
  306. // This allows content to properly follow target bounds when screen
  307. // rotates.
  308. layer()->SetClipRect(gfx::Rect());
  309. }
  310. }
  311. void SearchResultPageView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  312. if (!GetVisible())
  313. return;
  314. // With productivity launcher enabled, individual child result list views will
  315. // have the list box role.
  316. if (!features::IsProductivityLauncherEnabled())
  317. node_data->role = ax::mojom::Role::kListBox;
  318. std::u16string value;
  319. SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView();
  320. const std::u16string& query = search_box->current_query();
  321. if (!query.empty()) {
  322. if (last_search_result_count_ == 1) {
  323. value = l10n_util::GetStringFUTF16(
  324. IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT_SINGLE_RESULT,
  325. query);
  326. } else {
  327. value = l10n_util::GetStringFUTF16(
  328. IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT,
  329. base::NumberToString16(last_search_result_count_), query);
  330. }
  331. } else {
  332. // TODO(https://crbug.com/1216097) Zero state is removed for bubble launcher
  333. // so we need a new A11y announcement.
  334. value = l10n_util::GetStringUTF16(
  335. IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT_ZERO_STATE);
  336. }
  337. node_data->SetValue(value);
  338. }
  339. void SearchResultPageView::OnThemeChanged() {
  340. GetBackground()->SetNativeControlColor(
  341. features::IsProductivityLauncherEnabled()
  342. ? ColorProvider::Get()->GetBaseLayerColor(
  343. ColorProvider::BaseLayerType::kTransparent80)
  344. : AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor());
  345. // SchedulePaint() marks the entire SearchResultPageView's bounds as dirty.
  346. SchedulePaint();
  347. AppListPage::OnThemeChanged();
  348. }
  349. void SearchResultPageView::UpdateForNewSearch() {
  350. notify_a11y_results_changed_timer_.Stop();
  351. if (productivity_launcher_search_view_) {
  352. productivity_launcher_search_view_->UpdateForNewSearch(
  353. ShouldShowSearchResultView());
  354. }
  355. }
  356. void SearchResultPageView::UpdateResultContainersVisibility() {
  357. bool should_show_page_view = false;
  358. if (features::IsProductivityLauncherEnabled()) {
  359. should_show_page_view = ShouldShowSearchResultView();
  360. AnimateToSearchResultsState(should_show_page_view
  361. ? SearchResultsState::kExpanded
  362. : SearchResultsState::kActive);
  363. } else {
  364. bool should_show_search_result_view = ShouldShowSearchResultView();
  365. for (auto* container : result_container_views_) {
  366. // Containers are wrapped by a `SearchCardView`, so update the parent
  367. // visibility.
  368. bool should_show_container_view =
  369. container->num_results() && should_show_search_result_view;
  370. container->parent()->SetVisible(should_show_container_view);
  371. container->SetVisible(should_show_container_view);
  372. should_show_page_view =
  373. should_show_page_view || should_show_container_view;
  374. }
  375. result_lists_separator_->SetVisible(
  376. search_result_tile_item_list_view_->num_results() &&
  377. search_result_list_view_->num_results() &&
  378. ShouldShowSearchResultView());
  379. AppListPage::contents_view()
  380. ->GetSearchBoxView()
  381. ->OnResultContainerVisibilityChanged(should_show_page_view);
  382. }
  383. Layout();
  384. }
  385. void SearchResultPageView::SelectedResultChanged() {
  386. // Result selection should be handled by |productivity_launcher_search_page_|.
  387. DCHECK(!features::IsProductivityLauncherEnabled());
  388. if (!result_selection_controller_->selected_location_details() ||
  389. !result_selection_controller_->selected_result()) {
  390. return;
  391. }
  392. const ResultLocationDetails* selection_details =
  393. result_selection_controller_->selected_location_details();
  394. views::View* selected_row = nullptr;
  395. // For horizontal containers ensure that the whole container fits in the
  396. // scroll view, to account for vertical padding within the container.
  397. if (selection_details->container_is_horizontal) {
  398. selected_row = result_container_views_[selection_details->container_index];
  399. } else {
  400. selected_row = result_selection_controller_->selected_result();
  401. }
  402. selected_row->ScrollViewToVisible();
  403. NotifySelectedResultChanged();
  404. }
  405. void SearchResultPageView::SetIgnoreResultChangesForA11y(bool ignore) {
  406. if (ignore_result_changes_for_a11y_ == ignore)
  407. return;
  408. ignore_result_changes_for_a11y_ = ignore;
  409. GetViewAccessibility().OverrideIsLeaf(ignore);
  410. GetViewAccessibility().OverrideIsIgnored(ignore);
  411. NotifyAccessibilityEvent(ax::mojom::Event::kTreeChanged, true);
  412. }
  413. void SearchResultPageView::ScheduleResultsChangedA11yNotification() {
  414. if (!ignore_result_changes_for_a11y_) {
  415. NotifyA11yResultsChanged();
  416. return;
  417. }
  418. notify_a11y_results_changed_timer_.Start(
  419. FROM_HERE, kNotifyA11yDelay,
  420. base::BindOnce(&SearchResultPageView::NotifyA11yResultsChanged,
  421. base::Unretained(this)));
  422. }
  423. void SearchResultPageView::NotifyA11yResultsChanged() {
  424. SetIgnoreResultChangesForA11y(false);
  425. NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
  426. NotifySelectedResultChanged();
  427. }
  428. void SearchResultPageView::NotifySelectedResultChanged() {
  429. // Result selection should be handled by |productivity_launcher_search_page_|.
  430. DCHECK(!features::IsProductivityLauncherEnabled());
  431. if (ignore_result_changes_for_a11y_)
  432. return;
  433. SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView();
  434. if (!result_selection_controller_->selected_location_details() ||
  435. !result_selection_controller_->selected_result()) {
  436. search_box->SetA11yActiveDescendant(absl::nullopt);
  437. return;
  438. }
  439. views::View* selected_view =
  440. result_selection_controller_->selected_result()->GetSelectedView();
  441. if (!selected_view) {
  442. search_box->SetA11yActiveDescendant(absl::nullopt);
  443. return;
  444. }
  445. search_box->SetA11yActiveDescendant(
  446. selected_view->GetViewAccessibility().GetUniqueId().Get());
  447. }
  448. void SearchResultPageView::UpdatePageBoundsForState(
  449. AppListState state,
  450. const gfx::Rect& contents_bounds,
  451. const gfx::Rect& search_box_bounds) {
  452. if (!features::IsProductivityLauncherEnabled()) {
  453. AppListPage::UpdatePageBoundsForState(state, contents_bounds,
  454. search_box_bounds);
  455. return;
  456. }
  457. if (state != AppListState::kStateSearchResults)
  458. return;
  459. const gfx::Rect to_bounds =
  460. GetPageBoundsForResultState(current_search_results_state_);
  461. if (layer()->GetAnimator()->is_animating()) {
  462. DCHECK(!layer()->clip_rect().IsEmpty());
  463. // When already animating, for an increasing target height, set the bounds
  464. // before animating to keep the animation visible.
  465. if (to_bounds.height() > layer()->clip_rect().height())
  466. SetBoundsRect(to_bounds);
  467. AnimateBetweenBounds(layer()->clip_rect(), gfx::Rect(to_bounds.size()));
  468. } else {
  469. // When no animation is in progress, we only animate when the target
  470. // height is decreasing, otherwise set bounds immediately.
  471. if (to_bounds.height() < bounds().height()) {
  472. AnimateBetweenBounds(gfx::Rect(bounds().size()),
  473. gfx::Rect(to_bounds.size()));
  474. } else {
  475. SetBoundsRect(to_bounds);
  476. }
  477. }
  478. }
  479. void SearchResultPageView::AnimateToSearchResultsState(
  480. SearchResultsState to_state) {
  481. // The search results page is only visible in expanded state. Exit early when
  482. // transitioning between states where results UI is invisible.
  483. if (current_search_results_state_ != SearchResultsState::kExpanded &&
  484. to_state != SearchResultsState::kExpanded) {
  485. SetVisible(false);
  486. current_search_results_state_ = to_state;
  487. return;
  488. }
  489. gfx::Rect from_rect =
  490. GetPageBoundsForResultState(current_search_results_state_);
  491. const gfx::Rect to_rect = GetPageBoundsForResultState(to_state);
  492. if (current_search_results_state_ == SearchResultsState::kExpanded &&
  493. to_state == SearchResultsState::kExpanded) {
  494. // Use current bounds when animating within the expanded state.
  495. from_rect = bounds();
  496. // Only set bounds when the height is increasing so that the entire
  497. // animation between |to_rect| and |from_rect| is visible.
  498. if (to_rect.height() > from_rect.height())
  499. SetBoundsRect(to_rect);
  500. } else if (to_state == SearchResultsState::kExpanded) {
  501. // Set bounds here because this is a result opening transition. We avoid
  502. // setting bounds for closing transitions because then the animation would
  503. // be hidden, instead set the bounds for closing transitions once the
  504. // animation has completed.
  505. SetBoundsRect(to_rect);
  506. AppListPage::contents_view()
  507. ->GetSearchBoxView()
  508. ->OnResultContainerVisibilityChanged(true);
  509. }
  510. current_search_results_state_ = to_state;
  511. AnimateBetweenBounds(from_rect, to_rect);
  512. }
  513. void SearchResultPageView::AnimateBetweenBounds(const gfx::Rect& from_rect,
  514. const gfx::Rect& to_rect) {
  515. if (from_rect == to_rect)
  516. return;
  517. // Return if already animating to the correct target size.
  518. if (layer()->GetAnimator()->is_animating() &&
  519. to_rect.size() == layer()->GetTargetClipRect().size()) {
  520. return;
  521. }
  522. gfx::Rect clip_rect = from_rect;
  523. clip_rect -= to_rect.OffsetFromOrigin();
  524. layer()->SetClipRect(clip_rect);
  525. shadow_.reset();
  526. base::TimeDelta duration;
  527. if (from_rect.height() < to_rect.height()) {
  528. duration = kExpandingSearchResultDuration;
  529. } else {
  530. duration = (current_search_results_state_ == SearchResultsState::kExpanded)
  531. ? kDecreasingHeightSearchResultsDuration
  532. : kClosingSearchResultDuration;
  533. }
  534. views::AnimationBuilder()
  535. .SetPreemptionStrategy(
  536. ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
  537. .OnEnded(
  538. base::BindOnce(&SearchResultPageView::OnAnimationBetweenBoundsEnded,
  539. base::Unretained(this)))
  540. .Once()
  541. .SetDuration(duration)
  542. .SetClipRect(layer(), gfx::Rect(to_rect.size()),
  543. gfx::Tween::FAST_OUT_SLOW_IN)
  544. .SetRoundedCorners(
  545. layer(),
  546. gfx::RoundedCornersF(GetCornerRadiusForSearchResultsState(
  547. current_search_results_state_)),
  548. gfx::Tween::FAST_OUT_SLOW_IN);
  549. }
  550. void SearchResultPageView::OnAnimationBetweenBoundsEnded() {
  551. shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView(
  552. this, kSearchBoxSearchResultShadowType);
  553. shadow_->SetRoundedCornerRadius(
  554. GetCornerRadiusForSearchResultsState(current_search_results_state_));
  555. // To keep the animation visible for closing transitions from expanded search
  556. // results, bounds are set here once the animation completes.
  557. SetBoundsRect(GetPageBoundsForResultState(current_search_results_state_));
  558. // Avoid visible overlap with the search box when the search results are not
  559. // expanded.
  560. if (current_search_results_state_ != SearchResultsState::kExpanded) {
  561. SetVisible(false);
  562. AppListPage::contents_view()
  563. ->GetSearchBoxView()
  564. ->OnResultContainerVisibilityChanged(false);
  565. }
  566. }
  567. gfx::Rect SearchResultPageView::GetPageBoundsForResultState(
  568. SearchResultsState state) const {
  569. AppListState app_list_state = (state == SearchResultsState::kClosed)
  570. ? AppListState::kStateApps
  571. : AppListState::kStateSearchResults;
  572. const ContentsView* const contents_view = AppListPage::contents_view();
  573. const gfx::Rect contents_bounds = contents_view->GetContentsBounds();
  574. gfx::Rect final_bounds =
  575. GetPageBoundsForState(app_list_state, contents_bounds,
  576. contents_view->GetSearchBoxBounds(app_list_state));
  577. // Ensure the height is set according to |state|, because
  578. // GetPageBoundForState() returns a height according to |app_list_state| which
  579. // does not account for kActive search result state.
  580. if (state == SearchResultsState::kActive)
  581. final_bounds.set_height(kActiveSearchBoxHeight);
  582. return final_bounds;
  583. }
  584. int SearchResultPageView::GetCornerRadiusForSearchResultsState(
  585. SearchResultsState state) {
  586. switch (state) {
  587. case SearchResultsState::kClosed:
  588. return kSearchBoxBorderCornerRadius;
  589. case SearchResultsState::kActive:
  590. return kExpandedSearchBoxCornerRadiusForProductivityLauncher;
  591. case SearchResultsState::kExpanded:
  592. return kExpandedSearchBoxCornerRadiusForProductivityLauncher;
  593. }
  594. }
  595. void SearchResultPageView::OnActiveAppListModelsChanged(
  596. AppListModel* model,
  597. SearchModel* search_model) {
  598. for (auto* container : result_container_views_)
  599. container->SetResults(search_model->results());
  600. }
  601. void SearchResultPageView::OnSearchResultContainerResultsChanging() {
  602. // Result selection should be handled by |productivity_launcher_search_page_|.
  603. DCHECK(!features::IsProductivityLauncherEnabled());
  604. // Block any result selection changes while result updates are in flight.
  605. // The selection will be reset once the results are all updated.
  606. result_selection_controller_->set_block_selection_changes(true);
  607. notify_a11y_results_changed_timer_.Stop();
  608. SetIgnoreResultChangesForA11y(true);
  609. }
  610. void SearchResultPageView::OnSearchResultContainerResultsChanged() {
  611. // Skip updates during shutdown.
  612. if (!view_delegate_->HasValidProfile())
  613. return;
  614. // Result selection should be handled by |productivity_launcher_search_page_|.
  615. DCHECK(!features::IsProductivityLauncherEnabled());
  616. DCHECK(!result_container_views_.empty());
  617. int result_count = 0;
  618. // Only sort and layout the containers when they have all updated.
  619. for (SearchResultContainerView* view : result_container_views_) {
  620. if (view->UpdateScheduled())
  621. return;
  622. result_count += view->num_results();
  623. }
  624. last_search_result_count_ = result_count;
  625. UpdateResultContainersVisibility();
  626. ScheduleResultsChangedA11yNotification();
  627. // Find the first result view.
  628. first_result_view_ = nullptr;
  629. for (auto* container : result_container_views_) {
  630. first_result_view_ = container->GetFirstResultView();
  631. if (first_result_view_)
  632. break;
  633. }
  634. // Reset selection to first when things change. The first result is set as
  635. // as the default result.
  636. result_selection_controller_->set_block_selection_changes(false);
  637. result_selection_controller_->ResetSelection(nullptr /*key_event*/,
  638. true /* default_selection */);
  639. // Update SearchBoxView search box autocomplete as necessary based on new
  640. // first result view.
  641. AppListPage::contents_view()->GetSearchBoxView()->ProcessAutocomplete(
  642. first_result_view_);
  643. }
  644. bool SearchResultPageView::CanSelectSearchResults() const {
  645. if (!GetVisible())
  646. return false;
  647. if (features::IsProductivityLauncherEnabled())
  648. return productivity_launcher_search_view_->CanSelectSearchResults();
  649. return first_result_view_;
  650. }
  651. SkColor SearchResultPageView::GetBackgroundColorForState(
  652. AppListState state) const {
  653. if (state == AppListState::kStateSearchResults)
  654. return AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor();
  655. return AppListColorProvider::Get()->GetSearchBoxBackgroundColor();
  656. }
  657. PrivacyContainerView* SearchResultPageView::GetPrivacyContainerViewForTest() {
  658. return privacy_container_view_;
  659. }
  660. SearchResultTileItemListView*
  661. SearchResultPageView::GetSearchResultTileItemListViewForTest() {
  662. return search_result_tile_item_list_view_;
  663. }
  664. SearchResultListView* SearchResultPageView::GetSearchResultListViewForTest() {
  665. return search_result_list_view_;
  666. }
  667. bool SearchResultPageView::ShouldShowSearchResultView() const {
  668. SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView();
  669. return (!features::IsProductivityLauncherEnabled() ||
  670. search_box->HasValidQuery());
  671. }
  672. void SearchResultPageView::OnHidden() {
  673. // Hide the search results page when it is behind search box to avoid focus
  674. // being moved onto suggested apps when zero state is enabled.
  675. AppListPage::OnHidden();
  676. notify_a11y_results_changed_timer_.Stop();
  677. dialog_controller_->Reset(false);
  678. SetVisible(false);
  679. for (auto* container_view : result_container_views_) {
  680. container_view->SetShown(false);
  681. }
  682. AppListPage::contents_view()
  683. ->GetSearchBoxView()
  684. ->OnResultContainerVisibilityChanged(false);
  685. }
  686. void SearchResultPageView::OnShown() {
  687. AppListPage::OnShown();
  688. dialog_controller_->Reset(true);
  689. for (auto* container_view : result_container_views_) {
  690. container_view->SetShown(ShouldShowSearchResultView());
  691. }
  692. AppListPage::contents_view()
  693. ->GetSearchBoxView()
  694. ->OnResultContainerVisibilityChanged(ShouldShowSearchResultView());
  695. if (!features::IsProductivityLauncherEnabled())
  696. ScheduleResultsChangedA11yNotification();
  697. }
  698. void SearchResultPageView::AnimateYPosition(AppListViewState target_view_state,
  699. const TransformAnimator& animator,
  700. float default_offset) {
  701. // Search result page view may host a native view to show answer card results.
  702. // The native view hosts use view to widget coordinate conversion to calculate
  703. // the native view bounds, and thus depend on the view transform values.
  704. // Make sure the view is laid out before starting the transform animation so
  705. // native views are not placed according to interim, animated page transform
  706. // value.
  707. layer()->GetAnimator()->StopAnimatingProperty(
  708. ui::LayerAnimationElement::TRANSFORM);
  709. if (needs_layout())
  710. Layout();
  711. animator.Run(default_offset, layer());
  712. if (shadow_)
  713. animator.Run(default_offset, shadow_->GetNinePatchLayer());
  714. SearchResultPageAnchoredDialog* search_page_dialog =
  715. dialog_controller_->dialog();
  716. if (search_page_dialog) {
  717. const float offset =
  718. search_page_dialog->AdjustVerticalTransformOffset(default_offset);
  719. animator.Run(offset, search_page_dialog->widget()->GetLayer());
  720. }
  721. }
  722. void SearchResultPageView::UpdatePageOpacityForState(AppListState state,
  723. float search_box_opacity,
  724. bool restore_opacity) {
  725. layer()->SetOpacity(search_box_opacity);
  726. }
  727. gfx::Rect SearchResultPageView::GetPageBoundsForState(
  728. AppListState state,
  729. const gfx::Rect& contents_bounds,
  730. const gfx::Rect& search_box_bounds) const {
  731. if (state != AppListState::kStateSearchResults) {
  732. // Hides this view behind the search box by using the same bounds.
  733. return search_box_bounds;
  734. }
  735. gfx::Rect bounding_rect = contents_bounds;
  736. bounding_rect.Inset(
  737. gfx::Insets::TLBR(0, 0, kSearchResultPageMinimumBottomMargin, 0));
  738. gfx::Rect preferred_bounds = gfx::Rect(
  739. search_box_bounds.origin(),
  740. gfx::Size(search_box_bounds.width(), CalculatePreferredSize().height()));
  741. preferred_bounds.Intersect(bounding_rect);
  742. return preferred_bounds;
  743. }
  744. void SearchResultPageView::OnAnimationStarted(AppListState from_state,
  745. AppListState to_state) {
  746. if (from_state != AppListState::kStateSearchResults &&
  747. to_state != AppListState::kStateSearchResults) {
  748. return;
  749. }
  750. if (features::IsProductivityLauncherEnabled()) {
  751. SearchResultsState to_result_state;
  752. if (to_state == AppListState::kStateApps) {
  753. to_result_state = SearchResultsState::kClosed;
  754. } else {
  755. to_result_state = ShouldShowSearchResultView()
  756. ? SearchResultsState::kExpanded
  757. : SearchResultsState::kActive;
  758. }
  759. AnimateToSearchResultsState(to_result_state);
  760. } else {
  761. const ContentsView* const contents_view = AppListPage::contents_view();
  762. const gfx::Rect contents_bounds = contents_view->GetContentsBounds();
  763. const gfx::Rect from_rect =
  764. GetPageBoundsForState(from_state, contents_bounds,
  765. contents_view->GetSearchBoxBounds(from_state));
  766. const gfx::Rect to_rect = GetPageBoundsForState(
  767. to_state, contents_bounds, contents_view->GetSearchBoxBounds(to_state));
  768. if (from_rect == to_rect)
  769. return;
  770. const int to_radius =
  771. contents_view->GetSearchBoxView()
  772. ->GetSearchBoxBorderCornerRadiusForState(to_state);
  773. // Here does the following animations;
  774. // - clip-rect, so it looks like expanding from |from_rect| to |to_rect|.
  775. // - rounded-rect
  776. // - transform of the shadow
  777. SetBoundsRect(to_rect);
  778. gfx::Rect clip_rect = from_rect;
  779. clip_rect -= to_rect.OffsetFromOrigin();
  780. layer()->SetClipRect(clip_rect);
  781. {
  782. auto settings = contents_view->CreateTransitionAnimationSettings(layer());
  783. layer()->SetClipRect(gfx::Rect(to_rect.size()));
  784. // This changes the shadow's corner immediately while this corner bounds
  785. // gradually. This would be fine because this would be unnoticeable to
  786. // users.
  787. shadow_->SetRoundedCornerRadius(to_radius);
  788. }
  789. // Animate the shadow's bounds through transform.
  790. {
  791. gfx::Transform transform;
  792. transform.Translate(from_rect.origin() - to_rect.origin());
  793. transform.Scale(
  794. static_cast<float>(from_rect.width()) / to_rect.width(),
  795. static_cast<float>(from_rect.height()) / to_rect.height());
  796. shadow_->GetLayer()->SetTransform(transform);
  797. auto settings =
  798. contents_view->CreateTransitionAnimationSettings(shadow_->GetLayer());
  799. shadow_->GetLayer()->SetTransform(gfx::Transform());
  800. }
  801. }
  802. }
  803. void SearchResultPageView::OnAnimationUpdated(double progress,
  804. AppListState from_state,
  805. AppListState to_state) {
  806. if (from_state != AppListState::kStateSearchResults &&
  807. to_state != AppListState::kStateSearchResults) {
  808. return;
  809. }
  810. const SkColor color = gfx::Tween::ColorValueBetween(
  811. progress, GetBackgroundColorForState(from_state),
  812. GetBackgroundColorForState(to_state));
  813. if (color != background()->get_color()) {
  814. background()->SetNativeControlColor(color);
  815. SchedulePaint();
  816. }
  817. }
  818. gfx::Size SearchResultPageView::GetPreferredSearchBoxSize() const {
  819. static gfx::Size size = gfx::Size(kWidth, kActiveSearchBoxHeight);
  820. return size;
  821. }
  822. } // namespace ash