blob: fbe85b7dab0339dedd70381486f8765f06622cbd [file] [log] [blame]
// 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