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