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/native_combobox_views.h"
6
7#include <algorithm>
8
9#include "grit/ui_resources.h"
10#include "ui/base/events/event.h"
11#include "ui/base/keycodes/keyboard_codes.h"
12#include "ui/base/models/combobox_model.h"
13#include "ui/base/resource/resource_bundle.h"
14#include "ui/gfx/canvas.h"
15#include "ui/gfx/font.h"
16#include "ui/gfx/image/image.h"
17#include "ui/gfx/path.h"
18#include "ui/native_theme/native_theme.h"
19#include "ui/views/background.h"
20#include "ui/views/border.h"
21#include "ui/views/color_constants.h"
22#include "ui/views/controls/button/menu_button.h"
23#include "ui/views/controls/combobox/combobox.h"
24#include "ui/views/controls/focusable_border.h"
25#include "ui/views/controls/menu/menu_runner.h"
26#include "ui/views/controls/menu/submenu_view.h"
27#include "ui/views/widget/root_view.h"
28#include "ui/views/widget/widget.h"
29
30namespace views {
31
32namespace {
33
34// Define the size of the insets.
35const int kTopInsetSize = 4;
36const int kLeftInsetSize = 4;
37const int kBottomInsetSize = 4;
38const int kRightInsetSize = 4;
39
40// Menu border widths
41const int kMenuBorderWidthLeft = 1;
42const int kMenuBorderWidthTop = 1;
43const int kMenuBorderWidthRight = 1;
44const int kMenuBorderWidthBottom = 2;
45
46// Limit how small a combobox can be.
47const int kMinComboboxWidth = 25;
48
49// Size of the combobox arrow margins
50const int kDisclosureArrowLeftPadding = 7;
51const int kDisclosureArrowRightPadding = 7;
52
53// Define the id of the first item in the menu (since it needs to be > 0)
54const int kFirstMenuItemId = 1000;
55
56const SkColor kInvalidTextColor = SK_ColorWHITE;
57
58// The background to use for invalid comboboxes.
59class InvalidBackground : public Background {
60 public:
61  InvalidBackground() {}
62  virtual ~InvalidBackground() {}
63
64  // Overridden from Background:
65  virtual void Paint(gfx::Canvas* canvas, View* view) const OVERRIDE {
66    gfx::Rect bounds(view->GetLocalBounds());
67    // Inset by 2 to leave 1 empty pixel between background and border.
68    bounds.Inset(2, 2, 2, 2);
69    canvas->FillRect(bounds, kWarningColor);
70  }
71
72 private:
73  DISALLOW_COPY_AND_ASSIGN(InvalidBackground);
74};
75
76}  // namespace
77
78const char NativeComboboxViews::kViewClassName[] =
79    "views/NativeComboboxViews";
80
81NativeComboboxViews::NativeComboboxViews(Combobox* combobox)
82    : combobox_(combobox),
83      text_border_(new FocusableBorder()),
84      disclosure_arrow_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
85          IDR_MENU_DROPARROW).ToImageSkia()),
86      dropdown_open_(false),
87      selected_index_(-1),
88      content_width_(0),
89      content_height_(0) {
90  set_border(text_border_);
91}
92
93NativeComboboxViews::~NativeComboboxViews() {
94}
95
96////////////////////////////////////////////////////////////////////////////////
97// NativeComboboxViews, View overrides:
98
99bool NativeComboboxViews::OnMousePressed(const ui::MouseEvent& mouse_event) {
100  combobox_->RequestFocus();
101  const base::TimeDelta delta = base::Time::Now() - closed_time_;
102  if (mouse_event.IsLeftMouseButton() &&
103      (delta.InMilliseconds() > MenuButton::kMinimumTimeBetweenButtonClicks)) {
104    UpdateFromModel();
105    ShowDropDownMenu(ui::MENU_SOURCE_MOUSE);
106  }
107
108  return true;
109}
110
111bool NativeComboboxViews::OnMouseDragged(const ui::MouseEvent& mouse_event) {
112  return true;
113}
114
115bool NativeComboboxViews::OnKeyPressed(const ui::KeyEvent& key_event) {
116  // TODO(oshima): handle IME.
117  DCHECK_EQ(key_event.type(), ui::ET_KEY_PRESSED);
118
119  // Check if we are in the default state (-1) and set to first item.
120  if (selected_index_ == -1)
121    selected_index_ = 0;
122
123  bool show_menu = false;
124  int new_index = selected_index_;
125  switch (key_event.key_code()) {
126    // Show the menu on Space.
127    case ui::VKEY_SPACE:
128      show_menu = true;
129      break;
130
131    // Show the menu on Alt+Down (like Windows) or move to the next item if any.
132    case ui::VKEY_DOWN:
133      if (key_event.IsAltDown())
134        show_menu = true;
135      else if (new_index < (combobox_->model()->GetItemCount() - 1))
136        new_index++;
137      break;
138
139    // Move to the end of the list.
140    case ui::VKEY_END:
141    case ui::VKEY_NEXT:
142      new_index = combobox_->model()->GetItemCount() - 1;
143      break;
144
145    // Move to the beginning of the list.
146   case ui::VKEY_HOME:
147   case ui::VKEY_PRIOR:
148      new_index = 0;
149      break;
150
151    // Move to the previous item if any.
152    case ui::VKEY_UP:
153      if (new_index > 0)
154        new_index--;
155      break;
156
157    default:
158      return false;
159  }
160
161  if (show_menu) {
162    UpdateFromModel();
163    ShowDropDownMenu(ui::MENU_SOURCE_KEYBOARD);
164  } else if (new_index != selected_index_) {
165    selected_index_ = new_index;
166    combobox_->SelectionChanged();
167    SchedulePaint();
168  }
169
170  return true;
171}
172
173bool NativeComboboxViews::OnKeyReleased(const ui::KeyEvent& key_event) {
174  return true;
175}
176
177void NativeComboboxViews::OnPaint(gfx::Canvas* canvas) {
178  text_border_->set_has_focus(combobox_->HasFocus());
179  OnPaintBackground(canvas);
180  PaintText(canvas);
181  OnPaintBorder(canvas);
182}
183
184void NativeComboboxViews::OnFocus() {
185  NOTREACHED();
186}
187
188void NativeComboboxViews::OnBlur() {
189  NOTREACHED();
190}
191
192/////////////////////////////////////////////////////////////////
193// NativeComboboxViews, ui::EventHandler overrides:
194
195void NativeComboboxViews::OnGestureEvent(ui::GestureEvent* gesture) {
196  if (gesture->type() == ui::ET_GESTURE_TAP) {
197    UpdateFromModel();
198    ShowDropDownMenu(ui::MENU_SOURCE_TOUCH);
199    gesture->StopPropagation();
200    return;
201  }
202  View::OnGestureEvent(gesture);
203}
204
205/////////////////////////////////////////////////////////////////
206// NativeComboboxViews, NativeComboboxWrapper overrides:
207
208void NativeComboboxViews::UpdateFromModel() {
209  int max_width = 0;
210  const gfx::Font& font = Combobox::GetFont();
211
212  MenuItemView* menu = new MenuItemView(this);
213  // MenuRunner owns |menu|.
214  dropdown_list_menu_runner_.reset(new MenuRunner(menu));
215
216  int num_items = combobox_->model()->GetItemCount();
217  for (int i = 0; i < num_items; ++i) {
218    if (combobox_->model()->IsItemSeparatorAt(i)) {
219      menu->AppendSeparator();
220      continue;
221    }
222
223    string16 text = combobox_->model()->GetItemAt(i);
224
225    // Inserting the Unicode formatting characters if necessary so that the
226    // text is displayed correctly in right-to-left UIs.
227    base::i18n::AdjustStringForLocaleDirection(&text);
228
229    menu->AppendMenuItem(i + kFirstMenuItemId, text, MenuItemView::NORMAL);
230    max_width = std::max(max_width, font.GetStringWidth(text));
231  }
232
233  content_width_ = max_width;
234  content_height_ = font.GetHeight();
235}
236
237void NativeComboboxViews::UpdateSelectedIndex() {
238  selected_index_ = combobox_->selected_index();
239  SchedulePaint();
240}
241
242void NativeComboboxViews::UpdateEnabled() {
243  SetEnabled(combobox_->enabled());
244}
245
246int NativeComboboxViews::GetSelectedIndex() const {
247  return selected_index_;
248}
249
250bool NativeComboboxViews::IsDropdownOpen() const {
251  return dropdown_open_;
252}
253
254gfx::Size NativeComboboxViews::GetPreferredSize() {
255  if (content_width_ == 0)
256    UpdateFromModel();
257
258  // The preferred size will drive the local bounds which in turn is used to set
259  // the minimum width for the dropdown list.
260  gfx::Insets insets = GetInsets();
261  int total_width = std::max(kMinComboboxWidth, content_width_) +
262      insets.width() + kDisclosureArrowLeftPadding +
263      disclosure_arrow_->width() + kDisclosureArrowRightPadding;
264
265  return gfx::Size(total_width, content_height_ + insets.height());
266}
267
268View* NativeComboboxViews::GetView() {
269  return this;
270}
271
272void NativeComboboxViews::SetFocus() {
273  text_border_->set_has_focus(true);
274}
275
276void NativeComboboxViews::ValidityStateChanged() {
277  if (combobox_->invalid()) {
278    text_border_->SetColor(kWarningColor);
279    set_background(new InvalidBackground());
280  } else {
281    text_border_->UseDefaultColor();
282    set_background(NULL);
283  }
284  SchedulePaint();
285}
286
287bool NativeComboboxViews::HandleKeyPressed(const ui::KeyEvent& e) {
288  return OnKeyPressed(e);
289}
290
291bool NativeComboboxViews::HandleKeyReleased(const ui::KeyEvent& e) {
292  return false;  // crbug.com/127520
293}
294
295void NativeComboboxViews::HandleFocus() {
296  SchedulePaint();
297}
298
299void NativeComboboxViews::HandleBlur() {
300}
301
302gfx::NativeView NativeComboboxViews::GetTestingHandle() const {
303  NOTREACHED();
304  return NULL;
305}
306
307/////////////////////////////////////////////////////////////////
308// NativeComboboxViews, views::MenuDelegate overrides:
309// (note that the id received is offset by kFirstMenuItemId)
310
311bool NativeComboboxViews::IsItemChecked(int id) const {
312  return false;
313}
314
315bool NativeComboboxViews::IsCommandEnabled(int id) const {
316  return true;
317}
318
319void NativeComboboxViews::ExecuteCommand(int id) {
320  // Revert menu offset to map back to combobox model.
321  id -= kFirstMenuItemId;
322  DCHECK_LT(id, combobox_->model()->GetItemCount());
323  selected_index_ = id;
324  combobox_->SelectionChanged();
325  SchedulePaint();
326}
327
328bool NativeComboboxViews::GetAccelerator(int id, ui::Accelerator* accel) {
329  return false;
330}
331
332/////////////////////////////////////////////////////////////////
333// NativeComboboxViews private methods:
334
335void NativeComboboxViews::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
336  rect->set_x(GetMirroredXForRect(*rect));
337}
338
339void NativeComboboxViews::PaintText(gfx::Canvas* canvas) {
340  gfx::Insets insets = GetInsets();
341
342  canvas->Save();
343  canvas->ClipRect(GetContentsBounds());
344
345  int x = insets.left();
346  int y = insets.top();
347  int text_height = height() - insets.height();
348  SkColor text_color = combobox_->invalid() ? kInvalidTextColor :
349      GetNativeTheme()->GetSystemColor(
350          ui::NativeTheme::kColorId_LabelEnabledColor);
351
352  int index = GetSelectedIndex();
353  if (index < 0 || index > combobox_->model()->GetItemCount())
354    index = 0;
355  string16 text = combobox_->model()->GetItemAt(index);
356
357  int disclosure_arrow_offset = width() - disclosure_arrow_->width()
358      - kDisclosureArrowLeftPadding - kDisclosureArrowRightPadding;
359
360  const gfx::Font& font = Combobox::GetFont();
361  int text_width = font.GetStringWidth(text);
362  if ((text_width + insets.width()) > disclosure_arrow_offset)
363    text_width = disclosure_arrow_offset - insets.width();
364
365  gfx::Rect text_bounds(x, y, text_width, text_height);
366  AdjustBoundsForRTLUI(&text_bounds);
367  canvas->DrawStringInt(text, font, text_color, text_bounds);
368
369  gfx::Rect arrow_bounds(disclosure_arrow_offset + kDisclosureArrowLeftPadding,
370                         height() / 2 - disclosure_arrow_->height() / 2,
371                         disclosure_arrow_->width(),
372                         disclosure_arrow_->height());
373  AdjustBoundsForRTLUI(&arrow_bounds);
374
375  SkPaint paint;
376  // This makes the arrow subtractive.
377  if (combobox_->invalid())
378    paint.setXfermodeMode(SkXfermode::kDstOut_Mode);
379  canvas->DrawImageInt(*disclosure_arrow_, arrow_bounds.x(), arrow_bounds.y(),
380                       paint);
381
382  canvas->Restore();
383}
384
385void NativeComboboxViews::ShowDropDownMenu(ui::MenuSourceType source_type) {
386  if (!dropdown_list_menu_runner_.get())
387    UpdateFromModel();
388
389  // Extend the menu to the width of the combobox.
390  MenuItemView* menu = dropdown_list_menu_runner_->GetMenu();
391  SubmenuView* submenu = menu->CreateSubmenu();
392  submenu->set_minimum_preferred_width(size().width() -
393                                (kMenuBorderWidthLeft + kMenuBorderWidthRight));
394
395  gfx::Rect lb = GetLocalBounds();
396  gfx::Point menu_position(lb.origin());
397
398  // Inset the menu's requested position so the border of the menu lines up
399  // with the border of the combobox.
400  menu_position.set_x(menu_position.x() + kMenuBorderWidthLeft);
401  menu_position.set_y(menu_position.y() + kMenuBorderWidthTop);
402  lb.set_width(lb.width() - (kMenuBorderWidthLeft + kMenuBorderWidthRight));
403
404  View::ConvertPointToScreen(this, &menu_position);
405  if (menu_position.x() < 0)
406      menu_position.set_x(0);
407
408  gfx::Rect bounds(menu_position, lb.size());
409
410  dropdown_open_ = true;
411  if (dropdown_list_menu_runner_->RunMenuAt(
412          GetWidget(), NULL, bounds, MenuItemView::TOPLEFT, source_type, 0) ==
413      MenuRunner::MENU_DELETED)
414    return;
415  dropdown_open_ = false;
416  closed_time_ = base::Time::Now();
417
418  // Need to explicitly clear mouse handler so that events get sent
419  // properly after the menu finishes running. If we don't do this, then
420  // the first click to other parts of the UI is eaten.
421  SetMouseHandler(NULL);
422}
423
424////////////////////////////////////////////////////////////////////////////////
425// NativeComboboxWrapper, public:
426
427#if defined(USE_AURA)
428// static
429NativeComboboxWrapper* NativeComboboxWrapper::CreateWrapper(
430    Combobox* combobox) {
431  return new NativeComboboxViews(combobox);
432}
433#endif
434
435}  // namespace views
436