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/compositor/paint_recorder.h"
12 #include "ui/events/event.h"
13 #include "ui/gfx/canvas.h"
14 #include "ui/gfx/geometry/safe_integer_conversions.h"
15 #include "ui/views/controls/menu/menu_config.h"
16 #include "ui/views/controls/menu/menu_controller.h"
17 #include "ui/views/controls/menu/menu_host.h"
18 #include "ui/views/controls/menu/menu_item_view.h"
19 #include "ui/views/controls/menu/menu_scroll_view_container.h"
20 #include "ui/views/widget/root_view.h"
21 #include "ui/views/widget/widget.h"
25 // Height of the drop indicator. This should be an even number.
26 const int kDropIndicatorHeight
= 2;
28 // Color of the drop indicator.
29 const SkColor kDropIndicatorColor
= SK_ColorBLACK
;
36 const char SubmenuView::kViewClassName
[] = "SubmenuView";
38 SubmenuView::SubmenuView(MenuItemView
* parent
)
39 : parent_menu_item_(parent
),
42 drop_position_(MenuDelegate::DROP_NONE
),
43 scroll_view_container_(NULL
),
44 max_minor_text_width_(0),
45 minimum_preferred_width_(0),
46 resize_open_menu_(false),
47 scroll_animator_(new ScrollAnimator(this)),
49 prefix_selector_(this) {
51 // We'll delete ourselves, otherwise the ScrollView would delete us on close.
52 set_owned_by_client();
55 SubmenuView::~SubmenuView() {
56 // The menu may not have been closed yet (it will be hidden, but not
57 // necessarily closed).
60 delete scroll_view_container_
;
63 int SubmenuView::GetMenuItemCount() {
65 for (int i
= 0; i
< child_count(); ++i
) {
66 if (child_at(i
)->id() == MenuItemView::kMenuItemViewID
)
72 MenuItemView
* SubmenuView::GetMenuItemAt(int index
) {
73 for (int i
= 0, count
= 0; i
< child_count(); ++i
) {
74 if (child_at(i
)->id() == MenuItemView::kMenuItemViewID
&&
76 return static_cast<MenuItemView
*>(child_at(i
));
83 void SubmenuView::ChildPreferredSizeChanged(View
* child
) {
84 if (!resize_open_menu_
)
87 MenuItemView
*item
= GetMenuItem();
88 MenuController
* controller
= item
->GetMenuController();
92 gfx::Rect bounds
= controller
->CalculateMenuBounds(item
, false, &dir
);
97 void SubmenuView::Layout() {
98 // We're in a ScrollView, and need to set our width/height ourselves.
102 // Use our current y, unless it means part of the menu isn't visible anymore.
103 int pref_height
= GetPreferredSize().height();
105 if (pref_height
> parent()->height())
106 new_y
= std::max(parent()->height() - pref_height
, y());
109 SetBounds(x(), new_y
, parent()->width(), pref_height
);
111 gfx::Insets insets
= GetInsets();
112 int x
= insets
.left();
113 int y
= insets
.top();
114 int menu_item_width
= width() - insets
.width();
115 for (int i
= 0; i
< child_count(); ++i
) {
116 View
* child
= child_at(i
);
117 if (child
->visible()) {
118 int child_height
= child
->GetHeightForWidth(menu_item_width
);
119 child
->SetBounds(x
, y
, menu_item_width
, child_height
);
125 gfx::Size
SubmenuView::GetPreferredSize() const {
129 max_minor_text_width_
= 0;
130 // The maximum width of items which contain maybe a label and multiple views.
131 int max_complex_width
= 0;
132 // The max. width of items which contain a label and maybe an accelerator.
133 int max_simple_width
= 0;
135 // We perform the size calculation in two passes. In the first pass, we
136 // calculate the width of the menu. In the second, we calculate the height
137 // using that width. This allows views that have flexible widths to adjust
139 for (int i
= 0; i
< child_count(); ++i
) {
140 const View
* child
= child_at(i
);
141 if (!child
->visible())
143 if (child
->id() == MenuItemView::kMenuItemViewID
) {
144 const MenuItemView
* menu
= static_cast<const MenuItemView
*>(child
);
145 const MenuItemView::MenuItemDimensions
& dimensions
=
146 menu
->GetDimensions();
147 max_simple_width
= std::max(
148 max_simple_width
, dimensions
.standard_width
);
149 max_minor_text_width_
=
150 std::max(max_minor_text_width_
, dimensions
.minor_text_width
);
151 max_complex_width
= std::max(max_complex_width
,
152 dimensions
.standard_width
+ dimensions
.children_width
);
154 max_complex_width
= std::max(max_complex_width
,
155 child
->GetPreferredSize().width());
158 if (max_minor_text_width_
> 0) {
159 max_minor_text_width_
+=
160 GetMenuItem()->GetMenuConfig().label_to_minor_text_padding
;
162 // Finish calculating our optimum width.
163 gfx::Insets insets
= GetInsets();
164 int width
= std::max(max_complex_width
,
165 std::max(max_simple_width
+ max_minor_text_width_
+
167 minimum_preferred_width_
- 2 * insets
.width()));
169 // Then, the height for that width.
171 int menu_item_width
= width
- insets
.width();
172 for (int i
= 0; i
< child_count(); ++i
) {
173 const View
* child
= child_at(i
);
174 height
+= child
->visible() ? child
->GetHeightForWidth(menu_item_width
) : 0;
177 return gfx::Size(width
, height
+ insets
.height());
180 void SubmenuView::GetAccessibleState(ui::AXViewState
* state
) {
181 // Inherit most of the state from the parent menu item, except the role.
183 GetMenuItem()->GetAccessibleState(state
);
184 state
->role
= ui::AX_ROLE_MENU_LIST_POPUP
;
187 ui::TextInputClient
* SubmenuView::GetTextInputClient() {
188 return &prefix_selector_
;
191 void SubmenuView::PaintChildren(const ui::PaintContext
& context
) {
192 View::PaintChildren(context
);
194 bool paint_drop_indicator
= false;
196 switch (drop_position_
) {
197 case MenuDelegate::DROP_NONE
:
198 case MenuDelegate::DROP_ON
:
200 case MenuDelegate::DROP_UNKNOWN
:
201 case MenuDelegate::DROP_BEFORE
:
202 case MenuDelegate::DROP_AFTER
:
203 paint_drop_indicator
= true;
208 if (paint_drop_indicator
) {
209 gfx::Rect bounds
= CalculateDropIndicatorBounds(drop_item_
, drop_position_
);
210 ui::PaintRecorder
recorder(context
);
211 recorder
.canvas()->FillRect(bounds
, kDropIndicatorColor
);
215 bool SubmenuView::GetDropFormats(
217 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
218 DCHECK(GetMenuItem()->GetMenuController());
219 return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats
,
223 bool SubmenuView::AreDropTypesRequired() {
224 DCHECK(GetMenuItem()->GetMenuController());
225 return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
228 bool SubmenuView::CanDrop(const OSExchangeData
& data
) {
229 DCHECK(GetMenuItem()->GetMenuController());
230 return GetMenuItem()->GetMenuController()->CanDrop(this, data
);
233 void SubmenuView::OnDragEntered(const ui::DropTargetEvent
& event
) {
234 DCHECK(GetMenuItem()->GetMenuController());
235 GetMenuItem()->GetMenuController()->OnDragEntered(this, event
);
238 int SubmenuView::OnDragUpdated(const ui::DropTargetEvent
& event
) {
239 DCHECK(GetMenuItem()->GetMenuController());
240 return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event
);
243 void SubmenuView::OnDragExited() {
244 DCHECK(GetMenuItem()->GetMenuController());
245 GetMenuItem()->GetMenuController()->OnDragExited(this);
248 int SubmenuView::OnPerformDrop(const ui::DropTargetEvent
& event
) {
249 DCHECK(GetMenuItem()->GetMenuController());
250 return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event
);
253 bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent
& e
) {
254 gfx::Rect vis_bounds
= GetVisibleBounds();
255 int menu_item_count
= GetMenuItemCount();
256 if (vis_bounds
.height() == height() || !menu_item_count
) {
257 // All menu items are visible, nothing to scroll.
261 // Find the index of the first menu item whose y-coordinate is >= visible
264 while ((i
< menu_item_count
) && (GetMenuItemAt(i
)->y() < vis_bounds
.y()))
266 if (i
== menu_item_count
)
268 int first_vis_index
= std::max(0,
269 (GetMenuItemAt(i
)->y() == vis_bounds
.y()) ? i
: i
- 1);
271 // If the first item isn't entirely visible, make it visible, otherwise make
272 // the next/previous one entirely visible. If enough wasn't scrolled to show
273 // any new rows, then just scroll the amount so that smooth scrolling using
274 // the trackpad is possible.
275 int delta
= abs(e
.y_offset() / ui::MouseWheelEvent::kWheelDelta
);
277 return OnScroll(0, e
.y_offset());
278 for (bool scroll_up
= (e
.y_offset() > 0); delta
!= 0; --delta
) {
281 if (GetMenuItemAt(first_vis_index
)->y() == vis_bounds
.y()) {
282 if (first_vis_index
== 0)
286 scroll_target
= GetMenuItemAt(first_vis_index
)->y();
288 if (first_vis_index
+ 1 == menu_item_count
)
290 scroll_target
= GetMenuItemAt(first_vis_index
+ 1)->y();
291 if (GetMenuItemAt(first_vis_index
)->y() == vis_bounds
.y())
294 ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target
),
296 vis_bounds
= GetVisibleBounds();
302 void SubmenuView::OnGestureEvent(ui::GestureEvent
* event
) {
304 switch (event
->type()) {
305 case ui::ET_GESTURE_SCROLL_BEGIN
:
306 scroll_animator_
->Stop();
308 case ui::ET_GESTURE_SCROLL_UPDATE
:
309 handled
= OnScroll(0, event
->details().scroll_y());
311 case ui::ET_GESTURE_SCROLL_END
:
313 case ui::ET_SCROLL_FLING_START
:
314 if (event
->details().velocity_y() != 0.0f
)
315 scroll_animator_
->Start(0, event
->details().velocity_y());
317 case ui::ET_GESTURE_TAP_DOWN
:
318 case ui::ET_SCROLL_FLING_CANCEL
:
319 if (scroll_animator_
->is_scrolling())
320 scroll_animator_
->Stop();
332 int SubmenuView::GetRowCount() {
333 return GetMenuItemCount();
336 int SubmenuView::GetSelectedRow() {
338 for (int i
= 0; i
< child_count(); ++i
) {
339 if (child_at(i
)->id() != MenuItemView::kMenuItemViewID
)
342 if (static_cast<MenuItemView
*>(child_at(i
))->IsSelected())
351 void SubmenuView::SetSelectedRow(int row
) {
352 GetMenuItem()->GetMenuController()->SetSelection(
354 MenuController::SELECTION_DEFAULT
);
357 base::string16
SubmenuView::GetTextForRow(int row
) {
358 return GetMenuItemAt(row
)->title();
361 bool SubmenuView::IsShowing() {
362 return host_
&& host_
->IsMenuHostVisible();
365 void SubmenuView::ShowAt(Widget
* parent
,
366 const gfx::Rect
& bounds
,
369 host_
->ShowMenuHost(do_capture
);
371 host_
= new MenuHost(this);
372 // Force construction of the scroll view container.
373 GetScrollViewContainer();
374 // Force a layout since our preferred size may not have changed but our
377 host_
->InitMenuHost(parent
, bounds
, scroll_view_container_
, do_capture
);
380 GetScrollViewContainer()->NotifyAccessibilityEvent(
381 ui::AX_EVENT_MENU_START
,
383 NotifyAccessibilityEvent(
384 ui::AX_EVENT_MENU_POPUP_START
,
388 void SubmenuView::Reposition(const gfx::Rect
& bounds
) {
390 host_
->SetMenuHostBounds(bounds
);
393 void SubmenuView::Close() {
395 NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END
, true);
396 GetScrollViewContainer()->NotifyAccessibilityEvent(
397 ui::AX_EVENT_MENU_END
, true);
399 host_
->DestroyMenuHost();
404 void SubmenuView::Hide() {
406 host_
->HideMenuHost();
407 if (scroll_animator_
->is_scrolling())
408 scroll_animator_
->Stop();
411 void SubmenuView::ReleaseCapture() {
413 host_
->ReleaseMenuHostCapture();
416 bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent
& e
) {
417 return views::FocusManager::IsTabTraversalKeyEvent(e
);
420 MenuItemView
* SubmenuView::GetMenuItem() const {
421 return parent_menu_item_
;
424 void SubmenuView::SetDropMenuItem(MenuItemView
* item
,
425 MenuDelegate::DropPosition position
) {
426 if (drop_item_
== item
&& drop_position_
== position
)
428 SchedulePaintForDropIndicator(drop_item_
, drop_position_
);
430 drop_position_
= position
;
431 SchedulePaintForDropIndicator(drop_item_
, drop_position_
);
434 bool SubmenuView::GetShowSelection(MenuItemView
* item
) {
435 if (drop_item_
== NULL
)
437 // Something is being dropped on one of this menus items. Show the
438 // selection if the drop is on the passed in item and the drop position is
440 return (drop_item_
== item
&& drop_position_
== MenuDelegate::DROP_ON
);
443 MenuScrollViewContainer
* SubmenuView::GetScrollViewContainer() {
444 if (!scroll_view_container_
) {
445 scroll_view_container_
= new MenuScrollViewContainer(this);
446 // Otherwise MenuHost would delete us.
447 scroll_view_container_
->set_owned_by_client();
449 return scroll_view_container_
;
452 void SubmenuView::MenuHostDestroyed() {
454 GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED
);
457 const char* SubmenuView::GetClassName() const {
458 return kViewClassName
;
461 void SubmenuView::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
465 void SubmenuView::SchedulePaintForDropIndicator(
467 MenuDelegate::DropPosition position
) {
471 if (position
== MenuDelegate::DROP_ON
) {
472 item
->SchedulePaint();
473 } else if (position
!= MenuDelegate::DROP_NONE
) {
474 SchedulePaintInRect(CalculateDropIndicatorBounds(item
, position
));
478 gfx::Rect
SubmenuView::CalculateDropIndicatorBounds(
480 MenuDelegate::DropPosition position
) {
481 DCHECK(position
!= MenuDelegate::DROP_NONE
);
482 gfx::Rect item_bounds
= item
->bounds();
484 case MenuDelegate::DROP_BEFORE
:
485 item_bounds
.Offset(0, -kDropIndicatorHeight
/ 2);
486 item_bounds
.set_height(kDropIndicatorHeight
);
489 case MenuDelegate::DROP_AFTER
:
490 item_bounds
.Offset(0, item_bounds
.height() - kDropIndicatorHeight
/ 2);
491 item_bounds
.set_height(kDropIndicatorHeight
);
495 // Don't render anything for on.
500 bool SubmenuView::OnScroll(float dx
, float dy
) {
501 const gfx::Rect
& vis_bounds
= GetVisibleBounds();
502 const gfx::Rect
& full_bounds
= bounds();
503 int x
= vis_bounds
.x();
504 float y_f
= vis_bounds
.y() - dy
- roundoff_error_
;
505 int y
= gfx::ToRoundedInt(y_f
);
506 roundoff_error_
= y
- y_f
;
507 // clamp y to [0, full_height - vis_height)
508 y
= std::min(y
, full_bounds
.height() - vis_bounds
.height() - 1);
510 gfx::Rect
new_vis_bounds(x
, y
, vis_bounds
.width(), vis_bounds
.height());
511 if (new_vis_bounds
!= vis_bounds
) {
512 ScrollRectToVisible(new_vis_bounds
);