blob: 0053b426250e319bcc29738d0ad8e078cb678acf [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/clipboard/views/clipboard_history_item_view.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_resource_manager.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/views/clipboard_history_bitmap_item_view.h"
#include "ash/clipboard/views/clipboard_history_text_item_view.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/scoped_light_mode_as_default.h"
#include "base/auto_reset.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/layout/fill_layout.h"
namespace {
using Action = ash::ClipboardHistoryUtil::Action;
// The insets within the contents view.
constexpr gfx::Insets kContentsInsets(/*vertical=*/4, /*horizontal=*/16);
// The size of the `DeleteButton`.
constexpr int kDeleteButtonSizeDip = 16;
// The menu background's color type.
constexpr ash::AshColorProvider::BaseLayerType kMenuBackgroundColorType =
ash::AshColorProvider::BaseLayerType::kOpaque;
} // namespace
namespace ash {
ClipboardHistoryItemView::ContentsView::ContentsView(
ClipboardHistoryItemView* container)
: container_(container) {
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
SetBorder(views::CreateEmptyBorder(kContentsInsets));
}
ClipboardHistoryItemView::ContentsView::~ContentsView() = default;
void ClipboardHistoryItemView::ContentsView::InstallDeleteButton() {
delete_button_ = CreateDeleteButton();
}
const char* ClipboardHistoryItemView::ContentsView::GetClassName() const {
return "ContenstView";
}
// Accepts the event only when |delete_button_| should be the handler.
bool ClipboardHistoryItemView::ContentsView::DoesIntersectRect(
const views::View* target,
const gfx::Rect& rect) const {
if (!delete_button_->GetVisible())
return false;
gfx::RectF rect_in_delete_button(rect);
ConvertRectToTarget(this, delete_button_, &rect_in_delete_button);
return delete_button_->HitTestRect(
gfx::ToEnclosedRect(rect_in_delete_button));
}
// The view responding to mouse click or gesture tap events.
class ash::ClipboardHistoryItemView::MainButton : public views::Button {
public:
explicit MainButton(ClipboardHistoryItemView* container)
: Button(), container_(container) {
SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
// Let the parent handle accessibility features.
GetViewAccessibility().OverrideIsIgnored(/*value=*/true);
}
MainButton(const MainButton& rhs) = delete;
MainButton& operator=(const MainButton& rhs) = delete;
~MainButton() override = default;
private:
// views::Button:
void OnThemeChanged() override {
views::Button::OnThemeChanged();
SchedulePaint();
}
const char* GetClassName() const override { return "MainButton"; }
void OnGestureEvent(ui::GestureEvent* event) override {
// Give `container_` a chance to handle `event`.
container_->MaybeHandleGestureEventFromMainButton(event);
if (event->handled())
return;
views::Button::OnGestureEvent(event);
// Prevent the menu controller from handling gesture events. The menu
// controller may bring side-effects such as canceling the item selection.
event->SetHandled();
}
void PaintButtonContents(gfx::Canvas* canvas) override {
if (!container_->ShouldHighlight())
return;
// Use the light mode as default because the light mode is the default mode
// of the native theme which decides the context menu's background color.
// TODO(andrewxu): remove this line after https://crbug.com/1143009 is
// fixed.
ScopedLightModeAsDefault scoped_light_mode_as_default;
// Highlight the background when the menu item is selected or pressed.
cc::PaintFlags flags;
flags.setAntiAlias(true);
const auto* color_provider = AshColorProvider::Get();
const AshColorProvider::RippleAttributes ripple_attributes =
color_provider->GetRippleAttributes(
color_provider->GetBaseLayerColor(kMenuBackgroundColorType));
flags.setColor(SkColorSetA(ripple_attributes.base_color,
ripple_attributes.highlight_opacity * 0xFF));
flags.setStyle(cc::PaintFlags::kFill_Style);
canvas->DrawRect(GetLocalBounds(), flags);
}
// The parent view.
ash::ClipboardHistoryItemView* const container_;
};
ClipboardHistoryItemView::DeleteButton::DeleteButton(
ClipboardHistoryItemView* listener)
: views::ImageButton(base::BindRepeating(
[](ClipboardHistoryItemView* item_view, const ui::Event& event) {
item_view->Activate(Action::kDelete, event.flags());
},
base::Unretained(listener))) {
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
SetAccessibleName(base::ASCIIToUTF16(std::string(GetClassName())));
SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
SetPreferredSize(gfx::Size(kDeleteButtonSizeDip, kDeleteButtonSizeDip));
}
ClipboardHistoryItemView::DeleteButton::~DeleteButton() = default;
const char* ClipboardHistoryItemView::DeleteButton::GetClassName() const {
return "DeleteButton";
}
void ClipboardHistoryItemView::DeleteButton::OnThemeChanged() {
// Use the light mode as default because the light mode is the default mode of
// the native theme which decides the context menu's background color.
// TODO(andrewxu): remove this line after https://crbug.com/1143009 is fixed.
ScopedLightModeAsDefault scoped_light_mode_as_default;
views::ImageButton::OnThemeChanged();
AshColorProvider::Get()->DecorateCloseButton(this, kDeleteButtonSizeDip,
kCloseButtonIcon);
}
// static
std::unique_ptr<ClipboardHistoryItemView>
ClipboardHistoryItemView::CreateFromClipboardHistoryItem(
const ClipboardHistoryItem& item,
const ClipboardHistoryResourceManager* resource_manager,
views::MenuItemView* container) {
const auto display_format =
ClipboardHistoryUtil::CalculateDisplayFormat(item.data());
UMA_HISTOGRAM_ENUMERATION(
"Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", display_format);
switch (display_format) {
case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kText:
return std::make_unique<ClipboardHistoryTextItemView>(&item, container);
case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kBitmap:
case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kHtml:
return std::make_unique<ClipboardHistoryBitmapItemView>(
&item, resource_manager, container);
}
}
ClipboardHistoryItemView::~ClipboardHistoryItemView() = default;
ClipboardHistoryItemView::ClipboardHistoryItemView(
const ClipboardHistoryItem* clipboard_history_item,
views::MenuItemView* container)
: clipboard_history_item_(clipboard_history_item), container_(container) {}
void ClipboardHistoryItemView::Init() {
SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
GetViewAccessibility().OverrideRole(ax::mojom::Role::kMenuItem);
SetLayoutManager(std::make_unique<views::FillLayout>());
// Ensures that MainButton is below any other child views.
main_button_ = AddChildView(std::make_unique<MainButton>(this));
main_button_->SetCallback(base::BindRepeating(
[](ClipboardHistoryItemView* item, const ui::Event& event) {
// Note that the callback may be triggered through the ENTER key when
// the delete button is under the pseudo focus. Because the delete
// button is not hot-tracked by the menu controller. Meanwhile, the menu
// controller always sends the key event to the hot-tracked view.
// TODO(https://crbug.com/1144994): Modify this part after the clipboard
// history menu code is refactored.
// When an item view is under gesture tap, it may be not under pseudo
// focus yet.
if (event.type() == ui::ET_GESTURE_TAP)
item->pseudo_focus_ = PseudoFocus::kMainButton;
item->Activate(item->CalculateActionForMainButtonClick(),
event.flags());
},
base::Unretained(this)));
contents_view_ = AddChildView(CreateContentsView());
subscription_ = container_->AddSelectedChangedCallback(base::BindRepeating(
&ClipboardHistoryItemView::OnSelectionChanged, base::Unretained(this)));
}
void ClipboardHistoryItemView::OnSelectionChanged() {
if (!container_->IsSelected()) {
SetPseudoFocus(PseudoFocus::kEmpty);
return;
}
// If the pseudo focus is moved from another item view via focus traversal,
// `pseudo_focus_` is already up to date.
if (pseudo_focus_ != PseudoFocus::kEmpty)
return;
InitiatePseudoFocus(/*reverse=*/false);
}
bool ClipboardHistoryItemView::AdvancePseudoFocus(bool reverse) {
if (pseudo_focus_ == PseudoFocus::kEmpty) {
InitiatePseudoFocus(reverse);
return true;
}
// When the menu item is disabled, only the delete button is able to work.
if (!container_->GetEnabled()) {
DCHECK_EQ(PseudoFocus::kDeleteButton, pseudo_focus_);
SetPseudoFocus(PseudoFocus::kEmpty);
return false;
}
DCHECK(pseudo_focus_ == PseudoFocus::kMainButton ||
pseudo_focus_ == PseudoFocus::kDeleteButton);
int new_pseudo_focus = pseudo_focus_;
bool move_focus_out = false;
if (reverse) {
--new_pseudo_focus;
if (new_pseudo_focus == PseudoFocus::kEmpty)
move_focus_out = true;
} else {
++new_pseudo_focus;
if (new_pseudo_focus == PseudoFocus::kMaxValue)
move_focus_out = true;
}
if (move_focus_out) {
SetPseudoFocus(PseudoFocus::kEmpty);
return false;
}
SetPseudoFocus(static_cast<PseudoFocus>(new_pseudo_focus));
return true;
}
void ClipboardHistoryItemView::RecordButtonPressedHistogram() const {
switch (action_) {
case Action::kDelete:
ClipboardHistoryUtil::RecordClipboardHistoryItemDeleted(
*clipboard_history_item_);
return;
case Action::kPaste:
ClipboardHistoryUtil::RecordClipboardHistoryItemPasted(
*clipboard_history_item_);
return;
case Action::kSelect:
return;
case Action::kEmpty:
NOTREACHED();
return;
}
}
bool ClipboardHistoryItemView::IsItemEnabled() const {
return container_->GetEnabled();
}
gfx::Size ClipboardHistoryItemView::CalculatePreferredSize() const {
const int preferred_width =
views::MenuConfig::instance().touchable_menu_width;
return gfx::Size(preferred_width, GetHeightForWidth(preferred_width));
}
void ClipboardHistoryItemView::GetAccessibleNodeData(ui::AXNodeData* data) {
data->SetName(GetAccessibleName());
}
void ClipboardHistoryItemView::Activate(Action action, int event_flags) {
DCHECK(Action::kEmpty == action_ && action_ != action);
base::AutoReset<Action> action_to_take(&action_, action);
RecordButtonPressedHistogram();
views::MenuDelegate* delegate = container_->GetDelegate();
const int command_id = container_->GetCommand();
DCHECK(delegate->IsCommandEnabled(command_id));
delegate->ExecuteCommand(command_id, event_flags);
}
Action ClipboardHistoryItemView::CalculateActionForMainButtonClick() const {
// `main_button_` may be clicked when the delete button is under the pseudo
// focus. It happens when a user presses the ENTER key. Note that the menu
// controller sends the accelerator to the hot-tracked view and `main_button_`
// is hot-tracked when the delete button is under the pseudo focus. The menu
// controller should not hot-track the delete button. Otherwise, pressing the
// up/down arrow key will select a delete button instead of a neighboring
// menu item.
switch (pseudo_focus_) {
case PseudoFocus::kMainButton:
return Action::kPaste;
case PseudoFocus::kDeleteButton:
return Action::kDelete;
case PseudoFocus::kEmpty:
case PseudoFocus::kMaxValue:
NOTREACHED();
return Action::kEmpty;
}
}
bool ClipboardHistoryItemView::ShouldHighlight() const {
return pseudo_focus_ == PseudoFocus::kMainButton && IsItemEnabled();
}
bool ClipboardHistoryItemView::ShouldShowDeleteButton() const {
return (pseudo_focus_ == PseudoFocus::kMainButton && IsMouseHovered()) ||
pseudo_focus_ == PseudoFocus::kDeleteButton ||
under_gesture_long_press_;
}
void ClipboardHistoryItemView::InitiatePseudoFocus(bool reverse) {
PseudoFocus target_pseudo_focus;
if (!container_->GetEnabled() || reverse)
target_pseudo_focus = PseudoFocus::kDeleteButton;
else
target_pseudo_focus = PseudoFocus::kMainButton;
SetPseudoFocus(target_pseudo_focus);
}
void ClipboardHistoryItemView::SetPseudoFocus(PseudoFocus new_pseudo_focus) {
if (pseudo_focus_ == new_pseudo_focus)
return;
pseudo_focus_ = new_pseudo_focus;
contents_view_->delete_button()->SetVisible(ShouldShowDeleteButton());
main_button_->SchedulePaint();
switch (pseudo_focus_) {
case PseudoFocus::kEmpty:
break;
case PseudoFocus::kMainButton:
NotifyAccessibilityEvent(ax::mojom::Event::kSelection,
/*send_native_event=*/true);
break;
case PseudoFocus::kDeleteButton:
contents_view_->delete_button()->NotifyAccessibilityEvent(
ax::mojom::Event::kHover, /*send_native_event*/ true);
break;
case PseudoFocus::kMaxValue:
NOTREACHED();
break;
}
}
void ClipboardHistoryItemView::MaybeHandleGestureEventFromMainButton(
ui::GestureEvent* event) {
// `event` is always handled here if the menu item view is under the gesture
// long press. It prevents other event handlers from introducing side effects.
// For example, if `main_button_` handles the ui::ET_GESTURE_END event,
// `main_button_`'s state will be reset. However, `main_button_` is expected
// to be at the "hovered" state when the menu item is selected.
if (under_gesture_long_press_) {
DCHECK_NE(ui::ET_GESTURE_LONG_PRESS, event->type());
if (event->type() == ui::ET_GESTURE_END)
under_gesture_long_press_ = false;
event->SetHandled();
return;
}
if (event->type() == ui::ET_GESTURE_LONG_PRESS) {
under_gesture_long_press_ = true;
switch (pseudo_focus_) {
case PseudoFocus::kEmpty:
// Select the menu item if it is not selected yet.
Activate(Action::kSelect, event->flags());
break;
case PseudoFocus::kMainButton: {
// The menu item is already selected so show the delete button if the
// button is hidden.
views::View* delete_button = contents_view_->delete_button();
if (!delete_button->GetVisible())
delete_button->SetVisible(true);
break;
}
case PseudoFocus::kDeleteButton:
// The delete button already shows, so do nothing.
DCHECK(contents_view_->delete_button()->GetVisible());
break;
case PseudoFocus::kMaxValue:
NOTREACHED();
break;
}
event->SetHandled();
}
}
} // namespace ash