| OLD | NEW |
| (Empty) |
| 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 | |
| OLD | NEW |