// Copyright 2012 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_item_view.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "ash/app_list/app_collections_constants.h"
#include "ash/app_list/app_list_item_util.h"
#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/apps_collections_controller.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/folder_image.h"
#include "ash/app_list/views/app_list_menu_model_adapter.h"
#include "ash/app_list/views/apps_grid_context_menu.h"
#include "ash/constants/ash_features.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_types.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/dot_indicator.h"
#include "ash/style/style_util.h"
#include "ash/style/system_textfield.h"
#include "ash/style/typography.h"
#include "ash/system/progress_indicator/progress_indicator.h"
#include "ash/user_education/user_education_class_properties.h"
#include "ash/user_education/user_education_controller.h"
#include "ash/wm/window_util.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/pickle.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "cc/paint/paint_flags.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/utils/haptics_util.h"
#include "components/services/app_service/public/cpp/app_shortcut_image.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/base/l10n/l10n_util.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/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_owner.h"
#include "ui/compositor/layer_type.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/events/devices/haptic_touchpad_effects.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/insets_f.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_state.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/focus/focus_manager.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// Delay in milliseconds of when the dragging UI should be shown for mouse drag.
constexpr int kMouseDragUIDelayInMs = 200;
// Delay in milliseconds of when the dragging UI should be shown for touch drag.
// Note: For better user experience, this is made shorter than
// EventType::kGestureLongPress delay, which is too long for this case, e.g.,
// about 650ms.
constexpr int kTouchLongpressDelayInMs = 300;
// For touch initiated dragging, shift the cursor anchor point of the scaled
// icon by the following:
static const int kTouchDragImageVerticalOffset = 25;
// The drag and drop app icon should get scaled by this factor.
constexpr float kDragDropAppIconScale = 1.2f;
// The promise app placeholder icon should use this size.
constexpr int kPlaceholderIconDimension = 24;
// The width of the promise app progress ring.
constexpr int kPromiseRingStrokeSize = 2;
// The duration of the animation to animate an app list item view in as a
// promise app replacement.
constexpr base::TimeDelta kSwapPromiseIconDuration = base::Milliseconds(100);
// The amount of space between the progress ring and the promise app background
// and icon depending on the app_state.
constexpr gfx::Insets kProgressRingMarginInstalling = gfx::Insets(-2);
constexpr gfx::Insets kProgressRingMarginPending = gfx::Insets(-3);
// The drag and drop icon scaling up or down animation transition duration.
constexpr int kDragDropAppIconScaleTransitionInMs = 200;
// The size of the notification indicator circle over the size of the icon.
constexpr float kNotificationIndicatorWidthRatio = 14.0f / 64.0f;
// The size of the notification indicator circle padding over the size of the
// icon.
constexpr float kNotificationIndicatorPaddingRatio = 4.0f / 64.0f;
// Size of the "new install" blue dot that appears to the left of the title.
constexpr int kNewInstallDotSize = 8;
// Distance between the "new install" blue dot and the title.
constexpr int kNewInstallDotPadding = 4;
// The maximum number that can be shown on the item counter in refreshed folder
// icons.
constexpr size_t kMaxItemCounterCount = 100u;
// Creates a badged app shortcut image for the provided `app_list_config` from
// the shortcut's `main_icon` and the `badge_icon`.
gfx::ImageSkia CreateBadgedShortcutImage(
const AppListConfig& app_list_config,
const gfx::ImageSkia& main_icon,
const gfx::ImageSkia& badge_icon,
float icon_scale,
const ui::ColorProvider* color_provider) {
const gfx::Size badge_icon_size =
gfx::Size(app_list_config.shortcut_host_badge_icon_dimension(),
app_list_config.shortcut_host_badge_icon_dimension());
const int background_diameter =
app_list_config.GetShortcutBackgroundContainerDimension();
gfx::ImageSkia icon_with_badge =
apps::AppShortcutImage::CreateImageWithBadgeAndTeardropBackground(
background_diameter / 2,
app_list_config.GetShortcutTeardropCornerRadius(),
app_list_config.GetShortcutHostBadgeIconContainerDimension() / 2,
color_provider->GetColor(cros_tokens::kCrosSysSystemOnBaseOpaque),
gfx::ImageSkiaOperations::CreateResizedImage(
main_icon, skia::ImageOperations::RESIZE_BEST,
app_list_config.GetShortcutIconSize()),
gfx::ImageSkiaOperations::CreateResizedImage(
badge_icon, skia::ImageOperations::RESIZE_BEST, badge_icon_size));
return gfx::ImageSkiaOperations::CreateResizedImage(
icon_with_badge, skia::ImageOperations::RESIZE_BEST,
gfx::ScaleToRoundedSize(
gfx::Size(background_diameter, background_diameter), icon_scale));
}
// Draws a circular background for a promise icon view.
class PromiseIconBackground : public views::Background {
public:
PromiseIconBackground(ui::ColorId color_id,
const gfx::Rect& icon_bounds,
const gfx::Insets& insets)
: color_id_(color_id), icon_bounds_(icon_bounds), insets_(insets) {}
PromiseIconBackground(const PromiseIconBackground&) = delete;
PromiseIconBackground& operator=(const PromiseIconBackground&) = delete;
~PromiseIconBackground() override = default;
// views::Background:
void Paint(gfx::Canvas* canvas, views::View* view) const override {
gfx::RectF bounds = gfx::RectF(icon_bounds_);
bounds.Inset(gfx::InsetsF(insets_));
const float radius =
std::min(bounds.size().width(), bounds.size().height()) / 2.f;
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(get_color());
canvas->DrawCircle(bounds.CenterPoint(), radius, flags);
}
void OnViewThemeChanged(views::View* view) override {
SetNativeControlColor(view->GetColorProvider()->GetColor(color_id_));
view->SchedulePaint();
}
private:
const ui::ColorId color_id_;
const gfx::Rect icon_bounds_;
const gfx::Insets insets_;
};
// Draws a dot with no shadow.
class DotView : public views::View {
METADATA_HEADER(DotView, views::View)
public:
DotView() : color_id_(cros_tokens::kCrosSysTertiary) {
// The dot is not clickable.
SetCanProcessEventsWithinSubtree(false);
}
DotView(const DotView&) = delete;
DotView& operator=(const DotView&) = delete;
~DotView() override = default;
// views::View:
void OnPaint(gfx::Canvas* canvas) override {
DCHECK_EQ(width(), height());
const float radius = width() / 2.0f;
const float scale = canvas->UndoDeviceScaleFactor();
gfx::PointF center = gfx::RectF(GetLocalBounds()).CenterPoint();
center.Scale(scale);
cc::PaintFlags flags;
flags.setColor(GetColorProvider()->GetColor(color_id_));
flags.setAntiAlias(true);
canvas->DrawCircle(center, scale * radius, flags);
}
void OnThemeChanged() override {
views::View::OnThemeChanged();
SchedulePaint();
}
private:
const ui::ColorId color_id_;
};
BEGIN_METADATA(DotView)
END_METADATA
// Returns whether the `index` is considered on the left edge of a grid with
// `cols` columns.
bool IsIndexOnLeftEdge(GridIndex index, int cols) {
return (index.slot % cols) == 0;
}
// Returns whether the `index` is considered on the right edge of a grid with
// `cols` columns.
bool IsIndexOnRightEdge(GridIndex index, int cols) {
return ((index.slot + 1) % cols) == 0;
}
bool IsIndexMovingFromOneEdgeToAnother(GridIndex old_index,
GridIndex new_index,
int cols) {
return (IsIndexOnLeftEdge(new_index, cols) &&
IsIndexOnRightEdge(old_index, cols)) ||
(IsIndexOnLeftEdge(old_index, cols) &&
IsIndexOnRightEdge(new_index, cols));
}
bool IsIndexMovingToDifferentRow(GridIndex old_index,
GridIndex new_index,
int cols) {
return old_index.slot / cols != new_index.slot / cols ||
old_index.page != new_index.page;
}
bool IsReorderCommand(int command_id) {
CommandId command = static_cast<CommandId>(command_id);
return (command == CommandId::REORDER_BY_NAME_ALPHABETICAL ||
command == CommandId::REORDER_BY_NAME_REVERSE_ALPHABETICAL ||
command == CommandId::REORDER_BY_COLOR);
}
} // namespace
class AppListItemView::FolderIconView : public views::View,
public AppListItemListObserver {
METADATA_HEADER(FolderIconView, views::View)
public:
FolderIconView(AppListFolderItem* folder_item,
const AppListConfig* config,
float icon_scale)
: folder_item_(folder_item),
config_(config),
icon_scale_(icon_scale) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
folder_item_->item_list()->AddObserver(this);
}
FolderIconView(const FolderIconView&) = delete;
FolderIconView& operator=(const FolderIconView&) = delete;
~FolderIconView() override {
if (folder_item_) {
folder_item_->item_list()->RemoveObserver(this);
}
}
void ResetFolderItem() { folder_item_ = nullptr; }
void UpdateAppListConfig(const AppListConfig* config) { config_ = config; }
void SetIconScale(float scale) {
icon_scale_ = scale;
SchedulePaint();
}
AppListItem* GetDraggedItem() const {
return folder_item_ ? folder_item_->item_list()->FindItem(dragged_item_id_)
: nullptr;
}
// Sets the current dragged item to the item with id `item_id`.
// `dragged_item_` could be null if such item doesn't exist in `folder_item_`.
void UpdateDraggedItem(const std::string& item_id) {
dragged_item_id_ = item_id;
SchedulePaint();
}
// The count shows on the item counter is the number of items that aren't
// drawn on the folder icon. Returns nullopt if the counter should not be
// drawn.
std::optional<size_t> GetItemCounterCount() const {
size_t item_count = folder_item_->item_list()->item_count();
size_t icons_in_folder = GetDraggedItem() ? item_count - 1 : item_count;
if (icons_in_folder <= FolderImage::kNumFolderTopItems) {
return std::nullopt;
}
size_t count = icons_in_folder - (FolderImage::kNumFolderTopItems - 1);
return std::min(count, kMaxItemCounterCount);
}
gfx::ImageSkia CreateDragImage() {
const views::Widget* widget = GetWidget();
const float scale = widget->GetCompositor()->device_scale_factor();
const gfx::Rect paint_bounds(gfx::ScaleToCeiledSize(
config_->folder_icon_size(), kDragDropAppIconScale * scale));
const bool is_pixel_canvas = widget->GetCompositor()->is_pixel_canvas();
SkBitmap bitmap;
bitmap.allocN32Pixels(paint_bounds.width(), paint_bounds.height());
bitmap.eraseColor(SK_ColorTRANSPARENT);
// Draw the background circle of the icon.
SkCanvas canvas(bitmap);
SkPaint background_circle;
const ui::ColorId color_id = cros_tokens::kCrosSysSystemOnBase;
background_circle.setColor(GetColorProvider()->GetColor(color_id));
background_circle.setStyle(SkPaint::kFill_Style);
background_circle.setAntiAlias(true);
gfx::Point center = paint_bounds.CenterPoint();
canvas.drawCircle(
center.x(), center.y(),
config_->icon_visible_dimension() * kDragDropAppIconScale * scale / 2,
background_circle);
auto list = base::MakeRefCounted<cc::DisplayItemList>();
ui::PaintContext context(list.get(), scale, paint_bounds, is_pixel_canvas);
{
base::AutoReset<float> forced_icon_scale(&icon_scale_,
kDragDropAppIconScale);
Paint(
views::PaintInfo::CreateRootPaintInfo(context, paint_bounds.size()));
}
list->Finalize();
list->Raster(&canvas, nullptr);
return gfx::ImageSkia::CreateFromBitmap(bitmap, scale);
}
private:
// The scale factor that resize the item counter to match the visual size of
// other app icons.
static constexpr float kItemCounterSizeFactor = 0.93f;
// AppListItemListObserver:
void OnListItemAdded(size_t index, AppListItem* item) override {
SchedulePaint();
}
void OnListItemRemoved(size_t index, AppListItem* item) override {
SchedulePaint();
}
void OnListItemMoved(size_t from_index,
size_t to_index,
AppListItem* item) override {
// Only repaint if the move may reflect on the icon.
size_t indices_affected = GetDraggedItem()
? FolderImage::kNumFolderTopItems + 1
: FolderImage::kNumFolderTopItems;
if (from_index < indices_affected || to_index < indices_affected) {
SchedulePaint();
}
}
void DrawItemCounter(gfx::Canvas* canvas,
const gfx::Rect& bounds,
int count) {
const float item_icon_size = config_->item_icon_in_folder_icon_dimension() *
kItemCounterSizeFactor * icon_scale_;
const float counter_radius = item_icon_size / 2.f;
// Draw the item counter background circle.
const gfx::PointF draw_center(bounds.CenterPoint());
cc::PaintFlags flags;
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setAntiAlias(true);
flags.setColor(GetColorProvider()->GetColor(cros_tokens::kCrosSysPrimary));
canvas->DrawCircle(draw_center, counter_radius, flags);
// Paint the number of apps that are not showing in the folder icon.
const std::u16string text = base::NumberToString16(count);
gfx::FontList font_list = config_->item_counter_in_folder_icon_font();
canvas->DrawStringRectWithFlags(
text, font_list,
GetColorProvider()->GetColor(cros_tokens::kCrosSysOnPrimary), bounds,
gfx::Canvas::TEXT_ALIGN_CENTER);
}
void OnPaint(gfx::Canvas* canvas) override {
const AppListItemList* item_list = folder_item_->item_list();
size_t num_items = item_list->item_count();
// Exclude the dragged item that is dragged from the folder.
if (GetDraggedItem()) {
--num_items;
}
if (num_items == 0) {
return;
}
// Draw top items' icons.
const size_t num_icons =
std::min(FolderImage::kNumFolderTopItems, num_items);
std::vector<gfx::Rect> top_icon_bounds = GetTopIconsBounds(num_icons);
auto item_count = GetItemCounterCount();
// `icon_pos` is the position index on the folder icon for the app icons and
// item counter to be drawn, while `item_idx_drawn` is the item index in the
// folder that represent the icon to be drawn.
for (size_t icon_pos = 0, item_idx_drawn = 0; icon_pos < num_icons;
++icon_pos, ++item_idx_drawn) {
// Draw the item counter at the last position on the icon if needed.
if (item_count.has_value() && icon_pos == num_icons - 1) {
DrawItemCounter(canvas, top_icon_bounds[icon_pos], item_count.value());
break;
}
// Do not draw the dragged app item on the folder icon.
if (item_list->item_at(item_idx_drawn) == GetDraggedItem()) {
// Retain the icon_pos for the next iteration and check the next item in
// item list to see if it needs to be drawn.
--icon_pos;
continue;
}
const gfx::ImageSkia item_icon =
item_list->item_at(item_idx_drawn)->GetIcon(config_->type());
if (item_icon.isNull()) {
continue;
}
const gfx::Rect bounds = top_icon_bounds[icon_pos];
const gfx::ImageSkia resized(gfx::ImageSkiaOperations::CreateResizedImage(
item_icon, skia::ImageOperations::RESIZE_BEST, bounds.size()));
canvas->DrawImageInt(resized, bounds.x(), bounds.y());
}
}
std::vector<gfx::Rect> GetTopIconsBounds(size_t num_items) {
gfx::Rect folder_icon_bounds(config_->folder_icon_size());
std::vector<gfx::Rect> top_icon_bounds =
FolderImage::GetTopIconsBounds(*config_, folder_icon_bounds, num_items);
if (icon_scale_ == 1) {
return top_icon_bounds;
}
std::for_each(top_icon_bounds.begin(), top_icon_bounds.end(),
[&](auto& bounds) {
bounds = gfx::ScaleToRoundedRect(bounds, icon_scale_);
});
return top_icon_bounds;
}
// The folder item this icon view paints.
raw_ptr<AppListFolderItem> folder_item_;
raw_ptr<const AppListConfig, DanglingUntriaged> config_;
// The scaling factor used for cardified states in tablet mode.
float icon_scale_;
// The id of the currently dragged app item in the folder.
std::string dragged_item_id_;
};
// An AppMenuAdapter specific to AppListItems that are shown in the context of
// the AppsCollections. The adapter intercepts sort requests and delegates them
// to AppsCollectionsController.
class AppsCollectionsMenuModelAdapter : public AppListMenuModelAdapter {
public:
AppsCollectionsMenuModelAdapter(
const std::string& app_id,
std::unique_ptr<ui::SimpleMenuModel> menu_model,
views::Widget* widget_owner,
ui::MenuSourceType source_type,
const AppLaunchedMetricParams& metric_params,
AppListViewAppType type,
base::OnceClosure on_menu_closed_callback,
bool is_tablet_mode,
AppCollection collection)
: AppListMenuModelAdapter(app_id,
std::move(menu_model),
widget_owner,
source_type,
metric_params,
type,
std::move(on_menu_closed_callback),
is_tablet_mode,
collection) {}
AppsCollectionsMenuModelAdapter(const AppsCollectionsMenuModelAdapter&) =
delete;
AppsCollectionsMenuModelAdapter& operator=(
const AppsCollectionsMenuModelAdapter&) = delete;
~AppsCollectionsMenuModelAdapter() override = default;
void ExecuteCommand(int id, int mouse_event_flags) override {
// Intercept Reorder commands to show the reorder confirmation dialog.
if (IsReorderCommand(id)) {
AppsCollectionsController::Get()->RequestAppReorder(
static_cast<CommandId>(id) == CommandId::REORDER_BY_COLOR
? AppListSortOrder::kColor
: AppListSortOrder::kNameAlphabetical);
return;
}
// Note that ExecuteCommand might delete us.
AppListMenuModelAdapter::ExecuteCommand(id, mouse_event_flags);
}
};
BEGIN_METADATA(AppListItemView, FolderIconView)
END_METADATA
AppListItemView::AppListItemView(const AppListConfig* app_list_config,
AppListItemViewGridDelegate* grid_delegate,
AppListItem* item,
AppListViewDelegate* view_delegate,
Context context)
: views::Button(base::BindRepeating(
&AppListItemViewGridDelegate::OnAppListItemViewActivated,
base::Unretained(grid_delegate),
base::Unretained(this))),
app_list_config_(app_list_config),
is_folder_(item->GetItemType() == AppListFolderItem::kItemType),
item_weak_(item),
grid_delegate_(grid_delegate),
view_delegate_(view_delegate),
use_item_icon_(!is_folder_),
context_(context) {
DCHECK(app_list_config_);
DCHECK(grid_delegate_);
DCHECK(view_delegate_);
SetFocusBehavior(FocusBehavior::ALWAYS);
set_suppress_default_focus_handling();
GetViewAccessibility().SetIsLeaf(true);
is_promise_app_ =
item_weak_->GetMetadata()->app_status == AppStatus::kPending ||
item_weak_->GetMetadata()->app_status == AppStatus::kInstalling;
has_host_badge_ = !item_weak_->GetMetadata()->badge_icon.isNull();
// Draw the promise ring for the first time before waiting for updates.
if (is_promise_app_) {
UpdateProgressIndicatorState();
}
StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/false,
cros_tokens::kCrosSysRippleNeutralOnSubtle);
views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::OFF);
SetHideInkDropWhenShowingContextMenu(false);
SetShowInkDropWhenHotTracked(false);
SetHasInkDropActionOnClick(false);
views::FocusRing::Install(this);
views::FocusRing* const focus_ring = views::FocusRing::Get(this);
focus_ring->SetOutsetFocusRingDisabled(true);
focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
focus_ring->SetHasFocusPredicate(base::BindRepeating([](const View* view) {
const auto* v = views::AsViewClass<AppListItemView>(view);
CHECK(v);
// With a `view_delegate_` present, focus ring should only show when
// button is focused and keyboard traversal is engaged.
if (v->view_delegate_ && !v->view_delegate_->KeyboardTraversalEngaged()) {
return false;
}
if (v->drag_state_ != DragState::kNone) {
return false;
}
if (v->waiting_for_context_menu_options_ || v->IsShowingAppMenu()) {
return false;
}
return v->HasFocus();
}));
views::InstallRoundRectHighlightPathGenerator(
this, gfx::Insets(1), app_list_config_->grid_focus_corner_radius());
auto title = std::make_unique<views::Label>();
title->SetBackgroundColor(SK_ColorTRANSPARENT);
title->SetHandlesTooltips(false);
title->SetHorizontalAlignment(gfx::ALIGN_CENTER);
TypographyProvider::Get()->StyleLabel(
app_list_config_->type() == AppListConfigType::kDense
? TypographyToken::kCrosAnnotation1
: TypographyToken::kCrosButton2,
*title);
title->SetEnabledColorId(cros_tokens::kCrosSysOnSurface);
icon_background_ = AddChildView(std::make_unique<views::View>());
icon_background_->SetPaintToLayer(ui::LAYER_SOLID_COLOR);
icon_background_->layer()->SetFillsBoundsOpaquely(false);
icon_background_->SetCanProcessEventsWithinSubtree(false);
icon_background_->SetVisible(is_folder_);
if (use_item_icon_) {
// If the item icon is used, set the icon in ImageView and paint the view.
icon_ = AddChildView(std::make_unique<views::ImageView>());
icon_->SetCanProcessEventsWithinSubtree(false);
icon_->SetVerticalAlignment(views::ImageView::Alignment::kLeading);
if (has_host_badge_ &&
features::IsSeparateWebAppShortcutBadgeIconEnabled()) {
SetIconAndMaybeHostBadgeIcon(icon_image_, item_weak_->GetHostBadgeIcon());
}
} else {
// Refreshed folder icons are painted on FolderIconView directly instead of
// using the folder item icon.
folder_icon_ = AddChildView(std::make_unique<FolderIconView>(
item_weak_->AsFolderItem(), app_list_config_, icon_scale_));
folder_icon_->SetCanProcessEventsWithinSubtree(false);
}
if (is_folder_) {
// Set background blur for folder icon and use mask layer to clip it into
// circle. Note that blur is only enabled in tablet mode to improve dragging
// smoothness.
if (view_delegate_->IsInTabletMode()) {
SetBackgroundBlurEnabled(true);
}
SetBackgroundExtendedState(is_icon_extended_, /*animate=*/false);
}
notification_indicator_ =
AddChildView(std::make_unique<DotIndicator>(gfx::kPlaceholderColor));
notification_indicator_->SetVisible(item->has_notification_badge());
title_ = AddChildView(std::move(title));
new_install_dot_ = AddChildView(std::make_unique<DotView>());
new_install_dot_->SetVisible(item_weak_->is_new_install());
UpdateIconView(/*update_item_icon=*/true);
SetItemName(base::UTF8ToUTF16(item->name()),
base::UTF8ToUTF16(item->GetAccessibleName()));
item->AddObserver(this);
if (is_folder_) {
context_menu_for_folder_ = std::make_unique<AppsGridContextMenu>(
AppsGridContextMenu::GridType::kAppsGrid);
set_context_menu_controller(context_menu_for_folder_.get());
} else {
set_context_menu_controller(this);
}
SetAnimationDuration(base::TimeDelta());
preview_circle_radius_ = 0;
if (UserEducationController::Get()) {
switch (context) {
case Context::kRecentAppsView:
break;
case Context::kAppsGridView:
case Context::kAppsCollection:
if (std::optional<ui::ElementIdentifier> element_identifier =
UserEducationController::Get()->GetElementIdentifierForAppId(
item->id())) {
// NOTE: Set `kHelpBubbleContextKey` before
// `views::kElementIdentifierKey` in case registration causes a help
// bubble to be created synchronously.
SetProperty(kHelpBubbleContextKey, HelpBubbleContext::kAsh);
SetProperty(views::kElementIdentifierKey, *element_identifier);
}
break;
}
}
UpdateAccessibleDescription();
}
void AppListItemView::InitializeIconLoader() {
DCHECK(item_weak_);
// Creates app icon load helper. base::Unretained is safe because `this` owns
// `icon_load_helper_` and `view_delegate_` outlives `this`.
if (is_folder_) {
AppListFolderItem* folder_item = item_weak_->AsFolderItem();
icon_load_helper_.emplace(
folder_item->item_list(),
base::BindRepeating(&AppListViewDelegate::LoadIcon,
base::Unretained(view_delegate_)));
} else {
icon_load_helper_.emplace(
item_weak_, base::BindRepeating(&AppListViewDelegate::LoadIcon,
base::Unretained(view_delegate_)));
}
}
AppListItemView::~AppListItemView() {
set_context_menu_controller(nullptr);
if (item_weak_) {
item_weak_->RemoveObserver(this);
}
StopObservingImplicitAnimations();
}
void AppListItemView::UpdateIconView(bool update_item_icon) {
if (!use_item_icon_) {
folder_icon_->SetIconScale(icon_scale_);
DeprecatedLayoutImmediately();
return;
}
if (update_item_icon && item_weak_) {
has_host_badge_ = !item_weak_->GetHostBadgeIcon().isNull();
}
if (update_item_icon) {
if (ItemHasPlaceholderIcon()) {
icon_image_model_ = ui::ImageModel(ui::ImageModel::FromVectorIcon(
ash::kPlaceholderAppIcon, cros_tokens::kCrosSysPrimary));
} else {
icon_image_model_ = ui::ImageModel(ui::ImageModel::FromImageSkia(
item_weak_ ? item_weak_->GetIcon(app_list_config_->type())
: gfx::ImageSkia()));
}
}
const bool use_fallback_icon = ShouldUseFallbackIconImageModel();
const ui::ImageModel& image_model =
use_fallback_icon ? fallback_icon_image_model_ : icon_image_model_;
if (!use_fallback_icon && !fallback_icon_image_model_.IsEmpty()) {
fallback_icon_image_model_ = ui::ImageModel();
}
gfx::ImageSkia image_icon;
if (image_model.IsImage()) {
image_icon = image_model.GetImage().AsImageSkia();
} else if (image_model.IsVectorIcon() && GetColorProvider()) {
image_icon = ui::ThemedVectorIcon(image_model.GetVectorIcon())
.GetImageSkia(GetColorProvider());
}
if (features::IsSeparateWebAppShortcutBadgeIconEnabled()) {
SetIconAndMaybeHostBadgeIcon(
image_icon, update_item_icon ? item_weak_->GetHostBadgeIcon()
: host_badge_icon_image_);
} else {
SetIconAndMaybeHostBadgeIcon(image_icon, gfx::ImageSkia());
}
}
bool AppListItemView::ShouldUseFallbackIconImageModel() const {
if (fallback_icon_image_model_.IsEmpty()) {
return false;
}
if (prefer_fallback_icon_) {
return true;
}
if (!item_weak_) {
return true;
}
return item_weak_->GetMetadata()->is_placeholder_icon ||
item_weak_->GetDefaultIcon().isNull();
}
void AppListItemView::SetIconAndMaybeHostBadgeIcon(
const gfx::ImageSkia& icon,
const gfx::ImageSkia& host_badge_icon) {
// This function is used when AppListItem icons or host badge icons are used
// for painting.
DCHECK(use_item_icon_);
// Clear icon and bail out if item icon is empty.
if (icon.isNull()) {
icon_->SetImage(nullptr);
icon_image_model_ =
ui::ImageModel(ui::ImageModel::FromImageSkia(gfx::ImageSkia()));
return;
}
const gfx::Size icon_size =
has_host_badge_
? gfx::ScaleToRoundedSize(app_list_config_->GetShortcutIconSize(),
icon_scale_)
: gfx::ScaleToRoundedSize(GetIconSize(), icon_scale_);
icon_image_ = icon;
host_badge_icon_image_ = has_host_badge_ ? host_badge_icon : gfx::ImageSkia();
if (GetColorProvider() && !host_badge_icon_image_.isNull()) {
icon_->SetImage(CreateBadgedShortcutImage(*app_list_config_, icon,
host_badge_icon, icon_scale_,
GetColorProvider()));
} else {
icon_->SetImage(gfx::ImageSkiaOperations::CreateResizedImage(
icon, skia::ImageOperations::RESIZE_BEST, icon_size));
}
DeprecatedLayoutImmediately();
}
gfx::Size AppListItemView::GetIconSize() const {
if (is_folder_) {
return app_list_config_->folder_icon_size();
}
if (is_promise_app_ && features::ArePromiseIconsEnabled() && item_weak_) {
// Placeholder icons do not change size between states.
if (ImageModelHasPlaceholderIcon()) {
return gfx::Size(kPlaceholderIconDimension, kPlaceholderIconDimension);
}
return GetPreferredIconSizeForProgressRing();
}
return app_list_config_->grid_icon_size();
}
bool AppListItemView::ItemHasPlaceholderIcon() {
return is_promise_app_ && item_weak_ &&
item_weak_->GetMetadata()->is_placeholder_icon;
}
void AppListItemView::UpdateAppListConfig(
const AppListConfig* app_list_config) {
app_list_config_ = app_list_config;
DCHECK(app_list_config_);
views::InstallRoundRectHighlightPathGenerator(
this, gfx::Insets(1), app_list_config_->grid_focus_corner_radius());
if (!item_weak_ && use_item_icon_) {
SetIconAndMaybeHostBadgeIcon(gfx::ImageSkia(), gfx::ImageSkia());
return;
}
if (!use_item_icon_) {
folder_icon_->UpdateAppListConfig(app_list_config);
}
title()->SetFontList(app_list_config_->app_title_font());
UpdateIconView(/*update_item_icon=*/true);
SetBackgroundExtendedState(is_icon_extended_, /*animate=*/false);
}
void AppListItemView::UpdateDraggedItem(const AppListItem* dragged_item) {
if (!use_item_icon_) {
folder_icon_->UpdateDraggedItem(dragged_item ? dragged_item->id() : "");
}
}
gfx::Size AppListItemView::GetPreferredIconSizeForProgressRing() const {
DCHECK(is_promise_app_ || ShouldUseFallbackIconImageModel());
CHECK(item_weak_);
if (ImageModelHasPlaceholderIcon()) {
return gfx::Size(app_list_config_->promise_icon_dimension_pending(),
app_list_config_->promise_icon_dimension_pending());
}
switch (item_weak_->app_status()) {
case AppStatus::kPending:
return gfx::Size(app_list_config_->promise_icon_dimension_pending(),
app_list_config_->promise_icon_dimension_pending());
case AppStatus::kInstalling:
case AppStatus::kInstallCancelled:
case AppStatus::kInstallSuccess:
case AppStatus::kPaused:
return gfx::Size(app_list_config_->promise_icon_dimension_installing(),
app_list_config_->promise_icon_dimension_installing());
case AppStatus::kReady:
case AppStatus::kBlocked:
return app_list_config_->grid_icon_size();
}
}
void AppListItemView::ScaleIconImmediatly(float scale_factor) {
if (icon_scale_ == scale_factor) {
return;
}
icon_scale_ = scale_factor;
if (!use_item_icon_) {
folder_icon_->SetIconScale(icon_scale_);
}
UpdateIconView(/*update_item_icon=*/false);
layer()->SetTransform(gfx::Transform());
if (progress_indicator_) {
UpdateProgressRingBounds();
progress_indicator_->layer()->SetTransform(gfx::Transform());
}
}
void AppListItemView::UpdateBackgroundLayerBounds() {
gfx::Rect background_bounds = GetIconView()->bounds();
// Set icon bounds to it's max possible size - the background view will be
// clipped to the required size as the background extended state gets updated.
// This lets extended state animations run by updating the background layer
// clip rect only (without having to change the icon background bounds at
// different times depending on whether the background is shrinking or
// expanding).
int outset_from_icon =
(app_list_config_->icon_extended_background_dimension() * icon_scale_ -
background_bounds.width()) /
2;
background_bounds.Outset(outset_from_icon);
icon_background_->SetBoundsRect(background_bounds);
// Note that the background size should initially be the folder icon size
// instead of the grid icon size. This is because the app icon has a
// transparent ring around the visible icon which makes it look smaller.
background_bounds.ClampToCenteredSize(gfx::ScaleToRoundedSize(
app_list_config_->icon_visible_size(), icon_scale_));
icon_background_->layer()->SetRoundedCornerRadius(
gfx::RoundedCornersF(background_bounds.width() / 2));
}
void AppListItemView::SetUIState(UIState ui_state) {
if (ui_state_ == ui_state) {
return;
}
switch (ui_state) {
case UI_STATE_NORMAL:
title_->SetVisible(true);
if (item_weak_) {
ItemIsNewInstallChanged();
}
if (ui_state_ == UI_STATE_DRAGGING ||
ui_state_ == UI_STATE_TOUCH_DRAGGING) {
GetWidget()->SetCursor(ui::mojom::CursorType::kNull);
ScaleAppIcon(false);
}
break;
case UI_STATE_DRAGGING:
title_->SetVisible(false);
if (new_install_dot_) {
new_install_dot_->SetVisible(false);
}
if (ui_state_ == UI_STATE_NORMAL && !in_cardified_grid_) {
GetWidget()->SetCursor(ui::mojom::CursorType::kGrabbing);
ScaleAppIcon(true);
}
break;
case UI_STATE_DROPPING_IN_FOLDER:
break;
case UI_STATE_TOUCH_DRAGGING:
title_->SetVisible(false);
if (new_install_dot_) {
new_install_dot_->SetVisible(false);
}
ScaleAppIcon(false);
break;
}
ui_state_ = ui_state;
SchedulePaint();
}
void AppListItemView::ScaleAppIcon(bool scale_up) {
// If there is no layer and the icon will scale down, avoid creating an
// animation and just scale down.
if (!layer() && !scale_up) {
icon_scale_ = 1.0f;
UpdateIconView(false);
return;
}
EnsureLayer();
if (scale_up) {
icon_scale_ = kDragDropAppIconScale;
UpdateIconView(false);
const gfx::Transform scale_transform = gfx::GetScaleTransform(
GetIconView()->bounds().CenterPoint(), 1 / kDragDropAppIconScale);
layer()->SetTransform(scale_transform);
if (progress_indicator_) {
progress_indicator_->layer()->SetTransform(scale_transform);
}
} else if (drag_state_ != DragState::kNone) {
// If a drag view has been created for this icon, the item transition to
// target bounds is handled by the apps grid view bounds animator. At the
// end of that animation, the layer will be destroyed, causing the
// animation observer to get canceled. For this case, we need to scale
// down the icon immediately, with no animation.
ScaleIconImmediatly(1.0f);
}
ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
settings.SetTransitionDuration(
base::Milliseconds((kDragDropAppIconScaleTransitionInMs)));
settings.SetTweenType(app_list_features::IsDragAndDropRefactorEnabled()
? gfx::Tween::ACCEL_20_DECEL_100
: gfx::Tween::EASE_OUT_2);
if (scale_up) {
layer()->SetTransform(gfx::Transform());
if (progress_indicator_) {
progress_indicator_->layer()->SetTransform(gfx::Transform());
}
} else {
if (drag_state_ == DragState::kNone) {
// To avoid poor quality icons, update icon image with the correct scale
// after the transform animation is completed.
settings.AddObserver(this);
const gfx::Transform reverse_scale_transform = gfx::GetScaleTransform(
GetContentsBounds().CenterPoint(), 1 / kDragDropAppIconScale);
layer()->SetTransform(reverse_scale_transform);
if (progress_indicator_) {
progress_indicator_->layer()->SetTransform(reverse_scale_transform);
}
}
}
}
void AppListItemView::OnImplicitAnimationsCompleted() {
ScaleIconImmediatly(1.0f);
}
void AppListItemView::SetTouchDragging(bool touch_dragging) {
if (mouse_dragging_ || touch_dragging_ == touch_dragging) {
return;
}
touch_dragging_ = touch_dragging;
if (context_menu_for_folder_)
context_menu_for_folder_->set_owner_touch_dragging(touch_dragging_);
SetState(STATE_NORMAL);
SetUIState(touch_dragging_ ? UI_STATE_DRAGGING : UI_STATE_NORMAL);
// EndDrag may delete |this|.
if (!touch_dragging) {
grid_delegate_->EndDrag(/*cancel=*/false);
}
}
void AppListItemView::SetMouseDragging(bool mouse_dragging) {
if (touch_dragging_ || mouse_dragging_ == mouse_dragging) {
return;
}
mouse_dragging_ = mouse_dragging;
if (mouse_dragging) {
chromeos::haptics_util::PlayHapticTouchpadEffect(
ui::HapticTouchpadEffect::kTick,
ui::HapticTouchpadEffectStrength::kMedium);
}
SetState(STATE_NORMAL);
SetUIState(mouse_dragging_ ? UI_STATE_DRAGGING : UI_STATE_NORMAL);
}
void AppListItemView::OnMouseDragTimer() {
// Show scaled up app icon to indicate draggable state.
SetMouseDragging(true);
}
void AppListItemView::OnTouchDragTimer(
const gfx::Point& tap_down_location,
const gfx::Point& tap_down_root_location) {
// Show scaled up app icon to indicate draggable state.
if (!InitiateDrag(tap_down_location, tap_down_root_location)) {
return;
}
SetTouchDragging(true);
}
bool AppListItemView::InitiateDrag(const gfx::Point& location,
const gfx::Point& root_location) {
if (!app_list_features::IsDragAndDropRefactorEnabled() &&
!grid_delegate_->InitiateDrag(
this, location, root_location,
base::BindOnce(&AppListItemView::OnDragStarted,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&AppListItemView::OnDragEnded,
weak_ptr_factory_.GetWeakPtr()))) {
return false;
}
if (!IsItemDraggable()) {
return false;
}
drag_state_ = DragState::kInitialized;
SilentlyRequestFocus();
return true;
}
void AppListItemView::OnDragStarted() {
mouse_drag_timer_.Stop();
touch_drag_timer_.Stop();
drag_state_ = DragState::kStarted;
SetUIState(UI_STATE_DRAGGING);
CancelContextMenu();
}
void AppListItemView::OnDragEnded() {
mouse_dragging_ = false;
mouse_drag_timer_.Stop();
touch_dragging_ = false;
touch_drag_timer_.Stop();
if (context_menu_for_folder_)
context_menu_for_folder_->set_owner_touch_dragging(false);
SetUIState(UI_STATE_NORMAL);
drag_state_ = DragState::kNone;
}
void AppListItemView::OnDragDone() {
EnsureSelected();
OnDragEnded();
}
void AppListItemView::ScrollRectToVisible(const gfx::Rect& rect) {
gfx::Rect enlarged_rect = rect;
enlarged_rect.Outset(8);
views::Button::ScrollRectToVisible(enlarged_rect);
}
void AppListItemView::CancelContextMenu() {
if (item_menu_model_adapter_) {
menu_close_initiated_from_drag_ = true;
item_menu_model_adapter_->Cancel();
}
if (context_menu_for_folder_) {
context_menu_for_folder_->Cancel();
}
}
void AppListItemView::SetAsAttemptedFolderTarget(bool is_target_folder) {
if (is_target_folder)
SetUIState(UI_STATE_DROPPING_IN_FOLDER);
else
SetUIState(UI_STATE_NORMAL);
}
void AppListItemView::SilentlyRequestFocus() {
DCHECK(!focus_silently_);
base::AutoReset<bool> auto_reset(&focus_silently_, true);
RequestFocus();
}
void AppListItemView::EnsureSelected() {
grid_delegate_->SetSelectedView(/*view=*/this);
}
void AppListItemView::SetItemName(const std::u16string& display_name,
const std::u16string& full_name) {
const std::u16string folder_name_placeholder =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_APP_LIST_FOLDER_NAME_PLACEHOLDER);
if (is_folder_ && display_name.empty()) {
title_->SetText(folder_name_placeholder);
} else {
title_->SetText(display_name);
}
// Use full name for accessibility.
GetViewAccessibility().SetName(
is_folder_ ? l10n_util::GetStringFUTF16(
IDS_APP_LIST_FOLDER_BUTTON_ACCESSIBILE_NAME,
full_name.empty() ? folder_name_placeholder : full_name)
: full_name);
DeprecatedLayoutImmediately();
}
void AppListItemView::SetItemAccessibleName(const std::u16string& name) {
GetViewAccessibility().SetName(name);
}
void AppListItemView::OnContextMenuModelReceived(
const gfx::Point& point,
ui::MenuSourceType source_type,
std::unique_ptr<ui::SimpleMenuModel> menu_model) {
waiting_for_context_menu_options_ = false;
if (!menu_model || IsShowingAppMenu()) {
return;
}
// GetContextMenuModel is asynchronous and takes a nontrivial amount of time
// to complete. If a menu is shown after the icon has moved, |grid_delegate_|
// gets put in a bad state because the context menu begins to receive drag
// events, interrupting the app icon drag.
if (drag_state_ == DragState::kStarted) {
return;
}
menu_show_initiated_from_key_ = source_type == ui::MENU_SOURCE_KEYBOARD;
// Clear the existing focus in other elements to prevent having a focus
// indicator on other non-selected views.
views::View* focused_view = GetFocusManager()->GetFocusedView();
if (focused_view) {
// Set `focus_removed_by_context_menu_` to restore focus when the context
// menu closes. As an exception, do not restore focus on an inactive system
// textfield (e.g. the folder name view).
ash::SystemTextfield* as_system_textfield =
views::AsViewClass<ash::SystemTextfield>(focused_view);
focus_removed_by_context_menu_ =
!as_system_textfield || as_system_textfield->IsActive();
GetFocusManager()->ClearFocus();
}
if (!grid_delegate_->IsSelectedView(this))
grid_delegate_->ClearSelectedView();
int run_types = views::MenuRunner::HAS_MNEMONICS |
views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
views::MenuRunner::FIXED_ANCHOR |
views::MenuRunner::CONTEXT_MENU;
if (source_type == ui::MENU_SOURCE_TOUCH && touch_dragging_)
run_types |= views::MenuRunner::SEND_GESTURE_EVENTS_TO_OWNER;
// Screen bounds don't need RTL flipping.
gfx::Rect anchor_rect = GetBoundsInScreen();
// Assign the correct app type to `item_menu_model_adapter_` according to the
// parent view of the app list item view.
AppListMenuModelAdapter::AppListViewAppType app_type;
AppLaunchedMetricParams metric_params;
switch (context_) {
case Context::kAppsGridView:
app_type = AppListMenuModelAdapter::PRODUCTIVITY_LAUNCHER_APP_GRID;
metric_params.launched_from = AppListLaunchedFrom::kLaunchedFromGrid;
metric_params.launch_type = AppListLaunchType::kApp;
break;
case Context::kAppsCollection:
app_type =
AppListMenuModelAdapter::PRODUCTIVITY_LAUNCHER_APPS_COLLECTIONS;
metric_params.launched_from =
AppListLaunchedFrom::kLaunchedFromAppsCollections;
metric_params.launch_type = AppListLaunchType::kApp;
break;
case Context::kRecentAppsView:
app_type = AppListMenuModelAdapter::PRODUCTIVITY_LAUNCHER_RECENT_APP;
metric_params.launched_from =
AppListLaunchedFrom::kLaunchedFromRecentApps;
metric_params.launch_type = AppListLaunchType::kAppSearchResult;
break;
}
view_delegate_->GetAppLaunchedMetricParams(&metric_params);
if (context_ == Context::kAppsCollection) {
item_menu_model_adapter_ =
std::make_unique<AppsCollectionsMenuModelAdapter>(
item_weak_->GetMetadata()->id, std::move(menu_model), GetWidget(),
source_type, metric_params, app_type,
base::BindOnce(&AppListItemView::OnMenuClosed,
weak_ptr_factory_.GetWeakPtr()),
view_delegate_->IsInTabletMode(), item_weak_->collection_id());
} else {
item_menu_model_adapter_ = std::make_unique<AppListMenuModelAdapter>(
item_weak_->GetMetadata()->id, std::move(menu_model), GetWidget(),
source_type, metric_params, app_type,
base::BindOnce(&AppListItemView::OnMenuClosed,
weak_ptr_factory_.GetWeakPtr()),
view_delegate_->IsInTabletMode(), item_weak_->collection_id());
}
item_menu_model_adapter_->Run(
anchor_rect, views::MenuAnchorPosition::kBubbleRight, run_types);
if (!context_menu_shown_callback_.is_null()) {
context_menu_shown_callback_.Run();
}
grid_delegate_->SetSelectedView(this);
}
void AppListItemView::ShowContextMenuForViewImpl(
views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
if (IsShowingAppMenu()) {
return;
}
// Prevent multiple requests for context menus before the current request
// completes. If a second request is sent before the first one can respond,
// the Chrome side delegate will become unresponsive
// (https://crbug.com/881886).
if (waiting_for_context_menu_options_) {
return;
}
waiting_for_context_menu_options_ = true;
views::InkDrop::Get(this)->SetMode(
views::InkDropHost::InkDropMode::ON_NO_GESTURE_HANDLER);
views::InkDrop::Get(this)->AnimateToState(views::InkDropState::ACTIVATED,
nullptr);
// When the context menu comes from the apps grid or the apps collections grid
// it has sorting options. When it comes from recent apps it has an option to
// hide the continue section.
AppListItemContext item_context;
switch (context_) {
case Context::kAppsGridView:
item_context = AppListItemContext::kAppsGrid;
break;
case Context::kAppsCollection:
item_context = AppListItemContext::kAppsCollectionsGrid;
break;
case Context::kRecentAppsView:
item_context = AppListItemContext::kRecentApps;
break;
}
view_delegate_->GetContextMenuModel(
item_weak_->id(), item_context,
base::BindOnce(&AppListItemView::OnContextMenuModelReceived,
weak_ptr_factory_.GetWeakPtr(), point, source_type));
}
bool AppListItemView::ShouldEnterPushedState(const ui::Event& event) {
if (drag_state_ != DragState::kNone) {
return false;
}
// Don't enter pushed state for EventType::kGestureTapDown so that hover gray
// background does not show up during scroll.
if (event.type() == ui::EventType::kGestureTapDown) {
return false;
}
return views::Button::ShouldEnterPushedState(event);
}
bool AppListItemView::OnMousePressed(const ui::MouseEvent& event) {
Button::OnMousePressed(event);
if (!ShouldEnterPushedState(event)) {
return true;
}
if (!InitiateDrag(event.location(), event.root_location())) {
return true;
}
mouse_drag_timer_.Start(FROM_HERE, base::Milliseconds(kMouseDragUIDelayInMs),
this, &AppListItemView::OnMouseDragTimer);
return true;
}
void AppListItemView::Layout(PassKey) {
gfx::Rect rect(GetContentsBounds());
if (rect.IsEmpty()) {
return;
}
views::FocusRing::Get(this)->DeprecatedLayoutImmediately();
const gfx::Size icon_size = GetIconSize();
const gfx::Rect icon_bounds = GetIconBoundsForTargetViewBounds(
app_list_config_, rect, gfx::ScaleToRoundedSize(icon_size, icon_scale_),
icon_scale_);
GetIconView()->SetBoundsRect(icon_bounds);
UpdateBackgroundLayerBounds();
SetBackgroundExtendedState(is_icon_extended_, /*animate=*/false);
gfx::Rect title_bounds = GetTitleBoundsForTargetViewBounds(
app_list_config_, rect,
title_->GetPreferredSize(views::SizeBounds(title_->width(), {})),
icon_scale_);
if (new_install_dot_ && new_install_dot_->GetVisible()) {
// If the new install dot is showing, and the dot would extend outside the
// left edge of the tile, inset the title bounds to make space for the dot.
int dot_x = title_bounds.x() - kNewInstallDotSize - kNewInstallDotPadding;
if (dot_x < 0)
title_bounds.Inset(gfx::Insets::TLBR(0, kNewInstallDotSize, 0, 0));
}
title_->SetBoundsRect(title_bounds);
if (new_install_dot_) {
new_install_dot_->SetBounds(
title_bounds.x() - kNewInstallDotSize - kNewInstallDotPadding,
title_bounds.y() + title_bounds.height() / 2 - kNewInstallDotSize / 2,
kNewInstallDotSize, kNewInstallDotSize);
}
const float indicator_size =
icon_bounds.width() * kNotificationIndicatorWidthRatio;
const float indicator_padding =
is_folder_ ? 0
: std::round(icon_bounds.width() *
kNotificationIndicatorPaddingRatio);
const float indicator_x =
icon_bounds.right() - indicator_size - indicator_padding;
const float indicator_y = icon_bounds.y() + indicator_padding;
const gfx::Rect indicator_bounds = gfx::ToRoundedRect(
gfx::RectF(indicator_x, indicator_y, indicator_size, indicator_size));
notification_indicator_->SetIndicatorBounds(indicator_bounds);
if (progress_indicator_) {
UpdateProgressRingBounds();
}
}
gfx::Size AppListItemView::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
return gfx::Size(app_list_config_->grid_tile_width(),
app_list_config_->grid_tile_height());
}
bool AppListItemView::OnKeyPressed(const ui::KeyEvent& event) {
// Disable space key to press the button. The keyboard events received
// by this view are forwarded from a Textfield (SearchBoxView) and key
// released events are not forwarded. This leaves the button in pressed
// state.
if (event.key_code() == ui::VKEY_SPACE) {
return false;
}
return Button::OnKeyPressed(event);
}
void AppListItemView::OnMouseReleased(const ui::MouseEvent& event) {
auto weak_this = weak_ptr_factory_.GetWeakPtr();
// Reset all states if we are already dragging, and avoid triggering a button
// mouse release event.
if (drag_state_ == DragState::kInitialized &&
ui_state_ == UI_STATE_DRAGGING) {
SetMouseDragging(false);
drag_state_ = DragState::kNone;
return;
} else {
// Triggers the button's click handler callback, which might delete `this`.
Button::OnMouseReleased(event);
}
if (!weak_this) {
return;
}
SetMouseDragging(false);
if (app_list_features::IsDragAndDropRefactorEnabled()) {
// Cancel drag timer set when the mouse was pressed, to prevent the app
// item from entering dragged state.
mouse_drag_timer_.Stop();
drag_state_ = DragState::kNone;
return;
}
// EndDrag may delete |this|.
grid_delegate_->EndDrag(/*cancel=*/false);
}
void AppListItemView::OnMouseCaptureLost() {
Button::OnMouseCaptureLost();
SetMouseDragging(false);
if (app_list_features::IsDragAndDropRefactorEnabled()) {
return;
}
// EndDrag may delete |this|.
grid_delegate_->EndDrag(/*cancel=*/true);
}
bool AppListItemView::OnMouseDragged(const ui::MouseEvent& event) {
bool return_value = Button::OnMouseDragged(event);
if (app_list_features::IsDragAndDropRefactorEnabled()) {
return return_value;
}
if (drag_state_ != DragState::kNone && mouse_dragging_) {
// Update the drag location of the drag proxy if it has been created.
// If the drag is no longer happening, it could be because this item
// got removed, in which case this item has been destroyed. So, bail out
// now as there will be nothing else to do anyway as
// grid_delegate_->IsDragging() will be false.
if (!grid_delegate_->UpdateDragFromItem(/*is_touch=*/false, event))
return true;
}
if (!grid_delegate_->IsSelectedView(this))
grid_delegate_->ClearSelectedView();
return true;
}
bool AppListItemView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& event) {
// Ensure accelerators take priority in the app list. This ensures, e.g., that
// Ctrl+Space will switch input methods rather than activate the button.
return false;
}
void AppListItemView::OnFocus() {
if (focus_silently_) {
return;
}
grid_delegate_->SetSelectedView(this);
views::FocusRing::Get(this)->SchedulePaint();
}
void AppListItemView::OnBlur() {
if (grid_delegate_->IsSelectedView(this)) {
grid_delegate_->ClearSelectedView();
}
views::FocusRing::Get(this)->SchedulePaint();
}
int AppListItemView::GetDragOperations(const gfx::Point& press_pt) {
if (!IsItemDraggable()) {
return ui::DragDropTypes::DRAG_NONE;
}
return app_list_features::IsDragAndDropRefactorEnabled()
? ui::DragDropTypes::DRAG_MOVE
: views::View::GetDragOperations(press_pt);
}
void AppListItemView::WriteDragData(const gfx::Point& press_pt,
OSExchangeData* data) {
if (!app_list_features::IsDragAndDropRefactorEnabled()) {
views::View::WriteDragData(press_pt, data);
return;
}
if (item_weak_) {
data->provider().SetDragImage(GetDragImage(), press_pt.OffsetFromOrigin());
const DraggableAppType app_type = is_folder_
? DraggableAppType::kFolderAppGridItem
: DraggableAppType::kAppGridItem;
base::Pickle data_pickle;
data_pickle.WriteString(item_weak_->id());
data_pickle.WriteInt(static_cast<int>(app_type));
data->SetPickledData(GetAppItemFormatType(), data_pickle);
}
}
bool AppListItemView::MaybeStartTouchDrag(const gfx::Point& location) {
DCHECK(app_list_features::IsDragAndDropRefactorEnabled());
int drag_operations = GetDragOperations(location);
views::Widget* widget = GetWidget();
DCHECK(widget);
if (drag_operations == ui::DragDropTypes::DRAG_NONE ||
widget->dragged_view()) {
return false;
}
SetUIState(UI_STATE_TOUCH_DRAGGING);
auto data = std::make_unique<ui::OSExchangeData>();
WriteDragData(
location - gfx::Vector2d(0, std::ceil(kTouchDragImageVerticalOffset /
kDragDropAppIconScale)),
data.get());
gfx::Point widget_location(location);
views::View::ConvertPointToWidget(this, &widget_location);
widget->RunShellDrag(this, std::move(data), widget_location, drag_operations,
ui::mojom::DragEventSource::kTouch);
return true;
}
void AppListItemView::OnGestureEvent(ui::GestureEvent* event) {
const bool is_drag_and_drop_enabled =
app_list_features::IsDragAndDropRefactorEnabled();
switch (event->type()) {
case ui::EventType::kGestureScrollBegin:
if (touch_dragging_) {
if (is_drag_and_drop_enabled) {
OnDragStarted();
} else {
grid_delegate_->StartDragAndDropHostDragAfterLongPress();
}
event->SetHandled();
} else {
touch_drag_timer_.Stop();
}
break;
case ui::EventType::kGestureScrollUpdate:
if (touch_dragging_ && drag_state_ != DragState::kNone) {
if (is_drag_and_drop_enabled &&
MaybeStartTouchDrag(event->location())) {
event->SetHandled();
} else {
grid_delegate_->UpdateDragFromItem(/*is_touch=*/true, *event);
event->SetHandled();
}
}
break;
case ui::EventType::kGestureScrollEnd:
case ui::EventType::kScrollFlingStart:
if (touch_dragging_) {
if (!is_drag_and_drop_enabled) {
SetTouchDragging(false);
event->SetHandled();
}
}
break;
case ui::EventType::kGestureTapDown:
if (GetState() != STATE_DISABLED && IsItemDraggable()) {
SetState(STATE_PRESSED);
touch_drag_timer_.Start(
FROM_HERE, base::Milliseconds(kTouchLongpressDelayInMs),
base::BindOnce(&AppListItemView::OnTouchDragTimer,
base::Unretained(this), event->location(),
event->root_location()));
event->SetHandled();
}
break;
case ui::EventType::kGestureTap:
case ui::EventType::kGestureTapCancel:
if (GetState() != STATE_DISABLED) {
touch_drag_timer_.Stop();
SetState(STATE_NORMAL);
}
break;
case ui::EventType::kGestureLongTap:
case ui::EventType::kGestureEnd:
if (is_drag_and_drop_enabled && drag_state_ == DragState::kInitialized) {
// Reset `drag_state_` if there was an attempt to initiate it (i.e. the
// touch drag timer fired) but was not properly started (i.e. the app
// item was never actually dragged) before a release event occurred.
drag_state_ = DragState::kNone;
}
touch_drag_timer_.Stop();
SetTouchDragging(false);
if (IsShowingAppMenu()) {
grid_delegate_->SetSelectedView(this);
}
break;
case ui::EventType::kGestureLongPress:
if (is_drag_and_drop_enabled) {
// Handle the long press event on long press to avoid RootView to
// trigger View::DoDrag for this view before the item is dragged.
gfx::Point screen_location(event->location());
View::ConvertPointToScreen(this, &screen_location);
ShowContextMenu(screen_location, ui::MENU_SOURCE_TOUCH);
event->SetHandled();
}
break;
case ui::EventType::kGestureTwoFingerTap:
if (touch_dragging_) {
SetTouchDragging(false);
} else {
touch_drag_timer_.Stop();
}
break;
default:
break;
}
if (!event->handled()) {
Button::OnGestureEvent(event);
}
}
void AppListItemView::OnThemeChanged() {
views::Button::OnThemeChanged();
if (item_weak_) {
item_weak_->RequestFolderIconUpdate();
SkColor notification_indicator_color =
is_folder_ ? GetColorProvider()->GetColor(cros_tokens::kIconColorBlue)
: item_weak_->GetNotificationBadgeColor();
notification_indicator_->SetColor(notification_indicator_color);
if (icon_background_) {
icon_background_->layer()->SetColor(
GetColorProvider()->GetColor(GetBackgroundLayerColorId()));
}
}
UpdateIconView(/*update_item_icon=*/true);
// Redraw progress indicator to adjust colors.
if (progress_indicator_) {
progress_indicator_->InvalidateLayer();
}
SchedulePaint();
}
std::u16string AppListItemView::GetTooltipText(const gfx::Point& p) const {
// Use the label to generate a tooltip, so that it will consider its text
// truncation in making the tooltip. We do not want the label itself to have a
// tooltip, so we only temporarily enable it to get the tooltip text from the
// label, then disable it again.
title_->SetHandlesTooltips(true);
std::u16string tooltip = title_->GetTooltipText(p);
title_->SetHandlesTooltips(false);
if (new_install_dot_ && new_install_dot_->GetVisible() && !is_folder_) {
// Tooltip becomes two lines: "App Name" + "New install".
tooltip = l10n_util::GetStringFUTF16(IDS_APP_LIST_NEW_INSTALL, tooltip);
}
return tooltip;
}
void AppListItemView::OnDraggedViewEnter() {
SetBackgroundExtendedState(/*extend_icon=*/true, /*animate=*/true);
}
void AppListItemView::OnDraggedViewExit() {
SetBackgroundExtendedState(/*extend_icon=*/false, /*animate=*/true);
}
void AppListItemView::SetBackgroundBlurEnabled(bool enabled) {
DCHECK(is_folder_);
if (!enabled) {
if (GetIconBackgroundLayer()) {
GetIconBackgroundLayer()->SetBackgroundBlur(0);
}
return;
}
GetIconBackgroundLayer()->SetBackgroundBlur(
ColorProvider::kBackgroundBlurSigma);
GetIconBackgroundLayer()->SetBackdropFilterQuality(
ColorProvider::kBackgroundBlurQuality);
}
void AppListItemView::EnsureLayer() {
if (layer()) {
return;
}
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
}
bool AppListItemView::HasNotificationBadge() {
return item_weak_->has_notification_badge();
}
bool AppListItemView::FireMouseDragTimerForTest() {
if (!mouse_drag_timer_.IsRunning()) {
return false;
}
mouse_drag_timer_.FireNow();
return true;
}
bool AppListItemView::FireTouchDragTimerForTest() {
if (!touch_drag_timer_.IsRunning()) {
return false;
}
touch_drag_timer_.FireNow();
return true;
}
bool AppListItemView::IsShowingAppMenu() const {
return item_menu_model_adapter_ && item_menu_model_adapter_->IsShowingMenu();
}
bool AppListItemView::IsItemDraggable() const {
return context_ == Context::kAppsGridView;
}
bool AppListItemView::IsNotificationIndicatorShownForTest() const {
return notification_indicator_->GetVisible();
}
void AppListItemView::SetContextMenuShownCallbackForTest(
base::RepeatingClosure closure) {
context_menu_shown_callback_ = std::move(closure);
}
gfx::Rect AppListItemView::GetDefaultTitleBoundsForTest() {
return GetTitleBoundsForTargetViewBounds(
app_list_config_, GetContentsBounds(),
title_->GetPreferredSize(views::SizeBounds(title_->width(), {})),
icon_scale_);
}
void AppListItemView::SetMostRecentGridIndex(GridIndex new_grid_index,
int columns) {
if (new_grid_index == most_recent_grid_index_) {
has_pending_row_change_ = false;
return;
}
if (most_recent_grid_index_.IsValid()) {
// Pending row changes are only flagged when the item index changes from one
// edge of the grid to the other and into a different row.
if (IsIndexMovingFromOneEdgeToAnother(most_recent_grid_index_,
new_grid_index, columns) &&
IsIndexMovingToDifferentRow(most_recent_grid_index_, new_grid_index,
columns)) {
has_pending_row_change_ = true;
} else {
has_pending_row_change_ = false;
}
}
most_recent_grid_index_ = new_grid_index;
}
void AppListItemView::ClearItemDraggingState() {
SetState(STATE_NORMAL);
SetMouseDragging(false);
SetTouchDragging(false);
}
void AppListItemView::AnimateInFromPromiseApp(
const ui::ImageModel& fallback_image,
base::RepeatingClosure callback) {
// Set up the app list item view so it appears as a promise icon - add a
// progress ring (in completed state), scale the icon down, and hide the title
// and the new install indicator.
forced_progress_indicator_value_ = ProgressIndicator::kForcedShow;
UpdateProgressIndicatorState();
prefer_fallback_icon_ = true;
fallback_icon_image_model_ = fallback_image;
UpdateIconView(/*update_item_icon=*/false);
views::View* const icon_view = GetIconView();
icon_view->SetPaintToLayer();
icon_view->layer()->SetFillsBoundsOpaquely(false);
title_->SetPaintToLayer();
title_->layer()->SetFillsBoundsOpaquely(false);
title_->layer()->SetOpacity(0.0f);
new_install_dot_->SetPaintToLayer();
new_install_dot_->layer()->SetFillsBoundsOpaquely(false);
new_install_dot_->layer()->SetOpacity(0.0f);
const gfx::Point center_point = gfx::Rect(GetIconSize()).CenterPoint();
const float starting_size =
fallback_icon_image_model_.IsVectorIcon()
? kPlaceholderIconDimension
: static_cast<float>(
app_list_config_->promise_icon_dimension_installing());
icon_view->layer()->SetTransform(gfx::GetScaleTransform(
center_point,
starting_size /
static_cast<float>(app_list_config_->grid_icon_dimension())));
// Animate the app list view out of the promise app state.
views::AnimationBuilder animation;
animation.OnEnded(base::BindOnce(&AppListItemView::OnAnimatedInFromPromiseApp,
weak_ptr_factory_.GetWeakPtr(), callback));
animation.OnAborted(
base::BindOnce(&AppListItemView::OnAnimatedInFromPromiseApp,
weak_ptr_factory_.GetWeakPtr(), callback));
animation.Once()
.SetDuration(kSwapPromiseIconDuration)
.SetOpacity(progress_indicator_->layer(), 0.0f,
gfx::Tween::FAST_OUT_LINEAR_IN)
.SetOpacity(title_->layer(), 1.0f, gfx::Tween::FAST_OUT_LINEAR_IN)
.SetOpacity(new_install_dot_->layer(), 1.0f,
gfx::Tween::FAST_OUT_LINEAR_IN)
.SetTransform(icon_view->layer(), gfx::Transform(),
gfx::Tween::FAST_OUT_LINEAR_IN);
}
void AppListItemView::OnAnimatedInFromPromiseApp(
base::RepeatingClosure callback) {
title_->DestroyLayer();
new_install_dot_->DestroyLayer();
forced_progress_indicator_value_.reset();
if (progress_indicator_ && layer()) {
layer()->Remove(progress_indicator_->layer());
}
progress_indicator_.reset();
// Clear background set as a result of adding progress indicator.
SetBackground(nullptr);
prefer_fallback_icon_ = false;
if (!ShouldUseFallbackIconImageModel()) {
fallback_icon_image_model_ = ui::ImageModel();
}
GetIconView()->DestroyLayer();
UpdateIconView(/*update_item_icon=*/true);
callback.Run();
}
std::optional<size_t> AppListItemView::item_counter_count_for_test() const {
DCHECK(!use_item_icon_);
return folder_icon_->GetItemCounterCount();
}
ProgressIndicator* AppListItemView::GetProgressIndicatorForTest() const {
DCHECK(is_promise_app_);
return progress_indicator_.get();
}
void AppListItemView::OnMenuClosed() {
views::InkDrop::Get(this)->AnimateToState(views::InkDropState::HIDDEN,
nullptr);
views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::OFF);
// Release menu since its menu model delegate (AppContextMenu) could be
// released as a result of menu command execution.
item_menu_model_adapter_.reset();
if (!menu_close_initiated_from_drag_) {
// If the menu was not closed due to a drag sequence(e.g. multi touch) reset
// the drag state.
SetState(STATE_NORMAL);
SetTouchDragging(false);
}
menu_close_initiated_from_drag_ = false;
// Keep the item focused if the menu was shown via keyboard.
if (!menu_show_initiated_from_key_) {
OnBlur();
}
if (focus_removed_by_context_menu_) {
// Restore the last focused view when exiting the menu.
GetFocusManager()->RestoreFocusedView();
focus_removed_by_context_menu_ = false;
}
}
void AppListItemView::OnSyncDragEnd() {
SetUIState(UI_STATE_NORMAL);
}
views::View* AppListItemView::GetIconView() const {
if (use_item_icon_) {
return icon_;
}
return folder_icon_;
}
gfx::Rect AppListItemView::GetIconBounds() const {
return GetIconView()->bounds();
}
gfx::Rect AppListItemView::GetIconBoundsInScreen() const {
gfx::Rect icon_bounds = GetIconBounds();
ConvertRectToScreen(this, &icon_bounds);
return icon_bounds;
}
gfx::ImageSkia AppListItemView::GetDragImage() const {
if (!GetColorProvider() || !app_list_config_) {
return gfx::ImageSkia();
}
if (is_folder_) {
return folder_icon_->CreateDragImage();
}
if (has_host_badge_) {
return CreateBadgedShortcutImage(*app_list_config_, icon_image_,
host_badge_icon_image_,
kDragDropAppIconScale, GetColorProvider());
}
return icon_->GetImage();
}
void AppListItemView::SetIconVisible(bool visible) {
GetIconView()->SetVisible(visible);
}
void AppListItemView::EnterCardifyState() {
in_cardified_grid_ = true;
gfx::FontList font_size = app_list_config_->app_title_font();
const float cardified_scale = GetAppsGridCardifiedScale();
const int size_delta = font_size.GetFontSize() * (1 - cardified_scale);
title_->SetFontList(font_size.DeriveWithSizeDelta(-size_delta));
ScaleIconImmediatly(cardified_scale);
}
void AppListItemView::ExitCardifyState() {
title_->SetFontList(app_list_config_->app_title_font());
ScaleIconImmediatly(1.0f);
in_cardified_grid_ = false;
}
// static
gfx::Rect AppListItemView::GetIconBoundsForTargetViewBounds(
const AppListConfig* config,
const gfx::Rect& target_bounds,
const gfx::Size& icon_size,
const float icon_scale) {
gfx::Rect rect(target_bounds);
rect.Inset(gfx::Insets::TLBR(0, 0, config->grid_icon_bottom_padding(), 0));
rect.ClampToCenteredSize(icon_size);
return rect;
}
// static
gfx::Rect AppListItemView::GetHostBadgeIconBoundsForTargetViewBounds(
const gfx::Rect& main_icon_bounds,
const gfx::Size& host_badge_icon_with_background_size,
const float icon_scale) {
gfx::Rect rect(main_icon_bounds.CenterPoint(),
host_badge_icon_with_background_size);
rect.ClampToCenteredSize(host_badge_icon_with_background_size);
return rect;
}
// static
gfx::Rect AppListItemView::GetTitleBoundsForTargetViewBounds(
const AppListConfig* config,
const gfx::Rect& target_bounds,
const gfx::Size& title_size,
float icon_scale) {
gfx::Rect rect(target_bounds);
rect.Inset(
gfx::Insets::TLBR(config->grid_title_top_padding() * icon_scale,
config->grid_title_horizontal_padding() * icon_scale,
config->grid_title_bottom_padding() * icon_scale,
config->grid_title_horizontal_padding() * icon_scale));
rect.ClampToCenteredSize(title_size);
// Respect the title preferred height, to ensure the text does not get clipped
// due to padding if the item view gets too small.
if (rect.height() < title_size.height()) {
rect.set_y(rect.y() - (title_size.height() - rect.height()) / 2);
rect.set_height(title_size.height());
}
return rect;
}
void AppListItemView::ItemIconChanged(AppListConfigType config_type) {
if (config_type != app_list_config_->type()) {
return;
}
DCHECK(item_weak_);
UpdateIconView(/*update_item_icon=*/true);
}
void AppListItemView::ItemNameChanged() {
SetItemName(base::UTF8ToUTF16(item_weak_->name()),
base::UTF8ToUTF16(item_weak_->GetAccessibleName()));
}
void AppListItemView::ItemHostBadgeIconChanged() {
DCHECK(item_weak_);
UpdateIconView(/*update_item_icon=*/true);
}
void AppListItemView::ItemBadgeVisibilityChanged() {
if (GetIconView()) {
notification_indicator_->SetVisible(item_weak_->has_notification_badge());
}
}
void AppListItemView::ItemBadgeColorChanged() {
notification_indicator_->SetColor(item_weak_->GetNotificationBadgeColor());
}
void AppListItemView::ItemIsNewInstallChanged() {
DCHECK(item_weak_);
if (new_install_dot_) {
new_install_dot_->SetVisible(item_weak_->is_new_install());
DeprecatedLayoutImmediately();
}
UpdateAccessibleDescription();
}
void AppListItemView::ItemBeingDestroyed() {
DCHECK(item_weak_);
item_weak_->RemoveObserver(this);
item_weak_ = nullptr;
UpdateAccessibleDescription();
if (!use_item_icon_) {
folder_icon_->ResetFolderItem();
}
if (app_list_features::IsDragAndDropRefactorEnabled()) {
// When drag and drop refactor is enabled, AppsGridView observes dragged
// item destruction to ensure the drag is finalized.
return;
}
// `EndDrag()` may delete this.
if (drag_state_ != DragState::kNone) {
grid_delegate_->EndDrag(/*cancel=*/true);
}
}
void AppListItemView::ItemProgressUpdated() {
UpdateProgressIndicatorState();
}
void AppListItemView::ItemAppStatusUpdated() {
UpdateProgressIndicatorState();
UpdateAccessibleDescription();
}
void AppListItemView::ItemAppCollectionIdChanged() {
UpdateAccessibleDescription();
}
bool AppListItemView::ImageModelHasPlaceholderIcon() const {
return ShouldUseFallbackIconImageModel()
? fallback_icon_image_model_.IsVectorIcon()
: icon_image_model_.IsVectorIcon();
}
void AppListItemView::UpdateProgressIndicatorState() {
if ((!is_promise_app_ && !forced_progress_indicator_value_) ||
!features::ArePromiseIconsEnabled()) {
return;
}
if (!progress_indicator_) {
progress_indicator_ =
ProgressIndicator::CreateDefaultInstance(base::BindRepeating(
[](AppListItemView* view) -> std::optional<float> {
if (view->forced_progress_indicator_value_) {
return *view->forced_progress_indicator_value_;
}
if (view->item()->app_status() == AppStatus::kPending) {
return 0.0f;
}
// If download is in-progress, return the progress as a decimal.
// Otherwise, the progress indicator shouldn't be painted.
float progress = view->item()->GetMetadata()->progress;
return (progress >= 0.f && progress < 1.f)
? progress
: ProgressIndicator::kProgressComplete;
},
base::Unretained(this)));
progress_indicator_->SetInnerIconVisible(false);
progress_indicator_->SetInnerRingVisible(false);
progress_indicator_->SetOuterRingStrokeWidth(
static_cast<float>(kPromiseRingStrokeSize));
EnsureLayer();
layer()->Add(progress_indicator_->CreateLayer(base::BindRepeating(
[](AppListItemView* view, ui::ColorId color_id) {
return view->GetColorProvider()->GetColor(color_id);
},
base::Unretained(this))));
}
EnsureLayer();
if (item()->app_status() == AppStatus::kPending) {
progress_indicator_->SetColorId(cros_tokens::kCrosSysHighlightShape);
progress_indicator_->SetOuterRingTrackVisible(true);
} else {
progress_indicator_->SetColorId(cros_tokens::kCrosSysPrimary);
progress_indicator_->SetOuterRingTrackVisible(false);
}
UpdateProgressRingBounds();
}
void AppListItemView::UpdateProgressRingBounds() {
gfx::Rect rect(GetContentsBounds());
if (rect.IsEmpty()) {
return;
}
CHECK(!is_folder_);
gfx::Rect progress_bounds = gfx::Rect(
views::View::ConvertRectToTarget(icon_, this, icon_->GetImageBounds()));
const gfx::Size promise_icon_preferred_size = gfx::ScaleToRoundedSize(
GetPreferredIconSizeForProgressRing(), icon_scale_);
// If the icon is smaller than the expected icon size (i,e for placeholder
// icons), add padding to ensure the overall size of the promise icon is
// correct regardless of the image icon size.
progress_bounds.Outset(gfx::Outsets::VH(
std::max(
0,
(promise_icon_preferred_size.width() - progress_bounds.width()) / 2),
std::max(
0, (promise_icon_preferred_size.height() - progress_bounds.height()) /
2)));
const gfx::Insets progress_ring_padding =
ImageModelHasPlaceholderIcon() ||
item()->app_status() == AppStatus::kPending
? kProgressRingMarginPending
: kProgressRingMarginInstalling;
progress_bounds.Inset(progress_ring_padding);
// The Progress indicator paints the ring within the bounds of the layer, so
// add padding for the promise ring.
progress_bounds.Inset(-gfx::Insets(kPromiseRingStrokeSize));
// The masked icons include 1px padding.
progress_bounds.Inset(1);
progress_indicator_->layer()->SetBounds(progress_bounds);
layer()->StackAtBottom(progress_indicator_->layer());
progress_indicator_->InvalidateLayer();
SetBackground(std::make_unique<PromiseIconBackground>(
cros_tokens::kCrosSysSystemOnBase, progress_bounds,
progress_ring_padding));
}
void AppListItemView::SetBackgroundExtendedState(bool extend_icon,
bool animate) {
// App backgrounds are only created or updated if the extended state changes,
// while unchanged folders may update the icon clip rects. Return early for
// unchanged apps.
if (is_icon_extended_ == extend_icon && !is_folder_) {
return;
}
is_icon_extended_ = extend_icon;
icon_background_->SetVisible(true);
GetIconView()->SetPaintToLayer();
GetIconView()->layer()->SetFillsBoundsOpaquely(false);
base::AutoReset<bool> auto_reset(&setting_up_icon_animation_, true);
ui::Layer* const background_layer = GetIconBackgroundLayer();
DCHECK(background_layer);
views::AnimationBuilder builder;
const auto animation_tween_type = gfx::Tween::EASE_IN;
builder
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&AppListItemView::OnExtendingAnimationEnded,
weak_ptr_factory_.GetWeakPtr(), extend_icon))
.OnAborted(base::BindOnce(&AppListItemView::OnExtendingAnimationEnded,
weak_ptr_factory_.GetWeakPtr(), extend_icon))
.Once();
UpdateBackgroundLayerBounds();
const int width = extend_icon
? app_list_config_->icon_extended_background_dimension()
: app_list_config_->icon_visible_dimension();
gfx::Rect clip_rect(background_layer->size());
clip_rect.ClampToCenteredSize(
ScaleToRoundedSize(gfx::Size(width, width), icon_scale_));
const int corner_radius =
extend_icon ? app_list_config_->icon_extended_background_radius()
: width / 2;
const base::TimeDelta duration =
animate ? base::Milliseconds(125) : base::TimeDelta();
builder.GetCurrentSequence()
.SetDuration(duration)
.SetClipRect(background_layer, clip_rect, animation_tween_type)
.SetRoundedCorners(background_layer,
gfx::RoundedCornersF(corner_radius * icon_scale_),
animation_tween_type);
if (GetWidget()) {
builder.GetCurrentSequence().SetColor(
background_layer,
GetColorProvider()->GetColor(GetBackgroundLayerColorId()),
animation_tween_type);
}
}
ui::ColorId AppListItemView::GetBackgroundLayerColorId() const {
if (is_icon_extended_) {
return cros_tokens::kCrosSysRippleNeutralOnSubtle;
}
if (is_folder_) {
return cros_tokens::kCrosSysSystemOnBase;
}
return cros_tokens::kCrosSysRippleNeutralOnSubtle;
}
void AppListItemView::OnExtendingAnimationEnded(bool extend_icon) {
if (!setting_up_icon_animation_ && !extend_icon && !is_folder_) {
icon_background_->SetVisible(false);
GetIconView()->DestroyLayer();
}
}
ui::Layer* AppListItemView::GetIconBackgroundLayer() {
if (!icon_background_) {
return nullptr;
}
return icon_background_->layer();
}
bool AppListItemView::AlwaysPaintsToLayer() {
return is_promise_app_ || progress_indicator_;
}
void AppListItemView::UpdateAccessibleDescription() {
if (!item_weak_) {
GetViewAccessibility().RemoveDescription();
return;
}
// The list of descriptions to be announced.
std::vector<std::u16string> descriptions;
if (item_weak_->is_folder()) {
// For folder items, announce the number of apps in the folder.
std::u16string app_count_announcement = l10n_util::GetPluralStringFUTF16(
IDS_APP_LIST_FOLDER_NUMBER_OF_APPS_ACCESSIBILE_DESCRIPTION,
item_weak_->AsFolderItem()->ChildItemCount());
descriptions.push_back(app_count_announcement);
}
auto app_status = item_weak_->app_status();
std::u16string app_status_description;
switch (app_status) {
case AppStatus::kBlocked:
app_status_description =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_APP_LIST_BLOCKED_APP);
break;
case AppStatus::kPaused:
app_status_description =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_APP_LIST_PAUSED_APP);
break;
default:
if (item_weak_->is_new_install()) {
app_status_description =
ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
IDS_APP_LIST_NEW_INSTALL_ACCESSIBILE_DESCRIPTION);
}
break;
}
if (!app_status_description.empty()) {
descriptions.push_back(app_status_description);
}
if (context_ == Context::kAppsCollection) {
descriptions.push_back(GetAppCollectionName(item_weak_->collection_id()));
}
// Set the concatenated descriptions.
if (!descriptions.empty()) {
GetViewAccessibility().SetDescription(base::JoinString(descriptions, u" "));
} else {
GetViewAccessibility().RemoveDescription();
}
}
BEGIN_METADATA(AppListItemView)
END_METADATA
} // namespace ash