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/menu/submenu_view.h" 6 7 #include <algorithm> 8 9 #include "base/compiler_specific.h" 10 #include "ui/accessibility/ax_view_state.h" 11 #include "ui/events/event.h" 12 #include "ui/gfx/canvas.h" 13 #include "ui/gfx/geometry/safe_integer_conversions.h" 14 #include "ui/views/controls/menu/menu_config.h" 15 #include "ui/views/controls/menu/menu_controller.h" 16 #include "ui/views/controls/menu/menu_host.h" 17 #include "ui/views/controls/menu/menu_item_view.h" 18 #include "ui/views/controls/menu/menu_scroll_view_container.h" 19 #include "ui/views/widget/root_view.h" 20 #include "ui/views/widget/widget.h" 21 22 namespace { 23 24 // Height of the drop indicator. This should be an even number. 25 const int kDropIndicatorHeight = 2; 26 27 // Color of the drop indicator. 28 const SkColor kDropIndicatorColor = SK_ColorBLACK; 29 30 } // namespace 31 32 namespace views { 33 34 // static 35 const char SubmenuView::kViewClassName[] = "SubmenuView"; 36 37 SubmenuView::SubmenuView(MenuItemView* parent) 38 : parent_menu_item_(parent), 39 host_(NULL), 40 drop_item_(NULL), 41 drop_position_(MenuDelegate::DROP_NONE), 42 scroll_view_container_(NULL), 43 max_minor_text_width_(0), 44 minimum_preferred_width_(0), 45 resize_open_menu_(false), 46 scroll_animator_(new ScrollAnimator(this)), 47 roundoff_error_(0), 48 prefix_selector_(this) { 49 DCHECK(parent); 50 // We'll delete ourselves, otherwise the ScrollView would delete us on close. 51 set_owned_by_client(); 52 } 53 54 SubmenuView::~SubmenuView() { 55 // The menu may not have been closed yet (it will be hidden, but not 56 // necessarily closed). 57 Close(); 58 59 delete scroll_view_container_; 60 } 61 62 int SubmenuView::GetMenuItemCount() { 63 int count = 0; 64 for (int i = 0; i < child_count(); ++i) { 65 if (child_at(i)->id() == MenuItemView::kMenuItemViewID) 66 count++; 67 } 68 return count; 69 } 70 71 MenuItemView* SubmenuView::GetMenuItemAt(int index) { 72 for (int i = 0, count = 0; i < child_count(); ++i) { 73 if (child_at(i)->id() == MenuItemView::kMenuItemViewID && 74 count++ == index) { 75 return static_cast<MenuItemView*>(child_at(i)); 76 } 77 } 78 NOTREACHED(); 79 return NULL; 80 } 81 82 void SubmenuView::ChildPreferredSizeChanged(View* child) { 83 if (!resize_open_menu_) 84 return; 85 86 MenuItemView *item = GetMenuItem(); 87 MenuController* controller = item->GetMenuController(); 88 89 if (controller) { 90 bool dir; 91 gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir); 92 Reposition(bounds); 93 } 94 } 95 96 void SubmenuView::Layout() { 97 // We're in a ScrollView, and need to set our width/height ourselves. 98 if (!parent()) 99 return; 100 101 // Use our current y, unless it means part of the menu isn't visible anymore. 102 int pref_height = GetPreferredSize().height(); 103 int new_y; 104 if (pref_height > parent()->height()) 105 new_y = std::max(parent()->height() - pref_height, y()); 106 else 107 new_y = 0; 108 SetBounds(x(), new_y, parent()->width(), pref_height); 109 110 gfx::Insets insets = GetInsets(); 111 int x = insets.left(); 112 int y = insets.top(); 113 int menu_item_width = width() - insets.width(); 114 for (int i = 0; i < child_count(); ++i) { 115 View* child = child_at(i); 116 if (child->visible()) { 117 int child_height = child->GetHeightForWidth(menu_item_width); 118 child->SetBounds(x, y, menu_item_width, child_height); 119 y += child_height; 120 } 121 } 122 } 123 124 gfx::Size SubmenuView::GetPreferredSize() const { 125 if (!has_children()) 126 return gfx::Size(); 127 128 max_minor_text_width_ = 0; 129 // The maximum width of items which contain maybe a label and multiple views. 130 int max_complex_width = 0; 131 // The max. width of items which contain a label and maybe an accelerator. 132 int max_simple_width = 0; 133 134 // We perform the size calculation in two passes. In the first pass, we 135 // calculate the width of the menu. In the second, we calculate the height 136 // using that width. This allows views that have flexible widths to adjust 137 // accordingly. 138 for (int i = 0; i < child_count(); ++i) { 139 const View* child = child_at(i); 140 if (!child->visible()) 141 continue; 142 if (child->id() == MenuItemView::kMenuItemViewID) { 143 const MenuItemView* menu = static_cast<const MenuItemView*>(child); 144 const MenuItemView::MenuItemDimensions& dimensions = 145 menu->GetDimensions(); 146 max_simple_width = std::max( 147 max_simple_width, dimensions.standard_width); 148 max_minor_text_width_ = 149 std::max(max_minor_text_width_, dimensions.minor_text_width); 150 max_complex_width = std::max(max_complex_width, 151 dimensions.standard_width + dimensions.children_width); 152 } else { 153 max_complex_width = std::max(max_complex_width, 154 child->GetPreferredSize().width()); 155 } 156 } 157 if (max_minor_text_width_ > 0) { 158 max_minor_text_width_ += 159 GetMenuItem()->GetMenuConfig().label_to_minor_text_padding; 160 } 161 // Finish calculating our optimum width. 162 gfx::Insets insets = GetInsets(); 163 int width = std::max(max_complex_width, 164 std::max(max_simple_width + max_minor_text_width_ + 165 insets.width(), 166 minimum_preferred_width_ - 2 * insets.width())); 167 168 // Then, the height for that width. 169 int height = 0; 170 int menu_item_width = width - insets.width(); 171 for (int i = 0; i < child_count(); ++i) { 172 const View* child = child_at(i); 173 height += child->visible() ? child->GetHeightForWidth(menu_item_width) : 0; 174 } 175 176 return gfx::Size(width, height + insets.height()); 177 } 178 179 void SubmenuView::GetAccessibleState(ui::AXViewState* state) { 180 // Inherit most of the state from the parent menu item, except the role. 181 if (GetMenuItem()) 182 GetMenuItem()->GetAccessibleState(state); 183 state->role = ui::AX_ROLE_MENU_LIST_POPUP; 184 } 185 186 ui::TextInputClient* SubmenuView::GetTextInputClient() { 187 return &prefix_selector_; 188 } 189 190 void SubmenuView::PaintChildren(gfx::Canvas* canvas, 191 const views::CullSet& cull_set) { 192 View::PaintChildren(canvas, cull_set); 193 194 if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON) 195 PaintDropIndicator(canvas, drop_item_, drop_position_); 196 } 197 198 bool SubmenuView::GetDropFormats( 199 int* formats, 200 std::set<OSExchangeData::CustomFormat>* custom_formats) { 201 DCHECK(GetMenuItem()->GetMenuController()); 202 return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats, 203 custom_formats); 204 } 205 206 bool SubmenuView::AreDropTypesRequired() { 207 DCHECK(GetMenuItem()->GetMenuController()); 208 return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this); 209 } 210 211 bool SubmenuView::CanDrop(const OSExchangeData& data) { 212 DCHECK(GetMenuItem()->GetMenuController()); 213 return GetMenuItem()->GetMenuController()->CanDrop(this, data); 214 } 215 216 void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) { 217 DCHECK(GetMenuItem()->GetMenuController()); 218 GetMenuItem()->GetMenuController()->OnDragEntered(this, event); 219 } 220 221 int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) { 222 DCHECK(GetMenuItem()->GetMenuController()); 223 return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event); 224 } 225 226 void SubmenuView::OnDragExited() { 227 DCHECK(GetMenuItem()->GetMenuController()); 228 GetMenuItem()->GetMenuController()->OnDragExited(this); 229 } 230 231 int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) { 232 DCHECK(GetMenuItem()->GetMenuController()); 233 return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event); 234 } 235 236 bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent& e) { 237 gfx::Rect vis_bounds = GetVisibleBounds(); 238 int menu_item_count = GetMenuItemCount(); 239 if (vis_bounds.height() == height() || !menu_item_count) { 240 // All menu items are visible, nothing to scroll. 241 return true; 242 } 243 244 // Find the index of the first menu item whose y-coordinate is >= visible 245 // y-coordinate. 246 int i = 0; 247 while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y())) 248 ++i; 249 if (i == menu_item_count) 250 return true; 251 int first_vis_index = std::max(0, 252 (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1); 253 254 // If the first item isn't entirely visible, make it visible, otherwise make 255 // the next/previous one entirely visible. If enough wasn't scrolled to show 256 // any new rows, then just scroll the amount so that smooth scrolling using 257 // the trackpad is possible. 258 int delta = abs(e.y_offset() / ui::MouseWheelEvent::kWheelDelta); 259 if (delta == 0) 260 return OnScroll(0, e.y_offset()); 261 for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) { 262 int scroll_target; 263 if (scroll_up) { 264 if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) { 265 if (first_vis_index == 0) 266 break; 267 first_vis_index--; 268 } 269 scroll_target = GetMenuItemAt(first_vis_index)->y(); 270 } else { 271 if (first_vis_index + 1 == menu_item_count) 272 break; 273 scroll_target = GetMenuItemAt(first_vis_index + 1)->y(); 274 if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) 275 first_vis_index++; 276 } 277 ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target), 278 vis_bounds.size())); 279 vis_bounds = GetVisibleBounds(); 280 } 281 282 return true; 283 } 284 285 void SubmenuView::OnGestureEvent(ui::GestureEvent* event) { 286 bool handled = true; 287 switch (event->type()) { 288 case ui::ET_GESTURE_SCROLL_BEGIN: 289 scroll_animator_->Stop(); 290 break; 291 case ui::ET_GESTURE_SCROLL_UPDATE: 292 handled = OnScroll(0, event->details().scroll_y()); 293 break; 294 case ui::ET_GESTURE_SCROLL_END: 295 break; 296 case ui::ET_SCROLL_FLING_START: 297 if (event->details().velocity_y() != 0.0f) 298 scroll_animator_->Start(0, event->details().velocity_y()); 299 break; 300 case ui::ET_GESTURE_TAP_DOWN: 301 case ui::ET_SCROLL_FLING_CANCEL: 302 if (scroll_animator_->is_scrolling()) 303 scroll_animator_->Stop(); 304 else 305 handled = false; 306 break; 307 default: 308 handled = false; 309 break; 310 } 311 if (handled) 312 event->SetHandled(); 313 } 314 315 int SubmenuView::GetRowCount() { 316 return GetMenuItemCount(); 317 } 318 319 int SubmenuView::GetSelectedRow() { 320 int row = 0; 321 for (int i = 0; i < child_count(); ++i) { 322 if (child_at(i)->id() != MenuItemView::kMenuItemViewID) 323 continue; 324 325 if (static_cast<MenuItemView*>(child_at(i))->IsSelected()) 326 return row; 327 328 row++; 329 } 330 331 return -1; 332 } 333 334 void SubmenuView::SetSelectedRow(int row) { 335 GetMenuItem()->GetMenuController()->SetSelection( 336 GetMenuItemAt(row), 337 MenuController::SELECTION_DEFAULT); 338 } 339 340 base::string16 SubmenuView::GetTextForRow(int row) { 341 return GetMenuItemAt(row)->title(); 342 } 343 344 bool SubmenuView::IsShowing() { 345 return host_ && host_->IsMenuHostVisible(); 346 } 347 348 void SubmenuView::ShowAt(Widget* parent, 349 const gfx::Rect& bounds, 350 bool do_capture) { 351 if (host_) { 352 host_->ShowMenuHost(do_capture); 353 } else { 354 host_ = new MenuHost(this); 355 // Force construction of the scroll view container. 356 GetScrollViewContainer(); 357 // Force a layout since our preferred size may not have changed but our 358 // content may have. 359 InvalidateLayout(); 360 host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture); 361 } 362 363 GetScrollViewContainer()->NotifyAccessibilityEvent( 364 ui::AX_EVENT_MENU_START, 365 true); 366 NotifyAccessibilityEvent( 367 ui::AX_EVENT_MENU_POPUP_START, 368 true); 369 } 370 371 void SubmenuView::Reposition(const gfx::Rect& bounds) { 372 if (host_) 373 host_->SetMenuHostBounds(bounds); 374 } 375 376 void SubmenuView::Close() { 377 if (host_) { 378 NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true); 379 GetScrollViewContainer()->NotifyAccessibilityEvent( 380 ui::AX_EVENT_MENU_END, true); 381 382 host_->DestroyMenuHost(); 383 host_ = NULL; 384 } 385 } 386 387 void SubmenuView::Hide() { 388 if (host_) 389 host_->HideMenuHost(); 390 if (scroll_animator_->is_scrolling()) 391 scroll_animator_->Stop(); 392 } 393 394 void SubmenuView::ReleaseCapture() { 395 if (host_) 396 host_->ReleaseMenuHostCapture(); 397 } 398 399 bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) { 400 return views::FocusManager::IsTabTraversalKeyEvent(e); 401 } 402 403 MenuItemView* SubmenuView::GetMenuItem() const { 404 return parent_menu_item_; 405 } 406 407 void SubmenuView::SetDropMenuItem(MenuItemView* item, 408 MenuDelegate::DropPosition position) { 409 if (drop_item_ == item && drop_position_ == position) 410 return; 411 SchedulePaintForDropIndicator(drop_item_, drop_position_); 412 drop_item_ = item; 413 drop_position_ = position; 414 SchedulePaintForDropIndicator(drop_item_, drop_position_); 415 } 416 417 bool SubmenuView::GetShowSelection(MenuItemView* item) { 418 if (drop_item_ == NULL) 419 return true; 420 // Something is being dropped on one of this menus items. Show the 421 // selection if the drop is on the passed in item and the drop position is 422 // ON. 423 return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON); 424 } 425 426 MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() { 427 if (!scroll_view_container_) { 428 scroll_view_container_ = new MenuScrollViewContainer(this); 429 // Otherwise MenuHost would delete us. 430 scroll_view_container_->set_owned_by_client(); 431 } 432 return scroll_view_container_; 433 } 434 435 void SubmenuView::MenuHostDestroyed() { 436 host_ = NULL; 437 GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED); 438 } 439 440 const char* SubmenuView::GetClassName() const { 441 return kViewClassName; 442 } 443 444 void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) { 445 SchedulePaint(); 446 } 447 448 void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas, 449 MenuItemView* item, 450 MenuDelegate::DropPosition position) { 451 if (position == MenuDelegate::DROP_NONE) 452 return; 453 454 gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); 455 canvas->FillRect(bounds, kDropIndicatorColor); 456 } 457 458 void SubmenuView::SchedulePaintForDropIndicator( 459 MenuItemView* item, 460 MenuDelegate::DropPosition position) { 461 if (item == NULL) 462 return; 463 464 if (position == MenuDelegate::DROP_ON) { 465 item->SchedulePaint(); 466 } else if (position != MenuDelegate::DROP_NONE) { 467 SchedulePaintInRect(CalculateDropIndicatorBounds(item, position)); 468 } 469 } 470 471 gfx::Rect SubmenuView::CalculateDropIndicatorBounds( 472 MenuItemView* item, 473 MenuDelegate::DropPosition position) { 474 DCHECK(position != MenuDelegate::DROP_NONE); 475 gfx::Rect item_bounds = item->bounds(); 476 switch (position) { 477 case MenuDelegate::DROP_BEFORE: 478 item_bounds.Offset(0, -kDropIndicatorHeight / 2); 479 item_bounds.set_height(kDropIndicatorHeight); 480 return item_bounds; 481 482 case MenuDelegate::DROP_AFTER: 483 item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2); 484 item_bounds.set_height(kDropIndicatorHeight); 485 return item_bounds; 486 487 default: 488 // Don't render anything for on. 489 return gfx::Rect(); 490 } 491 } 492 493 bool SubmenuView::OnScroll(float dx, float dy) { 494 const gfx::Rect& vis_bounds = GetVisibleBounds(); 495 const gfx::Rect& full_bounds = bounds(); 496 int x = vis_bounds.x(); 497 float y_f = vis_bounds.y() - dy - roundoff_error_; 498 int y = gfx::ToRoundedInt(y_f); 499 roundoff_error_ = y - y_f; 500 // clamp y to [0, full_height - vis_height) 501 y = std::min(y, full_bounds.height() - vis_bounds.height() - 1); 502 y = std::max(y, 0); 503 gfx::Rect new_vis_bounds(x, y, vis_bounds.width(), vis_bounds.height()); 504 if (new_vis_bounds != vis_bounds) { 505 ScrollRectToVisible(new_vis_bounds); 506 return true; 507 } 508 return false; 509 } 510 511 } // namespace views 512