| // Copyright 2012 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/controls/combobox/combobox.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "build/build_config.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/base/menu_source_utils.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/models/image_model.h" |
| #include "ui/base/mojom/menu_source_type.mojom.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/color/color_id.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/scoped_canvas.h" |
| #include "ui/gfx/text_utils.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/flood_fill_ink_drop_ripple.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/animation/ink_drop_impl.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/button/button.h" |
| #include "ui/views/controls/button/button_controller.h" |
| #include "ui/views/controls/combobox/combobox_menu_model.h" |
| #include "ui/views/controls/combobox/combobox_util.h" |
| #include "ui/views/controls/combobox/empty_combobox_model.h" |
| #include "ui/views/controls/focus_ring.h" |
| #include "ui/views/controls/focusable_border.h" |
| #include "ui/views/controls/highlight_path_generator.h" |
| #include "ui/views/controls/menu/menu_config.h" |
| #include "ui/views/controls/menu/menu_runner.h" |
| #include "ui/views/controls/prefix_selector.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/mouse_constants.h" |
| #include "ui/views/style/platform_style.h" |
| #include "ui/views/style/typography.h" |
| #include "ui/views/style/typography_provider.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| constexpr int kBorderThickness = 1; |
| |
| float GetCornerRadius() { |
| return LayoutProvider::Get()->GetCornerRadiusMetric( |
| ShapeContextTokens::kComboboxRadius); |
| } |
| |
| SkColor GetTextColorForEnableState(const Combobox& combobox, bool enabled) { |
| const int style = enabled ? style::STYLE_PRIMARY : style::STYLE_DISABLED; |
| return combobox.GetColorProvider()->GetColor( |
| TypographyProvider::Get().GetColorId(style::CONTEXT_TEXTFIELD, style)); |
| } |
| |
| // The transparent button which holds a button state but is not rendered. |
| class TransparentButton : public Button { |
| METADATA_HEADER(TransparentButton, Button) |
| |
| public: |
| explicit TransparentButton(PressedCallback callback) |
| : Button(std::move(callback)) { |
| SetFocusBehavior(FocusBehavior::NEVER); |
| button_controller()->set_notify_action( |
| ButtonController::NotifyAction::kOnPress); |
| |
| views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(), |
| GetCornerRadius()); |
| ConfigureComboboxButtonInkDrop(this); |
| } |
| TransparentButton(const TransparentButton&) = delete; |
| TransparentButton& operator&=(const TransparentButton&) = delete; |
| ~TransparentButton() override = default; |
| |
| bool OnMousePressed(const ui::MouseEvent& mouse_event) override { |
| #if !BUILDFLAG(IS_MAC) |
| // On Mac, comboboxes do not take focus on mouse click, but on other |
| // platforms they do. |
| parent()->RequestFocus(); |
| #endif |
| return Button::OnMousePressed(mouse_event); |
| } |
| |
| double GetAnimationValue() const { |
| return hover_animation().GetCurrentValue(); |
| } |
| |
| void UpdateInkDrop(bool show_on_press_and_hover) { |
| if (show_on_press_and_hover) { |
| // We must use UseInkDropForFloodFillRipple here because |
| // UseInkDropForSquareRipple hides the InkDrop when the ripple effect is |
| // active instead of layering underneath it causing flashing. |
| InkDrop::UseInkDropForFloodFillRipple(InkDrop::Get(this), |
| /*highlight_on_hover=*/true); |
| } else { |
| InkDrop::UseInkDropForSquareRipple(InkDrop::Get(this), |
| /*highlight_on_hover=*/false); |
| } |
| } |
| }; |
| |
| BEGIN_METADATA(TransparentButton) |
| END_METADATA |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Combobox, public: |
| |
| Combobox::Combobox() |
| : Combobox(std::make_unique<internal::EmptyComboboxModel>()) {} |
| |
| Combobox::Combobox(std::unique_ptr<ui::ComboboxModel> model) |
| : Combobox(model.get()) { |
| owned_model_ = std::move(model); |
| } |
| |
| Combobox::Combobox(ui::ComboboxModel* model) { |
| SetModel(model); |
| #if BUILDFLAG(IS_MAC) |
| SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); |
| #else |
| SetFocusBehavior(FocusBehavior::ALWAYS); |
| #endif |
| |
| SetBackgroundColorId(ui::kColorComboboxBackground); |
| |
| UpdateBorder(); |
| |
| FocusRing::Install(this); |
| views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true); |
| |
| arrow_button_ = |
| AddChildView(std::make_unique<TransparentButton>(base::BindRepeating( |
| &Combobox::ArrowButtonPressed, base::Unretained(this)))); |
| |
| // TODO(crbug.com/40250124): This setter should be removed and the behavior |
| // made default when ChromeRefresh2023 is finalized. |
| SetEventHighlighting(true); |
| enabled_changed_subscription_ = |
| AddEnabledInViewsSubtreeChangedCallback(base::BindRepeating( |
| [](Combobox* combobox) { |
| combobox->SetBackgroundColorId( |
| combobox->GetEnabledInViewsSubtree() |
| ? ui::kColorComboboxBackground |
| : ui::kColorComboboxBackgroundDisabled); |
| combobox->UpdateBorder(); |
| combobox->UpdateAccessibleDefaultActionVerb(); |
| }, |
| base::Unretained(this))); |
| |
| // A layer is applied to make sure that canvas bounds are snapped to pixel |
| // boundaries (for the sake of drawing the arrow). |
| SetPaintToLayer(); |
| layer()->SetFillsBoundsOpaquely(false); |
| |
| views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(), |
| GetCornerRadius()); |
| |
| GetViewAccessibility().SetRole(ax::mojom::Role::kComboBoxSelect); |
| |
| UpdateAccessibleValue(); |
| UpdateExpandedCollapsedAccessibleState(); |
| UpdateAccessibleDefaultActionVerb(); |
| } |
| |
| Combobox::~Combobox() { |
| if (GetInputMethod() && selector_.get()) { |
| // Combobox should have been blurred before destroy. |
| DCHECK(selector_.get() != GetInputMethod()->GetTextInputClient()); |
| } |
| } |
| |
| const gfx::FontList& Combobox::GetFontList() const { |
| return TypographyProvider::Get().GetFont(kContext, kStyle); |
| } |
| |
| void Combobox::SetSelectedIndex(std::optional<size_t> index) { |
| if (selected_index_ == index) { |
| return; |
| } |
| // TODO(pbos): Add (D)CHECKs to validate the selected index. |
| selected_index_ = index; |
| |
| if (selected_index_.has_value()) { |
| GetViewAccessibility().SetPosInSet( |
| base::checked_cast<int>(selected_index_.value())); |
| } |
| |
| if (size_to_largest_label_) { |
| OnPropertyChanged(&selected_index_, kPropertyEffectsPaint); |
| } else { |
| content_size_ = GetContentSize(); |
| OnPropertyChanged(&selected_index_, kPropertyEffectsPreferredSizeChanged); |
| } |
| |
| UpdateAccessibleValue(); |
| } |
| |
| base::CallbackListSubscription Combobox::AddSelectedIndexChangedCallback( |
| views::PropertyChangedCallback callback) { |
| return AddPropertyChangedCallback(&selected_index_, std::move(callback)); |
| } |
| |
| bool Combobox::SelectValue(const std::u16string& value) { |
| for (size_t i = 0; i < GetModel()->GetItemCount(); ++i) { |
| if (value == GetModel()->GetItemAt(i)) { |
| SetSelectedIndex(i); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void Combobox::SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model) { |
| // The swap keeps the outgoing model alive for SetModel(). |
| owned_model_.swap(model); |
| SetModel(owned_model_.get()); |
| } |
| |
| void Combobox::SetModel(ui::ComboboxModel* model) { |
| if (!model) { |
| SetOwnedModel(std::make_unique<internal::EmptyComboboxModel>()); |
| return; |
| } |
| |
| if (model_) { |
| DCHECK(observation_.IsObservingSource(model_.get())); |
| observation_.Reset(); |
| } |
| |
| model_ = model; |
| |
| if (model_) { |
| model_ = model; |
| menu_model_ = std::make_unique<ComboboxMenuModel>(this, model_); |
| observation_.Observe(model_.get()); |
| SetSelectedIndex(model_->GetDefaultIndex()); |
| OnComboboxModelChanged(model_); |
| } |
| } |
| |
| std::u16string Combobox::GetTooltipTextAndAccessibleName() const { |
| return arrow_button_->GetTooltipText(); |
| } |
| |
| void Combobox::SetTooltipTextAndAccessibleName( |
| const std::u16string& tooltip_text) { |
| arrow_button_->SetTooltipText(tooltip_text); |
| if (GetViewAccessibility().GetCachedName().empty()) { |
| GetViewAccessibility().SetName(tooltip_text); |
| } |
| } |
| |
| void Combobox::SetInvalid(bool invalid) { |
| if (invalid == invalid_) { |
| return; |
| } |
| |
| invalid_ = invalid; |
| |
| if (views::FocusRing::Get(this)) { |
| views::FocusRing::Get(this)->SetInvalid(invalid); |
| } |
| |
| UpdateBorder(); |
| OnPropertyChanged(&selected_index_, kPropertyEffectsPaint); |
| } |
| |
| void Combobox::SetBorderColorId(ui::ColorId color_id) { |
| border_color_id_ = color_id; |
| UpdateBorder(); |
| } |
| |
| void Combobox::SetBackgroundColorId(ui::ColorId color_id) { |
| SetBackground(CreateRoundedRectBackground(color_id, GetCornerRadius())); |
| } |
| |
| void Combobox::SetForegroundColorId(ui::ColorId color_id) { |
| foreground_color_id_ = color_id; |
| SchedulePaint(); |
| } |
| |
| void Combobox::SetForegroundIconColorId(ui::ColorId color_id) { |
| foreground_icon_color_id_ = color_id; |
| SchedulePaint(); |
| } |
| |
| void Combobox::SetForegroundTextStyle(style::TextStyle text_style) { |
| foreground_text_style_ = text_style; |
| SchedulePaint(); |
| } |
| |
| void Combobox::SetEventHighlighting(bool should_highlight) { |
| should_highlight_ = should_highlight; |
| AsViewClass<TransparentButton>(arrow_button_) |
| ->UpdateInkDrop(should_highlight); |
| } |
| |
| void Combobox::SetSizeToLargestLabel(bool size_to_largest_label) { |
| if (size_to_largest_label_ == size_to_largest_label) { |
| return; |
| } |
| |
| size_to_largest_label_ = size_to_largest_label; |
| content_size_ = GetContentSize(); |
| OnPropertyChanged(&selected_index_, kPropertyEffectsPreferredSizeChanged); |
| } |
| |
| bool Combobox::IsMenuRunning() const { |
| return menu_runner_ && menu_runner_->IsRunning(); |
| } |
| |
| void Combobox::OnThemeChanged() { |
| View::OnThemeChanged(); |
| OnContentSizeMaybeChanged(); |
| } |
| |
| size_t Combobox::GetRowCount() { |
| return GetModel()->GetItemCount(); |
| } |
| |
| std::optional<size_t> Combobox::GetSelectedRow() { |
| return selected_index_; |
| } |
| |
| void Combobox::SetSelectedRow(std::optional<size_t> row) { |
| std::optional<size_t> prev_index = selected_index_; |
| SetSelectedIndex(row); |
| if (selected_index_ != prev_index) { |
| OnPerformAction(); |
| } |
| } |
| |
| std::u16string Combobox::GetTextForRow(size_t row) { |
| return GetModel()->IsItemSeparatorAt(row) ? std::u16string() |
| : GetModel()->GetItemAt(row); |
| } |
| |
| base::CallbackListSubscription Combobox::AddMenuWillShowCallback( |
| MenuWillShowCallback callback) { |
| return on_menu_will_show_.Add(std::move(callback)); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Combobox, View overrides: |
| |
| gfx::Size Combobox::CalculatePreferredSize( |
| const SizeBounds& /*available_size*/) const { |
| // Limit how small a combobox can be. |
| constexpr int kMinComboboxWidth = 25; |
| |
| // The preferred size will drive the local bounds which in turn is used to set |
| // the minimum width for the dropdown list. |
| int width = std::max(kMinComboboxWidth, content_size_.width()) + |
| LayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING) * |
| 2 + |
| GetInsets().width(); |
| |
| // If an arrow is being shown, add extra width to include that arrow. |
| if (should_show_arrow_) { |
| width += GetComboboxArrowContainerWidthAndMargins(); |
| } |
| |
| return gfx::Size(width, LayoutProvider::GetControlHeightForFont( |
| kContext, kStyle, GetForegroundFontList())); |
| } |
| |
| void Combobox::OnBoundsChanged(const gfx::Rect& previous_bounds) { |
| arrow_button_->SetBounds(0, 0, width(), height()); |
| } |
| |
| bool Combobox::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) { |
| // Escape should close the drop down list when it is active, not host UI. |
| if (e.key_code() != ui::VKEY_ESCAPE || e.IsShiftDown() || e.IsControlDown() || |
| e.IsAltDown() || e.IsAltGrDown()) { |
| return false; |
| } |
| return !!menu_runner_; |
| } |
| |
| bool Combobox::OnKeyPressed(const ui::KeyEvent& e) { |
| // TODO(oshima): handle IME. |
| CHECK_EQ(e.type(), ui::EventType::kKeyPressed); |
| |
| if (!selected_index_.has_value()) { |
| CHECK_EQ(model_->GetItemCount(), 0u); |
| return false; |
| } |
| CHECK_LT(selected_index_.value(), GetModel()->GetItemCount()); |
| |
| #if BUILDFLAG(IS_MAC) |
| if (e.key_code() != ui::VKEY_DOWN && e.key_code() != ui::VKEY_UP && |
| e.key_code() != ui::VKEY_SPACE && e.key_code() != ui::VKEY_HOME && |
| e.key_code() != ui::VKEY_END) { |
| return false; |
| } |
| ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard); |
| return true; |
| #else |
| const auto index_at_or_after = [](ui::ComboboxModel* model, |
| size_t index) -> std::optional<size_t> { |
| for (; index < model->GetItemCount(); ++index) { |
| if (!model->IsItemSeparatorAt(index) && model->IsItemEnabledAt(index)) { |
| return index; |
| } |
| } |
| return std::nullopt; |
| }; |
| const auto index_before = [](ui::ComboboxModel* model, |
| size_t index) -> std::optional<size_t> { |
| for (; index > 0; --index) { |
| const auto prev = index - 1; |
| if (!model->IsItemSeparatorAt(prev) && model->IsItemEnabledAt(prev)) { |
| return prev; |
| } |
| } |
| return std::nullopt; |
| }; |
| |
| std::optional<size_t> new_index; |
| switch (e.key_code()) { |
| // Show the menu on F4 without modifiers. |
| case ui::VKEY_F4: |
| if (e.IsAltDown() || e.IsAltGrDown() || e.IsControlDown()) { |
| return false; |
| } |
| ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard); |
| return true; |
| |
| // Move to the next item if any, or show the menu on Alt+Down like Windows. |
| case ui::VKEY_DOWN: |
| if (e.IsAltDown()) { |
| ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard); |
| return true; |
| } |
| new_index = index_at_or_after(GetModel(), selected_index_.value() + 1); |
| break; |
| |
| // Move to the end of the list. |
| case ui::VKEY_END: |
| case ui::VKEY_NEXT: // Page down. |
| new_index = index_before(GetModel(), GetModel()->GetItemCount()); |
| break; |
| |
| // Move to the beginning of the list. |
| case ui::VKEY_HOME: |
| case ui::VKEY_PRIOR: // Page up. |
| new_index = index_at_or_after(GetModel(), 0); |
| break; |
| |
| // Move to the previous item if any. |
| case ui::VKEY_UP: |
| new_index = index_before(GetModel(), selected_index_.value()); |
| break; |
| |
| case ui::VKEY_RETURN: |
| case ui::VKEY_SPACE: |
| ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard); |
| return true; |
| |
| default: |
| return false; |
| } |
| |
| if (new_index.has_value()) { |
| SetSelectedIndex(new_index); |
| OnPerformAction(); |
| } |
| return true; |
| #endif // BUILDFLAG(IS_MAC) |
| } |
| |
| void Combobox::OnPaint(gfx::Canvas* canvas) { |
| OnPaintBackground(canvas); |
| PaintIconAndText(canvas); |
| OnPaintBorder(canvas); |
| } |
| |
| void Combobox::OnFocus() { |
| if (GetInputMethod()) { |
| GetInputMethod()->SetFocusedTextInputClient(GetPrefixSelector()); |
| } |
| |
| View::OnFocus(); |
| // Border renders differently when focused. |
| SchedulePaint(); |
| } |
| |
| void Combobox::OnBlur() { |
| if (GetInputMethod()) { |
| GetInputMethod()->DetachTextInputClient(GetPrefixSelector()); |
| } |
| |
| if (selector_) { |
| selector_->OnViewBlur(); |
| } |
| // Border renders differently when focused. |
| SchedulePaint(); |
| } |
| |
| bool Combobox::HandleAccessibleAction(const ui::AXActionData& action_data) { |
| // The action handling in View would generate a mouse event and send it to |
| // |this|. However, mouse events for Combobox are handled by |arrow_button_|, |
| // which is hidden from the a11y tree (so can't expose actions). Rather than |
| // forwarding ax::mojom::Action::kDoDefault to View and then forwarding the |
| // mouse event it generates to |arrow_button_| to have it forward back to the |
| // callback on |this|, just handle the action explicitly here and bypass View. |
| if (GetEnabled() && action_data.action == ax::mojom::Action::kDoDefault) { |
| ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard); |
| return true; |
| } |
| return View::HandleAccessibleAction(action_data); |
| } |
| |
| void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) { |
| DCHECK_EQ(model_, model); |
| |
| if (IsMenuRunning()) { |
| menu_runner_.reset(); |
| UpdateExpandedCollapsedAccessibleState(); |
| } |
| |
| // If the selection is no longer valid (or the model is empty), restore the |
| // default index. |
| if (!selected_index_.has_value() || |
| selected_index_ >= model_->GetItemCount() || |
| model_->GetItemCount() == 0 || |
| model_->IsItemSeparatorAt(selected_index_.value())) { |
| SetSelectedIndex(model_->GetDefaultIndex()); |
| } |
| |
| OnContentSizeMaybeChanged(); |
| SchedulePaint(); |
| |
| GetViewAccessibility().SetSetSize( |
| base::checked_cast<int>(model_->GetItemCount())); |
| } |
| |
| void Combobox::OnComboboxModelDestroying(ui::ComboboxModel* model) { |
| SetModel(nullptr); |
| } |
| |
| const base::RepeatingClosure& Combobox::GetCallback() const { |
| return callback_; |
| } |
| |
| const std::unique_ptr<ui::ComboboxModel>& Combobox::GetOwnedModel() const { |
| return owned_model_; |
| } |
| |
| void Combobox::UpdateBorder() { |
| if (!GetEnabledInViewsSubtree()) { |
| SetBorder(nullptr); |
| return; |
| } |
| SetBorder(CreateRoundedRectBorder( |
| kBorderThickness, GetCornerRadius(), |
| invalid_ |
| ? ui::kColorAlertHighSeverity |
| : border_color_id_.value_or(ui::kColorComboboxContainerOutline))); |
| } |
| |
| void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const { |
| rect->set_x(GetMirroredXForRect(*rect)); |
| } |
| |
| void Combobox::PaintIconAndText(gfx::Canvas* canvas) { |
| if (!selected_index_.has_value()) { |
| return; |
| } |
| CHECK_LT(selected_index_.value(), GetModel()->GetItemCount()); |
| |
| gfx::Insets insets = GetInsets(); |
| insets += gfx::Insets::VH(0, LayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING)); |
| |
| gfx::ScopedCanvas scoped_canvas(canvas); |
| canvas->ClipRect(GetContentsBounds()); |
| |
| int x = insets.left(); |
| int y = insets.top(); |
| int contents_height = height() - insets.height(); |
| |
| // Draw the icon. |
| ui::ImageModel icon = GetModel()->GetIconAt(selected_index_.value()); |
| if (!icon.IsEmpty()) { |
| // Update the icon color if provided and if the icon color can be changed. |
| if (foreground_icon_color_id_ && icon.IsVectorIcon()) { |
| icon = ui::ImageModel::FromVectorIcon(*icon.GetVectorIcon().vector_icon(), |
| foreground_icon_color_id_.value(), |
| icon.GetVectorIcon().icon_size()); |
| } |
| gfx::ImageSkia icon_skia = icon.Rasterize(GetColorProvider()); |
| int icon_y = y + (contents_height - icon_skia.height()) / 2; |
| gfx::Rect icon_bounds(x, icon_y, icon_skia.width(), icon_skia.height()); |
| AdjustBoundsForRTLUI(&icon_bounds); |
| canvas->DrawImageInt(icon_skia, icon_bounds.x(), icon_bounds.y()); |
| x += icon_skia.width(); |
| } |
| |
| // Draw the text. |
| SkColor text_color = |
| foreground_color_id_ |
| ? GetColorProvider()->GetColor(*foreground_color_id_) |
| : GetTextColorForEnableState(*this, GetEnabledInViewsSubtree()); |
| std::u16string text = GetModel()->GetItemAt(*selected_index_); |
| const gfx::FontList& font_list = GetForegroundFontList(); |
| |
| // If the text is not empty, add padding between it and the icon. If there |
| // was an empty icon, this padding is not necessary. |
| if (!text.empty() && !icon.IsEmpty()) { |
| x += LayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_RELATED_LABEL_HORIZONTAL); |
| } |
| |
| // The total width of the text is the minimum of either the string width, |
| // or the available space, accounting for optional arrow. |
| int text_width = gfx::GetStringWidth(text, font_list); |
| int available_width = width() - x - insets.right(); |
| if (should_show_arrow_) { |
| available_width -= GetComboboxArrowContainerWidthAndMargins(); |
| } |
| text_width = std::min(text_width, available_width); |
| |
| gfx::Rect text_bounds(x, y, text_width, contents_height); |
| AdjustBoundsForRTLUI(&text_bounds); |
| canvas->DrawStringRect(text, font_list, text_color, text_bounds); |
| |
| // Draw the arrow. |
| // TODO(crbug.com/40247801): Replace placeholder spacing and color values for |
| // ChromeRefresh2023. |
| if (should_show_arrow_) { |
| gfx::Rect arrow_bounds(width() - GetComboboxArrowContainerWidthAndMargins(), |
| 0, GetComboboxArrowContainerWidth(), height()); |
| arrow_bounds.ClampToCenteredSize(ComboboxArrowSize()); |
| AdjustBoundsForRTLUI(&arrow_bounds); |
| |
| PaintComboboxArrow(text_color, arrow_bounds, canvas); |
| } |
| } |
| |
| void Combobox::ArrowButtonPressed(const ui::Event& event) { |
| if (!GetEnabled()) { |
| return; |
| } |
| |
| // TODO(hajimehoshi): Fix the problem that the arrow button blinks when |
| // cliking this while the dropdown menu is opened. |
| if ((base::TimeTicks::Now() - closed_time_) > |
| kMinimumTimeBetweenButtonClicks) { |
| ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event)); |
| } |
| } |
| |
| void Combobox::ShowDropDownMenu(ui::mojom::MenuSourceType source_type) { |
| on_menu_will_show_.Notify(); |
| |
| constexpr int kMenuBorderWidthTop = 1; |
| // Menu's requested position's width should be the same as local bounds so the |
| // border of the menu lines up with the border of the combobox. The y |
| // coordinate however should be shifted to the bottom with the border with not |
| // to overlap with the combobox border. |
| gfx::Rect lb = GetLocalBounds(); |
| gfx::Point menu_position(lb.origin()); |
| menu_position.set_y(menu_position.y() + kMenuBorderWidthTop); |
| |
| View::ConvertPointToScreen(this, &menu_position); |
| |
| gfx::Rect bounds(menu_position, lb.size()); |
| // If check marks exist in the combobox, adjust with bounds width to account |
| // for them. |
| if (!size_to_largest_label_) { |
| bounds.set_width(MaybeAdjustWidthForCheckmarks(bounds.width())); |
| } |
| |
| Button::ButtonState original_state = arrow_button_->GetState(); |
| arrow_button_->SetState(Button::STATE_PRESSED); |
| |
| // Allow |menu_runner_| to be set by the testing API, but if this method is |
| // ever invoked recursively, ensure the old menu is closed. |
| if (!menu_runner_ || IsMenuRunning()) { |
| menu_runner_ = std::make_unique<MenuRunner>( |
| menu_model_.get(), MenuRunner::COMBOBOX, |
| base::BindRepeating(&Combobox::OnMenuClosed, base::Unretained(this), |
| original_state)); |
| } |
| if (should_highlight_) { |
| InkDrop::Get(arrow_button_) |
| ->AnimateToState(InkDropState::ACTIVATED, nullptr); |
| } |
| menu_runner_->RunMenuAt(GetWidget(), nullptr, bounds, |
| MenuAnchorPosition::kTopLeft, source_type); |
| UpdateExpandedCollapsedAccessibleState(); |
| } |
| |
| void Combobox::OnMenuClosed(Button::ButtonState original_button_state) { |
| if (should_highlight_) { |
| InkDrop::Get(arrow_button_) |
| ->AnimateToState(InkDropState::DEACTIVATED, nullptr); |
| InkDrop::Get(arrow_button_)->GetInkDrop()->SetHovered(IsMouseHovered()); |
| } |
| menu_runner_.reset(); |
| arrow_button_->SetState(original_button_state); |
| closed_time_ = base::TimeTicks::Now(); |
| UpdateExpandedCollapsedAccessibleState(); |
| } |
| |
| void Combobox::MenuSelectionAt(size_t index) { |
| if (!menu_selection_at_callback_ || !menu_selection_at_callback_.Run(index)) { |
| SetSelectedIndex(index); |
| OnPerformAction(); |
| } |
| } |
| |
| void Combobox::OnPerformAction() { |
| SchedulePaint(); |
| |
| if (callback_) { |
| callback_.Run(); |
| } |
| |
| // Note |this| may be deleted by |callback_|. |
| } |
| |
| gfx::Size Combobox::GetContentSize() const { |
| const gfx::FontList& font_list = GetForegroundFontList(); |
| int height = font_list.GetHeight(); |
| int width = 0; |
| for (size_t i = 0; i < GetModel()->GetItemCount(); ++i) { |
| if (model_->IsItemSeparatorAt(i)) { |
| continue; |
| } |
| |
| if (size_to_largest_label_ || i == selected_index_) { |
| int item_width = 0; |
| ui::ImageModel icon = GetModel()->GetIconAt(i); |
| std::u16string text = GetModel()->GetItemAt(i); |
| if (!icon.IsEmpty()) { |
| gfx::ImageSkia icon_skia; |
| if (GetWidget()) { |
| icon_skia = icon.Rasterize(GetColorProvider()); |
| } |
| item_width += icon_skia.width(); |
| height = std::max(height, icon_skia.height()); |
| |
| // If both the text and icon are not empty, include padding between. |
| // We do not include this padding if there is no icon present. |
| if (!text.empty()) { |
| item_width += LayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_RELATED_LABEL_HORIZONTAL); |
| } |
| } |
| |
| // If text is not empty, the content size needs to include the text width |
| if (!text.empty()) { |
| item_width += gfx::GetStringWidth(GetModel()->GetItemAt(i), font_list); |
| } |
| |
| if (size_to_largest_label_) { |
| item_width = MaybeAdjustWidthForCheckmarks(item_width); |
| } |
| width = std::max(width, item_width); |
| } |
| } |
| return gfx::Size(width, height); |
| } |
| |
| int Combobox::MaybeAdjustWidthForCheckmarks(int original_width) const { |
| return MenuConfig::instance().check_selected_combobox_item |
| ? original_width + kMenuCheckSize + |
| LayoutProvider::Get()->GetDistanceMetric( |
| DISTANCE_RELATED_BUTTON_HORIZONTAL) |
| : original_width; |
| } |
| |
| void Combobox::OnContentSizeMaybeChanged() { |
| content_size_ = GetContentSize(); |
| PreferredSizeChanged(); |
| } |
| |
| PrefixSelector* Combobox::GetPrefixSelector() { |
| if (!selector_) { |
| selector_ = std::make_unique<PrefixSelector>(this, this); |
| } |
| return selector_.get(); |
| } |
| |
| const gfx::FontList& Combobox::GetForegroundFontList() const { |
| if (foreground_text_style_) { |
| return TypographyProvider::Get().GetFont(kContext, *foreground_text_style_); |
| } |
| return GetFontList(); |
| } |
| |
| void Combobox::UpdateExpandedCollapsedAccessibleState() const { |
| if (menu_runner_) { |
| GetViewAccessibility().SetIsExpanded(); |
| } else { |
| GetViewAccessibility().SetIsCollapsed(); |
| } |
| } |
| |
| void Combobox::UpdateAccessibleValue() const { |
| if (model_->GetItemCount() > 0 && selected_index_.has_value()) { |
| GetViewAccessibility().SetValue(model_->GetItemAt(selected_index_.value())); |
| } else { |
| GetViewAccessibility().RemoveValue(); |
| } |
| } |
| |
| void Combobox::UpdateAccessibleDefaultActionVerb() { |
| if (GetEnabledInViewsSubtree()) { |
| GetViewAccessibility().SetDefaultActionVerb( |
| ax::mojom::DefaultActionVerb::kOpen); |
| } else { |
| GetViewAccessibility().RemoveDefaultActionVerb(); |
| } |
| } |
| |
| BEGIN_METADATA(Combobox) |
| ADD_PROPERTY_METADATA(base::RepeatingClosure, Callback) |
| ADD_PROPERTY_METADATA(std::unique_ptr<ui::ComboboxModel>, OwnedModel) |
| ADD_PROPERTY_METADATA(ui::ComboboxModel*, Model) |
| ADD_PROPERTY_METADATA(std::optional<size_t>, SelectedIndex) |
| ADD_PROPERTY_METADATA(bool, Invalid) |
| ADD_PROPERTY_METADATA(bool, SizeToLargestLabel) |
| ADD_PROPERTY_METADATA(std::u16string, TooltipTextAndAccessibleName) |
| END_METADATA |
| |
| } // namespace views |