chromium/ash/app_list/views/search_result_actions_view.cc

// Copyright 2013 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_actions_view.h"

#include <stddef.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <utility>

#include "ash/app_list/app_list_util.h"
#include "ash/app_list/views/search_result_actions_view_delegate.h"
#include "ash/app_list/views/search_result_view.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
#include "ash/style/icon_button.h"
#include "ash/style/style_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_highlight.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view_utils.h"

namespace ash {

namespace {

constexpr int kActionButtonBetweenSpacing = 8;

}  // namespace

// SearchResultActionButton renders the button defined by SearchResult::Action.
class SearchResultActionButton : public IconButton {
  METADATA_HEADER(SearchResultActionButton, IconButton)

 public:
  SearchResultActionButton(SearchResultActionsView* parent,
                           const SearchResult::Action& action,
                           PressedCallback callback,
                           Type type,
                           const gfx::VectorIcon* icon,
                           const std::u16string& accessible_name);

  SearchResultActionButton(const SearchResultActionButton&) = delete;
  SearchResultActionButton& operator=(const SearchResultActionButton&) = delete;

  ~SearchResultActionButton() override {}

  // IconButton:
  void OnGestureEvent(ui::GestureEvent* event) override;

  // Updates the button visibility upon state change of the button or the
  // search result view associated with it.
  void UpdateOnStateChanged();

 private:
  int GetButtonRadius() const;

  raw_ptr<SearchResultActionsView> parent_;
  bool to_be_activate_by_long_press_ = false;
};

SearchResultActionButton::SearchResultActionButton(
    SearchResultActionsView* parent,
    const SearchResult::Action& action,
    PressedCallback callback,
    Type type,
    const gfx::VectorIcon* icon,
    const std::u16string& accessible_name)
    : IconButton(std::move(callback),
                 type,
                 icon,
                 action.tooltip_text,
                 /*is_togglable=*/false,
                 /*has_border=*/false),
      parent_(parent) {
  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetVisible(false);

  StyleUtil::SetUpFocusRingForView(this);
  views::FocusRing::Get(this)->SetHasFocusPredicate(
      base::BindRepeating([](const View* view) {
        const auto* v = views::AsViewClass<SearchResultActionButton>(view);
        CHECK(v);
        return v->HasFocus() || v->parent_->GetSelectedAction() == v->tag();
      }));
}

void SearchResultActionButton::OnGestureEvent(ui::GestureEvent* event) {
  switch (event->type()) {
    case ui::EventType::kGestureLongPress:
      to_be_activate_by_long_press_ = true;
      event->SetHandled();
      break;
    case ui::EventType::kGestureEnd:
      if (to_be_activate_by_long_press_) {
        NotifyClick(*event);
        SetState(STATE_NORMAL);
        to_be_activate_by_long_press_ = false;
        event->SetHandled();
      }
      break;
    default:
      break;
  }
  if (!event->handled())
    Button::OnGestureEvent(event);
}

void SearchResultActionButton::UpdateOnStateChanged() {
  // Show button if the associated result row is hovered or selected, or one
  // of the action buttons is selected.
  SetVisible(parent_->IsSearchResultHoveredOrSelected());
  views::FocusRing::Get(this)->SchedulePaint();
}

int SearchResultActionButton::GetButtonRadius() const {
  return width() / 2;
}

BEGIN_METADATA(SearchResultActionButton)
END_METADATA

SearchResultActionsView::SearchResultActionsView(
    SearchResultActionsViewDelegate* delegate)
    : delegate_(delegate) {
  DCHECK(delegate_);
  SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
      kActionButtonBetweenSpacing));
}

SearchResultActionsView::~SearchResultActionsView() {}

void SearchResultActionsView::SetActions(const SearchResult::Actions& actions) {
  if (selected_action_.has_value())
    selected_action_.reset();
  subscriptions_.clear();
  RemoveAllChildViews();

  for (size_t i = 0; i < actions.size(); ++i)
    CreateImageButton(actions[i], i);
  PreferredSizeChanged();
}

bool SearchResultActionsView::IsValidActionIndex(size_t action_index) const {
  return action_index < GetActionCount();
}

bool SearchResultActionsView::IsSearchResultHoveredOrSelected() const {
  return delegate_->IsSearchResultHoveredOrSelected();
}

void SearchResultActionsView::HideActions() {
  for (views::View* child : children())
    child->SetVisible(false);
}

void SearchResultActionsView::UpdateButtonsOnStateChanged() {
  for (views::View* child : children())
    static_cast<SearchResultActionButton*>(child)->UpdateOnStateChanged();
}

bool SearchResultActionsView::SelectInitialAction(bool reverse_tab_order) {
  if (GetActionCount() == 0)
    return false;

  if (reverse_tab_order) {
    selected_action_ = GetActionCount() - 1;
  } else {
    selected_action_.reset();
  }
  UpdateButtonsOnStateChanged();
  return selected_action_.has_value();
}

bool SearchResultActionsView::SelectNextAction(bool reverse_tab_order) {
  if (GetActionCount() == 0)
    return false;

  // For reverse tab order, consider moving to non-selected state.
  if (reverse_tab_order) {
    if (!selected_action_.has_value())
      return false;

    if (selected_action_.value() == 0) {
      ClearSelectedAction();
      return true;
    }
  }

  const int next_index =
      selected_action_.value_or(-1) + (reverse_tab_order ? -1 : 1);
  if (!IsValidActionIndex(next_index))
    return false;

  selected_action_ = next_index;
  UpdateButtonsOnStateChanged();
  return true;
}

views::View* SearchResultActionsView::GetSelectedView() {
  DCHECK(HasSelectedAction());

  int selected_action = GetSelectedAction();
  for (views::View* child : children()) {
    if (static_cast<views::Button*>(child)->tag() == selected_action)
      return child;
  }

  return nullptr;
}

void SearchResultActionsView::ClearSelectedAction() {
  selected_action_.reset();
  UpdateButtonsOnStateChanged();
}

int SearchResultActionsView::GetSelectedAction() const {
  return selected_action_.value_or(-1);
}

bool SearchResultActionsView::HasSelectedAction() const {
  return selected_action_.has_value();
}

void SearchResultActionsView::CreateImageButton(
    const SearchResult::Action& action,
    int action_index) {
  const gfx::VectorIcon* icon = nullptr;
  switch (action.type) {
    case SearchResultActionType::kRemove:
      icon = &ash::kSearchResultRemoveIcon;
      break;
  }

  DCHECK(icon);

  auto* const button = AddChildView(std::make_unique<SearchResultActionButton>(
      this, action,
      base::BindRepeating(
          &SearchResultActionsViewDelegate::OnSearchResultActionActivated,
          base::Unretained(delegate_), action_index),
      IconButton::Type::kMediumFloating, icon, action.tooltip_text));
  button->set_tag(action_index);
  subscriptions_.push_back(button->AddStateChangedCallback(
      base::BindRepeating(&SearchResultActionsView::UpdateButtonsOnStateChanged,
                          base::Unretained(this))));
}

size_t SearchResultActionsView::GetActionCount() const {
  return children().size();
}

void SearchResultActionsView::ChildVisibilityChanged(views::View* child) {
  PreferredSizeChanged();
}

BEGIN_METADATA(SearchResultActionsView)
END_METADATA

}  // namespace ash