Home | History | Annotate | Download | only in combobox
      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 
     30 namespace views {
     31 
     32 namespace {
     33 
     34 // Define the size of the insets.
     35 const int kTopInsetSize = 4;
     36 const int kLeftInsetSize = 4;
     37 const int kBottomInsetSize = 4;
     38 const int kRightInsetSize = 4;
     39 
     40 // Menu border widths
     41 const int kMenuBorderWidthLeft = 1;
     42 const int kMenuBorderWidthTop = 1;
     43 const int kMenuBorderWidthRight = 1;
     44 const int kMenuBorderWidthBottom = 2;
     45 
     46 // Limit how small a combobox can be.
     47 const int kMinComboboxWidth = 25;
     48 
     49 // Size of the combobox arrow margins
     50 const int kDisclosureArrowLeftPadding = 7;
     51 const int kDisclosureArrowRightPadding = 7;
     52 
     53 // Define the id of the first item in the menu (since it needs to be > 0)
     54 const int kFirstMenuItemId = 1000;
     55 
     56 const SkColor kInvalidTextColor = SK_ColorWHITE;
     57 
     58 // The background to use for invalid comboboxes.
     59 class 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 
     78 const char NativeComboboxViews::kViewClassName[] =
     79     "views/NativeComboboxViews";
     80 
     81 NativeComboboxViews::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 
     93 NativeComboboxViews::~NativeComboboxViews() {
     94 }
     95 
     96 ////////////////////////////////////////////////////////////////////////////////
     97 // NativeComboboxViews, View overrides:
     98 
     99 bool 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 
    111 bool NativeComboboxViews::OnMouseDragged(const ui::MouseEvent& mouse_event) {
    112   return true;
    113 }
    114 
    115 bool 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 
    173 bool NativeComboboxViews::OnKeyReleased(const ui::KeyEvent& key_event) {
    174   return true;
    175 }
    176 
    177 void NativeComboboxViews::OnPaint(gfx::Canvas* canvas) {
    178   text_border_->set_has_focus(combobox_->HasFocus());
    179   OnPaintBackground(canvas);
    180   PaintText(canvas);
    181   OnPaintBorder(canvas);
    182 }
    183 
    184 void NativeComboboxViews::OnFocus() {
    185   NOTREACHED();
    186 }
    187 
    188 void NativeComboboxViews::OnBlur() {
    189   NOTREACHED();
    190 }
    191 
    192 /////////////////////////////////////////////////////////////////
    193 // NativeComboboxViews, ui::EventHandler overrides:
    194 
    195 void 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 
    208 void 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 
    237 void NativeComboboxViews::UpdateSelectedIndex() {
    238   selected_index_ = combobox_->selected_index();
    239   SchedulePaint();
    240 }
    241 
    242 void NativeComboboxViews::UpdateEnabled() {
    243   SetEnabled(combobox_->enabled());
    244 }
    245 
    246 int NativeComboboxViews::GetSelectedIndex() const {
    247   return selected_index_;
    248 }
    249 
    250 bool NativeComboboxViews::IsDropdownOpen() const {
    251   return dropdown_open_;
    252 }
    253 
    254 gfx::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 
    268 View* NativeComboboxViews::GetView() {
    269   return this;
    270 }
    271 
    272 void NativeComboboxViews::SetFocus() {
    273   text_border_->set_has_focus(true);
    274 }
    275 
    276 void 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 
    287 bool NativeComboboxViews::HandleKeyPressed(const ui::KeyEvent& e) {
    288   return OnKeyPressed(e);
    289 }
    290 
    291 bool NativeComboboxViews::HandleKeyReleased(const ui::KeyEvent& e) {
    292   return false;  // crbug.com/127520
    293 }
    294 
    295 void NativeComboboxViews::HandleFocus() {
    296   SchedulePaint();
    297 }
    298 
    299 void NativeComboboxViews::HandleBlur() {
    300 }
    301 
    302 gfx::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 
    311 bool NativeComboboxViews::IsItemChecked(int id) const {
    312   return false;
    313 }
    314 
    315 bool NativeComboboxViews::IsCommandEnabled(int id) const {
    316   return true;
    317 }
    318 
    319 void 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 
    328 bool NativeComboboxViews::GetAccelerator(int id, ui::Accelerator* accel) {
    329   return false;
    330 }
    331 
    332 /////////////////////////////////////////////////////////////////
    333 // NativeComboboxViews private methods:
    334 
    335 void NativeComboboxViews::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
    336   rect->set_x(GetMirroredXForRect(*rect));
    337 }
    338 
    339 void 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 
    385 void 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
    429 NativeComboboxWrapper* NativeComboboxWrapper::CreateWrapper(
    430     Combobox* combobox) {
    431   return new NativeComboboxViews(combobox);
    432 }
    433 #endif
    434 
    435 }  // namespace views
    436