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