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.
5 #include "ui/views/controls/menu/submenu_view.h"
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"
23 // Height of the drop indicator. This should be an even number.
24 const int kDropIndicatorHeight = 2;
26 // Color of the drop indicator.
27 const SkColor kDropIndicatorColor = SK_ColorBLACK;
34 const char SubmenuView::kViewClassName[] = "SubmenuView";
36 SubmenuView::SubmenuView(MenuItemView* parent)
37 : parent_menu_item_(parent),
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)),
47 prefix_selector_(this) {
49 // We'll delete ourselves, otherwise the ScrollView would delete us on close.
50 set_owned_by_client();
53 SubmenuView::~SubmenuView() {
54 // The menu may not have been closed yet (it will be hidden, but not
55 // necessarily closed).
58 delete scroll_view_container_;
61 int SubmenuView::GetMenuItemCount() {
63 for (int i = 0; i < child_count(); ++i) {
64 if (child_at(i)->id() == MenuItemView::kMenuItemViewID)
70 MenuItemView* SubmenuView::GetMenuItemAt(int index) {
71 for (int i = 0, count = 0; i < child_count(); ++i) {
72 if (child_at(i)->id() == MenuItemView::kMenuItemViewID &&
74 return static_cast<MenuItemView*>(child_at(i));
81 void SubmenuView::ChildPreferredSizeChanged(View* child) {
82 if (!resize_open_menu_)
85 MenuItemView *item = GetMenuItem();
86 MenuController* controller = item->GetMenuController();
90 gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir);
95 void SubmenuView::Layout() {
96 // We're in a ScrollView, and need to set our width/height ourselves.
100 // Use our current y, unless it means part of the menu isn't visible anymore.
101 int pref_height = GetPreferredSize().height();
103 if (pref_height > parent()->height())
104 new_y = std::max(parent()->height() - pref_height, y());
107 SetBounds(x(), new_y, parent()->width(), pref_height);
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();
123 gfx::Size SubmenuView::GetPreferredSize() {
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;
133 for (int i = 0; i < child_count(); ++i) {
134 View* child = child_at(i);
135 if (!child->visible())
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;
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();
155 if (max_minor_text_width_ > 0) {
156 max_minor_text_width_ +=
157 GetMenuItem()->GetMenuConfig().label_to_minor_text_padding;
159 gfx::Insets insets = GetInsets();
161 std::max(max_complex_width,
162 std::max(max_simple_width + max_minor_text_width_ +
164 minimum_preferred_width_ - 2 * insets.width())),
165 height + insets.height());
168 void SubmenuView::GetAccessibleState(ui::AXViewState* state) {
169 // Inherit most of the state from the parent menu item, except the role.
171 GetMenuItem()->GetAccessibleState(state);
172 state->role = ui::AX_ROLE_MENU_LIST_POPUP;
175 ui::TextInputClient* SubmenuView::GetTextInputClient() {
176 return &prefix_selector_;
179 void SubmenuView::PaintChildren(gfx::Canvas* canvas) {
180 View::PaintChildren(canvas);
182 if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON)
183 PaintDropIndicator(canvas, drop_item_, drop_position_);
186 bool SubmenuView::GetDropFormats(
188 std::set<OSExchangeData::CustomFormat>* custom_formats) {
189 DCHECK(GetMenuItem()->GetMenuController());
190 return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats,
194 bool SubmenuView::AreDropTypesRequired() {
195 DCHECK(GetMenuItem()->GetMenuController());
196 return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
199 bool SubmenuView::CanDrop(const OSExchangeData& data) {
200 DCHECK(GetMenuItem()->GetMenuController());
201 return GetMenuItem()->GetMenuController()->CanDrop(this, data);
204 void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) {
205 DCHECK(GetMenuItem()->GetMenuController());
206 GetMenuItem()->GetMenuController()->OnDragEntered(this, event);
209 int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) {
210 DCHECK(GetMenuItem()->GetMenuController());
211 return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event);
214 void SubmenuView::OnDragExited() {
215 DCHECK(GetMenuItem()->GetMenuController());
216 GetMenuItem()->GetMenuController()->OnDragExited(this);
219 int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) {
220 DCHECK(GetMenuItem()->GetMenuController());
221 return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event);
224 bool 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.
232 // Find the index of the first menu item whose y-coordinate is >= visible
235 while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y()))
237 if (i == menu_item_count)
239 int first_vis_index = std::max(0,
240 (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1);
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);
248 return OnScroll(0, e.y_offset());
249 for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
252 if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) {
253 if (first_vis_index == 0)
257 scroll_target = GetMenuItemAt(first_vis_index)->y();
259 if (first_vis_index + 1 == menu_item_count)
261 scroll_target = GetMenuItemAt(first_vis_index + 1)->y();
262 if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y())
265 ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target),
267 vis_bounds = GetVisibleBounds();
273 void SubmenuView::OnGestureEvent(ui::GestureEvent* event) {
275 switch (event->type()) {
276 case ui::ET_GESTURE_SCROLL_BEGIN:
277 scroll_animator_->Stop();
279 case ui::ET_GESTURE_SCROLL_UPDATE:
280 handled = OnScroll(0, event->details().scroll_y());
282 case ui::ET_GESTURE_SCROLL_END:
284 case ui::ET_SCROLL_FLING_START:
285 if (event->details().velocity_y() != 0.0f)
286 scroll_animator_->Start(0, event->details().velocity_y());
288 case ui::ET_GESTURE_TAP_DOWN:
289 case ui::ET_SCROLL_FLING_CANCEL:
290 if (scroll_animator_->is_scrolling())
291 scroll_animator_->Stop();
303 int SubmenuView::GetRowCount() {
304 return GetMenuItemCount();
307 int SubmenuView::GetSelectedRow() {
309 for (int i = 0; i < child_count(); ++i) {
310 if (child_at(i)->id() != MenuItemView::kMenuItemViewID)
313 if (static_cast<MenuItemView*>(child_at(i))->IsSelected())
322 void SubmenuView::SetSelectedRow(int row) {
323 GetMenuItem()->GetMenuController()->SetSelection(
325 MenuController::SELECTION_DEFAULT);
328 base::string16 SubmenuView::GetTextForRow(int row) {
329 return GetMenuItemAt(row)->title();
332 bool SubmenuView::IsShowing() {
333 return host_ && host_->IsMenuHostVisible();
336 void SubmenuView::ShowAt(Widget* parent,
337 const gfx::Rect& bounds,
340 host_->ShowMenuHost(do_capture);
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
348 host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
351 GetScrollViewContainer()->NotifyAccessibilityEvent(
352 ui::AX_EVENT_MENU_START,
354 NotifyAccessibilityEvent(
355 ui::AX_EVENT_MENU_POPUP_START,
359 void SubmenuView::Reposition(const gfx::Rect& bounds) {
361 host_->SetMenuHostBounds(bounds);
364 void SubmenuView::Close() {
366 NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true);
367 GetScrollViewContainer()->NotifyAccessibilityEvent(
368 ui::AX_EVENT_MENU_END, true);
370 host_->DestroyMenuHost();
375 void SubmenuView::Hide() {
377 host_->HideMenuHost();
378 if (scroll_animator_->is_scrolling())
379 scroll_animator_->Stop();
382 void SubmenuView::ReleaseCapture() {
384 host_->ReleaseMenuHostCapture();
387 bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
388 return views::FocusManager::IsTabTraversalKeyEvent(e);
391 MenuItemView* SubmenuView::GetMenuItem() const {
392 return parent_menu_item_;
395 void SubmenuView::SetDropMenuItem(MenuItemView* item,
396 MenuDelegate::DropPosition position) {
397 if (drop_item_ == item && drop_position_ == position)
399 SchedulePaintForDropIndicator(drop_item_, drop_position_);
401 drop_position_ = position;
402 SchedulePaintForDropIndicator(drop_item_, drop_position_);
405 bool SubmenuView::GetShowSelection(MenuItemView* item) {
406 if (drop_item_ == NULL)
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
411 return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON);
414 MenuScrollViewContainer* 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();
420 return scroll_view_container_;
423 void SubmenuView::MenuHostDestroyed() {
425 GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED);
428 const char* SubmenuView::GetClassName() const {
429 return kViewClassName;
432 void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
436 void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas,
438 MenuDelegate::DropPosition position) {
439 if (position == MenuDelegate::DROP_NONE)
442 gfx::Rect bounds = CalculateDropIndicatorBounds(item, position);
443 canvas->FillRect(bounds, kDropIndicatorColor);
446 void SubmenuView::SchedulePaintForDropIndicator(
448 MenuDelegate::DropPosition position) {
452 if (position == MenuDelegate::DROP_ON) {
453 item->SchedulePaint();
454 } else if (position != MenuDelegate::DROP_NONE) {
455 SchedulePaintInRect(CalculateDropIndicatorBounds(item, position));
459 gfx::Rect SubmenuView::CalculateDropIndicatorBounds(
461 MenuDelegate::DropPosition position) {
462 DCHECK(position != MenuDelegate::DROP_NONE);
463 gfx::Rect item_bounds = item->bounds();
465 case MenuDelegate::DROP_BEFORE:
466 item_bounds.Offset(0, -kDropIndicatorHeight / 2);
467 item_bounds.set_height(kDropIndicatorHeight);
470 case MenuDelegate::DROP_AFTER:
471 item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
472 item_bounds.set_height(kDropIndicatorHeight);
476 // Don't render anything for on.
481 bool 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);
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);