blob: e277b0fe2a1515ec26002c782221482bdfe17a1d [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/examples/designer_example.h"
#include <utility>
#include "base/functional/bind.h"
#include "base/ranges/ranges.h"
#include "base/strings/utf_string_conversions.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/metadata/metadata_types.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/events/event.h"
#include "ui/events/event_targeter.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/skia_util.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/button/radio_button.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/combobox/combobox.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/table/table_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/examples/examples_color_id.h"
#include "ui/views/examples/grit/views_examples_resources.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view.h"
#include "ui/views/view_targeter.h"
namespace views::examples {
class DesignerSurface : public View {
METADATA_HEADER(DesignerSurface, View)
public:
explicit DesignerSurface(int grid_size = 8);
DesignerSurface(const DesignerSurface&) = delete;
DesignerSurface& operator=(const DesignerSurface&) = delete;
~DesignerSurface() override = default;
int GetGridSize() const { return grid_size_; }
void SetGridSize(int grid_size);
// View overrides.
void OnPaint(gfx::Canvas* canvas) override;
void OnThemeChanged() override;
private:
void RebuildGridImage();
gfx::ImageSkia grid_image_;
int grid_size_ = 8;
};
DesignerSurface::DesignerSurface(int grid_size) : grid_size_(grid_size) {}
void DesignerSurface::SetGridSize(int grid_size) {
if (grid_size_ != grid_size) {
grid_size_ = grid_size;
if (GetWidget()) {
RebuildGridImage();
}
}
}
void DesignerSurface::OnPaint(gfx::Canvas* canvas) {
views::View::OnPaint(canvas);
canvas->TileImageInt(grid_image_, 0, 0, size().width(), size().height());
}
void DesignerSurface::OnThemeChanged() {
View::OnThemeChanged();
RebuildGridImage();
}
void DesignerSurface::RebuildGridImage() {
const SkColor grid_color =
GetColorProvider()->GetColor(ExamplesColorIds::kColorDesignerGrid);
gfx::Size grid_size = grid_size_ <= 8
? gfx::Size(grid_size_ * 8, grid_size_ * 8)
: gfx::Size(grid_size_, grid_size_);
auto grid_canvas = std::make_unique<gfx::Canvas>(grid_size, 1.0, false);
for (int i = 0; i < grid_size.width(); i += grid_size_) {
for (int j = 0; j < grid_size.height(); j += grid_size_) {
grid_canvas->FillRect(gfx::Rect(i, j, 1, 1), grid_color);
}
}
grid_image_ = gfx::ImageSkia::CreateFrom1xBitmap(grid_canvas->GetBitmap());
}
BEGIN_METADATA(DesignerSurface)
ADD_PROPERTY_METADATA(int, GridSize)
END_METADATA
BEGIN_VIEW_BUILDER(/* no export */, DesignerSurface, View)
VIEW_BUILDER_PROPERTY(int, GridSize)
END_VIEW_BUILDER
} // namespace views::examples
DEFINE_VIEW_BUILDER(/* no export */, views::examples::DesignerSurface)
namespace views::examples {
namespace {
ui::TableColumn MakeColumn(int id, std::u16string title, bool sortable) {
ui::TableColumn column;
column.id = id;
column.title = title;
column.alignment = ui::TableColumn::LEFT;
column.width = -1;
column.percent = 0.5f;
column.sortable = sortable;
return column;
}
int SnapToInterval(int value, int interval) {
return value - (value % interval);
}
template <typename C>
class ClassRegistration : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override { return std::make_unique<C>(); }
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(C::MetaData()->type_name());
}
};
template <>
class ClassRegistration<Combobox> : public BaseClassRegistration,
public ui::ComboboxModel {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
return Builder<Combobox>()
.SetModel(this)
.SetAccessibleName(
l10n_util::GetStringUTF16(IDS_DESIGNER_COMBOBOX_NAME))
.Build();
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(Combobox::MetaData()->type_name());
}
// ui::ComboboxModel
size_t GetItemCount() const override { return 1; }
std::u16string GetItemAt(size_t index) const override { return u"<empty>"; }
std::optional<size_t> GetDefaultIndex() const override { return 0; }
};
template <>
class ClassRegistration<MdTextButton> : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
return Builder<MdTextButton>().SetText(u"Button").Build();
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(MdTextButton::MetaData()->type_name());
}
};
template <>
class ClassRegistration<Textfield> : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
const std::u16string text = u"<text field>";
return Builder<Textfield>()
.SetText(text)
.SetDefaultWidthInChars(text.size())
.SetAccessibleName(
l10n_util::GetStringUTF16(IDS_DESIGNER_TEXTFIELD_NAME))
.Build();
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(Textfield::MetaData()->type_name());
}
};
template <>
class ClassRegistration<Checkbox> : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
return std::make_unique<Checkbox>(u"<Checkbox>");
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(Checkbox::MetaData()->type_name());
}
};
template <>
class ClassRegistration<RadioButton> : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
return std::make_unique<RadioButton>(u"<RadioButton>", 0);
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(RadioButton::MetaData()->type_name());
}
};
template <>
class ClassRegistration<ToggleButton> : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
return Builder<ToggleButton>()
.SetAccessibleName(
l10n_util::GetStringUTF16(IDS_DESIGNER_IMAGEBUTTON_NAME))
.Build();
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(ToggleButton::MetaData()->type_name());
}
};
template <>
class ClassRegistration<ImageButton> : public BaseClassRegistration {
public:
ClassRegistration() = default;
~ClassRegistration() override = default;
std::unique_ptr<View> CreateView() override {
return Builder<ImageButton>()
.SetImageModel(Button::ButtonState::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(kPinIcon, ui::kColorIcon))
.SetAccessibleName(
l10n_util::GetStringUTF16(IDS_DESIGNER_IMAGEBUTTON_NAME))
.Build();
}
std::u16string GetViewClassName() override {
return base::ASCIIToUTF16(ImageButton::MetaData()->type_name());
}
};
std::vector<std::unique_ptr<BaseClassRegistration>> GetClassRegistrations() {
std::vector<std::unique_ptr<BaseClassRegistration>> registrations;
registrations.push_back(std::make_unique<ClassRegistration<Checkbox>>());
registrations.push_back(std::make_unique<ClassRegistration<Combobox>>());
registrations.push_back(std::make_unique<ClassRegistration<MdTextButton>>());
registrations.push_back(std::make_unique<ClassRegistration<RadioButton>>());
registrations.push_back(std::make_unique<ClassRegistration<Textfield>>());
registrations.push_back(std::make_unique<ClassRegistration<ToggleButton>>());
registrations.push_back(std::make_unique<ClassRegistration<ImageButton>>());
return registrations;
}
bool IsViewParent(View* parent, View* view) {
while (view) {
if (view == parent) {
return true;
}
view = view->parent();
}
return false;
}
View* GetDesignerChild(View* child_view, View* designer) {
while (child_view && child_view != designer &&
child_view->parent() != designer) {
child_view = child_view->parent();
}
return child_view;
}
} // namespace
BaseClassRegistration::~BaseClassRegistration() = default;
DesignerExample::GrabHandle::GrabHandle(GrabHandles* grab_handles,
GrabHandlePosition position)
: position_(position), grab_handles_(grab_handles) {}
DesignerExample::GrabHandle::~GrabHandle() = default;
void DesignerExample::GrabHandle::SetAttachedView(View* view) {
bool was_visible = GetVisible() && attached_view_ != view;
attached_view_ = view;
SetVisible(!!attached_view_);
UpdatePosition(!was_visible);
}
void DesignerExample::GrabHandle::UpdatePosition(bool reorder) {
if (GetVisible() && attached_view_) {
PositionOnView();
if (reorder) {
parent()->ReorderChildView(this, parent()->children().size());
}
}
}
ui::Cursor DesignerExample::GrabHandle::GetCursor(const ui::MouseEvent& event) {
switch (position_) {
case GrabHandlePosition::kTop:
case GrabHandlePosition::kBottom:
return ui::mojom::CursorType::kNorthSouthResize;
case GrabHandlePosition::kLeft:
case GrabHandlePosition::kRight:
return ui::mojom::CursorType::kEastWestResize;
case GrabHandlePosition::kTopLeft:
case GrabHandlePosition::kBottomRight:
return ui::mojom::CursorType::kNorthWestSouthEastResize;
case GrabHandlePosition::kTopRight:
case GrabHandlePosition::kBottomLeft:
return ui::mojom::CursorType::kNorthEastSouthWestResize;
}
}
gfx::Size DesignerExample::GrabHandle::CalculatePreferredSize(
const SizeBounds& /*available_size*/) const {
return gfx::Size(kGrabHandleSize, kGrabHandleSize);
}
void DesignerExample::GrabHandle::OnPaint(gfx::Canvas* canvas) {
SkPath path;
gfx::Point center = GetLocalBounds().CenterPoint();
path.addCircle(center.x(), center.y(), width() / 2);
cc::PaintFlags flags;
flags.setColor(
GetColorProvider()->GetColor(ExamplesColorIds::kColorDesignerGrabHandle));
flags.setAntiAlias(true);
canvas->DrawPath(path, flags);
}
bool DesignerExample::GrabHandle::OnMousePressed(const ui::MouseEvent& event) {
mouse_drag_pos_ = event.location();
return true;
}
bool DesignerExample::GrabHandle::OnMouseDragged(const ui::MouseEvent& event) {
gfx::Point new_location = event.location();
if (position_ == GrabHandlePosition::kTop ||
position_ == GrabHandlePosition::kBottom) {
new_location.set_x(mouse_drag_pos_.x());
}
if (position_ == GrabHandlePosition::kLeft ||
position_ == GrabHandlePosition::kRight) {
new_location.set_y(mouse_drag_pos_.y());
}
SetPosition(origin() + (new_location - mouse_drag_pos_));
UpdateViewSize();
grab_handles_->SetAttachedView(attached_view_);
return true;
}
void DesignerExample::GrabHandle::PositionOnView() {
DCHECK(attached_view_);
gfx::Rect view_bounds = attached_view_->bounds();
gfx::Point edge_position;
if (IsTop(position_)) {
edge_position.set_y(view_bounds.y());
if (position_ == GrabHandlePosition::kTop) {
edge_position.set_x(view_bounds.top_center().x());
}
} else if (IsBottom(position_)) {
edge_position.set_y(view_bounds.bottom());
if (position_ == GrabHandlePosition::kBottom) {
edge_position.set_x(view_bounds.bottom_center().x());
}
}
if (IsLeft(position_)) {
edge_position.set_x(view_bounds.x());
if (position_ == GrabHandlePosition::kLeft) {
edge_position.set_y(view_bounds.left_center().y());
}
} else if (IsRight(position_)) {
edge_position.set_x(view_bounds.right());
if (position_ == GrabHandlePosition::kRight) {
edge_position.set_y(view_bounds.right_center().y());
}
}
SetPosition(edge_position - (bounds().CenterPoint() - origin()));
}
void DesignerExample::GrabHandle::UpdateViewSize() {
DCHECK(attached_view_);
gfx::Rect view_bounds = attached_view_->bounds();
gfx::Point view_center = bounds().CenterPoint();
if (IsTop(position_)) {
view_bounds.set_height(view_bounds.height() -
(view_center.y() - view_bounds.y()));
view_bounds.set_y(view_center.y());
}
if (IsBottom(position_)) {
view_bounds.set_height(view_center.y() - view_bounds.y());
}
if (IsLeft(position_)) {
view_bounds.set_width(view_bounds.width() -
(view_center.x() - view_bounds.x()));
view_bounds.set_x(view_center.x());
}
if (IsRight(position_)) {
view_bounds.set_width(view_center.x() - view_bounds.x());
}
attached_view_->SetBoundsRect(view_bounds);
}
// static
bool DesignerExample::GrabHandle::IsTop(GrabHandlePosition position) {
return (position & GrabHandlePosition::kTop);
}
// static
bool DesignerExample::GrabHandle::IsBottom(GrabHandlePosition position) {
return (position & GrabHandlePosition::kBottom);
}
// static
bool DesignerExample::GrabHandle::IsLeft(GrabHandlePosition position) {
return (position & GrabHandlePosition::kLeft);
}
// static
bool DesignerExample::GrabHandle::IsRight(GrabHandlePosition position) {
return (position & GrabHandlePosition::kRight);
}
BEGIN_METADATA(DesignerExample, GrabHandle)
END_METADATA
DesignerExample::GrabHandles::GrabHandles() = default;
DesignerExample::GrabHandles::~GrabHandles() = default;
void DesignerExample::GrabHandles::Initialize(View* layout_panel) {
static constexpr GrabHandlePosition positions[] = {
GrabHandlePosition::kTop, GrabHandlePosition::kBottom,
GrabHandlePosition::kLeft, GrabHandlePosition::kRight,
GrabHandlePosition::kTopLeft, GrabHandlePosition::kTopRight,
GrabHandlePosition::kBottomLeft, GrabHandlePosition::kBottomRight,
};
DCHECK(grab_handles_.empty());
for (GrabHandlePosition position : positions) {
auto grab_handle = std::make_unique<GrabHandle>(this, position);
grab_handle->SizeToPreferredSize();
grab_handle->SetVisible(false);
grab_handles_.push_back(layout_panel->AddChildView(std::move(grab_handle)));
}
}
void DesignerExample::GrabHandles::SetAttachedView(View* view) {
for (GrabHandle* grab_handle : grab_handles_) {
grab_handle->SetAttachedView(view);
}
}
bool DesignerExample::GrabHandles::IsGrabHandle(View* view) {
return base::ranges::find(grab_handles_, view) != grab_handles_.end();
}
DesignerExample::DesignerExample() : ExampleBase("Designer") {}
DesignerExample::~DesignerExample() {
if (tracker_.view()) {
inspector_->SetModel(nullptr);
}
}
void DesignerExample::CreateExampleView(View* container) {
Builder<View>(container)
.SetUseDefaultFillLayout(true)
.AddChildren(
Builder<BoxLayoutView>()
.CopyAddressTo(&designer_container_)
.SetOrientation(BoxLayout::Orientation::kHorizontal)
.SetMainAxisAlignment(BoxLayout::MainAxisAlignment::kEnd)
.SetCrossAxisAlignment(BoxLayout::CrossAxisAlignment::kStretch)
.AddChildren(
Builder<DesignerSurface>()
.CopyAddressTo(&designer_panel_)
.CustomConfigure(base::BindOnce(
[](DesignerExample* designer_example,
DesignerSurface* designer_surface) {
designer_surface->AddPreTargetHandler(
designer_example);
},
base::Unretained(this))),
Builder<BoxLayoutView>()
.CopyAddressTo(&palette_panel_)
.SetOrientation(BoxLayout::Orientation::kVertical)
.SetInsideBorderInsets(gfx::Insets::TLBR(0, 3, 0, 0))
.SetBetweenChildSpacing(3)
.AddChildren(
Builder<Combobox>()
.CopyAddressTo(&view_type_)
.SetAccessibleName(u"View Type")
.SetModel(this),
Builder<MdTextButton>()
.SetCallback(base::BindRepeating(
&DesignerExample::CreateView,
base::Unretained(this)))
.SetText(u"Add"),
TableView::CreateScrollViewBuilderWithTable(
Builder<TableView>()
.CopyAddressTo(&inspector_)
.SetColumns({MakeColumn(0, u"Name", true),
MakeColumn(1, u"Value", false)})
.SetModel(this)
.SetTableType(views::TableType::kTextOnly)
.SetSingleSelection(true))
.SetPreferredSize(gfx::Size(250, 400)))))
.BuildChildren();
grab_handles_.Initialize(designer_panel_);
designer_container_->SetFlexForView(designer_panel_, 75);
class_registrations_ = GetClassRegistrations();
// TODO(crbug.com/40247792): Refactor such that the TableModel is not
// responsible for managing the lifetimes of views
tracker_.SetView(inspector_);
}
void DesignerExample::OnEvent(ui::Event* event) {
if (event->IsMouseEvent() && event->target()) {
View* view = static_cast<View*>(event->target());
if (IsViewParent(designer_panel_, view->parent()) ||
view == designer_panel_) {
HandleDesignerMouseEvent(event);
return;
}
}
ui::EventHandler::OnEvent(event);
}
void DesignerExample::HandleDesignerMouseEvent(ui::Event* event) {
ui::MouseEvent* mouse_event = event->AsMouseEvent();
switch (mouse_event->type()) {
case ui::EventType::kMousePressed:
if (mouse_event->IsOnlyLeftMouseButton()) {
DCHECK(!dragging_);
View* event_view = GetDesignerChild(static_cast<View*>(event->target()),
designer_panel_);
if (grab_handles_.IsGrabHandle(event_view)) {
dragging_ = event_view;
return;
}
grab_handles_.SetAttachedView(nullptr);
if (event_view == designer_panel_) {
SelectView(nullptr);
return;
}
SelectView(event_view);
dragging_ = selected_;
last_mouse_pos_ = mouse_event->location();
event->SetHandled();
return;
}
break;
case ui::EventType::kMouseDragged:
if (dragging_) {
if (grab_handles_.IsGrabHandle(dragging_)) {
return;
}
gfx::Point new_position =
selected_->origin() +
SnapToGrid(mouse_event->location() - last_mouse_pos_);
new_position.SetToMax(gfx::Point());
new_position.SetToMin(designer_panel_->GetLocalBounds().bottom_right() -
gfx::Vector2d(dragging_->size().width(),
dragging_->size().height()));
dragging_->SetPosition(new_position);
event->SetHandled();
return;
}
break;
case ui::EventType::kMouseReleased:
grab_handles_.SetAttachedView(selected_);
if (dragging_) {
bool dragging_handle = grab_handles_.IsGrabHandle(dragging_);
dragging_ = nullptr;
if (!dragging_handle) {
event->SetHandled();
}
return;
}
break;
default:
return;
}
}
void DesignerExample::SelectView(View* view) {
if (view != selected_) {
selected_ = view;
selected_members_.clear();
if (selected_) {
for (auto* member : *selected_->GetClassMetaData()) {
selected_members_.push_back(member);
}
}
if (model_observer_) {
model_observer_->OnModelChanged();
}
}
}
gfx::Vector2d DesignerExample::SnapToGrid(const gfx::Vector2d& distance) {
return gfx::Vector2d(
SnapToInterval(distance.x(), designer_panel_->GetGridSize()),
SnapToInterval(distance.y(), designer_panel_->GetGridSize()));
}
void DesignerExample::CreateView(const ui::Event& event) {
std::unique_ptr<View> new_view =
class_registrations_[view_type_->GetSelectedRow().value()]->CreateView();
new_view->SizeToPreferredSize();
gfx::Rect child_rect = designer_panel_->GetContentsBounds();
child_rect.ClampToCenteredSize(new_view->size());
child_rect.set_origin(gfx::Point() +
SnapToGrid(child_rect.OffsetFromOrigin()));
new_view->SetBoundsRect(child_rect);
designer_panel_->AddChildView(std::move(new_view));
}
size_t DesignerExample::RowCount() {
return selected_ ? selected_members_.size() : 0;
}
std::u16string DesignerExample::GetText(size_t row, int column_id) {
if (selected_) {
ui::metadata::MemberMetaDataBase* member = selected_members_[row];
if (column_id == 0) {
return base::ASCIIToUTF16(member->member_name());
}
return member->GetValueAsString(selected_);
}
return std::u16string();
}
void DesignerExample::SetObserver(ui::TableModelObserver* observer) {
model_observer_ = observer;
}
size_t DesignerExample::GetItemCount() const {
return class_registrations_.size();
}
std::u16string DesignerExample::GetItemAt(size_t index) const {
return class_registrations_[index]->GetViewClassName();
}
std::optional<size_t> DesignerExample::GetDefaultIndex() const {
return 0;
}
} // namespace views::examples