| // 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 <ranges> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/scoped_observation.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/models/simple_combobox_model.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/ui_base_features.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/paint_vector_icon.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/views/accessibility/view_accessibility.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/editable_combobox/editable_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/layout/flex_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) { |
| SetFocusBehavior(FocusBehavior::ALWAYS); |
| GetViewAccessibility().SetRole(ax::mojom::Role::kGenericContainer); |
| GetViewAccessibility().SetName(u"Designer Surface"); |
| } |
| |
| void DesignerSurface::SetGridSize(int grid_size) { |
| if (grid_size_ != grid_size) { |
| grid_size_ = grid_size; |
| if (GetWidget()) { |
| RebuildGridImage(); |
| } |
| } |
| } |
| |
| void DesignerSurface::OnPaint(gfx::Canvas* canvas) { |
| 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); |
| auto grid_canvas = std::make_unique<gfx::Canvas>( |
| gfx::Size(grid_size_, grid_size_), 1.0f, false); |
| grid_canvas->FillRect(gfx::Rect(0, 0, 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 { |
| |
| void RemoveViewAndChildrenFromTracking(View* view, |
| std::set<raw_ptr<View>>& tracking) { |
| for (View* child : view->children()) { |
| RemoveViewAndChildrenFromTracking(child, tracking); |
| } |
| tracking.erase(view); |
| } |
| |
| 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, bool is_container = false> |
| 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::kViewClassName); |
| } |
| bool IsContainer() const override { return is_container; } |
| }; |
| |
| class ClassRegistrationCombobox : public ClassRegistration<Combobox>, |
| public ui::ComboboxModel { |
| public: |
| ClassRegistrationCombobox() = default; |
| ~ClassRegistrationCombobox() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return Builder<Combobox>() |
| .SetModel(this) |
| .SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_DESIGNER_COMBOBOX_NAME)) |
| .Build(); |
| } |
| |
| // 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; } |
| }; |
| |
| class ClassRegistrationMdTextButton : public ClassRegistration<MdTextButton> { |
| public: |
| ClassRegistrationMdTextButton() = default; |
| ~ClassRegistrationMdTextButton() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return Builder<MdTextButton>().SetText(u"Button").Build(); |
| } |
| }; |
| |
| class ClassRegistrationTextfield : public ClassRegistration<Textfield> { |
| public: |
| ClassRegistrationTextfield() = default; |
| ~ClassRegistrationTextfield() 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(); |
| } |
| }; |
| |
| class ClassRegistrationCheckbox : public ClassRegistration<Checkbox> { |
| public: |
| ClassRegistrationCheckbox() = default; |
| ~ClassRegistrationCheckbox() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return std::make_unique<Checkbox>(u"<Checkbox>"); |
| } |
| }; |
| |
| class ClassRegistrationRadioButton : public ClassRegistration<RadioButton> { |
| public: |
| ClassRegistrationRadioButton() = default; |
| ~ClassRegistrationRadioButton() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return std::make_unique<RadioButton>(u"<RadioButton>", 0); |
| } |
| }; |
| |
| class ClassRegistrationToggleButton : public ClassRegistration<ToggleButton> { |
| public: |
| ClassRegistrationToggleButton() = default; |
| ~ClassRegistrationToggleButton() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return Builder<ToggleButton>() |
| .SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_DESIGNER_IMAGEBUTTON_NAME)) |
| .Build(); |
| } |
| }; |
| |
| class ClassRegistrationImageButton : public ClassRegistration<ImageButton> { |
| public: |
| ClassRegistrationImageButton() = default; |
| ~ClassRegistrationImageButton() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return Builder<ImageButton>() |
| .SetImageModel( |
| Button::ButtonState::STATE_NORMAL, |
| ui::ImageModel::FromVectorIcon( |
| features::IsRoundedIconsEnabled() ? kKeepIcon : kPinOldIcon, |
| ui::kColorIcon)) |
| .SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_DESIGNER_IMAGEBUTTON_NAME)) |
| .Build(); |
| } |
| }; |
| |
| class ClassRegistrationBoxLayoutView |
| : public ClassRegistration<BoxLayoutView, true> { |
| public: |
| ClassRegistrationBoxLayoutView() = default; |
| ~ClassRegistrationBoxLayoutView() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return Builder<BoxLayoutView>() |
| .SetBackground(CreateSolidBackground(SkColorSetA(SK_ColorGRAY, 128))) |
| .SetPreferredSize(gfx::Size(100, 100)) |
| .Build(); |
| } |
| }; |
| |
| class ClassRegistrationFlexLayoutView |
| : public ClassRegistration<FlexLayoutView, true> { |
| public: |
| ClassRegistrationFlexLayoutView() = default; |
| ~ClassRegistrationFlexLayoutView() override = default; |
| std::unique_ptr<View> CreateView() override { |
| return Builder<FlexLayoutView>() |
| .SetBackground(CreateSolidBackground(SkColorSetA(SK_ColorGRAY, 128))) |
| .SetPreferredSize(gfx::Size(100, 100)) |
| .Build(); |
| } |
| }; |
| |
| std::vector<std::unique_ptr<BaseClassRegistration>> GetClassRegistrations() { |
| std::vector<std::unique_ptr<BaseClassRegistration>> registrations; |
| registrations.push_back(std::make_unique<ClassRegistrationBoxLayoutView>()); |
| registrations.push_back(std::make_unique<ClassRegistrationCheckbox>()); |
| registrations.push_back(std::make_unique<ClassRegistrationCombobox>()); |
| registrations.push_back(std::make_unique<ClassRegistrationFlexLayoutView>()); |
| registrations.push_back(std::make_unique<ClassRegistrationMdTextButton>()); |
| registrations.push_back(std::make_unique<ClassRegistrationRadioButton>()); |
| registrations.push_back(std::make_unique<ClassRegistrationTextfield>()); |
| registrations.push_back(std::make_unique<ClassRegistrationToggleButton>()); |
| registrations.push_back(std::make_unique<ClassRegistrationImageButton>()); |
| 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, |
| const std::set<raw_ptr<View>>& designer_views) { |
| while (child_view && child_view != designer && |
| !designer_views.count(child_view)) { |
| child_view = child_view->parent(); |
| } |
| return child_view; |
| } |
| |
| } // namespace |
| |
| // InPlaceEditor provides a base for editing properties within the TableView. |
| class InPlaceEditor : public View, public TextfieldController { |
| METADATA_HEADER(InPlaceEditor, View) |
| |
| public: |
| explicit InPlaceEditor(DesignerPropertyEditor* editor, |
| base::OnceClosure on_commit) |
| : editor_(editor), on_commit_(std::move(on_commit)) { |
| SetFocusBehavior(FocusBehavior::ALWAYS); |
| GetViewAccessibility().SetRole(ax::mojom::Role::kGenericContainer); |
| GetViewAccessibility().SetName(editor->GetPropertyName()); |
| } |
| |
| ~InPlaceEditor() override = default; |
| |
| virtual void Commit() { |
| if (!on_commit_.is_null()) { |
| std::move(on_commit_).Run(); |
| } |
| } |
| |
| protected: |
| raw_ptr<DesignerPropertyEditor> editor_; |
| base::OnceClosure on_commit_; |
| }; |
| |
| BEGIN_METADATA(InPlaceEditor) |
| END_METADATA |
| |
| class TextInPlaceEditor : public InPlaceEditor { |
| METADATA_HEADER(TextInPlaceEditor, InPlaceEditor) |
| |
| public: |
| TextInPlaceEditor(DesignerPropertyEditor* editor, base::OnceClosure on_commit) |
| : InPlaceEditor(editor, std::move(on_commit)) { |
| Builder<View>(this) |
| .SetUseDefaultFillLayout(true) |
| .AddChildren(Builder<Textfield>() |
| .CopyAddressTo(&textfield_) |
| .SetAccessibleName(editor->GetPropertyName()) |
| .SetText(editor->GetValueAsString()) |
| .SetController(this) |
| .SetReadOnly(editor->IsReadOnly())) |
| .BuildChildren(); |
| } |
| |
| void RequestFocus() override { |
| if (textfield_->GetEnabled()) { |
| textfield_->RequestFocus(); |
| } else { |
| InPlaceEditor::RequestFocus(); |
| } |
| } |
| |
| void Commit() override { |
| if (!on_commit_.is_null() && !editor_->IsReadOnly()) { |
| editor_->SetValueFromString(std::u16string(textfield_->GetText())); |
| } |
| InPlaceEditor::Commit(); |
| } |
| |
| // TextfieldController overrides: |
| bool HandleKeyEvent(Textfield* sender, |
| const ui::KeyEvent& key_event) override { |
| if (key_event.type() == ui::EventType::kKeyPressed) { |
| if (key_event.key_code() == ui::VKEY_RETURN) { |
| Commit(); |
| return true; |
| } |
| if (key_event.key_code() == ui::VKEY_ESCAPE) { |
| // Cancel: run callback without saving. |
| if (!on_commit_.is_null()) { |
| std::move(on_commit_).Run(); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private: |
| raw_ptr<Textfield> textfield_; |
| }; |
| |
| BEGIN_METADATA(TextInPlaceEditor) |
| END_METADATA |
| |
| class ComboInPlaceEditor : public InPlaceEditor { |
| METADATA_HEADER(ComboInPlaceEditor, InPlaceEditor) |
| |
| public: |
| ComboInPlaceEditor(DesignerPropertyEditor* editor, |
| base::OnceClosure on_commit) |
| : InPlaceEditor(editor, std::move(on_commit)) { |
| SetUseDefaultFillLayout(true); |
| std::vector<ui::SimpleComboboxModel::Item> items; |
| for (const auto& val : editor->GetComboboxValues()) { |
| items.emplace_back(val); |
| } |
| auto model = std::make_unique<ui::SimpleComboboxModel>(std::move(items)); |
| combobox_ = AddChildView(std::make_unique<EditableCombobox>( |
| std::move(model), /*filter_on_edit=*/false)); |
| combobox_->GetViewAccessibility().SetName(editor->GetPropertyName()); |
| combobox_->SetText(editor->GetValueAsString()); |
| combobox_->SetCallback(base::BindRepeating( |
| &ComboInPlaceEditor::OnContentsChanged, base::Unretained(this))); |
| last_text_length_ = combobox_->GetText().length(); |
| if (editor->IsReadOnly()) { |
| combobox_->SetEnabled(false); |
| } |
| } |
| |
| void RequestFocus() override { |
| if (combobox_->GetEnabled()) { |
| static_cast<View*>(combobox_)->RequestFocus(); |
| } else { |
| InPlaceEditor::RequestFocus(); |
| } |
| } |
| |
| void Commit() override { |
| if (!on_commit_.is_null() && !editor_->IsReadOnly()) { |
| editor_->SetValueFromString(std::u16string(combobox_->GetText())); |
| } |
| InPlaceEditor::Commit(); |
| } |
| |
| // View overrides: |
| void OnKeyEvent(ui::KeyEvent* event) override { |
| if (event->type() == ui::EventType::kKeyPressed) { |
| if (event->key_code() == ui::VKEY_RETURN) { |
| Commit(); |
| event->SetHandled(); |
| } else if (event->key_code() == ui::VKEY_ESCAPE) { |
| if (!on_commit_.is_null()) { |
| std::move(on_commit_).Run(); |
| } |
| event->SetHandled(); |
| } |
| } |
| } |
| |
| void OnContentsChanged() { |
| std::u16string current_text = std::u16string(combobox_->GetText()); |
| |
| // If it's a full match, save it immediately. |
| auto values = editor_->GetComboboxValues(); |
| if (std::ranges::find(values, current_text) != values.end()) { |
| if (!editor_->IsReadOnly()) { |
| editor_->SetValueFromString(current_text); |
| } |
| } |
| |
| if (current_text.empty() || current_text.length() <= last_text_length_) { |
| last_text_length_ = current_text.length(); |
| return; |
| } |
| |
| last_text_length_ = current_text.length(); |
| |
| // Autocomplete logic: find first match that starts with current text. |
| for (const auto& val : values) { |
| if (base::StartsWith(val, current_text, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| if (val.length() > current_text.length()) { |
| combobox_->SetText(val); |
| combobox_->SelectRange( |
| gfx::Range(current_text.length(), val.length())); |
| last_text_length_ = val.length(); |
| } |
| break; |
| } |
| } |
| } |
| |
| private: |
| raw_ptr<EditableCombobox> combobox_; |
| size_t last_text_length_ = 0; |
| }; |
| |
| BEGIN_METADATA(ComboInPlaceEditor) |
| END_METADATA |
| |
| class CheckboxInPlaceEditor : public InPlaceEditor { |
| METADATA_HEADER(CheckboxInPlaceEditor, InPlaceEditor) |
| |
| public: |
| CheckboxInPlaceEditor(DesignerPropertyEditor* editor, |
| base::OnceClosure on_commit) |
| : InPlaceEditor(editor, std::move(on_commit)) { |
| Builder<View>(this) |
| .SetLayoutManager(std::make_unique<BoxLayout>( |
| BoxLayout::Orientation::kHorizontal, gfx::Insets(), 0)) |
| .CustomConfigure(base::BindOnce([](View* editor_view) { |
| auto* layout = |
| static_cast<BoxLayout*>(editor_view->GetLayoutManager()); |
| layout->set_main_axis_alignment( |
| BoxLayout::MainAxisAlignment::kCenter); |
| layout->set_cross_axis_alignment( |
| BoxLayout::CrossAxisAlignment::kCenter); |
| })) |
| .AddChildren( |
| Builder<Checkbox>() |
| .CopyAddressTo(&checkbox_) |
| .SetAccessibleName(editor->GetPropertyName()) |
| .SetChecked(editor->GetValueAsString() == u"true") |
| .SetCallback(base::BindRepeating( |
| &CheckboxInPlaceEditor::OnToggled, base::Unretained(this))) |
| .SetEnabled(!editor->IsReadOnly())) |
| .BuildChildren(); |
| } |
| |
| void RequestFocus() override { |
| if (checkbox_->GetEnabled()) { |
| checkbox_->RequestFocus(); |
| } else { |
| InPlaceEditor::RequestFocus(); |
| } |
| } |
| |
| void Commit() override { |
| if (!on_commit_.is_null() && !editor_->IsReadOnly()) { |
| editor_->SetValueFromString(checkbox_->GetChecked() ? u"true" : u"false"); |
| } |
| InPlaceEditor::Commit(); |
| } |
| |
| private: |
| void OnToggled() { |
| if (!editor_->IsReadOnly()) { |
| editor_->SetValueFromString(checkbox_->GetChecked() ? u"true" : u"false"); |
| } |
| } |
| |
| raw_ptr<Checkbox> checkbox_; |
| }; |
| |
| BEGIN_METADATA(CheckboxInPlaceEditor) |
| END_METADATA |
| |
| 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) { |
| attached_view_ = view; |
| if (attached_view_) { |
| PositionOnView(); |
| } |
| SetVisible(!!attached_view_); |
| } |
| |
| void DesignerExample::GrabHandle::UpdatePosition(bool reorder) { |
| if (attached_view_) { |
| PositionOnView(); |
| if (reorder) { |
| parent()->ReorderChildView(this, -1); |
| } |
| } |
| } |
| |
| ui::Cursor DesignerExample::GrabHandle::GetCursor(const ui::MouseEvent& event) { |
| if (!attached_view_ || |
| attached_view_->parent() != grab_handles_->layout_panel()) { |
| return ui::mojom::CursorType::kNull; |
| } |
| 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) { |
| cc::PaintFlags flags; |
| flags.setAntiAlias(true); |
| flags.setColor(GetColorProvider()->GetColor(ui::kColorAccent)); |
| flags.setStyle(cc::PaintFlags::kFill_Style); |
| canvas->DrawCircle(gfx::PointF(GetLocalBounds().CenterPoint()), |
| kGrabHandleSize / 2.0f, flags); |
| } |
| |
| bool DesignerExample::GrabHandle::OnMousePressed(const ui::MouseEvent& event) { |
| mouse_drag_pos_ = event.location(); |
| views::View::ConvertPointToTarget(this, parent(), &mouse_drag_pos_); |
| return true; |
| } |
| |
| bool DesignerExample::GrabHandle::OnMouseDragged(const ui::MouseEvent& event) { |
| if (!attached_view_ || |
| attached_view_->parent() != grab_handles_->layout_panel()) { |
| return true; |
| } |
| gfx::Point mouse_in_parent = event.location(); |
| views::View::ConvertPointToTarget(this, parent(), &mouse_in_parent); |
| |
| gfx::Vector2d delta = mouse_in_parent - mouse_drag_pos_; |
| if (delta.IsZero()) { |
| return true; |
| } |
| |
| gfx::Rect view_bounds = attached_view_->bounds(); |
| if (IsTop(position_)) { |
| view_bounds.set_y(view_bounds.y() + delta.y()); |
| view_bounds.set_height(view_bounds.height() - delta.y()); |
| } |
| if (IsBottom(position_)) { |
| view_bounds.set_height(view_bounds.height() + delta.y()); |
| } |
| if (IsLeft(position_)) { |
| view_bounds.set_x(view_bounds.x() + delta.x()); |
| view_bounds.set_width(view_bounds.width() - delta.x()); |
| } |
| if (IsRight(position_)) { |
| view_bounds.set_width(view_bounds.width() + delta.x()); |
| } |
| attached_view_->SetBoundsRect(view_bounds); |
| grab_handles_->UpdatePositions(); |
| mouse_drag_pos_ = mouse_in_parent; |
| return true; |
| } |
| |
| void DesignerExample::GrabHandle::PositionOnView() { |
| DCHECK(attached_view_); |
| gfx::RectF view_bounds_f(attached_view_->GetLocalBounds()); |
| views::View::ConvertRectToTarget(attached_view_, parent(), &view_bounds_f); |
| gfx::Rect view_bounds = gfx::ToEnclosingRect(view_bounds_f); |
| 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()); |
| } |
| } |
| 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()); |
| } |
| } |
| if (IsRight(position_)) { |
| edge_position.set_x(view_bounds.right()); |
| if (position_ == GrabHandlePosition::kRight) { |
| edge_position.set_y(view_bounds.right_center().y()); |
| } |
| } |
| if (position_ == GrabHandlePosition::kTopLeft) { |
| edge_position = view_bounds.origin(); |
| } |
| if (position_ == GrabHandlePosition::kTopRight) { |
| edge_position = view_bounds.top_right(); |
| } |
| if (position_ == GrabHandlePosition::kBottomLeft) { |
| edge_position = view_bounds.bottom_left(); |
| } |
| if (position_ == GrabHandlePosition::kBottomRight) { |
| edge_position = view_bounds.bottom_right(); |
| } |
| gfx::Rect handle_bounds(edge_position, GetPreferredSize({})); |
| handle_bounds.Offset(-kGrabHandleSize / 2, -kGrabHandleSize / 2); |
| SetBoundsRect(handle_bounds); |
| } |
| |
| void DesignerExample::GrabHandle::UpdateViewSize() { |
| 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); |
| PositionOnView(); |
| } |
| |
| // static |
| bool DesignerExample::GrabHandle::IsTop(GrabHandlePosition position) { |
| return (position & GrabHandlePosition::kTop) != 0; |
| } |
| |
| // static |
| bool DesignerExample::GrabHandle::IsBottom(GrabHandlePosition position) { |
| return (position & GrabHandlePosition::kBottom) != 0; |
| } |
| |
| // static |
| bool DesignerExample::GrabHandle::IsLeft(GrabHandlePosition position) { |
| return (position & GrabHandlePosition::kLeft) != 0; |
| } |
| |
| // static |
| bool DesignerExample::GrabHandle::IsRight(GrabHandlePosition position) { |
| return (position & GrabHandlePosition::kRight) != 0; |
| } |
| |
| BEGIN_METADATA(DesignerExample, GrabHandle) |
| END_METADATA |
| |
| DesignerExample::GrabHandles::GrabHandles() = default; |
| |
| DesignerExample::GrabHandles::~GrabHandles() = default; |
| |
| void DesignerExample::GrabHandles::Initialize(View* layout_panel) { |
| layout_panel_ = layout_panel; |
| static constexpr GrabHandlePosition positions[] = { |
| GrabHandlePosition::kTop, GrabHandlePosition::kBottom, |
| GrabHandlePosition::kLeft, GrabHandlePosition::kRight, |
| GrabHandlePosition::kTopLeft, GrabHandlePosition::kTopRight, |
| GrabHandlePosition::kBottomLeft, GrabHandlePosition::kBottomRight, |
| }; |
| for (auto position : positions) { |
| grab_handles_.push_back(layout_panel->AddChildView( |
| std::make_unique<GrabHandle>(this, position))); |
| } |
| } |
| |
| void DesignerExample::GrabHandles::SetAttachedView(View* view) { |
| // Suppress handles for the top-level container. |
| if (view == layout_panel_) { |
| view = nullptr; |
| } |
| for (auto handle : grab_handles_) { |
| handle->SetAttachedView(view); |
| } |
| } |
| |
| void DesignerExample::GrabHandles::UpdatePositions() { |
| for (auto handle : grab_handles_) { |
| handle->UpdatePosition(false); |
| } |
| } |
| |
| bool DesignerExample::GrabHandles::IsGrabHandle(View* view) { |
| return std::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) |
| .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) |
| .CustomConfigure(base::BindOnce( |
| [](DesignerExample* designer_example, |
| BoxLayoutView* palette_panel) { |
| palette_panel->AddPreTargetHandler( |
| designer_example); |
| }, |
| base::Unretained(this))) |
| .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) |
| .SetObserver(this)) |
| .CopyAddressTo(&inspector_scroller_) |
| .SetPreferredSize(gfx::Size(250, 400))))) |
| .BuildChildren(); |
| grab_handles_.Initialize(designer_panel_); |
| designer_container_->SetFlexForView(designer_panel_, 75); |
| palette_panel_->SetFlexForView(inspector_scroller_, 1); |
| 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->IsKeyEvent()) { |
| ui::KeyEvent* key_event = event->AsKeyEvent(); |
| if (key_event->type() == ui::EventType::kKeyPressed && |
| key_event->key_code() == ui::VKEY_ESCAPE && selected_) { |
| if (selected_ == designer_panel_) { |
| SelectView(nullptr); |
| } else { |
| View* parent_view = GetDesignerChild(selected_->parent(), |
| designer_panel_, designer_views_); |
| SelectView(parent_view); |
| } |
| grab_handles_.SetAttachedView(selected_); |
| event->SetHandled(); |
| return; |
| } |
| if (key_event->type() == ui::EventType::kKeyPressed && |
| key_event->key_code() == ui::VKEY_DELETE && selected_ && |
| selected_ != designer_panel_) { |
| if (active_inplace_editor_) { |
| active_inplace_editor_->Commit(); |
| } |
| |
| View* to_delete = selected_; |
| View* parent = to_delete->parent(); |
| |
| // Determine next selection. |
| View* next_selection = nullptr; |
| auto children = parent->children(); |
| auto it = std::ranges::find(children, to_delete); |
| if (it != children.end()) { |
| if (std::next(it) != children.end()) { |
| next_selection = *std::next(it); |
| } else if (it != children.begin()) { |
| next_selection = *std::prev(it); |
| } |
| } |
| |
| if (!next_selection || grab_handles_.IsGrabHandle(next_selection)) { |
| next_selection = parent; |
| } |
| |
| if (dragging_ == to_delete) { |
| dragging_ = nullptr; |
| } |
| |
| RemoveViewAndChildrenFromTracking(to_delete, designer_views_); |
| parent->RemoveChildViewT(to_delete); |
| |
| SelectView(next_selection); |
| grab_handles_.SetAttachedView(selected_); |
| |
| event->SetHandled(); |
| return; |
| } |
| if (key_event->type() == ui::EventType::kKeyPressed && |
| (key_event->key_code() == ui::VKEY_UP || |
| key_event->key_code() == ui::VKEY_DOWN) && |
| active_inplace_editor_) { |
| if (key_event->key_code() == ui::VKEY_DOWN && key_event->IsAltDown()) { |
| // Let it pass through to open the combobox. |
| } else { |
| std::optional<size_t> row = inspector_->GetFirstSelectedRow(); |
| if (row.has_value()) { |
| int delta = (key_event->key_code() == ui::VKEY_UP) ? -1 : 1; |
| int next_row = static_cast<int>(row.value()) + delta; |
| if (next_row >= 0 && |
| next_row < static_cast<int>(inspector_->GetRowCount())) { |
| active_inplace_editor_->Commit(); |
| inspector_->Select(static_cast<size_t>(next_row)); |
| event->SetHandled(); |
| return; |
| } |
| } |
| } |
| } |
| if (key_event->type() == ui::EventType::kKeyPressed && |
| (key_event->key_code() == ui::VKEY_LEFT || |
| key_event->key_code() == ui::VKEY_RIGHT)) { |
| std::optional<size_t> row = inspector_->GetFirstSelectedRow(); |
| if (row.has_value()) { |
| auto editor = display_list_[row.value()]; |
| if (editor->IsExpandable()) { |
| bool expand = key_event->key_code() == ui::VKEY_RIGHT; |
| if (editor->IsExpanded() != expand) { |
| editor->SetExpanded(expand); |
| RefreshDisplayList(); |
| if (model_observer_) { |
| model_observer_->OnModelChanged(); |
| } |
| inspector_->Select(row.value()); |
| event->SetHandled(); |
| return; |
| } |
| } |
| } |
| } |
| } |
| if (event->IsMouseEvent() && event->target()) { |
| View* view = static_cast<View*>(event->target()); |
| if (view == inspector_) { |
| ui::MouseEvent* mouse_event = event->AsMouseEvent(); |
| if (mouse_event->type() == ui::EventType::kMousePressed && |
| mouse_event->IsOnlyLeftMouseButton()) { |
| gfx::Point point = mouse_event->location(); |
| int row = point.y() / inspector_->GetRowHeight(); |
| if (row >= 0 && row < static_cast<int>(display_list_.size())) { |
| // Check if clicking in the first column (Name) |
| const auto& visible_columns = inspector_->visible_columns(); |
| if (point.x() < visible_columns[0].width) { |
| auto editor = display_list_[row]; |
| if (editor->IsExpandable()) { |
| editor->SetExpanded(!editor->IsExpanded()); |
| RefreshDisplayList(); |
| if (model_observer_) { |
| model_observer_->OnModelChanged(); |
| } |
| inspector_->Select(row); |
| event->SetHandled(); |
| return; |
| } |
| } |
| } |
| } |
| } |
| if (IsViewParent(designer_panel_, view->parent()) || |
| view == designer_panel_) { |
| HandleDesignerMouseEvent(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()) { |
| designer_panel_->RequestFocus(); |
| DCHECK(!dragging_); |
| View* target_view = static_cast<View*>(event->target()); |
| if (grab_handles_.IsGrabHandle(target_view)) { |
| dragging_ = target_view; |
| return; |
| } |
| View* event_view = |
| GetDesignerChild(target_view, designer_panel_, designer_views_); |
| if (mouse_event->IsAltDown() && event_view != designer_panel_) { |
| if (selected_ && (event_view == selected_ || |
| IsViewParent(selected_, event_view))) { |
| event_view = GetDesignerChild(selected_->parent(), designer_panel_, |
| designer_views_); |
| } else { |
| event_view = GetDesignerChild(event_view->parent(), designer_panel_, |
| designer_views_); |
| } |
| } |
| grab_handles_.SetAttachedView(nullptr); |
| SelectView(event_view); |
| if (selected_ && selected_ != designer_panel_) { |
| dragging_ = selected_; |
| last_mouse_pos_ = mouse_event->location(); |
| } |
| event->SetHandled(); |
| return; |
| } |
| break; |
| case ui::EventType::kMouseDragged: |
| if (dragging_) { |
| if (grab_handles_.IsGrabHandle(dragging_)) { |
| return; |
| } |
| if (dragging_->parent() != designer_panel_) { |
| 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_) { |
| if (model_observer_) { |
| model_observer_->OnModelChanged(); |
| } |
| 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_) { |
| if (active_inplace_editor_) { |
| active_inplace_editor_->Commit(); |
| } |
| selected_ = view; |
| active_editors_.clear(); |
| if (selected_) { |
| for (auto* member : *selected_->GetClassMetaData()) { |
| active_editors_.push_back(CreatePropertyEditor(selected_, member)); |
| } |
| } |
| RefreshDisplayList(); |
| if (model_observer_) { |
| model_observer_->OnModelChanged(); |
| } |
| } |
| } |
| |
| void DesignerExample::RefreshDisplayList() { |
| display_list_.clear(); |
| auto add_to_list = [this](auto& self, |
| DesignerPropertyEditor* editor) -> void { |
| display_list_.push_back(editor); |
| if (editor->IsExpandable() && editor->IsExpanded()) { |
| for (auto* sub : editor->GetSubEditors()) { |
| self(self, sub); |
| } |
| } |
| }; |
| |
| for (const auto& editor : active_editors_) { |
| add_to_list(add_to_list, editor.get()); |
| } |
| } |
| |
| void DesignerExample::OnSelectionChanged() { |
| std::optional<size_t> row = inspector_->GetFirstSelectedRow(); |
| |
| if (active_inplace_editor_) { |
| // Commit and remove previous editor. |
| active_inplace_editor_->Commit(); |
| } |
| |
| if (!row.has_value() || !selected_) { |
| return; |
| } |
| |
| auto editor = display_list_[row.value()]; |
| std::unique_ptr<InPlaceEditor> inplace_editor; |
| |
| size_t current_row = row.value(); |
| base::OnceClosure on_commit = base::BindOnce( |
| [](DesignerExample* example, size_t row_index) { |
| if (example->active_inplace_editor_) { |
| example->inspector_->RemoveChildViewT( |
| example->active_inplace_editor_.get()); |
| example->active_inplace_editor_ = nullptr; |
| if (example->selected_) { |
| example->selected_->InvalidateLayout(); |
| example->selected_->SchedulePaint(); |
| example->designer_panel_->SchedulePaint(); |
| } |
| if (example->model_observer_) { |
| example->model_observer_->OnItemsChanged(row_index, 1); |
| } |
| } |
| }, |
| base::Unretained(this), current_row); |
| |
| if (editor->GetEditorType() == |
| DesignerPropertyEditor::EditorType::kCombobox) { |
| inplace_editor = |
| std::make_unique<ComboInPlaceEditor>(editor, std::move(on_commit)); |
| } else if (editor->GetEditorType() == |
| DesignerPropertyEditor::EditorType::kCheckbox) { |
| inplace_editor = |
| std::make_unique<CheckboxInPlaceEditor>(editor, std::move(on_commit)); |
| } else { |
| inplace_editor = |
| std::make_unique<TextInPlaceEditor>(editor, std::move(on_commit)); |
| } |
| |
| size_t view_row = inspector_->ModelToView(row.value()); |
| const auto& visible_columns = inspector_->visible_columns(); |
| const auto& vis_col = visible_columns[1]; // Value column |
| gfx::Rect cell_bounds(vis_col.x, |
| static_cast<int>(view_row) * inspector_->GetRowHeight(), |
| vis_col.width, inspector_->GetRowHeight()); |
| |
| active_inplace_editor_ = inspector_->AddChildView(std::move(inplace_editor)); |
| active_inplace_editor_->SetBoundsRect(cell_bounds); |
| active_inplace_editor_->RequestFocus(); |
| } |
| |
| 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(); |
| designer_views_.insert(new_view.get()); |
| |
| View* parent = designer_panel_; |
| if (selected_) { |
| for (const auto& reg : class_registrations_) { |
| if (reg->GetViewClassName() == |
| base::ASCIIToUTF16(selected_->GetClassName())) { |
| if (reg->IsContainer()) { |
| parent = selected_; |
| } |
| break; |
| } |
| } |
| } |
| |
| if (parent == designer_panel_) { |
| 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); |
| } |
| parent->AddChildView(std::move(new_view)); |
| } |
| |
| size_t DesignerExample::RowCount() const { |
| return selected_ ? display_list_.size() : 0; |
| } |
| |
| std::u16string DesignerExample::GetText(size_t row, int column_id) const { |
| if (selected_) { |
| const auto editor = display_list_[row]; |
| if (column_id == 0) { |
| std::u16string name = editor->GetPropertyName(); |
| if (editor->IsExpandable()) { |
| name = (editor->IsExpanded() ? u"[-] " : u"[+] ") + name; |
| } |
| return std::u16string(editor->GetLevel() * 2, ' ') + name; |
| } |
| return editor->GetValueAsString(); |
| } |
| 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 |