// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/controls/combobox/combobox.h" #include #include #include #include "base/bind.h" #include "base/check_op.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/models/image_model.h" #include "ui/base/models/menu_model.h" #include "ui/base/ui_base_types.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/native_theme/native_theme.h" #include "ui/native_theme/themed_vector_icon.h" #include "ui/views/animation/flood_fill_ink_drop_ripple.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_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/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/metadata/metadata_impl_macros.h" #include "ui/views/mouse_constants.h" #include "ui/views/style/platform_style.h" #include "ui/views/style/typography.h" #include "ui/views/widget/widget.h" namespace views { namespace { // Used to indicate that no item is currently selected by the user. constexpr int kNoSelection = -1; SkColor GetTextColorForEnableState(const Combobox& combobox, bool enabled) { const int style = enabled ? style::STYLE_PRIMARY : style::STYLE_DISABLED; return style::GetColor(combobox, style::CONTEXT_TEXTFIELD, style); } gfx::ImageSkia GetImageSkiaFromImageModel(const ui::ImageModel* model, const ui::NativeTheme* native_theme) { DCHECK(model); DCHECK(!model->IsEmpty()); return model->IsImage() ? model->GetImage().AsImageSkia() : ui::ThemedVectorIcon(model->GetVectorIcon()) .GetImageSkia(native_theme); } // The transparent button which holds a button state but is not rendered. class TransparentButton : public Button { public: explicit TransparentButton(PressedCallback callback) : Button(std::move(callback)) { SetFocusBehavior(FocusBehavior::NEVER); button_controller()->set_notify_action( ButtonController::NotifyAction::kOnPress); SetInkDropMode(InkDropMode::ON); SetHasInkDropActionOnClick(true); } ~TransparentButton() override = default; bool OnMousePressed(const ui::MouseEvent& mouse_event) override { #if !defined(OS_APPLE) // 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(); } // Overridden from InkDropHost: std::unique_ptr CreateInkDrop() override { std::unique_ptr ink_drop = CreateDefaultInkDropImpl(); ink_drop->SetShowHighlightOnHover(false); return std::move(ink_drop); } std::unique_ptr CreateInkDropRipple() const override { return std::unique_ptr( new views::FloodFillInkDropRipple( size(), GetInkDropCenterBasedOnLastEvent(), GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_LabelEnabledColor), GetInkDropVisibleOpacity())); } private: DISALLOW_COPY_AND_ASSIGN(TransparentButton); }; #if !defined(OS_APPLE) // Returns the next or previous valid index (depending on |increment|'s value). // Skips separator or disabled indices. Returns -1 if there is no valid adjacent // index. int GetAdjacentIndex(ui::ComboboxModel* model, int increment, int index) { DCHECK(increment == -1 || increment == 1); index += increment; while (index >= 0 && index < model->GetItemCount()) { if (!model->IsItemSeparatorAt(index) || !model->IsItemEnabledAt(index)) return index; index += increment; } return kNoSelection; } #endif } // namespace // Adapts a ui::ComboboxModel to a ui::MenuModel. class Combobox::ComboboxMenuModel : public ui::MenuModel { public: ComboboxMenuModel(Combobox* owner, ui::ComboboxModel* model) : owner_(owner), model_(model) {} ~ComboboxMenuModel() override = default; private: bool UseCheckmarks() const { return MenuConfig::instance().check_selected_combobox_item; } // Overridden from MenuModel: bool HasIcons() const override { for (int i = 0; i < GetItemCount(); ++i) { if (!GetIconAt(i).IsEmpty()) return true; } return false; } int GetItemCount() const override { return model_->GetItemCount(); } ItemType GetTypeAt(int index) const override { if (model_->IsItemSeparatorAt(index)) return TYPE_SEPARATOR; return UseCheckmarks() ? TYPE_CHECK : TYPE_COMMAND; } ui::MenuSeparatorType GetSeparatorTypeAt(int index) const override { return ui::NORMAL_SEPARATOR; } int GetCommandIdAt(int index) const override { // Define the id of the first item in the menu (since it needs to be > 0) constexpr int kFirstMenuItemId = 1000; return index + kFirstMenuItemId; } base::string16 GetLabelAt(int index) const override { // Inserting the Unicode formatting characters if necessary so that the // text is displayed correctly in right-to-left UIs. base::string16 text = model_->GetDropDownTextAt(index); base::i18n::AdjustStringForLocaleDirection(&text); return text; } base::string16 GetSecondaryLabelAt(int index) const override { base::string16 text = model_->GetDropDownSecondaryTextAt(index); base::i18n::AdjustStringForLocaleDirection(&text); return text; } bool IsItemDynamicAt(int index) const override { return true; } const gfx::FontList* GetLabelFontListAt(int index) const override { return &owner_->GetFontList(); } bool GetAcceleratorAt(int index, ui::Accelerator* accelerator) const override { return false; } bool IsItemCheckedAt(int index) const override { return UseCheckmarks() && index == owner_->selected_index_; } int GetGroupIdAt(int index) const override { return -1; } ui::ImageModel GetIconAt(int index) const override { return model_->GetDropDownIconAt(index); } ui::ButtonMenuItemModel* GetButtonMenuItemAt(int index) const override { return nullptr; } bool IsEnabledAt(int index) const override { return model_->IsItemEnabledAt(index); } void ActivatedAt(int index) override { owner_->SetSelectedIndex(index); owner_->OnPerformAction(); } void ActivatedAt(int index, int event_flags) override { ActivatedAt(index); } MenuModel* GetSubmenuModelAt(int index) const override { return nullptr; } Combobox* owner_; // Weak. Owns this. ui::ComboboxModel* model_; // Weak. DISALLOW_COPY_AND_ASSIGN(ComboboxMenuModel); }; //////////////////////////////////////////////////////////////////////////////// // Combobox, public: Combobox::Combobox(int text_context, int text_style) : Combobox(std::make_unique()) {} Combobox::Combobox(std::unique_ptr model, int text_context, int text_style) : Combobox(model.get(), text_context, text_style) { owned_model_ = std::move(model); } Combobox::Combobox(ui::ComboboxModel* model, int text_context, int text_style) : text_context_(text_context), text_style_(text_style), arrow_button_(new TransparentButton( base::BindRepeating(&Combobox::ArrowButtonPressed, base::Unretained(this)))) { SetModel(model); #if defined(OS_APPLE) SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); #else SetFocusBehavior(FocusBehavior::ALWAYS); #endif UpdateBorder(); arrow_button_->SetVisible(true); AddChildView(arrow_button_); // 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); focus_ring_ = FocusRing::Install(this); } 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 style::GetFont(text_context_, text_style_); } void Combobox::SetSelectedIndex(int index) { if (selected_index_ == index) return; selected_index_ = index; if (size_to_largest_label_) { OnPropertyChanged(&selected_index_, kPropertyEffectsPaint); } else { content_size_ = GetContentSize(); OnPropertyChanged(&selected_index_, kPropertyEffectsPreferredSizeChanged); } } bool Combobox::SelectValue(const base::string16& value) { for (int i = 0; i < GetModel()->GetItemCount(); ++i) { if (value == GetModel()->GetItemAt(i)) { SetSelectedIndex(i); return true; } } return false; } void Combobox::SetOwnedModel(std::unique_ptr model) { // The swap keeps the outgoing model alive for SetModel(). owned_model_.swap(model); SetModel(owned_model_.get()); } void Combobox::SetModel(ui::ComboboxModel* model) { DCHECK(model) << "After construction, the model must not be null."; if (model_) observer_.Remove(model_); model_ = model; if (model_) { menu_model_ = std::make_unique(this, model_); observer_.Add(model_); SetSelectedIndex(model_->GetDefaultIndex()); OnComboboxModelChanged(model_); } } base::string16 Combobox::GetTooltipTextAndAccessibleName() const { return arrow_button_->GetTooltipText(); } void Combobox::SetTooltipTextAndAccessibleName( const base::string16& tooltip_text) { arrow_button_->SetTooltipText(tooltip_text); if (accessible_name_.empty()) accessible_name_ = tooltip_text; } void Combobox::SetAccessibleName(const base::string16& name) { accessible_name_ = name; } base::string16 Combobox::GetAccessibleName() const { return accessible_name_; } void Combobox::SetInvalid(bool invalid) { if (invalid == invalid_) return; invalid_ = invalid; if (focus_ring_) focus_ring_->SetInvalid(invalid); UpdateBorder(); OnPropertyChanged(&selected_index_, kPropertyEffectsPaint); } 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); } void Combobox::OnThemeChanged() { View::OnThemeChanged(); SetBackground( CreateBackgroundFromPainter(Painter::CreateSolidRoundRectPainter( GetNativeTheme()->GetSystemColor( ui::NativeTheme::kColorId_TextfieldDefaultBackground), FocusableBorder::kCornerRadiusDp))); } int Combobox::GetRowCount() { return GetModel()->GetItemCount(); } int Combobox::GetSelectedRow() { return selected_index_; } void Combobox::SetSelectedRow(int row) { int prev_index = selected_index_; SetSelectedIndex(row); if (selected_index_ != prev_index) OnPerformAction(); } base::string16 Combobox::GetTextForRow(int row) { return GetModel()->IsItemSeparatorAt(row) ? base::string16() : GetModel()->GetItemAt(row); } //////////////////////////////////////////////////////////////////////////////// // Combobox, View overrides: gfx::Size Combobox::CalculatePreferredSize() 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. gfx::Insets insets = GetInsets(); const LayoutProvider* provider = LayoutProvider::Get(); insets += gfx::Insets( provider->GetDistanceMetric(DISTANCE_CONTROL_VERTICAL_TEXT_PADDING), provider->GetDistanceMetric(DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING)); int total_width = std::max(kMinComboboxWidth, content_size_.width()) + insets.width() + kComboboxArrowContainerWidth; return gfx::Size(total_width, content_size_.height() + insets.height()); } 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. DCHECK_EQ(e.type(), ui::ET_KEY_PRESSED); DCHECK_GE(selected_index_, 0); DCHECK_LT(selected_index_, GetModel()->GetItemCount()); if (selected_index_ < 0 || selected_index_ > GetModel()->GetItemCount()) SetSelectedIndex(0); bool show_menu = false; int new_index = kNoSelection; switch (e.key_code()) { #if defined(OS_APPLE) case ui::VKEY_DOWN: case ui::VKEY_UP: case ui::VKEY_SPACE: case ui::VKEY_HOME: case ui::VKEY_END: // On Mac, navigation keys should always just show the menu first. show_menu = true; break; #else // Show the menu on F4 without modifiers. case ui::VKEY_F4: if (e.IsAltDown() || e.IsAltGrDown() || e.IsControlDown()) return false; show_menu = true; break; // Move to the next item if any, or show the menu on Alt+Down like Windows. case ui::VKEY_DOWN: if (e.IsAltDown()) show_menu = true; else new_index = GetAdjacentIndex(GetModel(), 1, selected_index_); break; // Move to the end of the list. case ui::VKEY_END: case ui::VKEY_NEXT: // Page down. new_index = GetAdjacentIndex(GetModel(), -1, GetModel()->GetItemCount()); break; // Move to the beginning of the list. case ui::VKEY_HOME: case ui::VKEY_PRIOR: // Page up. new_index = GetAdjacentIndex(GetModel(), 1, -1); break; // Move to the previous item if any. case ui::VKEY_UP: new_index = GetAdjacentIndex(GetModel(), -1, selected_index_); break; case ui::VKEY_RETURN: case ui::VKEY_SPACE: show_menu = true; break; #endif // OS_APPLE default: return false; } if (show_menu) { ShowDropDownMenu(ui::MENU_SOURCE_KEYBOARD); } else if (new_index != selected_index_ && new_index != kNoSelection) { DCHECK(!GetModel()->IsItemSeparatorAt(new_index)); SetSelectedIndex(new_index); OnPerformAction(); } return true; } 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(); } void Combobox::GetAccessibleNodeData(ui::AXNodeData* node_data) { // ax::mojom::Role::kComboBox is for UI elements with a dropdown and // an editable text field, which views::Combobox does not have. Use // ax::mojom::Role::kPopUpButton to match an HTML