| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/app_list/views/suggestion_chip_container_view.h" |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #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/search_box_view.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/app_list/app_list_notifier.h" |
| #include "ash/public/cpp/app_list/app_list_types.h" |
| #include "ash/public/cpp/app_list/internal_app_id_constants.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/layout/box_layout.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // The spacing between chips. |
| constexpr int kChipSpacing = 8; |
| |
| // The minimum allowed number of suggestion chips shown in the container |
| // (provided that the suggestion chip results contain at least that number of |
| // items). |
| constexpr int kMinimumSuggestionChipNumber = 3; |
| |
| bool IsPolicySuggestionChip(const SearchResult& result) { |
| return result.display_type() == SearchResultDisplayType::kChip && |
| result.display_index() != SearchResultDisplayIndex::kUndefined; |
| } |
| |
| struct CompareByDisplayIndexAndThenPositionPriority { |
| bool operator()(const SearchResult* result1, |
| const SearchResult* result2) const { |
| // Sort increasing by display index, then decreasing by position priority. |
| SearchResultDisplayIndex index1 = result1->display_index(); |
| SearchResultDisplayIndex index2 = result2->display_index(); |
| float priority1 = result1->position_priority(); |
| float priority2 = result2->position_priority(); |
| if (index1 != index2) |
| return index1 < index2; |
| return priority1 > priority2; |
| } |
| }; |
| |
| } // namespace |
| |
| SuggestionChipContainerView::SuggestionChipContainerView( |
| ContentsView* contents_view) |
| : SearchResultContainerView( |
| contents_view != nullptr |
| ? contents_view->GetAppListMainView()->view_delegate() |
| : nullptr), |
| contents_view_(contents_view) { |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| |
| DCHECK(contents_view); |
| layout_manager_ = SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kHorizontal, gfx::Insets(), kChipSpacing)); |
| layout_manager_->set_main_axis_alignment( |
| views::BoxLayout::MainAxisAlignment::kCenter); |
| |
| for (size_t i = 0; |
| i < static_cast<size_t>( |
| SharedAppListConfig::instance().num_start_page_tiles()); |
| ++i) { |
| SearchResultSuggestionChipView* chip = |
| new SearchResultSuggestionChipView(view_delegate()); |
| chip->SetVisible(false); |
| chip->set_index_in_container(i); |
| suggestion_chip_views_.emplace_back(chip); |
| AddChildView(chip); |
| } |
| } |
| |
| SuggestionChipContainerView::~SuggestionChipContainerView() = default; |
| |
| SearchResultSuggestionChipView* SuggestionChipContainerView::GetResultViewAt( |
| size_t index) { |
| DCHECK(index >= 0 && index < suggestion_chip_views_.size()); |
| return suggestion_chip_views_[index]; |
| } |
| |
| int SuggestionChipContainerView::DoUpdate() { |
| const size_t kMaxSuggestionChips = |
| SharedAppListConfig::instance().num_start_page_tiles(); |
| if (!results()) { |
| for (size_t i = 0; i < kMaxSuggestionChips; ++i) |
| suggestion_chip_views_[i]->SetResult(nullptr); |
| InvalidateLayout(); |
| return 0; |
| } |
| |
| // Filter out priority suggestion chips with a non-default value |
| // for |display_index|. |
| auto filter_requested_index_chips = [](const SearchResult& r) -> bool { |
| return IsPolicySuggestionChip(r); |
| }; |
| std::vector<SearchResult*> requested_index_results = |
| SearchModel::FilterSearchResultsByFunction( |
| results(), base::BindRepeating(filter_requested_index_chips), |
| kMaxSuggestionChips); |
| |
| std::sort(requested_index_results.begin(), requested_index_results.end(), |
| CompareByDisplayIndexAndThenPositionPriority()); |
| |
| // Handle placement issues that may arise when multiple app results have |
| // the same requested index. Reassign the |display_index| of results |
| // with lower priorities or with conflicting indexes so that the results are |
| // added to the final display_results list in the correct order. |
| int previous_index = -1; |
| for (auto* result : requested_index_results) { |
| int current_index = result->display_index(); |
| if (current_index <= previous_index) { |
| current_index = previous_index + 1; |
| } |
| SearchResultDisplayIndex final_index = |
| static_cast<SearchResultDisplayIndex>(current_index); |
| result->set_display_index(final_index); |
| previous_index = current_index; |
| } |
| |
| // Filter to only kChip results. Also filter out all policy chips to prevent |
| // duplicates. |
| auto filter_chip_and_policy = [](const SearchResult& r) -> bool { |
| return r.display_type() == SearchResultDisplayType::kChip && |
| !IsPolicySuggestionChip(r); |
| }; |
| std::vector<SearchResult*> display_results = |
| SearchModel::FilterSearchResultsByFunction( |
| results(), base::BindRepeating(filter_chip_and_policy), |
| kMaxSuggestionChips - requested_index_results.size()); |
| |
| // Update display results list by placing policy result chips at their |
| // specified |display_index|. Do not add with a |display_index| that is out |
| // of bounds. |
| for (auto* result : requested_index_results) { |
| if (result->display_index() <= kMaxSuggestionChips - 1) { |
| display_results.emplace(display_results.begin() + result->display_index(), |
| result); |
| } |
| } |
| |
| // Update search results here, but wait until layout to add them as child |
| // views when we know this view's bounds. |
| for (size_t i = 0; i < kMaxSuggestionChips; ++i) { |
| suggestion_chip_views_[i]->SetResult( |
| i < display_results.size() ? display_results[i] : nullptr); |
| } |
| |
| auto* notifier = view_delegate()->GetNotifier(); |
| if (notifier) { |
| std::vector<AppListNotifier::Result> notifier_results; |
| for (const auto* result : display_results) |
| notifier_results.emplace_back(result->id(), result->metrics_type()); |
| notifier->NotifyResultsUpdated(SearchResultDisplayType::kChip, |
| notifier_results); |
| } |
| |
| Layout(); |
| return std::min(kMaxSuggestionChips, display_results.size()); |
| } |
| |
| const char* SuggestionChipContainerView::GetClassName() const { |
| return "SuggestionChipContainerView"; |
| } |
| |
| void SuggestionChipContainerView::Layout() { |
| // Only show the chips that fit in this view's contents bounds. |
| int total_width = 0; |
| const int max_width = GetContentsBounds().width(); |
| |
| bool has_hidden_chip = false; |
| std::vector<views::View*> shown_chips; |
| for (auto* chip : suggestion_chip_views_) { |
| layout_manager_->ClearFlexForView(chip); |
| |
| if (!chip->result()) { |
| chip->SetVisible(false); |
| continue; |
| } |
| |
| const gfx::Size size = chip->GetPreferredSize(); |
| if (has_hidden_chip || |
| (size.width() + total_width > max_width && |
| shown_chips.size() >= kMinimumSuggestionChipNumber)) { |
| chip->SetVisible(false); |
| has_hidden_chip = true; |
| continue; |
| } |
| |
| chip->SetVisible(true); |
| shown_chips.push_back(chip); |
| |
| total_width += (total_width == 0 ? 0 : kChipSpacing) + size.width(); |
| } |
| |
| // If current suggestion chip width is over the max value, reduce the width by |
| // flexing views whose width is above average for the available space. |
| if (total_width > max_width && shown_chips.size() > 0) { |
| // Remove spacing between chips from total width to get the width available |
| // to visible suggestion chip views. |
| int available_width = std::max( |
| 0, max_width - (kMinimumSuggestionChipNumber - 1) * kChipSpacing); |
| |
| std::vector<views::View*> views_to_flex; |
| views_to_flex.swap(shown_chips); |
| |
| // Do not flex views whose width is below average available width per chip, |
| // as flexing those would actually increase their size. Repeat this until |
| // there are no more views to remove from consideration for flexing |
| // (removing a view increases the average available space for the remaining |
| // views, so another view's size might fit into the remaining space). |
| for (size_t i = 0; i < kMinimumSuggestionChipNumber - 1; ++i) { |
| if (views_to_flex.empty()) |
| break; |
| |
| std::vector<views::View*> next_views_to_flex; |
| const int avg_width = available_width / views_to_flex.size(); |
| for (auto* view : views_to_flex) { |
| gfx::Size view_size = view->GetPreferredSize(); |
| if (view_size.width() <= avg_width) { |
| available_width -= view_size.width(); |
| } else { |
| next_views_to_flex.push_back(view); |
| } |
| } |
| |
| if (views_to_flex.size() == next_views_to_flex.size()) |
| break; |
| views_to_flex.swap(next_views_to_flex); |
| } |
| |
| // Flex the views that are left over. |
| for (auto* view : views_to_flex) |
| layout_manager_->SetFlexForView(view, 1); |
| } |
| |
| views::View::Layout(); |
| } |
| |
| bool SuggestionChipContainerView::OnKeyPressed(const ui::KeyEvent& event) { |
| // Let the FocusManager handle Left/Right keys. |
| if (!IsUnhandledUpDownKeyEvent(event)) |
| return false; |
| |
| // Up key moves focus to the search box. Down key moves focus to the first |
| // app. |
| views::View* v = nullptr; |
| if (event.key_code() == ui::VKEY_UP) { |
| v = contents_view_->GetSearchBoxView()->search_box(); |
| } else { |
| // The first app is the next to this view's last focusable view. |
| views::View* last_focusable_view = |
| GetFocusManager()->GetNextFocusableView(this, nullptr, true, false); |
| v = GetFocusManager()->GetNextFocusableView(last_focusable_view, nullptr, |
| false, false); |
| } |
| if (v) |
| v->RequestFocus(); |
| return true; |
| } |
| |
| void SuggestionChipContainerView::DisableFocusForShowingActiveFolder( |
| bool disabled) { |
| for (auto* chip : suggestion_chip_views_) |
| chip->SetEnabled(!disabled); |
| |
| // Ignore the container view in accessibility tree so that suggestion chips |
| // will not be accessed by ChromeVox. |
| SetViewIgnoredForAccessibility(this, disabled); |
| } |
| |
| void SuggestionChipContainerView::OnTabletModeChanged(bool started) { |
| in_tablet_mode_ = started; |
| UpdateBlurState(); |
| } |
| |
| void SuggestionChipContainerView::SetBlurDisabled(bool blur_disabled) { |
| if (blur_disabled_ == blur_disabled) |
| return; |
| |
| blur_disabled_ = blur_disabled; |
| UpdateBlurState(); |
| } |
| |
| void SuggestionChipContainerView::UpdateBlurState() { |
| // Enable/Disable chips' background blur based on tablet mode, and whether |
| // blur has been explicitly disabled. |
| for (auto* chip : suggestion_chip_views_) |
| chip->SetBackgroundBlurEnabled(in_tablet_mode_ && !blur_disabled_); |
| } |
| |
| } // namespace ash |