chromium/ash/app_list/views/continue_task_view.cc

// Copyright 2021 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/continue_task_view.h"

#include <algorithm>
#include <string>
#include <utility>

#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/model/search/search_result.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/style_util.h"
#include "ash/style/typography.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "chromeos/constants/chromeos_features.h"
#include "extensions/common/constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/menu_separator_types.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/vector_icons.h"

namespace ash {
namespace {

constexpr int kIconSize = 20;
constexpr int kCircleRadius = 18;

constexpr int kBetweenChildPadding = 16;
constexpr auto kInteriorMarginClamshell = gfx::Insets::TLBR(7, 8, 7, 16);
constexpr auto kInteriorMarginTablet = gfx::Insets::TLBR(13, 16, 13, 20);

constexpr int kViewCornerRadiusClamshell = 8;
constexpr int kViewCornerRadiusTablet = 20;
constexpr int kTaskMinWidth = 204;
constexpr int kTaskMaxWidth = 264;

gfx::ImageSkia CreateIconWithCircleBackground(const gfx::ImageSkia& icon,
                                              SkColor color) {
  return gfx::ImageSkiaOperations::CreateImageWithCircleBackground(
      kCircleRadius, color, icon);
}

int GetCornerRadius(bool tablet_mode) {
  return tablet_mode ? kViewCornerRadiusTablet : kViewCornerRadiusClamshell;
}

}  // namespace

ContinueTaskView::ContinueTaskView(AppListViewDelegate* view_delegate,
                                   bool tablet_mode)
    : view_delegate_(view_delegate) {
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetCallback(base::BindRepeating(&ContinueTaskView::OnButtonPressed,
                                  base::Unretained(this)));
  auto ink_drop_highlight_path =
      std::make_unique<views::RoundRectHighlightPathGenerator>(
          gfx::Insets(), GetCornerRadius(tablet_mode));
  ink_drop_highlight_path->set_use_contents_bounds(true);
  ink_drop_highlight_path->set_use_mirrored_rect(true);
  views::HighlightPathGenerator::Install(this,
                                         std::move(ink_drop_highlight_path));
  SetInstallFocusRingOnFocus(true);

  const ui::ColorId focus_ring_color = cros_tokens::kCrosSysFocusRing;
  views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
  views::FocusRing::Get(this)->SetColorId(focus_ring_color);
  SetFocusPainter(nullptr);

  views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
  views::InkDrop::Get(this)->GetInkDrop()->SetShowHighlightOnHover(false);
  SetHasInkDropActionOnClick(true);
  SetShowInkDropWhenHotTracked(false);

  StyleUtil::ConfigureInkDropAttributes(
      this, StyleUtil::kBaseColor | StyleUtil::kInkDropOpacity,
      tablet_mode ? cros_tokens::kCrosSysRippleNeutralOnSubtle
                  : cros_tokens::kCrosSysHoverOnSubtle);

  if (tablet_mode) {
    layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
    layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
    layer()->SetRoundedCornerRadius(
        gfx::RoundedCornersF(GetCornerRadius(/*tablet_mode=*/true)));

    const ui::ColorId background_color =
        static_cast<ui::ColorId>(cros_tokens::kCrosSysSystemBaseElevated);
    SetBackground(views::CreateThemedSolidBackground(background_color));
    SetBorder(std::make_unique<views::HighlightBorder>(
        GetCornerRadius(/*tablet_mode=*/true),
        views::HighlightBorder::Type::kHighlightBorderNoShadow));
  }

  auto* layout_manager = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal,
      tablet_mode ? kInteriorMarginTablet : kInteriorMarginClamshell,
      kBetweenChildPadding));
  layout_manager->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  GetViewAccessibility().SetRole(ax::mojom::Role::kListItem);

  icon_ = AddChildView(std::make_unique<views::ImageView>());
  icon_->SetVerticalAlignment(views::ImageView::Alignment::kCenter);
  icon_->SetHorizontalAlignment(views::ImageView::Alignment::kCenter);

  auto* label_container = AddChildView(std::make_unique<views::View>());
  label_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical));

  title_ = label_container->AddChildView(
      std::make_unique<views::Label>(std::u16string()));
  bubble_utils::ApplyStyle(title_, TypographyToken::kCrosButton1,
                           cros_tokens::kCrosSysOnSurface);
  title_->GetViewAccessibility().SetName(std::u16string());
  title_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
  title_->SetElideBehavior(gfx::ElideBehavior::ELIDE_TAIL);

  subtitle_ = label_container->AddChildView(
      std::make_unique<views::Label>(std::u16string()));
  bubble_utils::ApplyStyle(subtitle_, TypographyToken::kCrosAnnotation1,
                           cros_tokens::kCrosSysOnSurfaceVariant);
  subtitle_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
  subtitle_->SetElideBehavior(gfx::ElideBehavior::ELIDE_MIDDLE);

  layout_manager->SetFlexForView(label_container, 1);

  UpdateResult();
  set_context_menu_controller(this);
}
ContinueTaskView::~ContinueTaskView() {}

void ContinueTaskView::OnThemeChanged() {
  views::View::OnThemeChanged();
  UpdateIcon();
}

gfx::Size ContinueTaskView::GetMaximumSize() const {
  return gfx::Size(kTaskMaxWidth,
                   GetLayoutManager()->GetPreferredSize(this).height());
}

gfx::Size ContinueTaskView::GetMinimumSize() const {
  return gfx::Size(kTaskMinWidth,
                   GetLayoutManager()->GetPreferredSize(this).height());
}

gfx::Size ContinueTaskView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return GetMinimumSize();
}

void ContinueTaskView::OnButtonPressed(const ui::Event& event) {
  views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
      views::InkDropState::ACTION_TRIGGERED);
  OpenResult(event.flags());
}

void ContinueTaskView::UpdateIcon() {
  if (!result()) {
    icon_->SetImage(gfx::ImageSkia());
    return;
  }

  if (!GetWidget()) {
    return;
  }

  gfx::ImageSkia icon;

  if (!result()->icon().icon.IsEmpty()) {
    icon = result()->icon().icon.Rasterize(GetColorProvider());
  } else {
    icon = result()->chip_icon();
  }

  icon_->SetImage(CreateIconWithCircleBackground(
      icon.size() == GetIconSize()
          ? icon
          : gfx::ImageSkiaOperations::CreateResizedImage(
                icon, skia::ImageOperations::RESIZE_BEST, GetIconSize()),
      GetColorProvider()->GetColor(GetIconBackgroundColorId())));
}

ui::ColorId ContinueTaskView::GetIconBackgroundColorId() const {
  if (result()->result_type() == AppListSearchResultType::kZeroStateHelpApp) {
    return cros_tokens::kCrosSysPrimary;
  }

  return cros_tokens::kCrosSysSystemOnBase;
}

gfx::Size ContinueTaskView::GetIconSize() const {
  return gfx::Size(kIconSize, kIconSize);
}

void ContinueTaskView::OnMetadataChanged() {
  UpdateResult();
}

void ContinueTaskView::UpdateResult() {
  SetVisible(!!result());
  views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
      views::InkDropState::HIDDEN);
  CloseContextMenu();
  UpdateIcon();

  if (!result()) {
    title_->SetText(std::u16string());
    subtitle_->SetText(std::u16string());
    GetViewAccessibility().SetName(
        std::u16string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
    return;
  }

  title_->SetText(result()->title());
  subtitle_->SetText(result()->details());
  subtitle_->SetVisible(!result()->details().empty());

  GetViewAccessibility().SetName(result()->title() + u" " + result()->details(),
                                 ax::mojom::NameFrom::kAttribute);
}

void ContinueTaskView::OnResultDestroying() {
  SetResult(nullptr);
}

void ContinueTaskView::SetResult(SearchResult* result) {
  if (result_ == result)
    return;

  search_result_observation_.Reset();

  result_ = result;
  if (result_) {
    search_result_observation_.Observe(result_.get());
    UpdateResult();
  }
}

void ContinueTaskView::ShowContextMenuForViewImpl(
    views::View* source,
    const gfx::Point& point,
    ui::MenuSourceType source_type) {
  // May be null if the result got reset, and the task view is animating out.
  if (!result())
    return;

  int run_types = views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
                  views::MenuRunner::CONTEXT_MENU |
                  views::MenuRunner::FIXED_ANCHOR;

  context_menu_runner_ =
      std::make_unique<views::MenuRunner>(BuildMenuModel(), run_types);

  context_menu_runner_->RunMenuAt(
      source->GetWidget(), nullptr /*button_controller*/,
      source->GetBoundsInScreen(), views::MenuAnchorPosition::kBubbleTopRight,
      source_type);
  views::InkDrop::Get(this)->GetInkDrop()->SnapToActivated();
}

void ContinueTaskView::ExecuteCommand(int command_id, int event_flags) {
  CloseContextMenu();
  switch (command_id) {
    case ContinueTaskCommandId::kOpenResult:
      OpenResult(event_flags);
      break;
    case ContinueTaskCommandId::kRemoveResult:
      RemoveResult();
      break;
    case ContinueTaskCommandId::kHideContinueSection:
      view_delegate_->SetHideContinueSection(true);
      break;
    default:
      NOTREACHED();
  }
}

ui::SimpleMenuModel* ContinueTaskView::BuildMenuModel() {
  DCHECK(result_);
  context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
  context_menu_model_->AddItemWithIcon(
      ContinueTaskCommandId::kOpenResult,
      l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_CONTINUE_SECTION_CONTEXT_MENU_OPEN),
      ui::ImageModel::FromVectorIcon(kLaunchIcon,
                                     ui::kColorAshSystemUIMenuIcon));

  // We won't create the `Remove suggestion` option for admin templates.
  // Reference: b/273800982.
  if (result_->result_type() != AppListSearchResultType::kDesksAdminTemplate) {
    context_menu_model_->AddItemWithIcon(
        ContinueTaskCommandId::kRemoveResult,
        l10n_util::GetStringUTF16(
            IDS_ASH_LAUNCHER_CONTINUE_SECTION_CONTEXT_MENU_REMOVE),
        ui::ImageModel::FromVectorIcon(kRemoveOutlineIcon,
                                       ui::kColorAshSystemUIMenuIcon));
  }

  if (Shell::Get()->IsInTabletMode()) {
    context_menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
    context_menu_model_->AddItemWithIcon(
        ContinueTaskCommandId::kHideContinueSection,
        l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_HIDE_CONTINUE_SECTION),
        ui::ImageModel::FromVectorIcon(kLauncherHideContinueSectionIcon,
                                       ui::kColorAshSystemUIMenuIcon));
  }
  return context_menu_model_.get();
}

void ContinueTaskView::MenuClosed(ui::SimpleMenuModel* menu) {
  views::InkDrop::Get(this)->GetInkDrop()->AnimateToState(
      views::InkDropState::HIDDEN);
}

void ContinueTaskView::OpenResult(int event_flags) {
  // May be null if the result got reset, and the task view is animating out.
  if (!result())
    return;

  view_delegate_->OpenSearchResult(
      result()->id(), event_flags,
      AppListLaunchedFrom::kLaunchedFromContinueTask,
      AppListLaunchType::kSearchResult, index_in_container(),
      false /* launch_as_default */);
}

ContinueTaskView::TaskResultType ContinueTaskView::GetTaskResultType() {
  switch (result()->result_type()) {
    case AppListSearchResultType::kZeroStateFile:
      return TaskResultType::kLocalFile;
    case AppListSearchResultType::kZeroStateDrive:
      return TaskResultType::kDriveFile;
    default:
      NOTREACHED();
  }
}

void ContinueTaskView::RemoveResult() {
  // May be null if the result got reset, and the task view is animating out.
  if (!result())
    return;

  LogMetricsOnResultRemoved();
  view_delegate_->InvokeSearchResultAction(result()->id(),
                                           SearchResultActionType::kRemove);
}

bool ContinueTaskView::IsMenuShowing() const {
  return context_menu_runner_ && context_menu_runner_->IsRunning();
}

void ContinueTaskView::CloseContextMenu() {
  if (!IsMenuShowing())
    return;
  context_menu_runner_->Cancel();
}

void ContinueTaskView::LogMetricsOnResultRemoved() {
  RecordCumulativeContinueSectionResultRemovedNumber();

  base::UmaHistogramEnumeration("Apps.AppList.Search.ContinueResultRemoved",
                                GetTaskResultType(), TaskResultType::kMaxValue);
}

BEGIN_METADATA(ContinueTaskView)
END_METADATA

}  // namespace ash