1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/views/controls/combobox/combobox.h"
6
7 #include <algorithm>
8 #include <memory>
9 #include <utility>
10
11 #include "base/bind.h"
12 #include "base/check_op.h"
13 #include "build/build_config.h"
14 #include "ui/accessibility/ax_action_data.h"
15 #include "ui/accessibility/ax_enums.mojom.h"
16 #include "ui/accessibility/ax_node_data.h"
17 #include "ui/base/ime/input_method.h"
18 #include "ui/base/models/image_model.h"
19 #include "ui/base/models/menu_model.h"
20 #include "ui/base/ui_base_types.h"
21 #include "ui/events/event.h"
22 #include "ui/gfx/canvas.h"
23 #include "ui/gfx/color_palette.h"
24 #include "ui/gfx/scoped_canvas.h"
25 #include "ui/gfx/text_utils.h"
26 #include "ui/native_theme/native_theme.h"
27 #include "ui/native_theme/themed_vector_icon.h"
28 #include "ui/views/animation/flood_fill_ink_drop_ripple.h"
29 #include "ui/views/animation/ink_drop_impl.h"
30 #include "ui/views/background.h"
31 #include "ui/views/controls/button/button.h"
32 #include "ui/views/controls/button/button_controller.h"
33 #include "ui/views/controls/combobox/combobox_util.h"
34 #include "ui/views/controls/combobox/empty_combobox_model.h"
35 #include "ui/views/controls/focus_ring.h"
36 #include "ui/views/controls/focusable_border.h"
37 #include "ui/views/controls/menu/menu_config.h"
38 #include "ui/views/controls/menu/menu_runner.h"
39 #include "ui/views/controls/prefix_selector.h"
40 #include "ui/views/layout/layout_provider.h"
41 #include "ui/views/metadata/metadata_impl_macros.h"
42 #include "ui/views/mouse_constants.h"
43 #include "ui/views/style/platform_style.h"
44 #include "ui/views/style/typography.h"
45 #include "ui/views/widget/widget.h"
46
47 namespace views {
48
49 namespace {
50
51 // Used to indicate that no item is currently selected by the user.
52 constexpr int kNoSelection = -1;
53
GetTextColorForEnableState(const Combobox & combobox,bool enabled)54 SkColor GetTextColorForEnableState(const Combobox& combobox, bool enabled) {
55 const int style = enabled ? style::STYLE_PRIMARY : style::STYLE_DISABLED;
56 return style::GetColor(combobox, style::CONTEXT_TEXTFIELD, style);
57 }
58
GetImageSkiaFromImageModel(const ui::ImageModel * model,const ui::NativeTheme * native_theme)59 gfx::ImageSkia GetImageSkiaFromImageModel(const ui::ImageModel* model,
60 const ui::NativeTheme* native_theme) {
61 DCHECK(model);
62 DCHECK(!model->IsEmpty());
63 return model->IsImage() ? model->GetImage().AsImageSkia()
64 : ui::ThemedVectorIcon(model->GetVectorIcon())
65 .GetImageSkia(native_theme);
66 }
67
68 // The transparent button which holds a button state but is not rendered.
69 class TransparentButton : public Button {
70 public:
TransparentButton(PressedCallback callback)71 explicit TransparentButton(PressedCallback callback)
72 : Button(std::move(callback)) {
73 SetFocusBehavior(FocusBehavior::NEVER);
74 button_controller()->set_notify_action(
75 ButtonController::NotifyAction::kOnPress);
76
77 SetInkDropMode(InkDropMode::ON);
78 SetHasInkDropActionOnClick(true);
79 }
80 ~TransparentButton() override = default;
81
OnMousePressed(const ui::MouseEvent & mouse_event)82 bool OnMousePressed(const ui::MouseEvent& mouse_event) override {
83 #if !defined(OS_APPLE)
84 // On Mac, comboboxes do not take focus on mouse click, but on other
85 // platforms they do.
86 parent()->RequestFocus();
87 #endif
88 return Button::OnMousePressed(mouse_event);
89 }
90
GetAnimationValue() const91 double GetAnimationValue() const {
92 return hover_animation().GetCurrentValue();
93 }
94
95 // Overridden from InkDropHost:
CreateInkDrop()96 std::unique_ptr<InkDrop> CreateInkDrop() override {
97 std::unique_ptr<views::InkDropImpl> ink_drop = CreateDefaultInkDropImpl();
98 ink_drop->SetShowHighlightOnHover(false);
99 return std::move(ink_drop);
100 }
101
CreateInkDropRipple() const102 std::unique_ptr<InkDropRipple> CreateInkDropRipple() const override {
103 return std::unique_ptr<views::InkDropRipple>(
104 new views::FloodFillInkDropRipple(
105 size(), GetInkDropCenterBasedOnLastEvent(),
106 GetNativeTheme()->GetSystemColor(
107 ui::NativeTheme::kColorId_LabelEnabledColor),
108 GetInkDropVisibleOpacity()));
109 }
110
111 private:
112 DISALLOW_COPY_AND_ASSIGN(TransparentButton);
113 };
114
115 #if !defined(OS_APPLE)
116 // Returns the next or previous valid index (depending on |increment|'s value).
117 // Skips separator or disabled indices. Returns -1 if there is no valid adjacent
118 // index.
GetAdjacentIndex(ui::ComboboxModel * model,int increment,int index)119 int GetAdjacentIndex(ui::ComboboxModel* model, int increment, int index) {
120 DCHECK(increment == -1 || increment == 1);
121
122 index += increment;
123 while (index >= 0 && index < model->GetItemCount()) {
124 if (!model->IsItemSeparatorAt(index) || !model->IsItemEnabledAt(index))
125 return index;
126 index += increment;
127 }
128 return kNoSelection;
129 }
130 #endif
131
132 } // namespace
133
134 // Adapts a ui::ComboboxModel to a ui::MenuModel.
135 class Combobox::ComboboxMenuModel : public ui::MenuModel {
136 public:
ComboboxMenuModel(Combobox * owner,ui::ComboboxModel * model)137 ComboboxMenuModel(Combobox* owner, ui::ComboboxModel* model)
138 : owner_(owner), model_(model) {}
139 ~ComboboxMenuModel() override = default;
140
141 private:
UseCheckmarks() const142 bool UseCheckmarks() const {
143 return MenuConfig::instance().check_selected_combobox_item;
144 }
145
146 // Overridden from MenuModel:
HasIcons() const147 bool HasIcons() const override {
148 for (int i = 0; i < GetItemCount(); ++i) {
149 if (!GetIconAt(i).IsEmpty())
150 return true;
151 }
152 return false;
153 }
154
GetItemCount() const155 int GetItemCount() const override { return model_->GetItemCount(); }
156
GetTypeAt(int index) const157 ItemType GetTypeAt(int index) const override {
158 if (model_->IsItemSeparatorAt(index))
159 return TYPE_SEPARATOR;
160 return UseCheckmarks() ? TYPE_CHECK : TYPE_COMMAND;
161 }
162
GetSeparatorTypeAt(int index) const163 ui::MenuSeparatorType GetSeparatorTypeAt(int index) const override {
164 return ui::NORMAL_SEPARATOR;
165 }
166
GetCommandIdAt(int index) const167 int GetCommandIdAt(int index) const override {
168 // Define the id of the first item in the menu (since it needs to be > 0)
169 constexpr int kFirstMenuItemId = 1000;
170 return index + kFirstMenuItemId;
171 }
172
GetLabelAt(int index) const173 base::string16 GetLabelAt(int index) const override {
174 // Inserting the Unicode formatting characters if necessary so that the
175 // text is displayed correctly in right-to-left UIs.
176 base::string16 text = model_->GetDropDownTextAt(index);
177 base::i18n::AdjustStringForLocaleDirection(&text);
178 return text;
179 }
180
GetSecondaryLabelAt(int index) const181 base::string16 GetSecondaryLabelAt(int index) const override {
182 base::string16 text = model_->GetDropDownSecondaryTextAt(index);
183 base::i18n::AdjustStringForLocaleDirection(&text);
184 return text;
185 }
186
IsItemDynamicAt(int index) const187 bool IsItemDynamicAt(int index) const override { return true; }
188
GetLabelFontListAt(int index) const189 const gfx::FontList* GetLabelFontListAt(int index) const override {
190 return &owner_->GetFontList();
191 }
192
GetAcceleratorAt(int index,ui::Accelerator * accelerator) const193 bool GetAcceleratorAt(int index,
194 ui::Accelerator* accelerator) const override {
195 return false;
196 }
197
IsItemCheckedAt(int index) const198 bool IsItemCheckedAt(int index) const override {
199 return UseCheckmarks() && index == owner_->selected_index_;
200 }
201
GetGroupIdAt(int index) const202 int GetGroupIdAt(int index) const override { return -1; }
203
GetIconAt(int index) const204 ui::ImageModel GetIconAt(int index) const override {
205 return model_->GetDropDownIconAt(index);
206 }
207
GetButtonMenuItemAt(int index) const208 ui::ButtonMenuItemModel* GetButtonMenuItemAt(int index) const override {
209 return nullptr;
210 }
211
IsEnabledAt(int index) const212 bool IsEnabledAt(int index) const override {
213 return model_->IsItemEnabledAt(index);
214 }
215
ActivatedAt(int index)216 void ActivatedAt(int index) override {
217 owner_->SetSelectedIndex(index);
218 owner_->OnPerformAction();
219 }
220
ActivatedAt(int index,int event_flags)221 void ActivatedAt(int index, int event_flags) override { ActivatedAt(index); }
222
GetSubmenuModelAt(int index) const223 MenuModel* GetSubmenuModelAt(int index) const override { return nullptr; }
224
225 Combobox* owner_; // Weak. Owns this.
226 ui::ComboboxModel* model_; // Weak.
227
228 DISALLOW_COPY_AND_ASSIGN(ComboboxMenuModel);
229 };
230
231 ////////////////////////////////////////////////////////////////////////////////
232 // Combobox, public:
233
Combobox(int text_context,int text_style)234 Combobox::Combobox(int text_context, int text_style)
235 : Combobox(std::make_unique<internal::EmptyComboboxModel>()) {}
236
Combobox(std::unique_ptr<ui::ComboboxModel> model,int text_context,int text_style)237 Combobox::Combobox(std::unique_ptr<ui::ComboboxModel> model,
238 int text_context,
239 int text_style)
240 : Combobox(model.get(), text_context, text_style) {
241 owned_model_ = std::move(model);
242 }
243
Combobox(ui::ComboboxModel * model,int text_context,int text_style)244 Combobox::Combobox(ui::ComboboxModel* model, int text_context, int text_style)
245 : text_context_(text_context),
246 text_style_(text_style),
247 arrow_button_(new TransparentButton(
248 base::BindRepeating(&Combobox::ArrowButtonPressed,
249 base::Unretained(this)))) {
250 SetModel(model);
251 #if defined(OS_APPLE)
252 SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
253 #else
254 SetFocusBehavior(FocusBehavior::ALWAYS);
255 #endif
256
257 UpdateBorder();
258
259 arrow_button_->SetVisible(true);
260 AddChildView(arrow_button_);
261
262 // A layer is applied to make sure that canvas bounds are snapped to pixel
263 // boundaries (for the sake of drawing the arrow).
264 SetPaintToLayer();
265 layer()->SetFillsBoundsOpaquely(false);
266
267 focus_ring_ = FocusRing::Install(this);
268 }
269
~Combobox()270 Combobox::~Combobox() {
271 if (GetInputMethod() && selector_.get()) {
272 // Combobox should have been blurred before destroy.
273 DCHECK(selector_.get() != GetInputMethod()->GetTextInputClient());
274 }
275 }
276
GetFontList() const277 const gfx::FontList& Combobox::GetFontList() const {
278 return style::GetFont(text_context_, text_style_);
279 }
280
SetSelectedIndex(int index)281 void Combobox::SetSelectedIndex(int index) {
282 if (selected_index_ == index)
283 return;
284 selected_index_ = index;
285 if (size_to_largest_label_) {
286 OnPropertyChanged(&selected_index_, kPropertyEffectsPaint);
287 } else {
288 content_size_ = GetContentSize();
289 OnPropertyChanged(&selected_index_, kPropertyEffectsPreferredSizeChanged);
290 }
291 }
292
SelectValue(const base::string16 & value)293 bool Combobox::SelectValue(const base::string16& value) {
294 for (int i = 0; i < GetModel()->GetItemCount(); ++i) {
295 if (value == GetModel()->GetItemAt(i)) {
296 SetSelectedIndex(i);
297 return true;
298 }
299 }
300 return false;
301 }
302
SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model)303 void Combobox::SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model) {
304 // The swap keeps the outgoing model alive for SetModel().
305 owned_model_.swap(model);
306 SetModel(owned_model_.get());
307 }
308
SetModel(ui::ComboboxModel * model)309 void Combobox::SetModel(ui::ComboboxModel* model) {
310 DCHECK(model) << "After construction, the model must not be null.";
311
312 if (model_)
313 observer_.Remove(model_);
314
315 model_ = model;
316
317 if (model_) {
318 menu_model_ = std::make_unique<ComboboxMenuModel>(this, model_);
319 observer_.Add(model_);
320 SetSelectedIndex(model_->GetDefaultIndex());
321 OnComboboxModelChanged(model_);
322 }
323 }
324
GetTooltipTextAndAccessibleName() const325 base::string16 Combobox::GetTooltipTextAndAccessibleName() const {
326 return arrow_button_->GetTooltipText();
327 }
328
SetTooltipTextAndAccessibleName(const base::string16 & tooltip_text)329 void Combobox::SetTooltipTextAndAccessibleName(
330 const base::string16& tooltip_text) {
331 arrow_button_->SetTooltipText(tooltip_text);
332 if (accessible_name_.empty())
333 accessible_name_ = tooltip_text;
334 }
335
SetAccessibleName(const base::string16 & name)336 void Combobox::SetAccessibleName(const base::string16& name) {
337 accessible_name_ = name;
338 }
339
GetAccessibleName() const340 base::string16 Combobox::GetAccessibleName() const {
341 return accessible_name_;
342 }
343
SetInvalid(bool invalid)344 void Combobox::SetInvalid(bool invalid) {
345 if (invalid == invalid_)
346 return;
347
348 invalid_ = invalid;
349
350 if (focus_ring_)
351 focus_ring_->SetInvalid(invalid);
352
353 UpdateBorder();
354 OnPropertyChanged(&selected_index_, kPropertyEffectsPaint);
355 }
356
SetSizeToLargestLabel(bool size_to_largest_label)357 void Combobox::SetSizeToLargestLabel(bool size_to_largest_label) {
358 if (size_to_largest_label_ == size_to_largest_label)
359 return;
360
361 size_to_largest_label_ = size_to_largest_label;
362 content_size_ = GetContentSize();
363 OnPropertyChanged(&selected_index_, kPropertyEffectsPreferredSizeChanged);
364 }
365
OnThemeChanged()366 void Combobox::OnThemeChanged() {
367 View::OnThemeChanged();
368 SetBackground(
369 CreateBackgroundFromPainter(Painter::CreateSolidRoundRectPainter(
370 GetNativeTheme()->GetSystemColor(
371 ui::NativeTheme::kColorId_TextfieldDefaultBackground),
372 FocusableBorder::kCornerRadiusDp)));
373 }
374
GetRowCount()375 int Combobox::GetRowCount() {
376 return GetModel()->GetItemCount();
377 }
378
GetSelectedRow()379 int Combobox::GetSelectedRow() {
380 return selected_index_;
381 }
382
SetSelectedRow(int row)383 void Combobox::SetSelectedRow(int row) {
384 int prev_index = selected_index_;
385 SetSelectedIndex(row);
386 if (selected_index_ != prev_index)
387 OnPerformAction();
388 }
389
GetTextForRow(int row)390 base::string16 Combobox::GetTextForRow(int row) {
391 return GetModel()->IsItemSeparatorAt(row) ? base::string16()
392 : GetModel()->GetItemAt(row);
393 }
394
395 ////////////////////////////////////////////////////////////////////////////////
396 // Combobox, View overrides:
397
CalculatePreferredSize() const398 gfx::Size Combobox::CalculatePreferredSize() const {
399 // Limit how small a combobox can be.
400 constexpr int kMinComboboxWidth = 25;
401
402 // The preferred size will drive the local bounds which in turn is used to set
403 // the minimum width for the dropdown list.
404 gfx::Insets insets = GetInsets();
405 const LayoutProvider* provider = LayoutProvider::Get();
406 insets += gfx::Insets(
407 provider->GetDistanceMetric(DISTANCE_CONTROL_VERTICAL_TEXT_PADDING),
408 provider->GetDistanceMetric(DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING));
409 int total_width = std::max(kMinComboboxWidth, content_size_.width()) +
410 insets.width() + kComboboxArrowContainerWidth;
411 return gfx::Size(total_width, content_size_.height() + insets.height());
412 }
413
OnBoundsChanged(const gfx::Rect & previous_bounds)414 void Combobox::OnBoundsChanged(const gfx::Rect& previous_bounds) {
415 arrow_button_->SetBounds(0, 0, width(), height());
416 }
417
SkipDefaultKeyEventProcessing(const ui::KeyEvent & e)418 bool Combobox::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
419 // Escape should close the drop down list when it is active, not host UI.
420 if (e.key_code() != ui::VKEY_ESCAPE || e.IsShiftDown() || e.IsControlDown() ||
421 e.IsAltDown() || e.IsAltGrDown()) {
422 return false;
423 }
424 return !!menu_runner_;
425 }
426
OnKeyPressed(const ui::KeyEvent & e)427 bool Combobox::OnKeyPressed(const ui::KeyEvent& e) {
428 // TODO(oshima): handle IME.
429 DCHECK_EQ(e.type(), ui::ET_KEY_PRESSED);
430
431 DCHECK_GE(selected_index_, 0);
432 DCHECK_LT(selected_index_, GetModel()->GetItemCount());
433 if (selected_index_ < 0 || selected_index_ > GetModel()->GetItemCount())
434 SetSelectedIndex(0);
435
436 bool show_menu = false;
437 int new_index = kNoSelection;
438 switch (e.key_code()) {
439 #if defined(OS_APPLE)
440 case ui::VKEY_DOWN:
441 case ui::VKEY_UP:
442 case ui::VKEY_SPACE:
443 case ui::VKEY_HOME:
444 case ui::VKEY_END:
445 // On Mac, navigation keys should always just show the menu first.
446 show_menu = true;
447 break;
448 #else
449 // Show the menu on F4 without modifiers.
450 case ui::VKEY_F4:
451 if (e.IsAltDown() || e.IsAltGrDown() || e.IsControlDown())
452 return false;
453 show_menu = true;
454 break;
455
456 // Move to the next item if any, or show the menu on Alt+Down like Windows.
457 case ui::VKEY_DOWN:
458 if (e.IsAltDown())
459 show_menu = true;
460 else
461 new_index = GetAdjacentIndex(GetModel(), 1, selected_index_);
462 break;
463
464 // Move to the end of the list.
465 case ui::VKEY_END:
466 case ui::VKEY_NEXT: // Page down.
467 new_index = GetAdjacentIndex(GetModel(), -1, GetModel()->GetItemCount());
468 break;
469
470 // Move to the beginning of the list.
471 case ui::VKEY_HOME:
472 case ui::VKEY_PRIOR: // Page up.
473 new_index = GetAdjacentIndex(GetModel(), 1, -1);
474 break;
475
476 // Move to the previous item if any.
477 case ui::VKEY_UP:
478 new_index = GetAdjacentIndex(GetModel(), -1, selected_index_);
479 break;
480
481 case ui::VKEY_RETURN:
482 case ui::VKEY_SPACE:
483 show_menu = true;
484 break;
485 #endif // OS_APPLE
486 default:
487 return false;
488 }
489
490 if (show_menu) {
491 ShowDropDownMenu(ui::MENU_SOURCE_KEYBOARD);
492 } else if (new_index != selected_index_ && new_index != kNoSelection) {
493 DCHECK(!GetModel()->IsItemSeparatorAt(new_index));
494 SetSelectedIndex(new_index);
495 OnPerformAction();
496 }
497
498 return true;
499 }
500
OnPaint(gfx::Canvas * canvas)501 void Combobox::OnPaint(gfx::Canvas* canvas) {
502 OnPaintBackground(canvas);
503 PaintIconAndText(canvas);
504 OnPaintBorder(canvas);
505 }
506
OnFocus()507 void Combobox::OnFocus() {
508 if (GetInputMethod())
509 GetInputMethod()->SetFocusedTextInputClient(GetPrefixSelector());
510
511 View::OnFocus();
512 // Border renders differently when focused.
513 SchedulePaint();
514 }
515
OnBlur()516 void Combobox::OnBlur() {
517 if (GetInputMethod())
518 GetInputMethod()->DetachTextInputClient(GetPrefixSelector());
519
520 if (selector_)
521 selector_->OnViewBlur();
522 // Border renders differently when focused.
523 SchedulePaint();
524 }
525
GetAccessibleNodeData(ui::AXNodeData * node_data)526 void Combobox::GetAccessibleNodeData(ui::AXNodeData* node_data) {
527 // ax::mojom::Role::kComboBox is for UI elements with a dropdown and
528 // an editable text field, which views::Combobox does not have. Use
529 // ax::mojom::Role::kPopUpButton to match an HTML <select> element.
530 node_data->role = ax::mojom::Role::kPopUpButton;
531
532 node_data->SetName(accessible_name_);
533 node_data->SetValue(model_->GetItemAt(selected_index_));
534 if (GetEnabled()) {
535 node_data->SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kOpen);
536 }
537 node_data->AddIntAttribute(ax::mojom::IntAttribute::kPosInSet,
538 selected_index_);
539 node_data->AddIntAttribute(ax::mojom::IntAttribute::kSetSize,
540 model_->GetItemCount());
541 }
542
HandleAccessibleAction(const ui::AXActionData & action_data)543 bool Combobox::HandleAccessibleAction(const ui::AXActionData& action_data) {
544 // The action handling in View would generate a mouse event and send it to
545 // |this|. However, mouse events for Combobox are handled by |arrow_button_|,
546 // which is hidden from the a11y tree (so can't expose actions). Rather than
547 // forwarding ax::mojom::Action::kDoDefault to View and then forwarding the
548 // mouse event it generates to |arrow_button_| to have it forward back to the
549 // callback on |this|, just handle the action explicitly here and bypass View.
550 if (GetEnabled() && action_data.action == ax::mojom::Action::kDoDefault) {
551 ShowDropDownMenu(ui::MENU_SOURCE_KEYBOARD);
552 return true;
553 }
554 return View::HandleAccessibleAction(action_data);
555 }
556
OnComboboxModelChanged(ui::ComboboxModel * model)557 void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) {
558 DCHECK_EQ(model_, model);
559
560 // If the selection is no longer valid (or the model is empty), restore the
561 // default index.
562 if (selected_index_ >= model_->GetItemCount() ||
563 model_->GetItemCount() == 0 ||
564 model_->IsItemSeparatorAt(selected_index_)) {
565 SetSelectedIndex(model_->GetDefaultIndex());
566 }
567
568 content_size_ = GetContentSize();
569 PreferredSizeChanged();
570 SchedulePaint();
571 }
572
GetCallback() const573 const base::RepeatingClosure& Combobox::GetCallback() const {
574 return callback_;
575 }
576
GetOwnedModel() const577 const std::unique_ptr<ui::ComboboxModel>& Combobox::GetOwnedModel() const {
578 return owned_model_;
579 }
580
UpdateBorder()581 void Combobox::UpdateBorder() {
582 std::unique_ptr<FocusableBorder> border(new FocusableBorder());
583 if (invalid_)
584 border->SetColorId(ui::NativeTheme::kColorId_AlertSeverityHigh);
585 SetBorder(std::move(border));
586 }
587
AdjustBoundsForRTLUI(gfx::Rect * rect) const588 void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
589 rect->set_x(GetMirroredXForRect(*rect));
590 }
591
PaintIconAndText(gfx::Canvas * canvas)592 void Combobox::PaintIconAndText(gfx::Canvas* canvas) {
593 gfx::Insets insets = GetInsets();
594 insets += gfx::Insets(0, LayoutProvider::Get()->GetDistanceMetric(
595 DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING));
596
597 gfx::ScopedCanvas scoped_canvas(canvas);
598 canvas->ClipRect(GetContentsBounds());
599
600 int x = insets.left();
601 int y = insets.top();
602 int contents_height = height() - insets.height();
603
604 // Draw the icon.
605 ui::ImageModel icon = GetModel()->GetIconAt(selected_index_);
606 if (!icon.IsEmpty()) {
607 gfx::ImageSkia icon_skia =
608 GetImageSkiaFromImageModel(&icon, GetNativeTheme());
609 int icon_y = y + (contents_height - icon_skia.height()) / 2;
610 gfx::Rect icon_bounds(x, icon_y, icon_skia.width(), icon_skia.height());
611 AdjustBoundsForRTLUI(&icon_bounds);
612 canvas->DrawImageInt(icon_skia, icon_bounds.x(), icon_bounds.y());
613 x += icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric(
614 DISTANCE_RELATED_LABEL_HORIZONTAL);
615 }
616
617 // Draw the text.
618 SkColor text_color = GetTextColorForEnableState(*this, GetEnabled());
619 if (selected_index_ < 0 || selected_index_ > GetModel()->GetItemCount()) {
620 NOTREACHED();
621 SetSelectedIndex(0);
622 }
623 base::string16 text = GetModel()->GetItemAt(selected_index_);
624
625 int disclosure_arrow_offset = width() - kComboboxArrowContainerWidth;
626
627 const gfx::FontList& font_list = GetFontList();
628 int text_width = gfx::GetStringWidth(text, font_list);
629 text_width =
630 std::min(text_width, disclosure_arrow_offset - insets.right() - x);
631
632 gfx::Rect text_bounds(x, y, text_width, contents_height);
633 AdjustBoundsForRTLUI(&text_bounds);
634 canvas->DrawStringRect(text, font_list, text_color, text_bounds);
635
636 gfx::Rect arrow_bounds(disclosure_arrow_offset, 0,
637 kComboboxArrowContainerWidth, height());
638 arrow_bounds.ClampToCenteredSize(ComboboxArrowSize());
639 AdjustBoundsForRTLUI(&arrow_bounds);
640
641 PaintComboboxArrow(text_color, arrow_bounds, canvas);
642 }
643
ArrowButtonPressed(const ui::Event & event)644 void Combobox::ArrowButtonPressed(const ui::Event& event) {
645 if (!GetEnabled())
646 return;
647
648 // TODO(hajimehoshi): Fix the problem that the arrow button blinks when
649 // cliking this while the dropdown menu is opened.
650 if ((base::TimeTicks::Now() - closed_time_) > kMinimumTimeBetweenButtonClicks)
651 ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event));
652 }
653
ShowDropDownMenu(ui::MenuSourceType source_type)654 void Combobox::ShowDropDownMenu(ui::MenuSourceType source_type) {
655 constexpr int kMenuBorderWidthTop = 1;
656 // Menu's requested position's width should be the same as local bounds so the
657 // border of the menu lines up with the border of the combobox. The y
658 // coordinate however should be shifted to the bottom with the border with not
659 // to overlap with the combobox border.
660 gfx::Rect lb = GetLocalBounds();
661 gfx::Point menu_position(lb.origin());
662 menu_position.set_y(menu_position.y() + kMenuBorderWidthTop);
663
664 View::ConvertPointToScreen(this, &menu_position);
665
666 gfx::Rect bounds(menu_position, lb.size());
667
668 Button::ButtonState original_state = arrow_button_->GetState();
669 arrow_button_->SetState(Button::STATE_PRESSED);
670
671 // Allow |menu_runner_| to be set by the testing API, but if this method is
672 // ever invoked recursively, ensure the old menu is closed.
673 if (!menu_runner_ || menu_runner_->IsRunning()) {
674 menu_runner_ = std::make_unique<MenuRunner>(
675 menu_model_.get(), MenuRunner::COMBOBOX,
676 base::BindRepeating(&Combobox::OnMenuClosed, base::Unretained(this),
677 original_state));
678 }
679 menu_runner_->RunMenuAt(GetWidget(), nullptr, bounds,
680 MenuAnchorPosition::kTopLeft, source_type);
681 }
682
OnMenuClosed(Button::ButtonState original_button_state)683 void Combobox::OnMenuClosed(Button::ButtonState original_button_state) {
684 menu_runner_.reset();
685 arrow_button_->SetState(original_button_state);
686 closed_time_ = base::TimeTicks::Now();
687 }
688
OnPerformAction()689 void Combobox::OnPerformAction() {
690 NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
691 SchedulePaint();
692
693 if (callback_)
694 callback_.Run();
695
696 // Note |this| may be deleted by |callback_|.
697 }
698
GetContentSize() const699 gfx::Size Combobox::GetContentSize() const {
700 const gfx::FontList& font_list = GetFontList();
701 int height = font_list.GetHeight();
702 int width = 0;
703 for (int i = 0; i < GetModel()->GetItemCount(); ++i) {
704 if (model_->IsItemSeparatorAt(i))
705 continue;
706
707 if (size_to_largest_label_ || i == selected_index_) {
708 int item_width = gfx::GetStringWidth(GetModel()->GetItemAt(i), font_list);
709 ui::ImageModel icon = GetModel()->GetIconAt(i);
710 if (!icon.IsEmpty()) {
711 gfx::ImageSkia icon_skia =
712 GetImageSkiaFromImageModel(&icon, GetNativeTheme());
713 item_width +=
714 icon_skia.width() + LayoutProvider::Get()->GetDistanceMetric(
715 DISTANCE_RELATED_LABEL_HORIZONTAL);
716 height = std::max(height, icon_skia.height());
717 }
718 width = std::max(width, item_width);
719 }
720 }
721 return gfx::Size(width, height);
722 }
723
GetPrefixSelector()724 PrefixSelector* Combobox::GetPrefixSelector() {
725 if (!selector_)
726 selector_ = std::make_unique<PrefixSelector>(this, this);
727 return selector_.get();
728 }
729
730 BEGIN_METADATA(Combobox, View)
731 ADD_PROPERTY_METADATA(base::RepeatingClosure, Callback)
732 ADD_PROPERTY_METADATA(std::unique_ptr<ui::ComboboxModel>, OwnedModel)
733 ADD_PROPERTY_METADATA(ui::ComboboxModel*, Model)
734 ADD_PROPERTY_METADATA(int, SelectedIndex)
735 ADD_PROPERTY_METADATA(bool, Invalid)
736 ADD_PROPERTY_METADATA(bool, SizeToLargestLabel)
737 ADD_PROPERTY_METADATA(base::string16, AccessibleName)
738 ADD_PROPERTY_METADATA(base::string16, TooltipTextAndAccessibleName)
739 END_METADATA
740
741 } // namespace views
742