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