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 "base/bind.h"
8#include "base/logging.h"
9#include "base/message_loop/message_loop_proxy.h"
10#include "base/strings/utf_string_conversions.h"
11#include "ui/accessibility/ax_view_state.h"
12#include "ui/base/models/combobox_model.h"
13#include "ui/base/resource/resource_bundle.h"
14#include "ui/events/event.h"
15#include "ui/events/keycodes/keyboard_codes.h"
16#include "ui/gfx/animation/throb_animation.h"
17#include "ui/gfx/canvas.h"
18#include "ui/gfx/image/image.h"
19#include "ui/gfx/scoped_canvas.h"
20#include "ui/gfx/text_utils.h"
21#include "ui/native_theme/common_theme.h"
22#include "ui/native_theme/native_theme.h"
23#include "ui/resources/grit/ui_resources.h"
24#include "ui/views/background.h"
25#include "ui/views/color_constants.h"
26#include "ui/views/controls/button/custom_button.h"
27#include "ui/views/controls/button/label_button.h"
28#include "ui/views/controls/combobox/combobox_listener.h"
29#include "ui/views/controls/focusable_border.h"
30#include "ui/views/controls/menu/menu_item_view.h"
31#include "ui/views/controls/menu/menu_runner.h"
32#include "ui/views/controls/menu/menu_runner_handler.h"
33#include "ui/views/controls/menu/submenu_view.h"
34#include "ui/views/controls/prefix_selector.h"
35#include "ui/views/controls/textfield/textfield.h"
36#include "ui/views/ime/input_method.h"
37#include "ui/views/mouse_constants.h"
38#include "ui/views/painter.h"
39#include "ui/views/widget/widget.h"
40
41namespace views {
42
43namespace {
44
45// Menu border widths
46const int kMenuBorderWidthLeft = 1;
47const int kMenuBorderWidthTop = 1;
48const int kMenuBorderWidthRight = 1;
49
50// Limit how small a combobox can be.
51const int kMinComboboxWidth = 25;
52
53// Size of the combobox arrow margins
54const int kDisclosureArrowLeftPadding = 7;
55const int kDisclosureArrowRightPadding = 7;
56const int kDisclosureArrowButtonLeftPadding = 11;
57const int kDisclosureArrowButtonRightPadding = 12;
58
59// Define the id of the first item in the menu (since it needs to be > 0)
60const int kFirstMenuItemId = 1000;
61
62// Used to indicate that no item is currently selected by the user.
63const int kNoSelection = -1;
64
65const int kBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON);
66const int kHoveredBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON_H);
67const int kPressedBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON_P);
68const int kFocusedBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON_F);
69const int kFocusedHoveredBodyButtonImages[] =
70    IMAGE_GRID(IDR_COMBOBOX_BUTTON_F_H);
71const int kFocusedPressedBodyButtonImages[] =
72    IMAGE_GRID(IDR_COMBOBOX_BUTTON_F_P);
73
74#define MENU_IMAGE_GRID(x) { \
75    x ## _MENU_TOP, x ## _MENU_CENTER, x ## _MENU_BOTTOM, }
76
77const int kMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON);
78const int kHoveredMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_H);
79const int kPressedMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_P);
80const int kFocusedMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_F);
81const int kFocusedHoveredMenuButtonImages[] =
82    MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_F_H);
83const int kFocusedPressedMenuButtonImages[] =
84    MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_F_P);
85
86#undef MENU_IMAGE_GRID
87
88// The transparent button which holds a button state but is not rendered.
89class TransparentButton : public CustomButton {
90 public:
91  TransparentButton(ButtonListener* listener)
92      : CustomButton(listener) {
93    SetAnimationDuration(LabelButton::kHoverAnimationDurationMs);
94  }
95  virtual ~TransparentButton() {}
96
97  virtual bool OnMousePressed(const ui::MouseEvent& mouse_event) OVERRIDE {
98    parent()->RequestFocus();
99    return true;
100  }
101
102  double GetAnimationValue() const {
103    return hover_animation_->GetCurrentValue();
104  }
105
106 private:
107  DISALLOW_COPY_AND_ASSIGN(TransparentButton);
108};
109
110// Returns the next or previous valid index (depending on |increment|'s value).
111// Skips separator or disabled indices. Returns -1 if there is no valid adjacent
112// index.
113int GetAdjacentIndex(ui::ComboboxModel* model, int increment, int index) {
114  DCHECK(increment == -1 || increment == 1);
115
116  index += increment;
117  while (index >= 0 && index < model->GetItemCount()) {
118    if (!model->IsItemSeparatorAt(index) || !model->IsItemEnabledAt(index))
119      return index;
120    index += increment;
121  }
122  return kNoSelection;
123}
124
125// Returns the image resource ids of an array for the body button.
126//
127// TODO(hajimehoshi): This function should return the images for the 'disabled'
128// status. (crbug/270052)
129const int* GetBodyButtonImageIds(bool focused,
130                                 Button::ButtonState state,
131                                 size_t* num) {
132  DCHECK(num);
133  *num = 9;
134  switch (state) {
135    case Button::STATE_DISABLED:
136      return focused ? kFocusedBodyButtonImages : kBodyButtonImages;
137    case Button::STATE_NORMAL:
138      return focused ? kFocusedBodyButtonImages : kBodyButtonImages;
139    case Button::STATE_HOVERED:
140      return focused ?
141          kFocusedHoveredBodyButtonImages : kHoveredBodyButtonImages;
142    case Button::STATE_PRESSED:
143      return focused ?
144          kFocusedPressedBodyButtonImages : kPressedBodyButtonImages;
145    default:
146      NOTREACHED();
147  }
148  return NULL;
149}
150
151// Returns the image resource ids of an array for the menu button.
152const int* GetMenuButtonImageIds(bool focused,
153                                 Button::ButtonState state,
154                                 size_t* num) {
155  DCHECK(num);
156  *num = 3;
157  switch (state) {
158    case Button::STATE_DISABLED:
159      return focused ? kFocusedMenuButtonImages : kMenuButtonImages;
160    case Button::STATE_NORMAL:
161      return focused ? kFocusedMenuButtonImages : kMenuButtonImages;
162    case Button::STATE_HOVERED:
163      return focused ?
164          kFocusedHoveredMenuButtonImages : kHoveredMenuButtonImages;
165    case Button::STATE_PRESSED:
166      return focused ?
167          kFocusedPressedMenuButtonImages : kPressedMenuButtonImages;
168    default:
169      NOTREACHED();
170  }
171  return NULL;
172}
173
174// Returns the images for the menu buttons.
175std::vector<const gfx::ImageSkia*> GetMenuButtonImages(
176    bool focused,
177    Button::ButtonState state) {
178  const int* ids;
179  size_t num_ids;
180  ids = GetMenuButtonImageIds(focused, state, &num_ids);
181  std::vector<const gfx::ImageSkia*> images;
182  images.reserve(num_ids);
183  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
184  for (size_t i = 0; i < num_ids; i++)
185    images.push_back(rb.GetImageSkiaNamed(ids[i]));
186  return images;
187}
188
189// Paints three images in a column at the given location. The center image is
190// stretched so as to fit the given height.
191void PaintImagesVertically(gfx::Canvas* canvas,
192                           const gfx::ImageSkia& top_image,
193                           const gfx::ImageSkia& center_image,
194                           const gfx::ImageSkia& bottom_image,
195                           int x, int y, int width, int height) {
196  canvas->DrawImageInt(top_image,
197                       0, 0, top_image.width(), top_image.height(),
198                       x, y, width, top_image.height(), false);
199  y += top_image.height();
200  int center_height = height - top_image.height() - bottom_image.height();
201  canvas->DrawImageInt(center_image,
202                       0, 0, center_image.width(), center_image.height(),
203                       x, y, width, center_height, false);
204  y += center_height;
205  canvas->DrawImageInt(bottom_image,
206                       0, 0, bottom_image.width(), bottom_image.height(),
207                       x, y, width, bottom_image.height(), false);
208}
209
210// Paints the arrow button.
211void PaintArrowButton(
212    gfx::Canvas* canvas,
213    const std::vector<const gfx::ImageSkia*>& arrow_button_images,
214    int x, int height) {
215  PaintImagesVertically(canvas,
216                        *arrow_button_images[0],
217                        *arrow_button_images[1],
218                        *arrow_button_images[2],
219                        x, 0, arrow_button_images[0]->width(), height);
220}
221
222}  // namespace
223
224// static
225const char Combobox::kViewClassName[] = "views/Combobox";
226
227////////////////////////////////////////////////////////////////////////////////
228// Combobox, public:
229
230Combobox::Combobox(ui::ComboboxModel* model)
231    : model_(model),
232      style_(STYLE_NORMAL),
233      listener_(NULL),
234      selected_index_(model_->GetDefaultIndex()),
235      invalid_(false),
236      menu_(NULL),
237      dropdown_open_(false),
238      text_button_(new TransparentButton(this)),
239      arrow_button_(new TransparentButton(this)),
240      weak_ptr_factory_(this) {
241  model_->AddObserver(this);
242  UpdateFromModel();
243  SetFocusable(true);
244  UpdateBorder();
245
246  // Initialize the button images.
247  Button::ButtonState button_states[] = {
248    Button::STATE_DISABLED,
249    Button::STATE_NORMAL,
250    Button::STATE_HOVERED,
251    Button::STATE_PRESSED,
252  };
253  for (int i = 0; i < 2; i++) {
254    for (size_t state_index = 0; state_index < arraysize(button_states);
255         state_index++) {
256      Button::ButtonState state = button_states[state_index];
257      size_t num;
258      bool focused = !!i;
259      const int* ids = GetBodyButtonImageIds(focused, state, &num);
260      body_button_painters_[focused][state].reset(
261          Painter::CreateImageGridPainter(ids));
262      menu_button_images_[focused][state] = GetMenuButtonImages(focused, state);
263    }
264  }
265
266  text_button_->SetVisible(true);
267  arrow_button_->SetVisible(true);
268  text_button_->SetFocusable(false);
269  arrow_button_->SetFocusable(false);
270  AddChildView(text_button_);
271  AddChildView(arrow_button_);
272}
273
274Combobox::~Combobox() {
275  model_->RemoveObserver(this);
276}
277
278// static
279const gfx::FontList& Combobox::GetFontList() {
280  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
281  return rb.GetFontList(ui::ResourceBundle::BaseFont);
282}
283
284void Combobox::SetStyle(Style style) {
285  if (style_ == style)
286    return;
287
288  style_ = style;
289  if (style_ == STYLE_ACTION)
290    selected_index_ = 0;
291
292  UpdateBorder();
293  UpdateFromModel();
294  PreferredSizeChanged();
295}
296
297void Combobox::ModelChanged() {
298  selected_index_ = std::min(0, model_->GetItemCount());
299  UpdateFromModel();
300  PreferredSizeChanged();
301}
302
303void Combobox::SetSelectedIndex(int index) {
304  if (style_ == STYLE_ACTION)
305    return;
306
307  selected_index_ = index;
308  SchedulePaint();
309}
310
311bool Combobox::SelectValue(const base::string16& value) {
312  if (style_ == STYLE_ACTION)
313    return false;
314
315  for (int i = 0; i < model()->GetItemCount(); ++i) {
316    if (value == model()->GetItemAt(i)) {
317      SetSelectedIndex(i);
318      return true;
319    }
320  }
321  return false;
322}
323
324void Combobox::SetAccessibleName(const base::string16& name) {
325  accessible_name_ = name;
326}
327
328void Combobox::SetInvalid(bool invalid) {
329  if (invalid == invalid_)
330    return;
331
332  invalid_ = invalid;
333
334  UpdateBorder();
335  SchedulePaint();
336}
337
338ui::TextInputClient* Combobox::GetTextInputClient() {
339  if (!selector_)
340    selector_.reset(new PrefixSelector(this));
341  return selector_.get();
342}
343
344void Combobox::Layout() {
345  PrefixDelegate::Layout();
346
347  gfx::Insets insets = GetInsets();
348  int text_button_width = 0;
349  int arrow_button_width = 0;
350
351  switch (style_) {
352    case STYLE_NORMAL: {
353      arrow_button_width = width();
354      break;
355    }
356    case STYLE_ACTION: {
357      arrow_button_width = GetDisclosureArrowLeftPadding() +
358          ArrowSize().width() +
359          GetDisclosureArrowRightPadding();
360      text_button_width = width() - arrow_button_width;
361      break;
362    }
363  }
364
365  int arrow_button_x = std::max(0, text_button_width);
366  text_button_->SetBounds(0, 0, std::max(0, text_button_width), height());
367  arrow_button_->SetBounds(arrow_button_x, 0, arrow_button_width, height());
368}
369
370bool Combobox::IsItemChecked(int id) const {
371  return false;
372}
373
374bool Combobox::IsCommandEnabled(int id) const {
375  return model()->IsItemEnabledAt(MenuCommandToIndex(id));
376}
377
378void Combobox::ExecuteCommand(int id) {
379  selected_index_ = MenuCommandToIndex(id);
380  OnPerformAction();
381}
382
383bool Combobox::GetAccelerator(int id, ui::Accelerator* accel) const {
384  return false;
385}
386
387int Combobox::GetRowCount() {
388  return model()->GetItemCount();
389}
390
391int Combobox::GetSelectedRow() {
392  return selected_index_;
393}
394
395void Combobox::SetSelectedRow(int row) {
396  int prev_index = selected_index_;
397  SetSelectedIndex(row);
398  if (selected_index_ != prev_index)
399    OnPerformAction();
400}
401
402base::string16 Combobox::GetTextForRow(int row) {
403  return model()->IsItemSeparatorAt(row) ? base::string16() :
404                                           model()->GetItemAt(row);
405}
406
407////////////////////////////////////////////////////////////////////////////////
408// Combobox, View overrides:
409
410gfx::Size Combobox::GetPreferredSize() const {
411  // The preferred size will drive the local bounds which in turn is used to set
412  // the minimum width for the dropdown list.
413  gfx::Insets insets = GetInsets();
414  insets += gfx::Insets(Textfield::kTextPadding,
415                        Textfield::kTextPadding,
416                        Textfield::kTextPadding,
417                        Textfield::kTextPadding);
418  int total_width = std::max(kMinComboboxWidth, content_size_.width()) +
419      insets.width() + GetDisclosureArrowLeftPadding() +
420      ArrowSize().width() + GetDisclosureArrowRightPadding();
421  return gfx::Size(total_width, content_size_.height() + insets.height());
422}
423
424const char* Combobox::GetClassName() const {
425  return kViewClassName;
426}
427
428bool Combobox::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
429  // Escape should close the drop down list when it is active, not host UI.
430  if (e.key_code() != ui::VKEY_ESCAPE ||
431      e.IsShiftDown() || e.IsControlDown() || e.IsAltDown()) {
432    return false;
433  }
434  return dropdown_open_;
435}
436
437bool Combobox::OnKeyPressed(const ui::KeyEvent& e) {
438  // TODO(oshima): handle IME.
439  DCHECK_EQ(e.type(), ui::ET_KEY_PRESSED);
440
441  DCHECK_GE(selected_index_, 0);
442  DCHECK_LT(selected_index_, model()->GetItemCount());
443  if (selected_index_ < 0 || selected_index_ > model()->GetItemCount())
444    selected_index_ = 0;
445
446  bool show_menu = false;
447  int new_index = kNoSelection;
448  switch (e.key_code()) {
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(model(), 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(model(), -1, model()->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(model(), 1, -1);
474      break;
475
476    // Move to the previous item if any.
477    case ui::VKEY_UP:
478      new_index = GetAdjacentIndex(model(), -1, selected_index_);
479      break;
480
481    // Click the button only when the button style mode.
482    case ui::VKEY_SPACE:
483      if (style_ == STYLE_ACTION) {
484        // When pressing space, the click event will be raised after the key is
485        // released.
486        text_button_->SetState(Button::STATE_PRESSED);
487      } else {
488        return false;
489      }
490      break;
491
492    // Click the button only when the button style mode.
493    case ui::VKEY_RETURN:
494      if (style_ != STYLE_ACTION)
495        return false;
496      OnPerformAction();
497      break;
498
499    default:
500      return false;
501  }
502
503  if (show_menu) {
504    UpdateFromModel();
505    ShowDropDownMenu(ui::MENU_SOURCE_KEYBOARD);
506  } else if (new_index != selected_index_ && new_index != kNoSelection &&
507             style_ != STYLE_ACTION) {
508    DCHECK(!model()->IsItemSeparatorAt(new_index));
509    selected_index_ = new_index;
510    OnPerformAction();
511  }
512
513  return true;
514}
515
516bool Combobox::OnKeyReleased(const ui::KeyEvent& e) {
517  if (style_ != STYLE_ACTION)
518    return false;  // crbug.com/127520
519
520  if (e.key_code() == ui::VKEY_SPACE && style_ == STYLE_ACTION)
521    OnPerformAction();
522
523  return false;
524}
525
526void Combobox::OnPaint(gfx::Canvas* canvas) {
527  switch (style_) {
528    case STYLE_NORMAL: {
529      OnPaintBackground(canvas);
530      PaintText(canvas);
531      OnPaintBorder(canvas);
532      break;
533    }
534    case STYLE_ACTION: {
535      PaintButtons(canvas);
536      PaintText(canvas);
537      break;
538    }
539  }
540}
541
542void Combobox::OnFocus() {
543  GetInputMethod()->OnFocus();
544  View::OnFocus();
545  // Border renders differently when focused.
546  SchedulePaint();
547}
548
549void Combobox::OnBlur() {
550  GetInputMethod()->OnBlur();
551  if (selector_)
552    selector_->OnViewBlur();
553  // Border renders differently when focused.
554  SchedulePaint();
555}
556
557void Combobox::GetAccessibleState(ui::AXViewState* state) {
558  state->role = ui::AX_ROLE_COMBO_BOX;
559  state->name = accessible_name_;
560  state->value = model_->GetItemAt(selected_index_);
561  state->index = selected_index_;
562  state->count = model_->GetItemCount();
563}
564
565void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) {
566  DCHECK_EQ(model, model_);
567  ModelChanged();
568}
569
570void Combobox::ButtonPressed(Button* sender, const ui::Event& event) {
571  if (!enabled())
572    return;
573
574  RequestFocus();
575
576  if (sender == text_button_) {
577    OnPerformAction();
578  } else {
579    DCHECK_EQ(arrow_button_, sender);
580    // TODO(hajimehoshi): Fix the problem that the arrow button blinks when
581    // cliking this while the dropdown menu is opened.
582    const base::TimeDelta delta = base::Time::Now() - closed_time_;
583    if (delta.InMilliseconds() <= kMinimumMsBetweenButtonClicks)
584      return;
585
586    ui::MenuSourceType source_type = ui::MENU_SOURCE_MOUSE;
587    if (event.IsKeyEvent())
588      source_type = ui::MENU_SOURCE_KEYBOARD;
589    else if (event.IsGestureEvent() || event.IsTouchEvent())
590      source_type = ui::MENU_SOURCE_TOUCH;
591    ShowDropDownMenu(source_type);
592  }
593}
594
595void Combobox::UpdateFromModel() {
596  const gfx::FontList& font_list = Combobox::GetFontList();
597
598  menu_ = new MenuItemView(this);
599  // MenuRunner owns |menu_|.
600  dropdown_list_menu_runner_.reset(new MenuRunner(menu_, MenuRunner::COMBOBOX));
601
602  int num_items = model()->GetItemCount();
603  int width = 0;
604  bool text_item_appended = false;
605  for (int i = 0; i < num_items; ++i) {
606    // When STYLE_ACTION is used, the first item and the following separators
607    // are not added to the dropdown menu. It is assumed that the first item is
608    // always selected and rendered on the top of the action button.
609    if (model()->IsItemSeparatorAt(i)) {
610      if (text_item_appended || style_ != STYLE_ACTION)
611        menu_->AppendSeparator();
612      continue;
613    }
614
615    base::string16 text = model()->GetItemAt(i);
616
617    // Inserting the Unicode formatting characters if necessary so that the
618    // text is displayed correctly in right-to-left UIs.
619    base::i18n::AdjustStringForLocaleDirection(&text);
620
621    if (style_ != STYLE_ACTION || i > 0) {
622      menu_->AppendMenuItem(i + kFirstMenuItemId, text, MenuItemView::NORMAL);
623      text_item_appended = true;
624    }
625
626    if (style_ != STYLE_ACTION || i == selected_index_)
627      width = std::max(width, gfx::GetStringWidth(text, font_list));
628  }
629
630  content_size_.SetSize(width, font_list.GetHeight());
631}
632
633void Combobox::UpdateBorder() {
634  scoped_ptr<FocusableBorder> border(new FocusableBorder());
635  if (style_ == STYLE_ACTION)
636    border->SetInsets(5, 10, 5, 10);
637  if (invalid_)
638    border->SetColor(kWarningColor);
639  SetBorder(border.PassAs<Border>());
640}
641
642void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
643  rect->set_x(GetMirroredXForRect(*rect));
644}
645
646void Combobox::PaintText(gfx::Canvas* canvas) {
647  gfx::Insets insets = GetInsets();
648  insets += gfx::Insets(0, Textfield::kTextPadding, 0, Textfield::kTextPadding);
649
650  gfx::ScopedCanvas scoped_canvas(canvas);
651  canvas->ClipRect(GetContentsBounds());
652
653  int x = insets.left();
654  int y = insets.top();
655  int text_height = height() - insets.height();
656  SkColor text_color = GetNativeTheme()->GetSystemColor(
657      ui::NativeTheme::kColorId_LabelEnabledColor);
658
659  DCHECK_GE(selected_index_, 0);
660  DCHECK_LT(selected_index_, model()->GetItemCount());
661  if (selected_index_ < 0 || selected_index_ > model()->GetItemCount())
662    selected_index_ = 0;
663  base::string16 text = model()->GetItemAt(selected_index_);
664
665  gfx::Size arrow_size = ArrowSize();
666  int disclosure_arrow_offset = width() - arrow_size.width() -
667      GetDisclosureArrowLeftPadding() - GetDisclosureArrowRightPadding();
668
669  const gfx::FontList& font_list = Combobox::GetFontList();
670  int text_width = gfx::GetStringWidth(text, font_list);
671  if ((text_width + insets.width()) > disclosure_arrow_offset)
672    text_width = disclosure_arrow_offset - insets.width();
673
674  gfx::Rect text_bounds(x, y, text_width, text_height);
675  AdjustBoundsForRTLUI(&text_bounds);
676  canvas->DrawStringRect(text, font_list, text_color, text_bounds);
677
678  int arrow_x = disclosure_arrow_offset + GetDisclosureArrowLeftPadding();
679  gfx::Rect arrow_bounds(arrow_x,
680                         height() / 2 - arrow_size.height() / 2,
681                         arrow_size.width(),
682                         arrow_size.height());
683  AdjustBoundsForRTLUI(&arrow_bounds);
684
685  // TODO(estade): hack alert! Remove this direct call into CommonTheme. For now
686  // STYLE_ACTION isn't properly themed so we have to override the NativeTheme
687  // behavior. See crbug.com/384071
688  if (style_ == STYLE_ACTION) {
689    ui::CommonThemePaintComboboxArrow(canvas->sk_canvas(), arrow_bounds);
690  } else {
691    ui::NativeTheme::ExtraParams ignored;
692    GetNativeTheme()->Paint(canvas->sk_canvas(),
693                            ui::NativeTheme::kComboboxArrow,
694                            ui::NativeTheme::kNormal,
695                            arrow_bounds,
696                            ignored);
697  }
698}
699
700void Combobox::PaintButtons(gfx::Canvas* canvas) {
701  DCHECK(style_ == STYLE_ACTION);
702
703  gfx::ScopedCanvas scoped_canvas(canvas);
704  if (base::i18n::IsRTL()) {
705    canvas->Translate(gfx::Vector2d(width(), 0));
706    canvas->Scale(-1, 1);
707  }
708
709  bool focused = HasFocus();
710  const std::vector<const gfx::ImageSkia*>& arrow_button_images =
711      menu_button_images_[focused][
712          arrow_button_->state() == Button::STATE_HOVERED ?
713          Button::STATE_NORMAL : arrow_button_->state()];
714
715  int text_button_hover_alpha =
716      text_button_->state() == Button::STATE_PRESSED ? 0 :
717      static_cast<int>(static_cast<TransparentButton*>(text_button_)->
718                       GetAnimationValue() * 255);
719  if (text_button_hover_alpha < 255) {
720    canvas->SaveLayerAlpha(255 - text_button_hover_alpha);
721    Painter* text_button_painter =
722        body_button_painters_[focused][
723            text_button_->state() == Button::STATE_HOVERED ?
724            Button::STATE_NORMAL : text_button_->state()].get();
725    Painter::PaintPainterAt(canvas, text_button_painter,
726                            gfx::Rect(0, 0, text_button_->width(), height()));
727    canvas->Restore();
728  }
729  if (0 < text_button_hover_alpha) {
730    canvas->SaveLayerAlpha(text_button_hover_alpha);
731    Painter* text_button_hovered_painter =
732        body_button_painters_[focused][Button::STATE_HOVERED].get();
733    Painter::PaintPainterAt(canvas, text_button_hovered_painter,
734                            gfx::Rect(0, 0, text_button_->width(), height()));
735    canvas->Restore();
736  }
737
738  int arrow_button_hover_alpha =
739      arrow_button_->state() == Button::STATE_PRESSED ? 0 :
740      static_cast<int>(static_cast<TransparentButton*>(arrow_button_)->
741                       GetAnimationValue() * 255);
742  if (arrow_button_hover_alpha < 255) {
743    canvas->SaveLayerAlpha(255 - arrow_button_hover_alpha);
744    PaintArrowButton(canvas, arrow_button_images, arrow_button_->x(), height());
745    canvas->Restore();
746  }
747  if (0 < arrow_button_hover_alpha) {
748    canvas->SaveLayerAlpha(arrow_button_hover_alpha);
749    const std::vector<const gfx::ImageSkia*>& arrow_button_hovered_images =
750        menu_button_images_[focused][Button::STATE_HOVERED];
751    PaintArrowButton(canvas, arrow_button_hovered_images,
752                     arrow_button_->x(), height());
753    canvas->Restore();
754  }
755}
756
757void Combobox::ShowDropDownMenu(ui::MenuSourceType source_type) {
758  if (!dropdown_list_menu_runner_.get())
759    UpdateFromModel();
760
761  // Extend the menu to the width of the combobox.
762  SubmenuView* submenu = menu_->CreateSubmenu();
763  submenu->set_minimum_preferred_width(
764      size().width() - (kMenuBorderWidthLeft + kMenuBorderWidthRight));
765
766  gfx::Rect lb = GetLocalBounds();
767  gfx::Point menu_position(lb.origin());
768
769  if (style_ == STYLE_NORMAL) {
770    // Inset the menu's requested position so the border of the menu lines up
771    // with the border of the combobox.
772    menu_position.set_x(menu_position.x() + kMenuBorderWidthLeft);
773    menu_position.set_y(menu_position.y() + kMenuBorderWidthTop);
774  }
775  lb.set_width(lb.width() - (kMenuBorderWidthLeft + kMenuBorderWidthRight));
776
777  View::ConvertPointToScreen(this, &menu_position);
778  if (menu_position.x() < 0)
779    menu_position.set_x(0);
780
781  gfx::Rect bounds(menu_position, lb.size());
782
783  Button::ButtonState original_state = Button::STATE_NORMAL;
784  if (arrow_button_) {
785    original_state = arrow_button_->state();
786    arrow_button_->SetState(Button::STATE_PRESSED);
787  }
788  dropdown_open_ = true;
789  MenuAnchorPosition anchor_position =
790      style_ == STYLE_ACTION ? MENU_ANCHOR_TOPRIGHT : MENU_ANCHOR_TOPLEFT;
791  if (dropdown_list_menu_runner_->RunMenuAt(
792          GetWidget(), NULL, bounds, anchor_position, source_type) ==
793      MenuRunner::MENU_DELETED) {
794    return;
795  }
796  dropdown_open_ = false;
797  if (arrow_button_)
798    arrow_button_->SetState(original_state);
799  closed_time_ = base::Time::Now();
800
801  // Need to explicitly clear mouse handler so that events get sent
802  // properly after the menu finishes running. If we don't do this, then
803  // the first click to other parts of the UI is eaten.
804  SetMouseHandler(NULL);
805}
806
807void Combobox::OnPerformAction() {
808  NotifyAccessibilityEvent(ui::AX_EVENT_VALUE_CHANGED, false);
809  SchedulePaint();
810
811  // This combobox may be deleted by the listener.
812  base::WeakPtr<Combobox> weak_ptr = weak_ptr_factory_.GetWeakPtr();
813  if (listener_)
814    listener_->OnPerformAction(this);
815
816  if (weak_ptr && style_ == STYLE_ACTION)
817    selected_index_ = 0;
818}
819
820int Combobox::MenuCommandToIndex(int menu_command_id) const {
821  // (note that the id received is offset by kFirstMenuItemId)
822  // Revert menu ID offset to map back to combobox model.
823  int index = menu_command_id - kFirstMenuItemId;
824  DCHECK_LT(index, model()->GetItemCount());
825  return index;
826}
827
828int Combobox::GetDisclosureArrowLeftPadding() const {
829  switch (style_) {
830    case STYLE_NORMAL:
831      return kDisclosureArrowLeftPadding;
832    case STYLE_ACTION:
833      return kDisclosureArrowButtonLeftPadding;
834  }
835  NOTREACHED();
836  return 0;
837}
838
839int Combobox::GetDisclosureArrowRightPadding() const {
840  switch (style_) {
841    case STYLE_NORMAL:
842      return kDisclosureArrowRightPadding;
843    case STYLE_ACTION:
844      return kDisclosureArrowButtonRightPadding;
845  }
846  NOTREACHED();
847  return 0;
848}
849
850gfx::Size Combobox::ArrowSize() const {
851#if defined(OS_LINUX) && !defined(OS_CHROMEOS)
852  // TODO(estade): hack alert! This should always use GetNativeTheme(). For now
853  // STYLE_ACTION isn't properly themed so we have to override the NativeTheme
854  // behavior. See crbug.com/384071
855  const ui::NativeTheme* native_theme_for_arrow = style_ == STYLE_ACTION ?
856      ui::NativeTheme::instance() :
857      GetNativeTheme();
858#else
859  const ui::NativeTheme* native_theme_for_arrow = GetNativeTheme();
860#endif
861
862  ui::NativeTheme::ExtraParams ignored;
863  return native_theme_for_arrow->GetPartSize(ui::NativeTheme::kComboboxArrow,
864                                             ui::NativeTheme::kNormal,
865                                             ignored);
866}
867
868}  // namespace views
869