chromium/ash/app_list/views/app_list_toast_container_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/app_list_toast_container_view.h"

#include <memory>

#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/views/app_list_a11y_announcer.h"
#include "ash/app_list/views/app_list_keyboard_controller.h"
#include "ash/app_list/views/app_list_nudge_controller.h"
#include "ash/app_list/views/app_list_toast_view.h"
#include "ash/app_list/views/apps_grid_context_menu.h"
#include "ash/public/cpp/app_list/app_list_model_delegate.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/feature_discovery_duration_reporter.h"
#include "ash/public/cpp/feature_discovery_metric_util.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_abort_handle.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/view_class_properties.h"

namespace ash {

namespace {

const gfx::VectorIcon* GetToastIconForOrder(AppListSortOrder order) {
  switch (order) {
    case AppListSortOrder::kNameAlphabetical:
    case AppListSortOrder::kNameReverseAlphabetical:
      return &kSortAlphabeticalIcon;
    case AppListSortOrder::kColor:
      return &kSortColorIcon;
    case AppListSortOrder::kCustom:
    case AppListSortOrder::kAlphabeticalEphemeralAppFirst:
      NOTREACHED();
  }
}

constexpr auto kReorderUndoInteriorMargin = gfx::Insets::TLBR(8, 16, 8, 8);

}  // namespace

AppsGridContextMenu::GridType
AppListToastContainerView::Delegate::GetGridTypeForContextMenu() {
  return AppsGridContextMenu::GridType::kAppsGrid;
}

AppListToastContainerView::AppListToastContainerView(
    AppListNudgeController* nudge_controller,
    AppListKeyboardController* keyboard_controller,
    AppListA11yAnnouncer* a11y_announcer,
    AppListViewDelegate* view_delegate,
    Delegate* delegate,
    bool tablet_mode)
    : a11y_announcer_(a11y_announcer),
      tablet_mode_(tablet_mode),
      view_delegate_(view_delegate),
      delegate_(delegate),
      nudge_controller_(nudge_controller),
      keyboard_controller_(keyboard_controller),
      current_toast_(AppListToastType::kNone) {
  DCHECK(a11y_announcer_);
  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
      .SetOrientation(views::LayoutOrientation::kHorizontal)
      .SetDefault(
          views::kFlexBehaviorKey,
          views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
                                   views::MaximumFlexSizeRule::kPreferred));

  if (!tablet_mode_) {
    // `context_menu_` is only set in clamshell mode. The sort options in tablet
    // mode are handled in RootWindowController with ShelfContextMenuModel.
    context_menu_ = std::make_unique<AppsGridContextMenu>(
        delegate->GetGridTypeForContextMenu());
    set_context_menu_controller(context_menu_.get());
  }
}

AppListToastContainerView::~AppListToastContainerView() {
  set_context_menu_controller(nullptr);
  toast_view_ = nullptr;
}

bool AppListToastContainerView::OnKeyPressed(const ui::KeyEvent& event) {
  if (!delegate_ || !keyboard_controller_) {
    return false;
  }

  if (event.key_code() == ui::VKEY_UP)
    return keyboard_controller_->MoveFocusUpFromToast(focused_app_column_);

  if (event.key_code() == ui::VKEY_DOWN)
    return keyboard_controller_->MoveFocusDownFromToast(focused_app_column_);

  return false;
}

bool AppListToastContainerView::HandleFocus(int column) {
  // Only handle the focus if a button on the toast exists.
  views::LabelButton* toast_button = GetToastButton();
  if (toast_button) {
    focused_app_column_ = column;
    toast_button->RequestFocus();
    return true;
  }

  views::Button* close_button = GetCloseButton();
  if (close_button) {
    focused_app_column_ = column;
    close_button->RequestFocus();
    return true;
  }

  return false;
}

void AppListToastContainerView::DisableFocusForShowingActiveFolder(
    bool disabled) {
  if (auto* toast_button = GetToastButton())
    toast_button->SetEnabled(!disabled);
  if (auto* close_button = GetCloseButton())
    close_button->SetEnabled(!disabled);

  // Prevent items from being accessed by ChromeVox.
  SetViewIgnoredForAccessibility(this, disabled);
}

void AppListToastContainerView::MaybeUpdateReorderNudgeView() {
  // If the expect state in `nudge_controller_` is different from the one in
  // toast container, update the actual reorder nudge view in the toast
  // container.
  if (nudge_controller_->ShouldShowReorderNudge() &&
      current_toast_ != AppListToastType::kReorderNudge) {
    CreateReorderNudgeView();
  } else if (!nudge_controller_->ShouldShowReorderNudge() &&
             current_toast_ == AppListToastType::kReorderNudge) {
    RemoveReorderNudgeView();
  }
}

void AppListToastContainerView::CreateReorderNudgeView() {
  if (toast_view_)
    return;

  int subtitle_message_id =
      tablet_mode_
          ? IDS_ASH_LAUNCHER_APP_LIST_REORDER_NUDGE_TABLET_MODE_SUBTITLE
          : IDS_ASH_LAUNCHER_APP_LIST_REORDER_NUDGE_CLAMSHELL_MODE_SUBTITLE;

  AppListToastView::Builder toast_view_builder(
      l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_APP_LIST_REORDER_NUDGE_TITLE));

  toast_view_builder.SetButton(
      l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_APP_LIST_REORDER_NUDGE_DISMISS_BUTTON),
      base::BindRepeating(&AppListToastContainerView::FadeOutToastView,
                          base::Unretained(this)));

  FeatureDiscoveryDurationReporter* reporter =
      FeatureDiscoveryDurationReporter::GetInstance();
  reporter->MaybeActivateObservation(
      feature_discovery::TrackableFeature::kAppListReorderAfterEducationNudge);
  reporter->MaybeActivateObservation(
      feature_discovery::TrackableFeature::
          kAppListReorderAfterEducationNudgePerTabletMode);
  toast_view_ = AddChildView(
      toast_view_builder.SetStyleForTabletMode(tablet_mode_)
          .SetSubtitle(l10n_util::GetStringUTF16(subtitle_message_id))
          .SetIcon(
              ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
                  IDR_APP_LIST_SORT_NUDGE_IMAGE))
          .SetIconBackground(true)
          .Build());
  if (available_width_) {
    toast_view_->SetAvailableWidth(*available_width_);
  }
  current_toast_ = AppListToastType::kReorderNudge;
}

void AppListToastContainerView::CreateTutorialNudgeView() {
  if (toast_view_) {
    return;
  }

  AppListToastView::Builder toast_view_builder(
      l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_APPS_COLLECTIONS_NUDGE_TITLE));

  toast_view_builder
      .SetButton(
          l10n_util::GetStringUTF16(
              IDS_ASH_LAUNCHER_APPS_COLLECTIONS_NUDGE_DISMISS_BUTTON),
          base::BindRepeating(&AppListToastContainerView::FadeOutToastView,
                              base::Unretained(this)))
      .SetStyleForTabletMode(tablet_mode_)
      .SetSubtitle(l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_APPS_COLLECTIONS_NUDGE_SUBTITLE))
      .SetIconBackground(true);

  toast_view_ = AddChildView(toast_view_builder.Build());
  toast_view_->GetViewAccessibility().SetRole(ax::mojom::Role::kRegion);
  toast_view_->toast_button()->GetViewAccessibility().SetName(
      l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_APPS_COLLECTIONS_NUDGE_DISMISS_BUTTON_SPOKEN_TEXT));
  if (available_width_) {
    toast_view_->SetAvailableWidth(*available_width_);
  }
  current_toast_ = AppListToastType::kTutorialViewNudge;
}

void AppListToastContainerView::RemoveReorderNudgeView() {
  // If the nudge is requested to be removed, it is likely that it won't be
  // shown to the user again. Therefore, the nudge child view is directly
  // removed instead of made invisible.
  if (current_toast_ == AppListToastType::kReorderNudge)
    RemoveCurrentView();
}

void AppListToastContainerView::RemoveCurrentView() {
  if (toast_view_)
    RemoveChildViewT(toast_view_.get());

  toast_view_ = nullptr;
  current_toast_ = AppListToastType::kNone;
}

void AppListToastContainerView::UpdateVisibilityState(VisibilityState state) {
  visibility_state_ = state;

  // Return early if the reorder nudge is not showing when the app list is
  // hiding.
  if (nudge_controller_->is_visible() &&
      (nudge_controller_->current_nudge() !=
       AppListNudgeController::NudgeType::kReorderNudge)) {
    return;
  }

  // Return early if the privacy notice should be showing.
  if (nudge_controller_->current_nudge() ==
      AppListNudgeController::NudgeType::kPrivacyNotice) {
    return;
  }

  AppListNudgeController::NudgeType new_nudge =
      AppListNudgeController::NudgeType::kNone;

  if (nudge_controller_->current_nudge() ==
      AppListNudgeController::NudgeType::kTutorialNudge) {
    new_nudge = AppListNudgeController::NudgeType::kTutorialNudge;
  } else if (nudge_controller_->ShouldShowReorderNudge()) {
    new_nudge = AppListNudgeController::NudgeType::kReorderNudge;
  }

  // Update the visible and active state in `nudge_controller_`.
  switch (state) {
    case VisibilityState::kShown:
      nudge_controller_->SetNudgeVisible(true, new_nudge);
      break;
    case VisibilityState::kShownInBackground:
      // The nudge must be visible to change to inactive state.
      if (!nudge_controller_->is_visible())
        nudge_controller_->SetNudgeVisible(true, new_nudge);
      nudge_controller_->SetNudgeActive(false, new_nudge);
      break;
    case VisibilityState::kHidden:
      nudge_controller_->SetNudgeVisible(false, new_nudge);
      break;
  }
}

void AppListToastContainerView::OnTemporarySortOrderChanged(
    const std::optional<AppListSortOrder>& new_order) {
  // Remove `toast_view_` when the temporary sorting order is cleared.
  if (!GetVisibilityForSortOrder(new_order)) {
    if (committing_sort_order_) {
      // When the toast view is closed due to committing the sort order via the
      // close button , the toast view should be faded out with animation.
      FadeOutToastView();
    } else {
      RemoveCurrentView();
    }
    return;
  }

  // The nudge view should be removed when the user triggers apps reordering.
  RemoveReorderNudgeView();

  const std::u16string toast_text = CalculateToastTextFromOrder(*new_order);
  const gfx::VectorIcon* toast_icon = GetToastIconForOrder(*new_order);
  const std::u16string a11y_text_on_undo_button =
      GetA11yTextOnUndoButtonFromOrder(*new_order);
  const ui::ColorId toast_icon_color_id = cros_tokens::kCrosSysOnSurface;

  if (toast_view_) {
    // If the reorder undo toast is showing, updates the title and icon of the
    // toast.
    toast_view_->SetTitle(toast_text);
    toast_view_->SetIcon(
        ui::ImageModel::FromVectorIcon(*toast_icon, toast_icon_color_id));
    toast_view_->toast_button()->GetViewAccessibility().SetName(
        a11y_text_on_undo_button, ax::mojom::NameFrom::kAttribute);
    return;
  }

  AppListToastView::Builder toast_view_builder(toast_text);

  toast_view_builder.SetCloseButton(base::BindRepeating(
      &AppListToastContainerView::OnReorderCloseButtonClicked,
      base::Unretained(this)));

  toast_view_ = AddChildView(
      toast_view_builder.SetStyleForTabletMode(tablet_mode_)
          .SetIcon(
              ui::ImageModel::FromVectorIcon(*toast_icon, toast_icon_color_id))
          .SetButton(l10n_util::GetStringUTF16(
                         IDS_ASH_LAUNCHER_UNDO_SORT_TOAST_ACTION_BUTTON),
                     base::BindRepeating(
                         &AppListToastContainerView::OnReorderUndoButtonClicked,
                         base::Unretained(this)))
          .SetViewDelegate(view_delegate_)
          .Build());
  toast_view_->toast_button()->GetViewAccessibility().SetName(
      a11y_text_on_undo_button, ax::mojom::NameFrom::kAttribute);

  toast_view_->UpdateInteriorMargins(kReorderUndoInteriorMargin);
  if (available_width_) {
    toast_view_->SetAvailableWidth(*available_width_);
  }
  current_toast_ = AppListToastType::kReorderUndo;
}

bool AppListToastContainerView::GetVisibilityForSortOrder(
    const std::optional<AppListSortOrder>& new_order) const {
  return new_order && *new_order != AppListSortOrder::kCustom &&
         *new_order != AppListSortOrder::kAlphabeticalEphemeralAppFirst;
}

void AppListToastContainerView::AnnounceSortOrder(AppListSortOrder new_order) {
  a11y_announcer_->Announce(CalculateToastTextFromOrder(new_order));
}

void AppListToastContainerView::AnnounceUndoSort() {
  a11y_announcer_->Announce(
      l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_UNDO_SORT_DONE_SPOKEN_TEXT));
}

void AppListToastContainerView::ConfigureLayoutForAvailableWidth(
    int available_width) {
  available_width_ = available_width;
  if (toast_view_) {
    toast_view_->SetAvailableWidth(available_width);
  }
}

views::LabelButton* AppListToastContainerView::GetToastButton() {
  if (!toast_view_)
    return nullptr;

  return toast_view_->toast_button();
}

views::Button* AppListToastContainerView::GetCloseButton() {
  if (!toast_view_)
    return nullptr;

  return toast_view_->close_button();
}

void AppListToastContainerView::OnReorderUndoButtonClicked() {
  toast_view_->toast_button()->SetEnabled(false);
  AppListModelProvider::Get()->model()->delegate()->RequestAppListSortRevert();
}

void AppListToastContainerView::OnReorderCloseButtonClicked() {
  // Prevent the close button from being clicked again during the fade out
  // animation.
  toast_view_->close_button()->SetEnabled(false);

  base::AutoReset auto_reset(&committing_sort_order_, true);
  AppListModelProvider::Get()
      ->model()
      ->delegate()
      ->RequestCommitTemporarySortOrder();
}

bool AppListToastContainerView::IsToastVisible() const {
  return toast_view_ && !(toast_view_->layer() &&
                          toast_view_->layer()->GetTargetOpacity() == 0.0f);
}

void AppListToastContainerView::FadeOutToastView() {
  views::AnimationBuilder builder;
  toast_view_fade_out_animation_abort_handle_ = builder.GetAbortHandle();
  if (!toast_view_) {
    // Aborting an existing fade out animation deletes the `toast_view_`, so
    // avoid creating new animations.
    return;
  }

  if (!toast_view_->layer()) {
    toast_view_->SetPaintToLayer();
    toast_view_->layer()->SetFillsBoundsOpaquely(false);
  }
  builder
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .OnEnded(
          base::BindOnce(&AppListToastContainerView::OnFadeOutToastViewComplete,
                         weak_factory_.GetWeakPtr()))
      .OnAborted(
          base::BindOnce(&AppListToastContainerView::OnFadeOutToastViewComplete,
                         weak_factory_.GetWeakPtr()))
      .Once()
      .SetDuration(base::Milliseconds(200))
      .SetOpacity(toast_view_->layer(), 0.0f, gfx::Tween::LINEAR);
}

void AppListToastContainerView::OnFadeOutToastViewComplete() {
  if (current_toast_ == AppListToastType::kReorderNudge)
    nudge_controller_->OnReorderNudgeConfirmed();
  RemoveCurrentView();
  delegate_->OnNudgeRemoved();
}

std::u16string AppListToastContainerView::CalculateToastTextFromOrder(
    AppListSortOrder order) const {
  switch (order) {
    case AppListSortOrder::kNameAlphabetical:
    case AppListSortOrder::kNameReverseAlphabetical:
      return l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_UNDO_SORT_TOAST_FOR_NAME_SORT);
    case AppListSortOrder::kColor:
      return l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_UNDO_SORT_TOAST_FOR_COLOR_SORT);
    case AppListSortOrder::kCustom:
    case AppListSortOrder::kAlphabeticalEphemeralAppFirst:
      NOTREACHED();
  }
}

std::u16string AppListToastContainerView::GetA11yTextOnUndoButtonFromOrder(
    AppListSortOrder order) const {
  switch (order) {
    case AppListSortOrder::kNameAlphabetical:
    case AppListSortOrder::kNameReverseAlphabetical:
      return l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_UNDO_NAME_SORT_TOAST_SPOKEN_TEXT);
    case AppListSortOrder::kColor:
      return l10n_util::GetStringUTF16(
          IDS_ASH_LAUNCHER_UNDO_COLOR_SORT_TOAST_SPOKEN_TEXT);
    case AppListSortOrder::kCustom:
    case AppListSortOrder::kAlphabeticalEphemeralAppFirst:
      NOTREACHED();
  }
}

BEGIN_METADATA(AppListToastContainerView)
END_METADATA

}  // namespace ash