// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/controls/menu/submenu_view.h" #include <algorithm> #include "base/compiler_specific.h" #include "ui/accessibility/ax_view_state.h" #include "ui/events/event.h" #include "ui/gfx/canvas.h" #include "ui/gfx/geometry/safe_integer_conversions.h" #include "ui/views/controls/menu/menu_config.h" #include "ui/views/controls/menu/menu_controller.h" #include "ui/views/controls/menu/menu_host.h" #include "ui/views/controls/menu/menu_item_view.h" #include "ui/views/controls/menu/menu_scroll_view_container.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/widget.h" namespace { // Height of the drop indicator. This should be an even number. const int kDropIndicatorHeight = 2; // Color of the drop indicator. const SkColor kDropIndicatorColor = SK_ColorBLACK; } // namespace namespace views { // static const char SubmenuView::kViewClassName[] = "SubmenuView"; SubmenuView::SubmenuView(MenuItemView* parent) : parent_menu_item_(parent), host_(NULL), drop_item_(NULL), drop_position_(MenuDelegate::DROP_NONE), scroll_view_container_(NULL), max_minor_text_width_(0), minimum_preferred_width_(0), resize_open_menu_(false), scroll_animator_(new ScrollAnimator(this)), roundoff_error_(0), prefix_selector_(this) { DCHECK(parent); // We'll delete ourselves, otherwise the ScrollView would delete us on close. set_owned_by_client(); } SubmenuView::~SubmenuView() { // The menu may not have been closed yet (it will be hidden, but not // necessarily closed). Close(); delete scroll_view_container_; } int SubmenuView::GetMenuItemCount() { int count = 0; for (int i = 0; i < child_count(); ++i) { if (child_at(i)->id() == MenuItemView::kMenuItemViewID) count++; } return count; } MenuItemView* SubmenuView::GetMenuItemAt(int index) { for (int i = 0, count = 0; i < child_count(); ++i) { if (child_at(i)->id() == MenuItemView::kMenuItemViewID && count++ == index) { return static_cast<MenuItemView*>(child_at(i)); } } NOTREACHED(); return NULL; } void SubmenuView::ChildPreferredSizeChanged(View* child) { if (!resize_open_menu_) return; MenuItemView *item = GetMenuItem(); MenuController* controller = item->GetMenuController(); if (controller) { bool dir; gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir); Reposition(bounds); } } void SubmenuView::Layout() { // We're in a ScrollView, and need to set our width/height ourselves. if (!parent()) return; // Use our current y, unless it means part of the menu isn't visible anymore. int pref_height = GetPreferredSize().height(); int new_y; if (pref_height > parent()->height()) new_y = std::max(parent()->height() - pref_height, y()); else new_y = 0; SetBounds(x(), new_y, parent()->width(), pref_height); gfx::Insets insets = GetInsets(); int x = insets.left(); int y = insets.top(); int menu_item_width = width() - insets.width(); for (int i = 0; i < child_count(); ++i) { View* child = child_at(i); if (child->visible()) { gfx::Size child_pref_size = child->GetPreferredSize(); child->SetBounds(x, y, menu_item_width, child_pref_size.height()); y += child_pref_size.height(); } } } gfx::Size SubmenuView::GetPreferredSize() const { if (!has_children()) return gfx::Size(); max_minor_text_width_ = 0; // The maximum width of items which contain maybe a label and multiple views. int max_complex_width = 0; // The max. width of items which contain a label and maybe an accelerator. int max_simple_width = 0; int height = 0; for (int i = 0; i < child_count(); ++i) { const View* child = child_at(i); if (!child->visible()) continue; if (child->id() == MenuItemView::kMenuItemViewID) { const MenuItemView* menu = static_cast<const MenuItemView*>(child); const MenuItemView::MenuItemDimensions& dimensions = menu->GetDimensions(); max_simple_width = std::max( max_simple_width, dimensions.standard_width); max_minor_text_width_ = std::max(max_minor_text_width_, dimensions.minor_text_width); max_complex_width = std::max(max_complex_width, dimensions.standard_width + dimensions.children_width); height += dimensions.height; } else { gfx::Size child_pref_size = child->visible() ? child->GetPreferredSize() : gfx::Size(); max_complex_width = std::max(max_complex_width, child_pref_size.width()); height += child_pref_size.height(); } } if (max_minor_text_width_ > 0) { max_minor_text_width_ += GetMenuItem()->GetMenuConfig().label_to_minor_text_padding; } gfx::Insets insets = GetInsets(); return gfx::Size( std::max(max_complex_width, std::max(max_simple_width + max_minor_text_width_ + insets.width(), minimum_preferred_width_ - 2 * insets.width())), height + insets.height()); } void SubmenuView::GetAccessibleState(ui::AXViewState* state) { // Inherit most of the state from the parent menu item, except the role. if (GetMenuItem()) GetMenuItem()->GetAccessibleState(state); state->role = ui::AX_ROLE_MENU_LIST_POPUP; } ui::TextInputClient* SubmenuView::GetTextInputClient() { return &prefix_selector_; } void SubmenuView::PaintChildren(gfx::Canvas* canvas, const views::CullSet& cull_set) { View::PaintChildren(canvas, cull_set); if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON) PaintDropIndicator(canvas, drop_item_, drop_position_); } bool SubmenuView::GetDropFormats( int* formats, std::set<OSExchangeData::CustomFormat>* custom_formats) { DCHECK(GetMenuItem()->GetMenuController()); return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats, custom_formats); } bool SubmenuView::AreDropTypesRequired() { DCHECK(GetMenuItem()->GetMenuController()); return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this); } bool SubmenuView::CanDrop(const OSExchangeData& data) { DCHECK(GetMenuItem()->GetMenuController()); return GetMenuItem()->GetMenuController()->CanDrop(this, data); } void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) { DCHECK(GetMenuItem()->GetMenuController()); GetMenuItem()->GetMenuController()->OnDragEntered(this, event); } int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) { DCHECK(GetMenuItem()->GetMenuController()); return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event); } void SubmenuView::OnDragExited() { DCHECK(GetMenuItem()->GetMenuController()); GetMenuItem()->GetMenuController()->OnDragExited(this); } int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) { DCHECK(GetMenuItem()->GetMenuController()); return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event); } bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent& e) { gfx::Rect vis_bounds = GetVisibleBounds(); int menu_item_count = GetMenuItemCount(); if (vis_bounds.height() == height() || !menu_item_count) { // All menu items are visible, nothing to scroll. return true; } // Find the index of the first menu item whose y-coordinate is >= visible // y-coordinate. int i = 0; while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y())) ++i; if (i == menu_item_count) return true; int first_vis_index = std::max(0, (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1); // If the first item isn't entirely visible, make it visible, otherwise make // the next/previous one entirely visible. If enough wasn't scrolled to show // any new rows, then just scroll the amount so that smooth scrolling using // the trackpad is possible. int delta = abs(e.y_offset() / ui::MouseWheelEvent::kWheelDelta); if (delta == 0) return OnScroll(0, e.y_offset()); for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) { int scroll_target; if (scroll_up) { if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) { if (first_vis_index == 0) break; first_vis_index--; } scroll_target = GetMenuItemAt(first_vis_index)->y(); } else { if (first_vis_index + 1 == menu_item_count) break; scroll_target = GetMenuItemAt(first_vis_index + 1)->y(); if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) first_vis_index++; } ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target), vis_bounds.size())); vis_bounds = GetVisibleBounds(); } return true; } void SubmenuView::OnGestureEvent(ui::GestureEvent* event) { bool handled = true; switch (event->type()) { case ui::ET_GESTURE_SCROLL_BEGIN: scroll_animator_->Stop(); break; case ui::ET_GESTURE_SCROLL_UPDATE: handled = OnScroll(0, event->details().scroll_y()); break; case ui::ET_GESTURE_SCROLL_END: break; case ui::ET_SCROLL_FLING_START: if (event->details().velocity_y() != 0.0f) scroll_animator_->Start(0, event->details().velocity_y()); break; case ui::ET_GESTURE_TAP_DOWN: case ui::ET_SCROLL_FLING_CANCEL: if (scroll_animator_->is_scrolling()) scroll_animator_->Stop(); else handled = false; break; default: handled = false; break; } if (handled) event->SetHandled(); } int SubmenuView::GetRowCount() { return GetMenuItemCount(); } int SubmenuView::GetSelectedRow() { int row = 0; for (int i = 0; i < child_count(); ++i) { if (child_at(i)->id() != MenuItemView::kMenuItemViewID) continue; if (static_cast<MenuItemView*>(child_at(i))->IsSelected()) return row; row++; } return -1; } void SubmenuView::SetSelectedRow(int row) { GetMenuItem()->GetMenuController()->SetSelection( GetMenuItemAt(row), MenuController::SELECTION_DEFAULT); } base::string16 SubmenuView::GetTextForRow(int row) { return GetMenuItemAt(row)->title(); } bool SubmenuView::IsShowing() { return host_ && host_->IsMenuHostVisible(); } void SubmenuView::ShowAt(Widget* parent, const gfx::Rect& bounds, bool do_capture) { if (host_) { host_->ShowMenuHost(do_capture); } else { host_ = new MenuHost(this); // Force construction of the scroll view container. GetScrollViewContainer(); // Force a layout since our preferred size may not have changed but our // content may have. InvalidateLayout(); host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture); } GetScrollViewContainer()->NotifyAccessibilityEvent( ui::AX_EVENT_MENU_START, true); NotifyAccessibilityEvent( ui::AX_EVENT_MENU_POPUP_START, true); } void SubmenuView::Reposition(const gfx::Rect& bounds) { if (host_) host_->SetMenuHostBounds(bounds); } void SubmenuView::Close() { if (host_) { NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true); GetScrollViewContainer()->NotifyAccessibilityEvent( ui::AX_EVENT_MENU_END, true); host_->DestroyMenuHost(); host_ = NULL; } } void SubmenuView::Hide() { if (host_) host_->HideMenuHost(); if (scroll_animator_->is_scrolling()) scroll_animator_->Stop(); } void SubmenuView::ReleaseCapture() { if (host_) host_->ReleaseMenuHostCapture(); } bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) { return views::FocusManager::IsTabTraversalKeyEvent(e); } MenuItemView* SubmenuView::GetMenuItem() const { return parent_menu_item_; } void SubmenuView::SetDropMenuItem(MenuItemView* item, MenuDelegate::DropPosition position) { if (drop_item_ == item && drop_position_ == position) return; SchedulePaintForDropIndicator(drop_item_, drop_position_); drop_item_ = item; drop_position_ = position; SchedulePaintForDropIndicator(drop_item_, drop_position_); } bool SubmenuView::GetShowSelection(MenuItemView* item) { if (drop_item_ == NULL) return true; // Something is being dropped on one of this menus items. Show the // selection if the drop is on the passed in item and the drop position is // ON. return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON); } MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() { if (!scroll_view_container_) { scroll_view_container_ = new MenuScrollViewContainer(this); // Otherwise MenuHost would delete us. scroll_view_container_->set_owned_by_client(); } return scroll_view_container_; } void SubmenuView::MenuHostDestroyed() { host_ = NULL; GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED); } const char* SubmenuView::GetClassName() const { return kViewClassName; } void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) { SchedulePaint(); } void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas, MenuItemView* item, MenuDelegate::DropPosition position) { if (position == MenuDelegate::DROP_NONE) return; gfx::Rect bounds = CalculateDropIndicatorBounds(item, position); canvas->FillRect(bounds, kDropIndicatorColor); } void SubmenuView::SchedulePaintForDropIndicator( MenuItemView* item, MenuDelegate::DropPosition position) { if (item == NULL) return; if (position == MenuDelegate::DROP_ON) { item->SchedulePaint(); } else if (position != MenuDelegate::DROP_NONE) { SchedulePaintInRect(CalculateDropIndicatorBounds(item, position)); } } gfx::Rect SubmenuView::CalculateDropIndicatorBounds( MenuItemView* item, MenuDelegate::DropPosition position) { DCHECK(position != MenuDelegate::DROP_NONE); gfx::Rect item_bounds = item->bounds(); switch (position) { case MenuDelegate::DROP_BEFORE: item_bounds.Offset(0, -kDropIndicatorHeight / 2); item_bounds.set_height(kDropIndicatorHeight); return item_bounds; case MenuDelegate::DROP_AFTER: item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2); item_bounds.set_height(kDropIndicatorHeight); return item_bounds; default: // Don't render anything for on. return gfx::Rect(); } } bool SubmenuView::OnScroll(float dx, float dy) { const gfx::Rect& vis_bounds = GetVisibleBounds(); const gfx::Rect& full_bounds = bounds(); int x = vis_bounds.x(); float y_f = vis_bounds.y() - dy - roundoff_error_; int y = gfx::ToRoundedInt(y_f); roundoff_error_ = y - y_f; // clamp y to [0, full_height - vis_height) y = std::min(y, full_bounds.height() - vis_bounds.height() - 1); y = std::max(y, 0); gfx::Rect new_vis_bounds(x, y, vis_bounds.width(), vis_bounds.height()); if (new_vis_bounds != vis_bounds) { ScrollRectToVisible(new_vis_bounds); return true; } return false; } } // namespace views