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
41 namespace views {
42
43 namespace {
44
45 // Menu border widths
46 const int kMenuBorderWidthLeft = 1;
47 const int kMenuBorderWidthTop = 1;
48 const int kMenuBorderWidthRight = 1;
49
50 // Limit how small a combobox can be.
51 const int kMinComboboxWidth = 25;
52
53 // Size of the combobox arrow margins
54 const int kDisclosureArrowLeftPadding = 7;
55 const int kDisclosureArrowRightPadding = 7;
56 const int kDisclosureArrowButtonLeftPadding = 11;
57 const int kDisclosureArrowButtonRightPadding = 12;
58
59 // Define the id of the first item in the menu (since it needs to be > 0)
60 const int kFirstMenuItemId = 1000;
61
62 // Used to indicate that no item is currently selected by the user.
63 const int kNoSelection = -1;
64
65 const int kBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON);
66 const int kHoveredBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON_H);
67 const int kPressedBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON_P);
68 const int kFocusedBodyButtonImages[] = IMAGE_GRID(IDR_COMBOBOX_BUTTON_F);
69 const int kFocusedHoveredBodyButtonImages[] =
70 IMAGE_GRID(IDR_COMBOBOX_BUTTON_F_H);
71 const 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
77 const int kMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON);
78 const int kHoveredMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_H);
79 const int kPressedMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_P);
80 const int kFocusedMenuButtonImages[] = MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_F);
81 const int kFocusedHoveredMenuButtonImages[] =
82 MENU_IMAGE_GRID(IDR_COMBOBOX_BUTTON_F_H);
83 const 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.
89 class TransparentButton : public CustomButton {
90 public:
TransparentButton(ButtonListener * listener)91 TransparentButton(ButtonListener* listener)
92 : CustomButton(listener) {
93 SetAnimationDuration(LabelButton::kHoverAnimationDurationMs);
94 }
~TransparentButton()95 virtual ~TransparentButton() {}
96
OnMousePressed(const ui::MouseEvent & mouse_event)97 virtual bool OnMousePressed(const ui::MouseEvent& mouse_event) OVERRIDE {
98 parent()->RequestFocus();
99 return true;
100 }
101
GetAnimationValue() const102 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.
GetAdjacentIndex(ui::ComboboxModel * model,int increment,int index)113 int 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)
GetBodyButtonImageIds(bool focused,Button::ButtonState state,size_t * num)129 const 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.
GetMenuButtonImageIds(bool focused,Button::ButtonState state,size_t * num)152 const 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.
GetMenuButtonImages(bool focused,Button::ButtonState state)175 std::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.
PaintImagesVertically(gfx::Canvas * canvas,const gfx::ImageSkia & top_image,const gfx::ImageSkia & center_image,const gfx::ImageSkia & bottom_image,int x,int y,int width,int height)191 void 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.
PaintArrowButton(gfx::Canvas * canvas,const std::vector<const gfx::ImageSkia * > & arrow_button_images,int x,int height)211 void 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
225 const char Combobox::kViewClassName[] = "views/Combobox";
226
227 ////////////////////////////////////////////////////////////////////////////////
228 // Combobox, public:
229
Combobox(ui::ComboboxModel * model)230 Combobox::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
~Combobox()274 Combobox::~Combobox() {
275 model_->RemoveObserver(this);
276 }
277
278 // static
GetFontList()279 const gfx::FontList& Combobox::GetFontList() {
280 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
281 return rb.GetFontList(ui::ResourceBundle::BaseFont);
282 }
283
SetStyle(Style style)284 void 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
ModelChanged()297 void Combobox::ModelChanged() {
298 selected_index_ = std::min(0, model_->GetItemCount());
299 UpdateFromModel();
300 PreferredSizeChanged();
301 }
302
SetSelectedIndex(int index)303 void Combobox::SetSelectedIndex(int index) {
304 if (style_ == STYLE_ACTION)
305 return;
306
307 selected_index_ = index;
308 SchedulePaint();
309 }
310
SelectValue(const base::string16 & value)311 bool 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
SetAccessibleName(const base::string16 & name)324 void Combobox::SetAccessibleName(const base::string16& name) {
325 accessible_name_ = name;
326 }
327
SetInvalid(bool invalid)328 void Combobox::SetInvalid(bool invalid) {
329 if (invalid == invalid_)
330 return;
331
332 invalid_ = invalid;
333
334 UpdateBorder();
335 SchedulePaint();
336 }
337
GetTextInputClient()338 ui::TextInputClient* Combobox::GetTextInputClient() {
339 if (!selector_)
340 selector_.reset(new PrefixSelector(this));
341 return selector_.get();
342 }
343
Layout()344 void 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
IsItemChecked(int id) const370 bool Combobox::IsItemChecked(int id) const {
371 return false;
372 }
373
IsCommandEnabled(int id) const374 bool Combobox::IsCommandEnabled(int id) const {
375 return model()->IsItemEnabledAt(MenuCommandToIndex(id));
376 }
377
ExecuteCommand(int id)378 void Combobox::ExecuteCommand(int id) {
379 selected_index_ = MenuCommandToIndex(id);
380 OnPerformAction();
381 }
382
GetAccelerator(int id,ui::Accelerator * accel) const383 bool Combobox::GetAccelerator(int id, ui::Accelerator* accel) const {
384 return false;
385 }
386
GetRowCount()387 int Combobox::GetRowCount() {
388 return model()->GetItemCount();
389 }
390
GetSelectedRow()391 int Combobox::GetSelectedRow() {
392 return selected_index_;
393 }
394
SetSelectedRow(int row)395 void Combobox::SetSelectedRow(int row) {
396 int prev_index = selected_index_;
397 SetSelectedIndex(row);
398 if (selected_index_ != prev_index)
399 OnPerformAction();
400 }
401
GetTextForRow(int row)402 base::string16 Combobox::GetTextForRow(int row) {
403 return model()->IsItemSeparatorAt(row) ? base::string16() :
404 model()->GetItemAt(row);
405 }
406
407 ////////////////////////////////////////////////////////////////////////////////
408 // Combobox, View overrides:
409
GetPreferredSize() const410 gfx::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
GetClassName() const424 const char* Combobox::GetClassName() const {
425 return kViewClassName;
426 }
427
SkipDefaultKeyEventProcessing(const ui::KeyEvent & e)428 bool 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
OnKeyPressed(const ui::KeyEvent & e)437 bool 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
OnKeyReleased(const ui::KeyEvent & e)516 bool 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
OnPaint(gfx::Canvas * canvas)526 void 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
OnFocus()542 void Combobox::OnFocus() {
543 GetInputMethod()->OnFocus();
544 View::OnFocus();
545 // Border renders differently when focused.
546 SchedulePaint();
547 }
548
OnBlur()549 void Combobox::OnBlur() {
550 GetInputMethod()->OnBlur();
551 if (selector_)
552 selector_->OnViewBlur();
553 // Border renders differently when focused.
554 SchedulePaint();
555 }
556
GetAccessibleState(ui::AXViewState * state)557 void 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
OnComboboxModelChanged(ui::ComboboxModel * model)565 void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) {
566 DCHECK_EQ(model, model_);
567 ModelChanged();
568 }
569
ButtonPressed(Button * sender,const ui::Event & event)570 void 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
UpdateFromModel()595 void 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
UpdateBorder()633 void 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
AdjustBoundsForRTLUI(gfx::Rect * rect) const642 void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
643 rect->set_x(GetMirroredXForRect(*rect));
644 }
645
PaintText(gfx::Canvas * canvas)646 void 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
PaintButtons(gfx::Canvas * canvas)700 void 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
ShowDropDownMenu(ui::MenuSourceType source_type)757 void 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
OnPerformAction()807 void 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
MenuCommandToIndex(int menu_command_id) const820 int 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
GetDisclosureArrowLeftPadding() const828 int 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
GetDisclosureArrowRightPadding() const839 int 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
ArrowSize() const850 gfx::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