| // Copyright 2014 The Chromium Authors |
| // 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/search_result_page_view.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| |
| #include "ash/app_list/app_list_model_provider.h" |
| #include "ash/app_list/app_list_util.h" |
| #include "ash/app_list/views/app_list_main_view.h" |
| #include "ash/app_list/views/contents_view.h" |
| #include "ash/app_list/views/productivity_launcher_search_view.h" |
| #include "ash/app_list/views/search_box_view.h" |
| #include "ash/app_list/views/search_result_base_view.h" |
| #include "ash/app_list/views/search_result_list_view.h" |
| #include "ash/app_list/views/search_result_page_anchored_dialog.h" |
| #include "ash/constants/ash_features.h" |
| #include "ash/public/cpp/app_list/app_list_color_provider.h" |
| #include "ash/public/cpp/app_list/app_list_config.h" |
| #include "ash/public/cpp/app_list/app_list_features.h" |
| #include "ash/public/cpp/style/color_provider.h" |
| #include "ash/search_box/search_box_constants.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/style/system_shadow.h" |
| #include "base/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/time.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/accessibility/platform/ax_unique_id.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/color/color_id.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/animation_builder.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/controls/scrollbar/overlay_scroll_bar.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/window/dialog_delegate.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| constexpr int kMinHeight = 440; |
| constexpr int kWidth = 640; |
| |
| // The height of the active search box in this page. |
| constexpr int kActiveSearchBoxHeight = 56; |
| |
| // Minimum spacing between shelf and bottom of search box. |
| constexpr int kSearchResultPageMinimumBottomMargin = 24; |
| |
| // The shadow type for the shadow of the expanded search box. |
| constexpr SystemShadow::Type kSearchBoxSearchResultShadowType = |
| SystemShadow::Type::kElevation12; |
| |
| // The amount of time by which notifications to accessibility framework about |
| // result page changes are delayed. |
| constexpr base::TimeDelta kNotifyA11yDelay = base::Milliseconds(1500); |
| |
| // The duration of the search result page view expanding animation. |
| constexpr base::TimeDelta kExpandingSearchResultDuration = |
| base::Milliseconds(200); |
| |
| // The duration of the search result page view closing animation. |
| constexpr base::TimeDelta kClosingSearchResultDuration = |
| base::Milliseconds(100); |
| |
| // The duration of the search result page view decreasing height animation |
| // within the kExpanded state. |
| constexpr base::TimeDelta kDecreasingHeightSearchResultsDuration = |
| base::Milliseconds(200); |
| |
| // A container view that ensures the card background and the shadow are painted |
| // in the correct order. |
| class SearchCardView : public views::View { |
| public: |
| METADATA_HEADER(SearchCardView); |
| explicit SearchCardView(std::unique_ptr<views::View> content_view) { |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| AddChildView(std::move(content_view)); |
| } |
| SearchCardView(const SearchCardView&) = delete; |
| SearchCardView& operator=(const SearchCardView&) = delete; |
| ~SearchCardView() override = default; |
| }; |
| |
| BEGIN_METADATA(SearchCardView, views::View) |
| END_METADATA |
| |
| class ZeroWidthVerticalScrollBar : public views::OverlayScrollBar { |
| public: |
| ZeroWidthVerticalScrollBar() : OverlayScrollBar(false) {} |
| ZeroWidthVerticalScrollBar(const ZeroWidthVerticalScrollBar&) = delete; |
| ZeroWidthVerticalScrollBar& operator=(const ZeroWidthVerticalScrollBar&) = |
| delete; |
| ~ZeroWidthVerticalScrollBar() override = default; |
| |
| // OverlayScrollBar overrides: |
| int GetThickness() const override { return 0; } |
| |
| bool OnKeyPressed(const ui::KeyEvent& event) override { |
| // Arrow keys should be handled by FocusManager to move focus. When a search |
| // result is focused, it will be set visible in scroll view. |
| return false; |
| } |
| }; |
| |
| } // namespace |
| |
| SearchResultPageView::SearchResultPageView() : contents_view_(new views::View) { |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| contents_view_->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0)); |
| |
| shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView( |
| this, kSearchBoxSearchResultShadowType); |
| shadow_->SetRoundedCornerRadius(kSearchBoxBorderCornerRadiusSearchResult); |
| |
| // Hides this view behind the search box by using the same color and |
| // background border corner radius. All child views' background should be |
| // set transparent so that the rounded corner is not overwritten. |
| SetBackground(views::CreateSolidBackground(SK_ColorTRANSPARENT)); |
| if (features::IsProductivityLauncherEnabled()) { |
| layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma); |
| layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality); |
| layer()->SetRoundedCornerRadius(gfx::RoundedCornersF( |
| kExpandedSearchBoxCornerRadiusForProductivityLauncher)); |
| } |
| SetLayoutManager(std::make_unique<views::FillLayout>()); |
| |
| // App list bubble search page has its own scroller and result selection |
| // controller so we do not need to construct new ones here. |
| if (features::IsProductivityLauncherEnabled()) { |
| contents_view_->SetBorder(views::CreateEmptyBorder( |
| gfx::Insets::TLBR(kActiveSearchBoxHeight, 0, 0, 0))); |
| AddChildView(contents_view_); |
| } else { |
| auto scroller = std::make_unique<views::ScrollView>(); |
| // Leaves a placeholder area for the search box and the separator below it. |
| scroller->SetBorder(views::CreateEmptyBorder( |
| gfx::Insets::TLBR(kActiveSearchBoxHeight, 0, 0, 0))); |
| scroller->SetDrawOverflowIndicator(false); |
| scroller->SetContents(base::WrapUnique(contents_view_)); |
| // Setting clip height is necessary to make ScrollView take into account its |
| // contents' size. Using zeroes doesn't prevent it from scrolling and sizing |
| // correctly. |
| scroller->ClipHeightTo(0, 0); |
| scroller->SetVerticalScrollBar( |
| std::make_unique<ZeroWidthVerticalScrollBar>()); |
| scroller->SetBackgroundColor(absl::nullopt); |
| AddChildView(std::move(scroller)); |
| |
| result_selection_controller_ = std::make_unique<ResultSelectionController>( |
| &result_container_views_, |
| base::BindRepeating(&SearchResultPageView::SelectedResultChanged, |
| base::Unretained(this))); |
| } |
| |
| AppListModelProvider* const model_provider = AppListModelProvider::Get(); |
| model_provider->AddObserver(this); |
| } |
| |
| SearchResultPageView::~SearchResultPageView() { |
| AppListModelProvider::Get()->RemoveObserver(this); |
| } |
| |
| void SearchResultPageView::InitializeContainers( |
| AppListViewDelegate* view_delegate, |
| AppListMainView* app_list_main_view, |
| SearchBoxView* search_box_view) { |
| DCHECK(view_delegate); |
| view_delegate_ = view_delegate; |
| |
| if (features::IsProductivityLauncherEnabled()) { |
| // For productivity launcher, the dialog will be anchored to the search box |
| // to keep the position of dialogs consistent. |
| dialog_controller_ = |
| std::make_unique<SearchResultPageDialogController>(search_box_view); |
| std::unique_ptr<ProductivityLauncherSearchView> search_view_ptr = |
| std::make_unique<ProductivityLauncherSearchView>( |
| view_delegate, dialog_controller_.get(), search_box_view); |
| productivity_launcher_search_view_ = search_view_ptr.get(); |
| contents_view_->AddChildView( |
| std::make_unique<SearchCardView>(std::move(search_view_ptr))); |
| } else { |
| dialog_controller_ = |
| std::make_unique<SearchResultPageDialogController>(this); |
| |
| search_box_view->SetResultSelectionController( |
| result_selection_controller()); |
| } |
| } |
| |
| void SearchResultPageView::AddSearchResultContainerViewInternal( |
| std::unique_ptr<SearchResultContainerView> result_container) { |
| auto* result_container_ptr = result_container.get(); |
| contents_view_->AddChildView( |
| std::make_unique<SearchCardView>(std::move(result_container))); |
| result_container_views_.push_back(result_container_ptr); |
| result_container_ptr->SetResults( |
| AppListModelProvider::Get()->search_model()->results()); |
| result_container_ptr->set_delegate(this); |
| } |
| |
| bool SearchResultPageView::IsFirstResultTile() const { |
| // In the event that the result does not exist, it is not a tile. |
| if (!first_result_view_ || !first_result_view_->result()) |
| return false; |
| |
| return first_result_view_->result()->display_type() == |
| SearchResultDisplayType::kTile; |
| } |
| |
| bool SearchResultPageView::IsFirstResultHighlighted() const { |
| DCHECK(first_result_view_); |
| return first_result_view_->selected(); |
| } |
| |
| const char* SearchResultPageView::GetClassName() const { |
| return "SearchResultPageView"; |
| } |
| |
| gfx::Size SearchResultPageView::CalculatePreferredSize() const { |
| // TODO(https://crbug.com/1216097) Update height based on available space. |
| if (!features::IsProductivityLauncherEnabled()) |
| return gfx::Size(kWidth, kMinHeight); |
| int adjusted_height = std::min( |
| std::max(kMinHeight, |
| productivity_launcher_search_view_->TabletModePreferredHeight() + |
| kActiveSearchBoxHeight + |
| kExpandedSearchBoxCornerRadiusForProductivityLauncher), |
| AppListPage::contents_view()->height()); |
| return gfx::Size(kWidth, adjusted_height); |
| } |
| |
| void SearchResultPageView::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| if (previous_bounds.size() != bounds().size()) { |
| // If the clip rect is currently animating, then animate from the current |
| // clip rect bounds to the newly set bounds. |
| if (features::IsProductivityLauncherEnabled() && |
| layer()->GetAnimator()->is_animating()) { |
| AnimateBetweenBounds(layer()->clip_rect(), gfx::Rect(bounds().size())); |
| return; |
| } |
| |
| // The clip rect set for page state animations needs to be reset when the |
| // bounds change because page size change invalidates the previous bounds. |
| // This allows content to properly follow target bounds when screen |
| // rotates. |
| layer()->SetClipRect(gfx::Rect()); |
| } |
| } |
| |
| void SearchResultPageView::GetAccessibleNodeData(ui::AXNodeData* node_data) { |
| if (!GetVisible()) |
| return; |
| |
| // With productivity launcher enabled, individual child result list views will |
| // have the list box role. |
| if (!features::IsProductivityLauncherEnabled()) |
| node_data->role = ax::mojom::Role::kListBox; |
| |
| std::u16string value; |
| SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView(); |
| const std::u16string& query = search_box->current_query(); |
| if (!query.empty()) { |
| if (last_search_result_count_ == 1) { |
| value = l10n_util::GetStringFUTF16( |
| IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT_SINGLE_RESULT, |
| query); |
| } else { |
| value = l10n_util::GetStringFUTF16( |
| IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT, |
| base::NumberToString16(last_search_result_count_), query); |
| } |
| } else { |
| // TODO(https://crbug.com/1216097) Zero state is removed for bubble launcher |
| // so we need a new A11y announcement. |
| value = l10n_util::GetStringUTF16( |
| IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT_ZERO_STATE); |
| } |
| |
| node_data->SetValue(value); |
| } |
| |
| void SearchResultPageView::OnThemeChanged() { |
| GetBackground()->SetNativeControlColor( |
| features::IsProductivityLauncherEnabled() |
| ? ColorProvider::Get()->GetBaseLayerColor( |
| ColorProvider::BaseLayerType::kTransparent80) |
| : AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor( |
| GetWidget())); |
| // SchedulePaint() marks the entire SearchResultPageView's bounds as dirty. |
| SchedulePaint(); |
| AppListPage::OnThemeChanged(); |
| } |
| |
| void SearchResultPageView::UpdateForNewSearch() { |
| notify_a11y_results_changed_timer_.Stop(); |
| if (productivity_launcher_search_view_) { |
| productivity_launcher_search_view_->UpdateForNewSearch( |
| ShouldShowSearchResultView()); |
| } |
| } |
| |
| void SearchResultPageView::UpdateResultContainersVisibility() { |
| bool should_show_page_view = false; |
| if (features::IsProductivityLauncherEnabled()) { |
| should_show_page_view = ShouldShowSearchResultView(); |
| AnimateToSearchResultsState(should_show_page_view |
| ? SearchResultsState::kExpanded |
| : SearchResultsState::kActive); |
| } else { |
| bool should_show_search_result_view = ShouldShowSearchResultView(); |
| for (auto* container : result_container_views_) { |
| // Containers are wrapped by a `SearchCardView`, so update the parent |
| // visibility. |
| bool should_show_container_view = |
| container->num_results() && should_show_search_result_view; |
| container->parent()->SetVisible(should_show_container_view); |
| container->SetVisible(should_show_container_view); |
| should_show_page_view = |
| should_show_page_view || should_show_container_view; |
| } |
| |
| AppListPage::contents_view() |
| ->GetSearchBoxView() |
| ->OnResultContainerVisibilityChanged(should_show_page_view); |
| } |
| Layout(); |
| } |
| |
| void SearchResultPageView::SelectedResultChanged() { |
| // Result selection should be handled by |productivity_launcher_search_page_|. |
| DCHECK(!features::IsProductivityLauncherEnabled()); |
| if (!result_selection_controller_->selected_location_details() || |
| !result_selection_controller_->selected_result()) { |
| return; |
| } |
| |
| const ResultLocationDetails* selection_details = |
| result_selection_controller_->selected_location_details(); |
| views::View* selected_row = nullptr; |
| // For horizontal containers ensure that the whole container fits in the |
| // scroll view, to account for vertical padding within the container. |
| if (selection_details->container_is_horizontal) { |
| selected_row = result_container_views_[selection_details->container_index]; |
| } else { |
| selected_row = result_selection_controller_->selected_result(); |
| } |
| |
| selected_row->ScrollViewToVisible(); |
| |
| NotifySelectedResultChanged(); |
| } |
| |
| void SearchResultPageView::SetIgnoreResultChangesForA11y(bool ignore) { |
| if (ignore_result_changes_for_a11y_ == ignore) |
| return; |
| ignore_result_changes_for_a11y_ = ignore; |
| |
| GetViewAccessibility().OverrideIsLeaf(ignore); |
| GetViewAccessibility().OverrideIsIgnored(ignore); |
| NotifyAccessibilityEvent(ax::mojom::Event::kTreeChanged, true); |
| } |
| |
| void SearchResultPageView::ScheduleResultsChangedA11yNotification() { |
| if (!ignore_result_changes_for_a11y_) { |
| NotifyA11yResultsChanged(); |
| return; |
| } |
| |
| notify_a11y_results_changed_timer_.Start( |
| FROM_HERE, kNotifyA11yDelay, |
| base::BindOnce(&SearchResultPageView::NotifyA11yResultsChanged, |
| base::Unretained(this))); |
| } |
| |
| void SearchResultPageView::NotifyA11yResultsChanged() { |
| SetIgnoreResultChangesForA11y(false); |
| |
| NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); |
| NotifySelectedResultChanged(); |
| } |
| |
| void SearchResultPageView::NotifySelectedResultChanged() { |
| // Result selection should be handled by |productivity_launcher_search_page_|. |
| DCHECK(!features::IsProductivityLauncherEnabled()); |
| if (ignore_result_changes_for_a11y_) |
| return; |
| |
| SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView(); |
| if (!result_selection_controller_->selected_location_details() || |
| !result_selection_controller_->selected_result()) { |
| search_box->SetA11yActiveDescendant(absl::nullopt); |
| return; |
| } |
| |
| views::View* selected_view = |
| result_selection_controller_->selected_result()->GetSelectedView(); |
| if (!selected_view) { |
| search_box->SetA11yActiveDescendant(absl::nullopt); |
| return; |
| } |
| |
| search_box->SetA11yActiveDescendant( |
| selected_view->GetViewAccessibility().GetUniqueId().Get()); |
| } |
| |
| void SearchResultPageView::UpdatePageBoundsForState( |
| AppListState state, |
| const gfx::Rect& contents_bounds, |
| const gfx::Rect& search_box_bounds) { |
| if (!features::IsProductivityLauncherEnabled()) { |
| AppListPage::UpdatePageBoundsForState(state, contents_bounds, |
| search_box_bounds); |
| return; |
| } |
| |
| if (state != AppListState::kStateSearchResults) |
| return; |
| |
| const gfx::Rect to_bounds = |
| GetPageBoundsForResultState(current_search_results_state_); |
| |
| if (layer()->GetAnimator()->is_animating()) { |
| DCHECK(!layer()->clip_rect().IsEmpty()); |
| // When already animating, for an increasing target height, set the bounds |
| // before animating to keep the animation visible. |
| if (to_bounds.height() > layer()->clip_rect().height()) |
| SetBoundsRect(to_bounds); |
| AnimateBetweenBounds(layer()->clip_rect(), gfx::Rect(to_bounds.size())); |
| } else { |
| // When no animation is in progress, we only animate when the target |
| // height is decreasing, otherwise set bounds immediately. |
| if (to_bounds.height() < bounds().height()) { |
| AnimateBetweenBounds(gfx::Rect(bounds().size()), |
| gfx::Rect(to_bounds.size())); |
| } else { |
| SetBoundsRect(to_bounds); |
| } |
| } |
| } |
| |
| void SearchResultPageView::AnimateToSearchResultsState( |
| SearchResultsState to_state) { |
| // The search results page is only visible in expanded state. Exit early when |
| // transitioning between states where results UI is invisible. |
| if (current_search_results_state_ != SearchResultsState::kExpanded && |
| to_state != SearchResultsState::kExpanded) { |
| SetVisible(false); |
| current_search_results_state_ = to_state; |
| return; |
| } |
| |
| gfx::Rect from_rect = |
| GetPageBoundsForResultState(current_search_results_state_); |
| const gfx::Rect to_rect = GetPageBoundsForResultState(to_state); |
| |
| if (current_search_results_state_ == SearchResultsState::kExpanded && |
| to_state == SearchResultsState::kExpanded) { |
| // Use current bounds when animating within the expanded state. |
| from_rect = bounds(); |
| |
| // Only set bounds when the height is increasing so that the entire |
| // animation between |to_rect| and |from_rect| is visible. |
| if (to_rect.height() > from_rect.height()) |
| SetBoundsRect(to_rect); |
| |
| } else if (to_state == SearchResultsState::kExpanded) { |
| // Set bounds here because this is a result opening transition. We avoid |
| // setting bounds for closing transitions because then the animation would |
| // be hidden, instead set the bounds for closing transitions once the |
| // animation has completed. |
| SetBoundsRect(to_rect); |
| AppListPage::contents_view() |
| ->GetSearchBoxView() |
| ->OnResultContainerVisibilityChanged(true); |
| } |
| |
| current_search_results_state_ = to_state; |
| AnimateBetweenBounds(from_rect, to_rect); |
| } |
| |
| void SearchResultPageView::AnimateBetweenBounds(const gfx::Rect& from_rect, |
| const gfx::Rect& to_rect) { |
| if (from_rect == to_rect) |
| return; |
| |
| // Return if already animating to the correct target size. |
| if (layer()->GetAnimator()->is_animating() && |
| to_rect.size() == layer()->GetTargetClipRect().size()) { |
| return; |
| } |
| |
| gfx::Rect clip_rect = from_rect; |
| clip_rect -= to_rect.OffsetFromOrigin(); |
| layer()->SetClipRect(clip_rect); |
| shadow_.reset(); |
| |
| base::TimeDelta duration; |
| if (from_rect.height() < to_rect.height()) { |
| duration = kExpandingSearchResultDuration; |
| } else { |
| duration = (current_search_results_state_ == SearchResultsState::kExpanded) |
| ? kDecreasingHeightSearchResultsDuration |
| : kClosingSearchResultDuration; |
| } |
| |
| views::AnimationBuilder() |
| .SetPreemptionStrategy( |
| ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET) |
| .OnEnded( |
| base::BindOnce(&SearchResultPageView::OnAnimationBetweenBoundsEnded, |
| base::Unretained(this))) |
| .Once() |
| .SetDuration(duration) |
| .SetClipRect(layer(), gfx::Rect(to_rect.size()), |
| gfx::Tween::FAST_OUT_SLOW_IN) |
| .SetRoundedCorners( |
| layer(), |
| gfx::RoundedCornersF(GetCornerRadiusForSearchResultsState( |
| current_search_results_state_)), |
| gfx::Tween::FAST_OUT_SLOW_IN); |
| } |
| |
| void SearchResultPageView::OnAnimationBetweenBoundsEnded() { |
| shadow_ = SystemShadow::CreateShadowOnNinePatchLayerForView( |
| this, kSearchBoxSearchResultShadowType); |
| shadow_->SetRoundedCornerRadius( |
| GetCornerRadiusForSearchResultsState(current_search_results_state_)); |
| |
| // To keep the animation visible for closing transitions from expanded search |
| // results, bounds are set here once the animation completes. |
| SetBoundsRect(GetPageBoundsForResultState(current_search_results_state_)); |
| |
| // Avoid visible overlap with the search box when the search results are not |
| // expanded. |
| if (current_search_results_state_ != SearchResultsState::kExpanded) { |
| SetVisible(false); |
| AppListPage::contents_view() |
| ->GetSearchBoxView() |
| ->OnResultContainerVisibilityChanged(false); |
| } |
| } |
| |
| gfx::Rect SearchResultPageView::GetPageBoundsForResultState( |
| SearchResultsState state) const { |
| AppListState app_list_state = (state == SearchResultsState::kClosed) |
| ? AppListState::kStateApps |
| : AppListState::kStateSearchResults; |
| const ContentsView* const contents_view = AppListPage::contents_view(); |
| const gfx::Rect contents_bounds = contents_view->GetContentsBounds(); |
| |
| gfx::Rect final_bounds = |
| GetPageBoundsForState(app_list_state, contents_bounds, |
| contents_view->GetSearchBoxBounds(app_list_state)); |
| |
| // Ensure the height is set according to |state|, because |
| // GetPageBoundForState() returns a height according to |app_list_state| which |
| // does not account for kActive search result state. |
| if (state == SearchResultsState::kActive) |
| final_bounds.set_height(kActiveSearchBoxHeight); |
| |
| return final_bounds; |
| } |
| |
| int SearchResultPageView::GetCornerRadiusForSearchResultsState( |
| SearchResultsState state) { |
| switch (state) { |
| case SearchResultsState::kClosed: |
| return kSearchBoxBorderCornerRadius; |
| case SearchResultsState::kActive: |
| return kExpandedSearchBoxCornerRadiusForProductivityLauncher; |
| case SearchResultsState::kExpanded: |
| return kExpandedSearchBoxCornerRadiusForProductivityLauncher; |
| } |
| } |
| |
| void SearchResultPageView::OnActiveAppListModelsChanged( |
| AppListModel* model, |
| SearchModel* search_model) { |
| for (auto* container : result_container_views_) |
| container->SetResults(search_model->results()); |
| } |
| |
| void SearchResultPageView::OnSearchResultContainerResultsChanging() { |
| // Result selection should be handled by |productivity_launcher_search_page_|. |
| DCHECK(!features::IsProductivityLauncherEnabled()); |
| // Block any result selection changes while result updates are in flight. |
| // The selection will be reset once the results are all updated. |
| result_selection_controller_->set_block_selection_changes(true); |
| |
| notify_a11y_results_changed_timer_.Stop(); |
| SetIgnoreResultChangesForA11y(true); |
| } |
| |
| void SearchResultPageView::OnSearchResultContainerResultsChanged() { |
| // Skip updates during shutdown. |
| if (!view_delegate_->HasValidProfile()) |
| return; |
| |
| // Result selection should be handled by |productivity_launcher_search_page_|. |
| DCHECK(!features::IsProductivityLauncherEnabled()); |
| DCHECK(!result_container_views_.empty()); |
| |
| int result_count = 0; |
| // Only sort and layout the containers when they have all updated. |
| for (SearchResultContainerView* view : result_container_views_) { |
| if (view->UpdateScheduled()) |
| return; |
| result_count += view->num_results(); |
| } |
| |
| last_search_result_count_ = result_count; |
| |
| UpdateResultContainersVisibility(); |
| |
| ScheduleResultsChangedA11yNotification(); |
| |
| // Find the first result view. |
| first_result_view_ = nullptr; |
| for (auto* container : result_container_views_) { |
| first_result_view_ = container->GetFirstResultView(); |
| if (first_result_view_) |
| break; |
| } |
| |
| // Reset selection to first when things change. The first result is set as |
| // as the default result. |
| result_selection_controller_->set_block_selection_changes(false); |
| result_selection_controller_->ResetSelection(nullptr /*key_event*/, |
| true /* default_selection */); |
| // Update SearchBoxView search box autocomplete as necessary based on new |
| // first result view. |
| AppListPage::contents_view()->GetSearchBoxView()->ProcessAutocomplete( |
| first_result_view_); |
| } |
| |
| bool SearchResultPageView::CanSelectSearchResults() const { |
| if (!GetVisible()) |
| return false; |
| |
| if (features::IsProductivityLauncherEnabled()) |
| return productivity_launcher_search_view_->CanSelectSearchResults(); |
| |
| return first_result_view_; |
| } |
| |
| SkColor SearchResultPageView::GetBackgroundColorForState( |
| AppListState state) const { |
| const auto* app_list_widget = GetWidget(); |
| if (state == AppListState::kStateSearchResults) |
| return AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor( |
| app_list_widget); |
| return AppListColorProvider::Get()->GetSearchBoxBackgroundColor( |
| app_list_widget); |
| } |
| |
| SearchResultListView* SearchResultPageView::GetSearchResultListViewForTest() { |
| return search_result_list_view_; |
| } |
| |
| bool SearchResultPageView::ShouldShowSearchResultView() const { |
| SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView(); |
| return (!features::IsProductivityLauncherEnabled() || |
| search_box->HasValidQuery()); |
| } |
| |
| void SearchResultPageView::OnHidden() { |
| // Hide the search results page when it is behind search box to avoid focus |
| // being moved onto suggested apps when zero state is enabled. |
| AppListPage::OnHidden(); |
| notify_a11y_results_changed_timer_.Stop(); |
| dialog_controller_->Reset(false); |
| SetVisible(false); |
| |
| AppListPage::contents_view() |
| ->GetSearchBoxView() |
| ->OnResultContainerVisibilityChanged(false); |
| } |
| |
| void SearchResultPageView::OnShown() { |
| AppListPage::OnShown(); |
| |
| dialog_controller_->Reset(true); |
| |
| AppListPage::contents_view() |
| ->GetSearchBoxView() |
| ->OnResultContainerVisibilityChanged(ShouldShowSearchResultView()); |
| if (!features::IsProductivityLauncherEnabled()) |
| ScheduleResultsChangedA11yNotification(); |
| } |
| |
| void SearchResultPageView::AnimateYPosition(AppListViewState target_view_state, |
| const TransformAnimator& animator, |
| float default_offset) { |
| // Search result page view may host a native view to show answer card results. |
| // The native view hosts use view to widget coordinate conversion to calculate |
| // the native view bounds, and thus depend on the view transform values. |
| // Make sure the view is laid out before starting the transform animation so |
| // native views are not placed according to interim, animated page transform |
| // value. |
| layer()->GetAnimator()->StopAnimatingProperty( |
| ui::LayerAnimationElement::TRANSFORM); |
| if (needs_layout()) |
| Layout(); |
| |
| animator.Run(default_offset, layer()); |
| if (shadow_) |
| animator.Run(default_offset, shadow_->GetNinePatchLayer()); |
| SearchResultPageAnchoredDialog* search_page_dialog = |
| dialog_controller_->dialog(); |
| if (search_page_dialog) { |
| const float offset = |
| search_page_dialog->AdjustVerticalTransformOffset(default_offset); |
| animator.Run(offset, search_page_dialog->widget()->GetLayer()); |
| } |
| } |
| |
| void SearchResultPageView::UpdatePageOpacityForState(AppListState state, |
| float search_box_opacity) { |
| layer()->SetOpacity(search_box_opacity); |
| } |
| |
| gfx::Rect SearchResultPageView::GetPageBoundsForState( |
| AppListState state, |
| const gfx::Rect& contents_bounds, |
| const gfx::Rect& search_box_bounds) const { |
| if (state != AppListState::kStateSearchResults) { |
| // Hides this view behind the search box by using the same bounds. |
| return search_box_bounds; |
| } |
| |
| gfx::Rect bounding_rect = contents_bounds; |
| bounding_rect.Inset( |
| gfx::Insets::TLBR(0, 0, kSearchResultPageMinimumBottomMargin, 0)); |
| |
| gfx::Rect preferred_bounds = gfx::Rect( |
| search_box_bounds.origin(), |
| gfx::Size(search_box_bounds.width(), CalculatePreferredSize().height())); |
| preferred_bounds.Intersect(bounding_rect); |
| |
| return preferred_bounds; |
| } |
| |
| void SearchResultPageView::OnAnimationStarted(AppListState from_state, |
| AppListState to_state) { |
| if (from_state != AppListState::kStateSearchResults && |
| to_state != AppListState::kStateSearchResults) { |
| return; |
| } |
| |
| if (features::IsProductivityLauncherEnabled()) { |
| SearchResultsState to_result_state; |
| if (to_state == AppListState::kStateApps) { |
| to_result_state = SearchResultsState::kClosed; |
| } else { |
| to_result_state = ShouldShowSearchResultView() |
| ? SearchResultsState::kExpanded |
| : SearchResultsState::kActive; |
| } |
| |
| AnimateToSearchResultsState(to_result_state); |
| } else { |
| const ContentsView* const contents_view = AppListPage::contents_view(); |
| const gfx::Rect contents_bounds = contents_view->GetContentsBounds(); |
| const gfx::Rect from_rect = |
| GetPageBoundsForState(from_state, contents_bounds, |
| contents_view->GetSearchBoxBounds(from_state)); |
| const gfx::Rect to_rect = GetPageBoundsForState( |
| to_state, contents_bounds, contents_view->GetSearchBoxBounds(to_state)); |
| if (from_rect == to_rect) |
| return; |
| |
| const int to_radius = |
| contents_view->GetSearchBoxView() |
| ->GetSearchBoxBorderCornerRadiusForState(to_state); |
| |
| // Here does the following animations; |
| // - clip-rect, so it looks like expanding from |from_rect| to |to_rect|. |
| // - rounded-rect |
| // - transform of the shadow |
| SetBoundsRect(to_rect); |
| gfx::Rect clip_rect = from_rect; |
| clip_rect -= to_rect.OffsetFromOrigin(); |
| layer()->SetClipRect(clip_rect); |
| { |
| auto settings = contents_view->CreateTransitionAnimationSettings(layer()); |
| layer()->SetClipRect(gfx::Rect(to_rect.size())); |
| // This changes the shadow's corner immediately while this corner bounds |
| // gradually. This would be fine because this would be unnoticeable to |
| // users. |
| shadow_->SetRoundedCornerRadius(to_radius); |
| } |
| |
| // Animate the shadow's bounds through transform. |
| { |
| gfx::Transform transform; |
| transform.Translate(from_rect.origin() - to_rect.origin()); |
| transform.Scale( |
| static_cast<float>(from_rect.width()) / to_rect.width(), |
| static_cast<float>(from_rect.height()) / to_rect.height()); |
| shadow_->GetLayer()->SetTransform(transform); |
| |
| auto settings = |
| contents_view->CreateTransitionAnimationSettings(shadow_->GetLayer()); |
| shadow_->GetLayer()->SetTransform(gfx::Transform()); |
| } |
| } |
| } |
| |
| void SearchResultPageView::OnAnimationUpdated(double progress, |
| AppListState from_state, |
| AppListState to_state) { |
| if (from_state != AppListState::kStateSearchResults && |
| to_state != AppListState::kStateSearchResults) { |
| return; |
| } |
| const SkColor color = gfx::Tween::ColorValueBetween( |
| progress, GetBackgroundColorForState(from_state), |
| GetBackgroundColorForState(to_state)); |
| |
| if (color != background()->get_color()) { |
| background()->SetNativeControlColor(color); |
| SchedulePaint(); |
| } |
| } |
| |
| gfx::Size SearchResultPageView::GetPreferredSearchBoxSize() const { |
| static gfx::Size size = gfx::Size(kWidth, kActiveSearchBoxHeight); |
| return size; |
| } |
| |
| } // namespace ash |