| // 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() { |
| 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) { |
| View* child = child_at(i); |
| if (!child->visible()) |
| continue; |
| if (child->id() == MenuItemView::kMenuItemViewID) { |
| MenuItemView* menu = static_cast<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 |