[go: nahoru, domu]

blob: 9aabe3f7ea0b282ed6d60aeb628f65b1f133d109 [file] [log] [blame]
// Copyright 2014 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/search_result_page_view.h"
#include <stddef.h>
#include <algorithm>
#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/privacy_container_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/app_list/views/search_result_tile_item_list_view.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/view_shadow.h"
#include "ash/search_box/search_box_constants.h"
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/optional.h"
#include "base/strings/string_number_conversions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/compositor_extra/shadow.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.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/focus/focus_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/metadata/metadata_header_macros.h"
#include "ui/views/metadata/metadata_impl_macros.h"
#include "ui/views/window/dialog_delegate.h"
namespace ash {
namespace {
constexpr int kHeight = 440;
constexpr int kWidth = 640;
// The horizontal padding of the separator.
constexpr int kSeparatorPadding = 12;
constexpr int kSeparatorThickness = 1;
// The height of the search box in this page.
constexpr int kSearchBoxHeight = 56;
// The spacing between search box bottom and separator line.
// Add 1 pixel spacing so that the search bbox bottom will not paint over
// the separator line drawn by SearchResultPageBackground in some scale factors
// due to the round up.
constexpr int kSearchBoxBottomSpacing = 1;
// Minimum spacing between shelf and bottom of search box.
constexpr int kSearchResultPageMinimumBottomMargin = 24;
// The shadow elevation value for the shadow of the expanded search box.
constexpr int kSearchBoxSearchResultShadowElevation = 12;
// The amount of time by which notifications to accessibility framework about
// result page changes are delayed.
constexpr base::TimeDelta kNotifyA11yDelay =
base::TimeDelta::FromMilliseconds(1500);
// 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 {}
};
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;
}
};
class SearchResultPageBackground : public views::Background {
public:
explicit SearchResultPageBackground(SkColor color) {
SetNativeControlColor(color);
}
SearchResultPageBackground(const SearchResultPageBackground&) = delete;
SearchResultPageBackground& operator=(const SearchResultPageBackground&) =
delete;
~SearchResultPageBackground() override = default;
private:
// views::Background overrides:
void Paint(gfx::Canvas* canvas, views::View* view) const override {
canvas->DrawColor(get_color());
gfx::Rect bounds = view->GetContentsBounds();
if (bounds.height() <= kSearchBoxHeight)
return;
// Draw a separator between SearchBoxView and SearchResultPageView.
bounds.set_y(kSearchBoxHeight + kSearchBoxBottomSpacing);
bounds.set_height(kSeparatorThickness);
canvas->FillRect(bounds, AppListColorProvider::Get()->GetSeparatorColor());
}
};
} // namespace
class SearchResultPageView::HorizontalSeparator : public views::View {
public:
explicit HorizontalSeparator(int preferred_width)
: preferred_width_(preferred_width) {
SetBorder(views::CreateEmptyBorder(
gfx::Insets(0, kSeparatorPadding, 0, kSeparatorPadding)));
}
~HorizontalSeparator() override {}
// views::View overrides:
const char* GetClassName() const override { return "HorizontalSeparator"; }
gfx::Size CalculatePreferredSize() const override {
return gfx::Size(preferred_width_, kSeparatorThickness);
}
void OnPaint(gfx::Canvas* canvas) override {
gfx::Rect rect = GetContentsBounds();
canvas->FillRect(rect, AppListColorProvider::Get()->GetSeparatorColor());
View::OnPaint(canvas);
}
private:
const int preferred_width_;
DISALLOW_COPY_AND_ASSIGN(HorizontalSeparator);
};
SearchResultPageView::SearchResultPageView(SearchModel* search_model)
: search_model_(search_model), contents_view_(new views::View) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
contents_view_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, gfx::Insets(), 0));
view_shadow_ =
std::make_unique<ViewShadow>(this, kSearchBoxSearchResultShadowElevation);
view_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(std::make_unique<SearchResultPageBackground>(
AppListColorProvider::Get()->GetSearchBoxCardBackgroundColor()));
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(
kSearchBoxHeight + kSearchBoxBottomSpacing + kSeparatorThickness, 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(base::nullopt);
AddChildView(std::move(scroller));
SetLayoutManager(std::make_unique<views::FillLayout>());
result_selection_controller_ = std::make_unique<ResultSelectionController>(
&result_container_views_,
base::BindRepeating(&SearchResultPageView::SelectedResultChanged,
base::Unretained(this)));
search_box_observation_.Observe(search_model->search_box());
}
SearchResultPageView::~SearchResultPageView() = default;
void SearchResultPageView::AddSearchResultContainerViewInternal(
std::unique_ptr<SearchResultContainerView> result_container) {
if (!result_container_views_.empty()) {
separators_.push_back(contents_view_->AddChildView(
std::make_unique<HorizontalSeparator>(bounds().width())));
}
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(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 {
return gfx::Size(kWidth, kHeight);
}
void SearchResultPageView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
// 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.
if (previous_bounds.size() != bounds().size())
layer()->SetClipRect(gfx::Rect());
}
void SearchResultPageView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
if (!GetVisible())
return;
node_data->role = ax::mojom::Role::kListBox;
std::u16string value;
std::u16string query = search_model_->search_box()->text();
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 {
value = l10n_util::GetStringUTF16(
IDS_APP_LIST_SEARCHBOX_RESULTS_ACCESSIBILITY_ANNOUNCEMENT_ZERO_STATE);
}
node_data->SetValue(value);
}
void SearchResultPageView::ReorderSearchResultContainers() {
// Sort the result container views by their score.
std::sort(result_container_views_.begin(), result_container_views_.end(),
[](const SearchResultContainerView* a,
const SearchResultContainerView* b) -> bool {
return a->container_score() > b->container_score();
});
for (size_t i = 0; i < result_container_views_.size(); ++i) {
SearchResultContainerView* view = result_container_views_[i];
if (i > 0) {
HorizontalSeparator* separator = separators_[i - 1];
const bool preceded_by_privacy_container =
result_container_views_[i - 1] ==
AppListPage::contents_view()->privacy_container_view();
// Hides the separator above the container that has no results, and below
// the privacy container.
separator->SetVisible(view->num_results() &&
!preceded_by_privacy_container);
contents_view_->ReorderChildView(separator, i * 2 - 1);
contents_view_->ReorderChildView(view->parent(), i * 2);
} else {
contents_view_->ReorderChildView(view->parent(), i);
}
}
Layout();
}
void SearchResultPageView::SelectedResultChanged() {
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() {
if (ignore_result_changes_for_a11y_ ||
!result_selection_controller_->selected_location_details() ||
!result_selection_controller_->selected_result()) {
return;
}
SearchBoxView* search_box = AppListPage::contents_view()->GetSearchBoxView();
// Ignore result selection change if the focus moved away from the search boc
// textfield, for example to the close button.
if (!search_box->search_box()->HasFocus())
return;
views::View* selected_view =
result_selection_controller_->selected_result()->GetSelectedView();
if (!selected_view)
return;
selected_view->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
NotifyAccessibilityEvent(ax::mojom::Event::kSelectedChildrenChanged, true);
search_box->set_a11y_selection_on_search_result(true);
}
void SearchResultPageView::OnSearchResultContainerResultsChanging() {
// 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() {
DCHECK(!result_container_views_.empty());
DCHECK(result_container_views_.size() == separators_.size() + 1);
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;
ReorderSearchResultContainers();
ScheduleResultsChangedA11yNotification();
first_result_view_ = result_container_views_[0]->GetFirstResultView();
// 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();
}
void SearchResultPageView::Update() {
notify_a11y_results_changed_timer_.Stop();
}
void SearchResultPageView::SearchEngineChanged() {}
void SearchResultPageView::ShowAssistantChanged() {}
void SearchResultPageView::ShowAnchoredDialog(
std::unique_ptr<views::DialogDelegateView> dialog) {
ContentsView* const contents_view = AppListPage::contents_view();
if (contents_view->GetActiveState() != AppListState::kStateSearchResults)
return;
anchored_dialog_ = std::make_unique<SearchResultPageAnchoredDialog>(
std::move(dialog), contents_view,
base::BindOnce(&SearchResultPageView::OnAnchoredDialogClosed,
base::Unretained(this)));
const gfx::Rect anchor_bounds =
contents_view->GetSearchBoxBounds(AppListState::kStateSearchResults);
anchored_dialog_->UpdateBounds(anchor_bounds);
anchored_dialog_->widget()->Show();
}
void SearchResultPageView::OnWillBeHidden() {
anchored_dialog_.reset();
}
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();
SetVisible(false);
for (auto* container_view : result_container_views_) {
container_view->SetShown(false);
}
}
void SearchResultPageView::OnShown() {
AppListPage::OnShown();
for (auto* container_view : result_container_views_) {
container_view->SetShown(true);
}
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());
animator.Run(default_offset, view_shadow_->shadow()->shadow_layer());
if (anchored_dialog_) {
const float offset =
anchored_dialog_->AdjustVerticalTransformOffset(default_offset);
animator.Run(offset, anchored_dialog_->widget()->GetLayer());
}
}
void SearchResultPageView::UpdatePageOpacityForState(AppListState state,
float search_box_opacity,
bool restore_opacity) {
layer()->SetOpacity(search_box_opacity);
}
void SearchResultPageView::UpdatePageBoundsForState(
AppListState state,
const gfx::Rect& contents_bounds,
const gfx::Rect& search_box_bounds) {
AppListPage::UpdatePageBoundsForState(state, contents_bounds,
search_box_bounds);
if (anchored_dialog_)
anchored_dialog_->UpdateBounds(search_box_bounds);
}
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(0, 0, 0, kSearchResultPageMinimumBottomMargin);
gfx::Rect preferred_bounds =
gfx::Rect(search_box_bounds.origin(),
gfx::Size(search_box_bounds.width(), kHeight));
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;
}
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.
view_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());
view_shadow_->shadow()->layer()->SetTransform(transform);
auto settings = contents_view->CreateTransitionAnimationSettings(
view_shadow_->shadow()->layer());
view_shadow_->shadow()->layer()->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 SearchBoxView* search_box =
AppListPage::contents_view()->GetSearchBoxView();
const SkColor color = gfx::Tween::ColorValueBetween(
progress, search_box->GetBackgroundColorForState(from_state),
search_box->GetBackgroundColorForState(to_state));
if (color != background()->get_color()) {
background()->SetNativeControlColor(color);
SchedulePaint();
}
}
gfx::Size SearchResultPageView::GetPreferredSearchBoxSize() const {
static gfx::Size size = gfx::Size(kWidth, kSearchBoxHeight);
return size;
}
base::Optional<int> SearchResultPageView::GetSearchBoxTop(
AppListViewState view_state) const {
if (view_state == AppListViewState::kPeeking ||
view_state == AppListViewState::kHalf) {
return AppListConfig::instance().search_box_fullscreen_top_padding();
}
// For other view states, return base::nullopt so the ContentsView
// sets the default search box widget origin.
return base::nullopt;
}
views::View* SearchResultPageView::GetFirstFocusableView() {
return GetFocusManager()->GetNextFocusableView(
this, GetWidget(), false /* reverse */, false /* dont_loop */);
}
views::View* SearchResultPageView::GetLastFocusableView() {
return GetFocusManager()->GetNextFocusableView(
this, GetWidget(), true /* reverse */, false /* dont_loop */);
}
void SearchResultPageView::OnAnchoredDialogClosed() {
anchored_dialog_.reset();
}
} // namespace ash