blob: fc685e89c13b1659166505eab6d7afc2cb181f89 [file] [log] [blame]
// Copyright 2021 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/capture_mode/capture_mode_menu_group.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/style_util.h"
#include "base/containers/cxx20_erase_vector.h"
#include "base/ranges/algorithm.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/button.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/layout/box_layout.h"
namespace ash {
namespace {
constexpr auto kMenuGroupPadding = gfx::Insets::VH(8, 0);
constexpr auto kMenuHeaderPadding = gfx::Insets::VH(8, 16);
constexpr auto kOptionPadding = gfx::Insets::TLBR(8, 52, 8, 16);
constexpr auto kMenuItemPadding = gfx::Insets::TLBR(10, 52, 10, 16);
constexpr int kSpaceBetweenMenuItem = 0;
constexpr gfx::Size kIconSize{20, 20};
void ConfigLabelView(views::Label* label_view) {
label_view->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorPrimary));
label_view->SetBackgroundColor(SK_ColorTRANSPARENT);
label_view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
label_view->SetVerticalAlignment(gfx::VerticalAlignment::ALIGN_MIDDLE);
}
views::BoxLayout* CreateAndInitBoxLayoutForView(views::View* view) {
auto* box_layout = view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
capture_mode::kBetweenChildSpacing));
box_layout->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
return box_layout;
}
void SetInkDropForButton(views::Button* button) {
StyleUtil::SetUpInkDropForButton(button, gfx::Insets(),
/*highlight_on_hover=*/false,
/*highlight_on_focus=*/false);
views::InstallRectHighlightPathGenerator(button);
}
} // namespace
// -----------------------------------------------------------------------------
// CaptureModeMenuHeader:
// The header of the menu group, which has an icon and a text label. Not user
// interactable.
class CaptureModeMenuHeader
: public views::View,
public CaptureModeSessionFocusCycler::HighlightableView {
public:
METADATA_HEADER(CaptureModeMenuHeader);
CaptureModeMenuHeader(const gfx::VectorIcon& icon,
std::u16string header_laber,
bool managed_by_policy)
: icon_view_(AddChildView(std::make_unique<views::ImageView>())),
label_view_(AddChildView(
std::make_unique<views::Label>(std::move(header_laber)))),
managed_icon_view_(
managed_by_policy
? AddChildView(std::make_unique<views::ImageView>())
: nullptr) {
icon_view_->SetImageSize(kIconSize);
icon_view_->SetPreferredSize(kIconSize);
const auto icon_color = AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kButtonIconColor);
icon_view_->SetImage(gfx::CreateVectorIcon(icon, icon_color));
if (managed_icon_view_) {
managed_icon_view_->SetImageSize(kIconSize);
managed_icon_view_->SetPreferredSize(kIconSize);
managed_icon_view_->SetImage(
gfx::CreateVectorIcon(kCaptureModeManagedIcon, icon_color));
managed_icon_view_->SetTooltipText(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_MANAGED_BY_POLICY));
}
SetBorder(views::CreateEmptyBorder(kMenuHeaderPadding));
ConfigLabelView(label_view_);
auto* box_layout = CreateAndInitBoxLayoutForView(this);
box_layout->SetFlexForView(label_view_, 1);
}
CaptureModeMenuHeader(const CaptureModeMenuHeader&) = delete;
CaptureModeMenuHeader& operator=(const CaptureModeMenuHeader&) = delete;
~CaptureModeMenuHeader() override = default;
const std::u16string& GetHeaderLabel() const {
return label_view_->GetText();
}
// views::View:
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
View::GetAccessibleNodeData(node_data);
node_data->SetName(GetHeaderLabel());
node_data->role = ax::mojom::Role::kHeader;
}
// CaptureModeSessionFocusCycler::HighlightableView:
views::View* GetView() override { return this; }
private:
views::ImageView* icon_view_;
views::Label* label_view_;
// `nullptr` if the menu group is not for a setting that is managed by a
// policy.
views::ImageView* managed_icon_view_;
};
BEGIN_METADATA(CaptureModeMenuHeader, views::View)
END_METADATA
// -----------------------------------------------------------------------------
// CaptureModeMenuItem:
// A button which has a text label. Its behavior on click can be customized.
// For selecting folder, a folder window will be opened on click.
class CaptureModeMenuItem
: public views::Button,
public CaptureModeSessionFocusCycler::HighlightableView {
public:
METADATA_HEADER(CaptureModeMenuItem);
CaptureModeMenuItem(views::Button::PressedCallback callback,
std::u16string item_label)
: views::Button(callback),
label_view_(AddChildView(
std::make_unique<views::Label>(std::move(item_label)))) {
SetBorder(views::CreateEmptyBorder(kMenuItemPadding));
ConfigLabelView(label_view_);
CreateAndInitBoxLayoutForView(this);
SetInkDropForButton(this);
GetViewAccessibility().OverrideIsLeaf(true);
SetAccessibleName(label_view_->GetText());
}
CaptureModeMenuItem(const CaptureModeMenuItem&) = delete;
CaptureModeMenuItem& operator=(const CaptureModeMenuItem&) = delete;
~CaptureModeMenuItem() override = default;
// CaptureModeSessionFocusCycler::HighlightableView:
views::View* GetView() override { return this; }
private:
views::Label* label_view_;
};
BEGIN_METADATA(CaptureModeMenuItem, views::Button)
END_METADATA
// -----------------------------------------------------------------------------
// CaptureModeOption:
// A button which represents an option of the menu group. It has a text label
// and a checked icon. The checked icon will be shown on button click and any
// other option's checked icon will be set to invisible in the meanwhile. One
// and only one checked icon is visible in the menu group.
class CaptureModeOption
: public views::Button,
public CaptureModeSessionFocusCycler::HighlightableView {
public:
METADATA_HEADER(CaptureModeOption);
CaptureModeOption(views::Button::PressedCallback callback,
std::u16string option_label,
int option_id,
bool checked,
bool enabled)
: views::Button(callback),
label_view_(AddChildView(
std::make_unique<views::Label>(std::move(option_label)))),
checked_icon_view_(AddChildView(std::make_unique<views::ImageView>())),
id_(option_id) {
checked_icon_view_->SetImageSize(kIconSize);
checked_icon_view_->SetPreferredSize(kIconSize);
SetBorder(views::CreateEmptyBorder(kOptionPadding));
ConfigLabelView(label_view_);
auto* box_layout = CreateAndInitBoxLayoutForView(this);
box_layout->SetFlexForView(label_view_, 1);
SetInkDropForButton(this);
GetViewAccessibility().OverrideIsLeaf(true);
SetAccessibleName(GetOptionLabel());
checked_icon_view_->SetVisible(checked);
// Calling `SetEnabled()` will result in calling `UpdateState()` only when
// the state changes, but by default the view's state is enabled, so we only
// need to call `UpdateState()` explicitly if `enabled` is true.
if (enabled)
UpdateState();
else
SetEnabled(false);
}
CaptureModeOption(const CaptureModeOption&) = delete;
CaptureModeOption& operator=(const CaptureModeOption&) = delete;
~CaptureModeOption() override = default;
int id() const { return id_; }
const std::u16string& GetOptionLabel() const {
return label_view_->GetText();
}
void SetOptionLabel(std::u16string option_label) {
SetAccessibleName(option_label);
label_view_->SetText(std::move(option_label));
}
void SetOptionChecked(bool checked) {
checked_icon_view_->SetVisible(checked);
}
bool IsOptionChecked() { return checked_icon_view_->GetVisible(); }
// views::Button:
void StateChanged(ButtonState old_state) override { UpdateState(); }
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
Button::GetAccessibleNodeData(node_data);
node_data->SetName(GetOptionLabel());
node_data->role = ax::mojom::Role::kRadioButton;
node_data->SetCheckedState(IsOptionChecked()
? ax::mojom::CheckedState::kTrue
: ax::mojom::CheckedState::kFalse);
}
// CaptureModeSessionFocusCycler::HighlightableView:
views::View* GetView() override { return this; }
private:
// Dims out the label and the checked icon if this view is disabled.
void UpdateState() {
auto* provider = AshColorProvider::Get();
const auto label_enabled_color = provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorPrimary);
const auto icon_enabled_color = provider->GetContentLayerColor(
AshColorProvider::ContentLayerType::kButtonLabelColorBlue);
const bool is_disabled = GetState() == STATE_DISABLED;
label_view_->SetEnabledColor(
is_disabled ? provider->GetDisabledColor(label_enabled_color)
: label_enabled_color);
checked_icon_view_->SetImage(gfx::CreateVectorIcon(
kHollowCheckCircleIcon,
is_disabled ? provider->GetDisabledColor(icon_enabled_color)
: icon_enabled_color));
}
views::Label* label_view_;
views::ImageView* checked_icon_view_;
const int id_;
};
BEGIN_METADATA(CaptureModeOption, views::Button)
END_METADATA
// -----------------------------------------------------------------------------
// CaptureModeMenuGroup:
CaptureModeMenuGroup::CaptureModeMenuGroup(Delegate* delegate,
const gfx::VectorIcon& header_icon,
std::u16string header_label,
bool managed_by_policy)
: delegate_(delegate),
menu_header_(AddChildView(
std::make_unique<CaptureModeMenuHeader>(header_icon,
std::move(header_label),
managed_by_policy))) {
options_container_ = AddChildView(std::make_unique<views::View>());
options_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, kMenuGroupPadding,
kSpaceBetweenMenuItem));
}
CaptureModeMenuGroup::~CaptureModeMenuGroup() = default;
void CaptureModeMenuGroup::AddOption(std::u16string option_label,
int option_id) {
options_.push_back(
options_container_->AddChildView(std::make_unique<CaptureModeOption>(
base::BindRepeating(&CaptureModeMenuGroup::HandleOptionClick,
base::Unretained(this), option_id),
std::move(option_label), option_id,
/*checked=*/delegate_->IsOptionChecked(option_id),
/*enabled=*/delegate_->IsOptionEnabled(option_id))));
}
void CaptureModeMenuGroup::DeleteOptions() {
for (CaptureModeOption* option : options_)
options_container_->RemoveChildViewT(option);
options_.clear();
}
void CaptureModeMenuGroup::AddOrUpdateExistingOption(
std::u16string option_label,
int option_id) {
auto* option = GetOptionById(option_id);
if (option) {
option->SetOptionLabel(std::move(option_label));
return;
}
AddOption(std::move(option_label), option_id);
}
void CaptureModeMenuGroup::RefreshOptionsSelections() {
for (auto* option : options_) {
option->SetOptionChecked(delegate_->IsOptionChecked(option->id()));
option->SetEnabled(delegate_->IsOptionEnabled(option->id()));
}
}
void CaptureModeMenuGroup::RemoveOptionIfAny(int option_id) {
auto* option = GetOptionById(option_id);
if (!option)
return;
options_container_->RemoveChildViewT(option);
base::Erase(options_, option);
}
void CaptureModeMenuGroup::AddMenuItem(views::Button::PressedCallback callback,
std::u16string item_label) {
menu_items_.push_back(views::View::AddChildView(
std::make_unique<CaptureModeMenuItem>(callback, std::move(item_label))));
}
bool CaptureModeMenuGroup::IsOptionChecked(int option_id) const {
auto* option = GetOptionById(option_id);
return option && option->IsOptionChecked();
}
bool CaptureModeMenuGroup::IsOptionEnabled(int option_id) const {
auto* option = GetOptionById(option_id);
return option && option->GetEnabled();
}
void CaptureModeMenuGroup::AppendHighlightableItems(
std::vector<CaptureModeSessionFocusCycler::HighlightableView*>&
highlightable_items) {
// The camera menu group can be hidden if there are no cameras connected. In
// this case no items in this group should be highlightable.
if (!GetVisible())
return;
highlightable_items.push_back(menu_header_);
for (auto* option : options_) {
if (option->GetEnabled())
highlightable_items.push_back(option);
}
for (auto* menu_item : menu_items_)
highlightable_items.push_back(menu_item);
}
views::View* CaptureModeMenuGroup::GetOptionForTesting(int option_id) {
return GetOptionById(option_id);
}
views::View* CaptureModeMenuGroup::GetSelectFolderMenuItemForTesting() {
DCHECK_EQ(1u, menu_items_.size());
return menu_items_[0];
}
std::u16string CaptureModeMenuGroup::GetOptionLabelForTesting(
int option_id) const {
auto* option = GetOptionById(option_id);
DCHECK(option);
return option->GetOptionLabel();
}
CaptureModeOption* CaptureModeMenuGroup::GetOptionById(int option_id) const {
auto iter =
base::ranges::find_if(options_, [option_id](CaptureModeOption* option) {
return option->id() == option_id;
});
return iter == options_.end() ? nullptr : *iter;
}
void CaptureModeMenuGroup::HandleOptionClick(int option_id) {
DCHECK(GetOptionById(option_id));
// The order here matters. We need to tell the delegate first about a change
// in the selection, before we refresh the checked icons, since for that we
// need to query the delegate.
delegate_->OnOptionSelected(option_id);
RefreshOptionsSelections();
}
BEGIN_METADATA(CaptureModeMenuGroup, views::View)
END_METADATA
} // namespace ash