| // Copyright 2019 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 "chrome/browser/ui/views/tabs/color_picker_view.h" |
| |
| #include <memory> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/containers/span.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/tabs/tab_group_theme.h" |
| #include "chrome/browser/ui/views/chrome_layout_provider.h" |
| #include "components/tab_groups/tab_group_color.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/metadata/metadata_header_macros.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/pointer/touch_ui_controller.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/highlight_path_generator.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/layout/flex_layout_types.h" |
| #include "ui/views/view_class_properties.h" |
| |
| namespace { |
| |
| class ColorPickerHighlightPathGenerator : public views::HighlightPathGenerator { |
| public: |
| ColorPickerHighlightPathGenerator() = default; |
| ColorPickerHighlightPathGenerator(const ColorPickerHighlightPathGenerator&) = |
| delete; |
| ColorPickerHighlightPathGenerator& operator=( |
| const ColorPickerHighlightPathGenerator&) = delete; |
| |
| // views::HighlightPathGenerator: |
| SkPath GetHighlightPath(const views::View* view) override { |
| // Our highlight path should be slightly larger than the circle we paint. |
| gfx::RectF bounds(view->GetContentsBounds()); |
| bounds.Inset(gfx::Insets(-2.0f)); |
| const gfx::PointF center = bounds.CenterPoint(); |
| return SkPath().addCircle(center.x(), center.y(), bounds.width() / 2.0f); |
| } |
| }; |
| |
| } // namespace |
| |
| // Represents one of the colors the user can pick from. Displayed as a solid |
| // circle of the given color. |
| class ColorPickerElementView : public views::Button { |
| public: |
| METADATA_HEADER(ColorPickerElementView); |
| |
| ColorPickerElementView( |
| base::RepeatingCallback<void(ColorPickerElementView*)> selected_callback, |
| const views::BubbleDialogDelegateView* bubble_view, |
| tab_groups::TabGroupColorId color_id, |
| std::u16string color_name) |
| : Button(base::BindRepeating(&ColorPickerElementView::ButtonPressed, |
| base::Unretained(this))), |
| selected_callback_(std::move(selected_callback)), |
| bubble_view_(bubble_view), |
| color_id_(color_id), |
| color_name_(color_name) { |
| DCHECK(selected_callback_); |
| |
| SetAccessibleName(color_name); |
| SetInstallFocusRingOnFocus(true); |
| views::HighlightPathGenerator::Install( |
| this, std::make_unique<ColorPickerHighlightPathGenerator>()); |
| |
| // When calculating padding, halve the value because color elements are |
| // displayed side-by-side and each contribute half the spacing between them. |
| const int padding = ChromeLayoutProvider::Get()->GetDistanceMetric( |
| views::DISTANCE_RELATED_BUTTON_HORIZONTAL) / |
| 2; |
| // The padding of the color element circle is adaptive, to improve the hit |
| // target size on touch devices. |
| gfx::Insets insets = ui::TouchUiController::Get()->touch_ui() |
| ? gfx::Insets(padding * 2) |
| : gfx::Insets(padding); |
| SetBorder(views::CreateEmptyBorder(insets)); |
| |
| views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::OFF); |
| SetAnimateOnStateChange(true); |
| } |
| |
| void SetSelected(bool selected) { |
| if (selected_ == selected) |
| return; |
| selected_ = selected; |
| SchedulePaint(); |
| } |
| |
| bool GetSelected() const { return selected_; } |
| |
| // views::Button: |
| bool IsGroupFocusTraversable() const override { |
| // Tab should only focus the selected element. |
| return false; |
| } |
| |
| views::View* GetSelectedViewForGroup(int group) override { |
| DCHECK(parent()); |
| return parent()->GetSelectedViewForGroup(group); |
| } |
| |
| void GetAccessibleNodeData(ui::AXNodeData* node_data) override { |
| views::Button::GetAccessibleNodeData(node_data); |
| node_data->role = ax::mojom::Role::kRadioButton; |
| node_data->SetCheckedState(GetSelected() ? ax::mojom::CheckedState::kTrue |
| : ax::mojom::CheckedState::kFalse); |
| } |
| |
| std::u16string GetTooltipText(const gfx::Point& p) const override { |
| return color_name_; |
| } |
| |
| gfx::Size CalculatePreferredSize() const override { |
| const gfx::Insets insets = GetInsets(); |
| // The size of the color element circle is adaptive, to improve the hit |
| // target size on touch devices. |
| const int circle_size = ui::TouchUiController::Get()->touch_ui() |
| ? 3 * gfx::kFaviconSize / 2 |
| : gfx::kFaviconSize; |
| gfx::Size size(circle_size, circle_size); |
| size.Enlarge(insets.width(), insets.height()); |
| return size; |
| } |
| |
| int GetHeightForWidth(int width) const override { return width; } |
| |
| void PaintButtonContents(gfx::Canvas* canvas) override { |
| // Paint a colored circle surrounded by a bit of empty space. |
| gfx::RectF bounds(GetContentsBounds()); |
| |
| // We should be a circle. |
| DCHECK_EQ(bounds.width(), bounds.height()); |
| |
| const SkColor color = |
| GetThemeProvider()->GetColor(GetTabGroupDialogColorId(color_id_)); |
| |
| cc::PaintFlags flags; |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| flags.setColor(color); |
| flags.setAntiAlias(true); |
| canvas->DrawCircle(bounds.CenterPoint(), bounds.width() / 2.0f, flags); |
| |
| PaintSelectionIndicator(canvas); |
| } |
| |
| private: |
| // Paints a ring in our color circle to indicate selection or mouse hover. |
| // Does nothing if not selected or hovered. |
| void PaintSelectionIndicator(gfx::Canvas* canvas) { |
| if (!selected_) { |
| return; |
| } |
| |
| // Visual parameters of our ring. |
| constexpr float kInset = 3.0f; |
| constexpr float kThickness = 2.0f; |
| cc::PaintFlags flags; |
| flags.setStyle(cc::PaintFlags::kStroke_Style); |
| flags.setStrokeWidth(kThickness); |
| flags.setAntiAlias(true); |
| flags.setColor(bubble_view_->color()); |
| |
| gfx::RectF indicator_bounds(GetContentsBounds()); |
| indicator_bounds.Inset(gfx::InsetsF(kInset)); |
| DCHECK(!indicator_bounds.size().IsEmpty()); |
| canvas->DrawCircle(indicator_bounds.CenterPoint(), |
| indicator_bounds.width() / 2.0f, flags); |
| } |
| |
| void ButtonPressed() { |
| // Pressing this a second time shouldn't do anything. |
| if (!selected_) { |
| selected_ = true; |
| SchedulePaint(); |
| selected_callback_.Run(this); |
| } |
| } |
| |
| const base::RepeatingCallback<void(ColorPickerElementView*)> |
| selected_callback_; |
| const views::BubbleDialogDelegateView* bubble_view_; |
| const tab_groups::TabGroupColorId color_id_; |
| const std::u16string color_name_; |
| bool selected_ = false; |
| }; |
| |
| BEGIN_METADATA(ColorPickerElementView, views::Button) |
| ADD_PROPERTY_METADATA(bool, Selected) |
| END_METADATA |
| |
| ColorPickerView::ColorPickerView( |
| const views::BubbleDialogDelegateView* bubble_view, |
| const TabGroupEditorBubbleView::Colors& colors, |
| tab_groups::TabGroupColorId initial_color_id, |
| ColorSelectedCallback callback) |
| : callback_(std::move(callback)) { |
| DCHECK(!colors.empty()); |
| |
| elements_.reserve(colors.size()); |
| for (const auto& color : colors) { |
| // Create the views for each color, passing them our callback and saving |
| // references to them. base::Unretained() is safe here since we delete these |
| // views in our destructor, ensuring we outlive them. |
| elements_.push_back(AddChildView(std::make_unique<ColorPickerElementView>( |
| base::BindRepeating(&ColorPickerView::OnColorSelected, |
| base::Unretained(this)), |
| bubble_view, color.first, color.second))); |
| if (initial_color_id == color.first) |
| elements_.back()->SetSelected(true); |
| } |
| |
| // Set the internal padding to be equal to the horizontal insets of a color |
| // picker element, since that is the amount by which the color picker's |
| // margins should be adjusted to make it visually align with other controls. |
| gfx::Insets child_insets = elements_[0]->GetInsets(); |
| SetProperty(views::kInternalPaddingKey, |
| gfx::Insets(0, child_insets.left(), 0, child_insets.right())); |
| |
| // Our children should take keyboard focus, not us. |
| SetFocusBehavior(views::View::FocusBehavior::NEVER); |
| for (View* view : elements_) { |
| // Group the colors so they can be navigated with arrows. |
| view->SetGroup(0); |
| } |
| |
| auto* layout = SetLayoutManager(std::make_unique<views::FlexLayout>()); |
| layout->SetOrientation(views::LayoutOrientation::kHorizontal) |
| .SetDefault( |
| views::kFlexBehaviorKey, |
| views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred, |
| views::MaximumFlexSizeRule::kUnbounded) |
| .WithAlignment(views::LayoutAlignment::kCenter) |
| .WithWeight(1)); |
| } |
| |
| ColorPickerView::~ColorPickerView() { |
| // Remove child views early since they have references to us through a |
| // callback. |
| RemoveAllChildViews(true); |
| } |
| |
| absl::optional<int> ColorPickerView::GetSelectedElement() const { |
| for (size_t i = 0; i < elements_.size(); ++i) { |
| if (elements_[i]->GetSelected()) |
| return static_cast<int>(i); |
| } |
| return absl::nullopt; |
| } |
| |
| views::View* ColorPickerView::GetSelectedViewForGroup(int group) { |
| for (ColorPickerElementView* element : elements_) { |
| if (element->GetSelected()) |
| return element; |
| } |
| return nullptr; |
| } |
| |
| views::Button* ColorPickerView::GetElementAtIndexForTesting(int index) { |
| DCHECK_GE(index, 0); |
| DCHECK_LT(index, static_cast<int>(elements_.size())); |
| return elements_[index]; |
| } |
| |
| void ColorPickerView::OnColorSelected(ColorPickerElementView* element) { |
| // Unselect all other elements so that only one can be selected at a time. |
| for (ColorPickerElementView* other_element : elements_) { |
| if (other_element != element) |
| other_element->SetSelected(false); |
| } |
| |
| if (callback_) |
| callback_.Run(); |
| } |
| |
| BEGIN_METADATA(ColorPickerView, views::View) |
| ADD_READONLY_PROPERTY_METADATA(absl::optional<int>, SelectedElement) |
| END_METADATA |