blob: cc12e4ba71dd27202940749635c0cd1175cc0a3a [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ash/arc/input_overlay/ui/action_label.h"
#include <set>
#include "ash/style/style_util.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/ash/arc/input_overlay/actions/action.h"
#include "chrome/browser/ash/arc/input_overlay/constants.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view.h"
#include "chrome/browser/ash/arc/input_overlay/ui/touch_point.h"
#include "chrome/browser/ash/arc/input_overlay/ui/ui_utils.h"
#include "chrome/browser/ash/arc/input_overlay/util.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/view_utils.h"
namespace arc::input_overlay {
namespace {
constexpr char kFontStyle[] = "Google Sans";
constexpr int kIconSize = 20;
// About colors.
constexpr SkColor kBackgroundColorDefault = SK_ColorWHITE;
constexpr SkColor kTextColorDefault = gfx::kGoogleGrey900;
constexpr SkColor kEditedUnboundBgColor = gfx::kGoogleRed300;
// UI specs.
constexpr gfx::Size kLabelSize(22, 22);
constexpr int kCornerRadius = 4;
constexpr int kFontSize = 14;
constexpr int kSideInset = 4;
// For ActionMove.
constexpr int kCrossPadding =
9; // 4 + 4(kCrossOutsideStrokeThickness) + 1(kCrossInsideStrokeThickness)
// TODO(b/241966781): remove this and replace it with image asset.
constexpr char16_t kMouseCursorLock[] = u"mouse cursor lock (esc)";
bool IsLeft(TapLabelPosition position) {
return position == TapLabelPosition::kTopLeft ||
position == TapLabelPosition::kBottomLeft;
}
bool IsRight(TapLabelPosition position) {
return !IsLeft(position) && position != TapLabelPosition::kNone;
}
bool IsTop(TapLabelPosition position) {
return position == TapLabelPosition::kTopLeft ||
position == TapLabelPosition::kTopRight;
}
bool IsBottom(TapLabelPosition position) {
return !IsTop(position) && position != TapLabelPosition::kNone;
}
class ActionLabelTap : public ActionLabel {
METADATA_HEADER(ActionLabelTap, ActionLabel)
public:
ActionLabelTap(MouseAction mouse_action, TapLabelPosition label_position)
: ActionLabel(mouse_action), label_position_(label_position) {
DCHECK(mouse_action == MouseAction::PRIMARY_CLICK ||
mouse_action == MouseAction::SECONDARY_CLICK);
}
ActionLabelTap(const std::u16string& text, TapLabelPosition label_position)
: ActionLabel(text), label_position_(label_position) {}
~ActionLabelTap() override = default;
void UpdateBounds() override {
SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, kSideInset)));
const auto label_size = CalculatePreferredSize({});
SetSize(label_size);
// Label position is not set yet.
if (label_position_ == TapLabelPosition::kNone) {
return;
}
auto* action_view = GetParent();
switch (label_position_) {
case TapLabelPosition::kBottomLeft:
SetPosition(
gfx::Point(0, touch_point_size_.height() + kOffsetToTouchPoint));
action_view->SetTouchPointCenter(
gfx::Point(label_size.width() + touch_point_size_.width() / 2 +
kOffsetToTouchPoint,
touch_point_size_.height() / 2));
break;
case TapLabelPosition::kBottomRight:
SetPosition(
gfx::Point(touch_point_size_.width() + kOffsetToTouchPoint,
touch_point_size_.height() + kOffsetToTouchPoint));
action_view->SetTouchPointCenter(gfx::Point(
touch_point_size_.width() / 2, touch_point_size_.height() / 2));
break;
case TapLabelPosition::kTopLeft:
SetPosition(gfx::Point());
action_view->SetTouchPointCenter(
gfx::Point(label_size.width() + kOffsetToTouchPoint +
touch_point_size_.width() / 2,
label_size.height() + kOffsetToTouchPoint +
touch_point_size_.height() / 2));
break;
case TapLabelPosition::kTopRight:
SetPosition(
gfx::Point(touch_point_size_.width() + kOffsetToTouchPoint, 0));
action_view->SetTouchPointCenter(gfx::Point(
touch_point_size_.width() / 2, label_size.height() +
kOffsetToTouchPoint +
touch_point_size_.height() / 2));
break;
default:
NOTREACHED();
}
}
void UpdateLabelPositionType(TapLabelPosition label_position) override {
if (label_position_ == label_position) {
return;
}
parent()->SetPosition(
CalculateParentPositionWithFixedTouchPoint(label_position));
label_position_ = label_position;
UpdateBounds();
}
private:
gfx::Point CalculateParentPositionWithFixedTouchPoint(
TapLabelPosition label_position) {
DCHECK_NE(label_position_, label_position);
DCHECK_NE(label_position, TapLabelPosition::kNone);
auto* action_view = GetParent();
auto fix_pos = action_view->GetTouchCenterInWindow();
fix_pos.Offset(-touch_point_size_.width() / 2,
-touch_point_size_.height() / 2);
fix_pos.SetToMax(gfx::Point());
auto new_pos = action_view->origin();
const auto& label_size = size();
if (IsLeft(label_position_) && IsRight(label_position)) {
new_pos.set_x(fix_pos.x());
} else if (!IsLeft(label_position_) && IsLeft(label_position)) {
new_pos.set_x(std::max(
0, fix_pos.x() - (label_size.width() + kOffsetToTouchPoint)));
}
if (IsTop(label_position_) && IsBottom(label_position)) {
new_pos.set_y(fix_pos.y());
} else if (!IsTop(label_position_) && IsTop(label_position)) {
new_pos.set_y(std::max(
0, fix_pos.y() - (label_size.height() + kOffsetToTouchPoint)));
}
return new_pos;
}
TapLabelPosition label_position_ = TapLabelPosition::kNone;
};
BEGIN_METADATA(ActionLabelTap)
END_METADATA
class ActionLabelMove : public ActionLabel {
METADATA_HEADER(ActionLabelMove, ActionLabel)
public:
ActionLabelMove(const std::u16string& text, size_t index)
: ActionLabel(text, index) {}
explicit ActionLabelMove(MouseAction mouse) : ActionLabel(mouse) {}
~ActionLabelMove() override = default;
void UpdateBounds() override {
SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, 0)));
auto label_size = CalculatePreferredSize({});
SetSize(label_size);
// TODO(b/241966781): Mouse is not supported yet.
DCHECK_EQ(mouse_action_, MouseAction::NONE);
auto center = touch_point_size_.width() / 2;
int offset_to_center =
touch_point_size_.width() / 2 - kCrossPadding - label_size.height() / 2;
int x = center + kDirection[index_][0] * offset_to_center -
label_size.width() / 2;
int y = center + kDirection[index_][1] * offset_to_center -
label_size.height() / 2;
SetPosition(gfx::Point(x, y));
GetParent()->SetTouchPointCenter(gfx::Point(center, center));
}
void UpdateLabelPositionType(TapLabelPosition label_position) override {}
};
BEGIN_METADATA(ActionLabelMove)
END_METADATA
} // namespace
std::vector<raw_ptr<ActionLabel, VectorExperimental>> ActionLabel::Show(
views::View* parent,
ActionType action_type,
const InputElement& input_element,
TapLabelPosition label_position) {
std::vector<raw_ptr<ActionLabel, VectorExperimental>> labels;
gfx::Size touch_point_size;
switch (action_type) {
case ActionType::TAP:
if (IsKeyboardBound(input_element)) {
DCHECK_EQ(1u, input_element.keys().size());
labels.emplace_back(
parent->AddChildView(std::make_unique<ActionLabelTap>(
GetDisplayText(input_element.keys()[0]), label_position)));
} else if (IsMouseBound(input_element)) {
labels.emplace_back(
parent->AddChildView(std::make_unique<ActionLabelTap>(
input_element.mouse_action(), label_position)));
} else {
labels.emplace_back(parent->AddChildView(
std::make_unique<ActionLabelTap>(kUnknownBind, label_position)));
}
touch_point_size = TouchPoint::GetSize(ActionType::TAP);
break;
case ActionType::MOVE:
if (IsKeyboardBound(input_element)) {
const auto& keys = input_element.keys();
for (size_t i = 0; i < kActionMoveKeysSize; i++) {
labels.emplace_back(parent->AddChildView(
std::make_unique<ActionLabelMove>(GetDisplayText(keys[i]), i)));
}
} else if (IsMouseBound(input_element)) {
labels.emplace_back(parent->AddChildView(
std::make_unique<ActionLabelMove>(kMouseCursorLock, 0)));
NOTIMPLEMENTED();
} else {
for (size_t i = 0; i < kActionMoveKeysSize; i++) {
labels.emplace_back(parent->AddChildView(
std::make_unique<ActionLabelMove>(kUnknownBind, i)));
}
}
touch_point_size = TouchPoint::GetSize(ActionType::MOVE);
break;
default:
NOTREACHED();
}
for (arc::input_overlay::ActionLabel* label : labels) {
label->Init();
label->set_touch_point_size(touch_point_size);
}
return labels;
}
void ActionLabel::Init() {
SetHorizontalAlignment(gfx::ALIGN_CENTER);
SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, kSideInset)));
GetViewAccessibility().SetRole(ax::mojom::Role::kLabelText);
GetViewAccessibility().SetName(CalculateAccessibleName());
}
ActionLabel::ActionLabel(MouseAction mouse_action)
: mouse_action_(mouse_action) {}
ActionLabel::ActionLabel(const std::u16string& text, size_t index)
: views::LabelButton(base::BindRepeating(&ActionLabel::OnButtonPressed,
base::Unretained(this)),
text),
index_(index) {
DCHECK(index_ >= 0 && index_ < kActionMoveKeysSize);
}
ActionLabel::~ActionLabel() = default;
void ActionLabel::SetTextActionLabel(const std::u16string& text) {
label()->SetText(text);
GetViewAccessibility().SetName(CalculateAccessibleName());
if (text == kUnknownBind && !GetParent()->action()->is_new()) {
SetToEditUnbindInput();
} else {
SetToEditDefault();
}
}
void ActionLabel::SetImageActionLabel(MouseAction mouse_action) {
set_mouse_action(mouse_action);
GetViewAccessibility().SetName(CalculateAccessibleName());
}
void ActionLabel::SetDisplayMode(DisplayMode mode) {
switch (mode) {
case DisplayMode::kView:
SetToViewMode();
SetFocusBehavior(FocusBehavior::NEVER);
break;
case DisplayMode::kEdit:
SetToEditDefault();
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
break;
default:
NOTREACHED();
}
}
void ActionLabel::RemoveNewState() {
SetBackgroundForEdit();
}
ActionView* ActionLabel::GetParent() {
auto* view = views::AsViewClass<ActionView>(parent());
DCHECK(view);
return view;
}
gfx::Size ActionLabel::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
auto size = LabelButton::CalculatePreferredSize(available_size);
size.SetToMax(kLabelSize);
return size;
}
void ActionLabel::ChildPreferredSizeChanged(View* child) {
UpdateBounds();
LabelButton::ChildPreferredSizeChanged(this);
}
void ActionLabel::OnButtonPressed() {
GetParent()->ShowButtonOptionsMenu();
}
void ActionLabel::SetToViewMode() {
display_mode_ = DisplayMode::kView;
SetInstallFocusRingOnFocus(false);
label()->SetFontList(gfx::FontList({kFontStyle}, gfx::Font::NORMAL, kFontSize,
gfx::Font::Weight::BOLD));
SetEnabledTextColors(kTextColorDefault);
if (mouse_action_ != MouseAction::NONE) {
if (mouse_action_ == MouseAction::PRIMARY_CLICK) {
auto left_click_icon = ui::ImageModel::FromVectorIcon(
kMouseLeftClickViewIcon, gfx::kPlaceholderColor, kIconSize);
SetImageModel(views::Button::STATE_NORMAL, left_click_icon);
} else {
auto right_click_icon = ui::ImageModel::FromVectorIcon(
kMouseRightClickViewIcon, gfx::kPlaceholderColor, kIconSize);
SetImageModel(views::Button::STATE_NORMAL, right_click_icon);
}
}
SetBackground(views::CreateRoundedRectBackground(kBackgroundColorDefault,
kCornerRadius));
SetPreferredSize(CalculatePreferredSize({}));
}
void ActionLabel::SetToEditDefault() {
label()->SetFontList(gfx::FontList({kFontStyle}, gfx::Font::NORMAL, kFontSize,
gfx::Font::Weight::BOLD));
SetEnabledTextColors(kTextColorDefault);
SetBackgroundForEdit();
}
void ActionLabel::SetToEditUnbindInput() {
SetPreferredSize(CalculatePreferredSize({}));
SetBackground(
views::CreateRoundedRectBackground(kEditedUnboundBgColor, kCornerRadius));
}
void ActionLabel::SetBackgroundForEdit() {
SetBackground(views::CreateRoundedRectBackground(
IsInputUnbound() && !(GetParent()->action()->is_new())
? kEditedUnboundBgColor
: kBackgroundColorDefault,
kCornerRadius));
}
bool ActionLabel::IsInputUnbound() {
return GetText().compare(kUnknownBind) == 0;
}
std::u16string ActionLabel::CalculateAccessibleName() {
if (mouse_action_ != MouseAction::NONE) {
// TODO(accessibility): The accessible name is expected to be end-user
// consumable.
return base::UTF8ToUTF16(GetClassName());
}
return l10n_util::GetStringUTF16(IDS_INPUT_OVERLAY_KEYMAPPING_KEY)
.append(u" ")
.append(GetDisplayTextAccessibleName(std::u16string(label()->GetText())));
}
BEGIN_METADATA(ActionLabel)
END_METADATA
} // namespace arc::input_overlay