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