blob: 7f75d1ff3453f21d2ac666d61a1247e46dc8d8bd [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/system/holding_space/holding_space_item_view.h"
#include "ash/public/cpp/holding_space/holding_space_client.h"
#include "ash/public/cpp/holding_space/holding_space_constants.h"
#include "ash/public/cpp/holding_space/holding_space_controller.h"
#include "ash/public/cpp/holding_space/holding_space_item.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/style/ash_color_provider.h"
#include "ash/system/holding_space/holding_space_util.h"
#include "ash/system/holding_space/holding_space_view_delegate.h"
#include "base/bind.h"
#include "ui/base/class_property.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/painter.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/vector_icons.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// A UI class property used to identify if a view is an instance of
// `HoldingSpaceItemView`. Class name is not an adequate identifier as it may be
// overridden by subclasses.
DEFINE_UI_CLASS_PROPERTY_KEY(bool, kIsHoldingSpaceItemViewProperty, false)
// Appearance.
constexpr size_t kCheckmarkBackgroundSize = 18;
// Helpers ---------------------------------------------------------------------
// Schedules repaint of `layer`.
void InvalidateLayer(ui::Layer* layer) {
layer->SchedulePaint(gfx::Rect(layer->size()));
}
// CallbackPainter -------------------------------------------------------------
// A painter which delegates painting to a callback.
class CallbackPainter : public views::Painter {
public:
using Callback = base::RepeatingCallback<void(gfx::Canvas*, gfx::Size)>;
CallbackPainter(const CallbackPainter&) = delete;
CallbackPainter& operator=(const CallbackPainter&) = delete;
~CallbackPainter() override = default;
// Creates a painted layer which delegates painting to `callback`.
static std::unique_ptr<ui::LayerOwner> CreatePaintedLayer(Callback callback) {
auto owner = views::Painter::CreatePaintedLayer(
base::WrapUnique(new CallbackPainter(callback)));
owner->layer()->SetFillsBoundsOpaquely(false);
return owner;
}
private:
explicit CallbackPainter(Callback callback) : callback_(callback) {}
// views::Painter:
gfx::Size GetMinimumSize() const override { return gfx::Size(); }
void Paint(gfx::Canvas* canvas, const gfx::Size& size) override {
callback_.Run(canvas, size);
}
Callback callback_;
};
// MinimumSizableView ---------------------------------------------------------
// A view which respects a minimum size restriction.
class MinimumSizableView : public views::View {
public:
explicit MinimumSizableView(const gfx::Size& min_size)
: min_size_(min_size) {}
MinimumSizableView(const MinimumSizableView&) = delete;
MinimumSizableView& operator=(const MinimumSizableView&) = delete;
~MinimumSizableView() override = default;
private:
// views::View:
gfx::Size CalculatePreferredSize() const override {
gfx::Size preferred_size(views::View::CalculatePreferredSize());
preferred_size.SetToMax(min_size_);
return preferred_size;
}
int GetHeightForWidth(int width) const override {
return std::max(views::View::GetHeightForWidth(width), min_size_.height());
}
const gfx::Size min_size_;
};
} // namespace
// HoldingSpaceItemView --------------------------------------------------------
HoldingSpaceItemView::HoldingSpaceItemView(HoldingSpaceViewDelegate* delegate,
const HoldingSpaceItem* item)
: delegate_(delegate), item_(item), item_id_(item->id()) {
model_observer_.Observe(HoldingSpaceController::Get()->model());
SetProperty(kIsHoldingSpaceItemViewProperty, true);
set_context_menu_controller(delegate_);
set_drag_controller(delegate_);
SetNotifyEnterExitOnChild(true);
// Accessibility.
GetViewAccessibility().OverrideName(item->text());
GetViewAccessibility().OverrideRole(ax::mojom::Role::kListItem);
// Layer.
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
// Focus.
SetFocusBehavior(FocusBehavior::ALWAYS);
focused_layer_owner_ =
CallbackPainter::CreatePaintedLayer(base::BindRepeating(
&HoldingSpaceItemView::OnPaintFocus, base::Unretained(this)));
layer()->Add(focused_layer_owner_->layer());
// Selection.
selected_layer_owner_ =
CallbackPainter::CreatePaintedLayer(base::BindRepeating(
&HoldingSpaceItemView::OnPaintSelect, base::Unretained(this)));
layer()->Add(selected_layer_owner_->layer());
// This view's `selected_` state is represented differently depending on
// `delegate_`'s selection UI. Register to be notified of changes.
selection_ui_changed_subscription_ =
delegate_->AddSelectionUiChangedCallback(base::BindRepeating(
&HoldingSpaceItemView::OnSelectionUiChanged, base::Unretained(this)));
delegate_->OnHoldingSpaceItemViewCreated(this);
}
HoldingSpaceItemView::~HoldingSpaceItemView() {
if (delegate_)
delegate_->OnHoldingSpaceItemViewDestroying(this);
}
// static
HoldingSpaceItemView* HoldingSpaceItemView::Cast(views::View* view) {
return const_cast<HoldingSpaceItemView*>(
Cast(const_cast<const views::View*>(view)));
}
// static
const HoldingSpaceItemView* HoldingSpaceItemView::Cast(
const views::View* view) {
DCHECK(HoldingSpaceItemView::IsInstance(view));
return static_cast<const HoldingSpaceItemView*>(view);
}
// static
bool HoldingSpaceItemView::IsInstance(const views::View* view) {
return view->GetProperty(kIsHoldingSpaceItemViewProperty);
}
void HoldingSpaceItemView::Reset() {
delegate_ = nullptr;
}
bool HoldingSpaceItemView::HandleAccessibleAction(
const ui::AXActionData& action_data) {
return (delegate_ && delegate_->OnHoldingSpaceItemViewAccessibleAction(
this, action_data)) ||
views::View::HandleAccessibleAction(action_data);
}
void HoldingSpaceItemView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
gfx::Rect bounds = GetLocalBounds();
// Selection ring.
selected_layer_owner_->layer()->SetBounds(bounds);
InvalidateLayer(selected_layer_owner_->layer());
// Focus ring.
// NOTE: The focus ring is painted just outside the bounds for this view.
bounds.Inset(gfx::Insets(kHoldingSpaceFocusInsets));
focused_layer_owner_->layer()->SetBounds(bounds);
InvalidateLayer(focused_layer_owner_->layer());
}
void HoldingSpaceItemView::OnFocus() {
InvalidateLayer(focused_layer_owner_->layer());
}
void HoldingSpaceItemView::OnBlur() {
InvalidateLayer(focused_layer_owner_->layer());
}
void HoldingSpaceItemView::OnGestureEvent(ui::GestureEvent* event) {
if (delegate_ && delegate_->OnHoldingSpaceItemViewGestureEvent(this, *event))
event->SetHandled();
}
bool HoldingSpaceItemView::OnKeyPressed(const ui::KeyEvent& event) {
return delegate_ && delegate_->OnHoldingSpaceItemViewKeyPressed(this, event);
}
void HoldingSpaceItemView::OnMouseEvent(ui::MouseEvent* event) {
switch (event->type()) {
case ui::ET_MOUSE_ENTERED:
case ui::ET_MOUSE_EXITED:
UpdatePrimaryAction();
break;
default:
break;
}
views::View::OnMouseEvent(event);
}
bool HoldingSpaceItemView::OnMousePressed(const ui::MouseEvent& event) {
return delegate_ &&
delegate_->OnHoldingSpaceItemViewMousePressed(this, event);
}
void HoldingSpaceItemView::OnMouseReleased(const ui::MouseEvent& event) {
if (delegate_)
delegate_->OnHoldingSpaceItemViewMouseReleased(this, event);
}
void HoldingSpaceItemView::OnThemeChanged() {
views::View::OnThemeChanged();
AshColorProvider* const ash_color_provider = AshColorProvider::Get();
// Background.
SetBackground(views::CreateRoundedRectBackground(
ash_color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kControlBackgroundColorInactive),
kHoldingSpaceCornerRadius));
// Checkmark.
checkmark_->SetBackground(holding_space_util::CreateCircleBackground(
ash_color_provider->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor),
kCheckmarkBackgroundSize));
checkmark_->SetImage(gfx::CreateVectorIcon(
kCheckIcon, kHoldingSpaceIconSize,
ash_color_provider->IsDarkModeEnabled() ? gfx::kGoogleGrey900
: SK_ColorWHITE));
// Focused/selected layers.
InvalidateLayer(focused_layer_owner_->layer());
InvalidateLayer(selected_layer_owner_->layer());
if (!primary_action_container_)
return;
// Cancel.
const SkColor icon_color = AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kButtonIconColor);
primary_action_cancel_->SetImage(
views::Button::STATE_NORMAL,
gfx::CreateVectorIcon(kCancelIcon, kHoldingSpaceIconSize, icon_color));
// Pin.
const gfx::ImageSkia unpinned_icon = gfx::CreateVectorIcon(
views::kUnpinIcon, kHoldingSpaceIconSize, icon_color);
const gfx::ImageSkia pinned_icon =
gfx::CreateVectorIcon(views::kPinIcon, kHoldingSpaceIconSize, icon_color);
primary_action_pin_->SetImage(views::Button::STATE_NORMAL, unpinned_icon);
primary_action_pin_->SetToggledImage(views::Button::STATE_NORMAL,
&pinned_icon);
}
void HoldingSpaceItemView::OnHoldingSpaceItemUpdated(
const HoldingSpaceItem* item) {
if (item_ == item) {
GetViewAccessibility().OverrideName(item->text());
UpdatePrimaryAction();
}
}
void HoldingSpaceItemView::StartDrag(const ui::LocatedEvent& event,
ui::mojom::DragEventSource source) {
int drag_operations = GetDragOperations(event.location());
if (drag_operations == ui::DragDropTypes::DRAG_NONE)
return;
views::Widget* widget = GetWidget();
DCHECK(widget);
if (widget->dragged_view())
return;
auto data = std::make_unique<ui::OSExchangeData>();
WriteDragData(event.location(), data.get());
gfx::Point widget_location(event.location());
views::View::ConvertPointToWidget(this, &widget_location);
widget->RunShellDrag(this, std::move(data), widget_location, drag_operations,
source);
}
void HoldingSpaceItemView::SetSelected(bool selected) {
if (selected_ == selected)
return;
selected_ = selected;
InvalidateLayer(selected_layer_owner_->layer());
if (delegate_)
delegate_->OnHoldingSpaceItemViewSelectedChanged(this);
OnSelectionUiChanged();
}
std::unique_ptr<views::ImageView> HoldingSpaceItemView::CreateCheckmark() {
DCHECK(!checkmark_);
return views::Builder<views::ImageView>()
.CopyAddressTo(&checkmark_)
.SetID(kHoldingSpaceItemCheckmarkId)
.SetVisible(selected())
.Build();
}
std::unique_ptr<views::View> HoldingSpaceItemView::CreatePrimaryAction(
const gfx::Size& min_size) {
DCHECK(!primary_action_container_);
DCHECK(!primary_action_cancel_);
DCHECK(!primary_action_pin_);
using HorizontalAlignment = views::ImageButton::HorizontalAlignment;
using VerticalAlignment = views::ImageButton::VerticalAlignment;
gfx::Size preferred_size(kHoldingSpaceIconSize, kHoldingSpaceIconSize);
preferred_size.SetToMax(min_size);
return views::Builder<views::View>()
.CopyAddressTo(&primary_action_container_)
.SetID(kHoldingSpaceItemPrimaryActionContainerId)
.SetUseDefaultFillLayout(true)
.SetVisible(false)
.AddChild(
views::Builder<views::ImageButton>()
.CopyAddressTo(&primary_action_cancel_)
.SetID(kHoldingSpaceItemCancelButtonId)
.SetCallback(base::BindRepeating(
&HoldingSpaceItemView::OnPrimaryActionPressed,
base::Unretained(this)))
.SetFocusBehavior(views::View::FocusBehavior::NEVER)
.SetImageHorizontalAlignment(HorizontalAlignment::ALIGN_CENTER)
.SetImageVerticalAlignment(VerticalAlignment::ALIGN_MIDDLE)
.SetPreferredSize(preferred_size)
.SetVisible(false))
.AddChild(
views::Builder<views::ToggleImageButton>()
.CopyAddressTo(&primary_action_pin_)
.SetID(kHoldingSpaceItemPinButtonId)
.SetCallback(base::BindRepeating(
&HoldingSpaceItemView::OnPrimaryActionPressed,
base::Unretained(this)))
.SetFocusBehavior(views::View::FocusBehavior::NEVER)
.SetImageHorizontalAlignment(HorizontalAlignment::ALIGN_CENTER)
.SetImageVerticalAlignment(VerticalAlignment::ALIGN_MIDDLE)
.SetPreferredSize(preferred_size)
.SetVisible(false))
.Build();
}
void HoldingSpaceItemView::OnSelectionUiChanged() {
const bool multiselect =
delegate_ && delegate_->selection_ui() ==
HoldingSpaceViewDelegate::SelectionUi::kMultiSelect;
checkmark_->SetVisible(selected() && multiselect);
}
void HoldingSpaceItemView::OnPaintFocus(gfx::Canvas* canvas, gfx::Size size) {
if (!HasFocus())
return;
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(AshColorProvider::Get()->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor));
flags.setStrokeWidth(views::PlatformStyle::kFocusHaloThickness);
flags.setStyle(cc::PaintFlags::kStroke_Style);
gfx::Rect bounds = gfx::Rect(size);
bounds.Inset(gfx::Insets(flags.getStrokeWidth() / 2));
canvas->DrawRoundRect(bounds, kHoldingSpaceFocusCornerRadius, flags);
}
void HoldingSpaceItemView::OnPaintSelect(gfx::Canvas* canvas, gfx::Size size) {
if (!selected_)
return;
const SkColor color =
SkColorSetA(AshColorProvider::Get()->GetControlsLayerColor(
AshColorProvider::ControlsLayerType::kFocusRingColor),
kHoldingSpaceSelectedOverlayOpacity * 0xFF);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(color);
canvas->DrawRoundRect(gfx::Rect(size), kHoldingSpaceCornerRadius, flags);
}
void HoldingSpaceItemView::OnPrimaryActionPressed() {
DCHECK_NE(primary_action_cancel_->GetVisible(),
primary_action_pin_->GetVisible());
// Cancel.
if (primary_action_cancel_->GetVisible()) {
HoldingSpaceController::Get()->client()->CancelItems({item()});
return;
}
// Pin.
const bool is_item_pinned =
HoldingSpaceController::Get()->model()->ContainsItem(
HoldingSpaceItem::Type::kPinnedFile, item()->file_path());
// Unpinning `item()` may result in the destruction of this view.
auto weak_ptr = weak_factory_.GetWeakPtr();
if (is_item_pinned)
HoldingSpaceController::Get()->client()->UnpinItems({item()});
else
HoldingSpaceController::Get()->client()->PinItems({item()});
if (weak_ptr)
UpdatePrimaryAction();
}
void HoldingSpaceItemView::UpdatePrimaryAction() {
if (!IsMouseHovered()) {
primary_action_container_->SetVisible(false);
OnPrimaryActionVisibilityChanged(false);
return;
}
// Cancel.
const bool is_item_in_progress = item()->IsInProgress();
primary_action_cancel_->SetVisible(is_item_in_progress);
// Pin.
const bool is_item_pinned =
HoldingSpaceController::Get()->model()->ContainsItem(
HoldingSpaceItem::Type::kPinnedFile, item()->file_path());
primary_action_pin_->SetToggled(!is_item_pinned);
primary_action_pin_->SetVisible(!is_item_in_progress);
primary_action_container_->SetVisible(true);
OnPrimaryActionVisibilityChanged(true);
}
BEGIN_METADATA(HoldingSpaceItemView, views::View)
END_METADATA
} // namespace ash