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/base/ime/input_method.h"
12 #include "ui/compositor/paint_recorder.h"
13 #include "ui/events/event.h"
14 #include "ui/gfx/canvas.h"
15 #include "ui/gfx/geometry/safe_integer_conversions.h"
16 #include "ui/views/controls/menu/menu_config.h"
17 #include "ui/views/controls/menu/menu_controller.h"
18 #include "ui/views/controls/menu/menu_host.h"
19 #include "ui/views/controls/menu/menu_item_view.h"
20 #include "ui/views/controls/menu/menu_scroll_view_container.h"
21 #include "ui/views/widget/root_view.h"
22 #include "ui/views/widget/widget.h"
26 // Height of the drop indicator. This should be an even number.
27 const int kDropIndicatorHeight
= 2;
29 // Color of the drop indicator.
30 const SkColor kDropIndicatorColor
= SK_ColorBLACK
;
37 const char SubmenuView::kViewClassName
[] = "SubmenuView";
39 SubmenuView::SubmenuView(MenuItemView
* parent
)
40 : parent_menu_item_(parent
),
43 drop_position_(MenuDelegate::DROP_NONE
),
44 scroll_view_container_(NULL
),
45 max_minor_text_width_(0),
46 minimum_preferred_width_(0),
47 resize_open_menu_(false),
48 scroll_animator_(new ScrollAnimator(this)),
50 prefix_selector_(this) {
52 // We'll delete ourselves, otherwise the ScrollView would delete us on close.
53 set_owned_by_client();
56 SubmenuView::~SubmenuView() {
57 // The menu may not have been closed yet (it will be hidden, but not
58 // necessarily closed).
61 delete scroll_view_container_
;
64 int SubmenuView::GetMenuItemCount() {
66 for (int i
= 0; i
< child_count(); ++i
) {
67 if (child_at(i
)->id() == MenuItemView::kMenuItemViewID
)
73 MenuItemView
* SubmenuView::GetMenuItemAt(int index
) {
74 for (int i
= 0, count
= 0; i
< child_count(); ++i
) {
75 if (child_at(i
)->id() == MenuItemView::kMenuItemViewID
&&
77 return static_cast<MenuItemView
*>(child_at(i
));
84 PrefixSelector
* SubmenuView::GetPrefixSelector() {
85 return &prefix_selector_
;
88 void SubmenuView::ChildPreferredSizeChanged(View
* child
) {
89 if (!resize_open_menu_
)
92 MenuItemView
*item
= GetMenuItem();
93 MenuController
* controller
= item
->GetMenuController();
97 gfx::Rect bounds
= controller
->CalculateMenuBounds(item
, false, &dir
);
102 void SubmenuView::Layout() {
103 // We're in a ScrollView, and need to set our width/height ourselves.
107 // Use our current y, unless it means part of the menu isn't visible anymore.
108 int pref_height
= GetPreferredSize().height();
110 if (pref_height
> parent()->height())
111 new_y
= std::max(parent()->height() - pref_height
, y());
114 SetBounds(x(), new_y
, parent()->width(), pref_height
);
116 gfx::Insets insets
= GetInsets();
117 int x
= insets
.left();
118 int y
= insets
.top();
119 int menu_item_width
= width() - insets
.width();
120 for (int i
= 0; i
< child_count(); ++i
) {
121 View
* child
= child_at(i
);
122 if (child
->visible()) {
123 int child_height
= child
->GetHeightForWidth(menu_item_width
);
124 child
->SetBounds(x
, y
, menu_item_width
, child_height
);
130 gfx::Size
SubmenuView::GetPreferredSize() const {
134 max_minor_text_width_
= 0;
135 // The maximum width of items which contain maybe a label and multiple views.
136 int max_complex_width
= 0;
137 // The max. width of items which contain a label and maybe an accelerator.
138 int max_simple_width
= 0;
140 // We perform the size calculation in two passes. In the first pass, we
141 // calculate the width of the menu. In the second, we calculate the height
142 // using that width. This allows views that have flexible widths to adjust
144 for (int i
= 0; i
< child_count(); ++i
) {
145 const View
* child
= child_at(i
);
146 if (!child
->visible())
148 if (child
->id() == MenuItemView::kMenuItemViewID
) {
149 const MenuItemView
* menu
= static_cast<const MenuItemView
*>(child
);
150 const MenuItemView::MenuItemDimensions
& dimensions
=
151 menu
->GetDimensions();
152 max_simple_width
= std::max(
153 max_simple_width
, dimensions
.standard_width
);
154 max_minor_text_width_
=
155 std::max(max_minor_text_width_
, dimensions
.minor_text_width
);
156 max_complex_width
= std::max(max_complex_width
,
157 dimensions
.standard_width
+ dimensions
.children_width
);
159 max_complex_width
= std::max(max_complex_width
,
160 child
->GetPreferredSize().width());
163 if (max_minor_text_width_
> 0) {
164 max_minor_text_width_
+=
165 GetMenuItem()->GetMenuConfig().label_to_minor_text_padding
;
167 // Finish calculating our optimum width.
168 gfx::Insets insets
= GetInsets();
169 int width
= std::max(max_complex_width
,
170 std::max(max_simple_width
+ max_minor_text_width_
+
172 minimum_preferred_width_
- 2 * insets
.width()));
174 // Then, the height for that width.
176 int menu_item_width
= width
- insets
.width();
177 for (int i
= 0; i
< child_count(); ++i
) {
178 const View
* child
= child_at(i
);
179 height
+= child
->visible() ? child
->GetHeightForWidth(menu_item_width
) : 0;
182 return gfx::Size(width
, height
+ insets
.height());
185 void SubmenuView::GetAccessibleState(ui::AXViewState
* state
) {
186 // Inherit most of the state from the parent menu item, except the role.
188 GetMenuItem()->GetAccessibleState(state
);
189 state
->role
= ui::AX_ROLE_MENU_LIST_POPUP
;
192 void SubmenuView::PaintChildren(const ui::PaintContext
& context
) {
193 View::PaintChildren(context
);
195 bool paint_drop_indicator
= false;
197 switch (drop_position_
) {
198 case MenuDelegate::DROP_NONE
:
199 case MenuDelegate::DROP_ON
:
201 case MenuDelegate::DROP_UNKNOWN
:
202 case MenuDelegate::DROP_BEFORE
:
203 case MenuDelegate::DROP_AFTER
:
204 paint_drop_indicator
= true;
209 if (paint_drop_indicator
) {
210 gfx::Rect bounds
= CalculateDropIndicatorBounds(drop_item_
, drop_position_
);
211 ui::PaintRecorder
recorder(context
, size());
212 recorder
.canvas()->FillRect(bounds
, kDropIndicatorColor
);
216 bool SubmenuView::GetDropFormats(
218 std::set
<OSExchangeData::CustomFormat
>* custom_formats
) {
219 DCHECK(GetMenuItem()->GetMenuController());
220 return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats
,
224 bool SubmenuView::AreDropTypesRequired() {
225 DCHECK(GetMenuItem()->GetMenuController());
226 return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
229 bool SubmenuView::CanDrop(const OSExchangeData
& data
) {
230 DCHECK(GetMenuItem()->GetMenuController());
231 return GetMenuItem()->GetMenuController()->CanDrop(this, data
);
234 void SubmenuView::OnDragEntered(const ui::DropTargetEvent
& event
) {
235 DCHECK(GetMenuItem()->GetMenuController());
236 GetMenuItem()->GetMenuController()->OnDragEntered(this, event
);
239 int SubmenuView::OnDragUpdated(const ui::DropTargetEvent
& event
) {
240 DCHECK(GetMenuItem()->GetMenuController());
241 return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event
);
244 void SubmenuView::OnDragExited() {
245 DCHECK(GetMenuItem()->GetMenuController());
246 GetMenuItem()->GetMenuController()->OnDragExited(this);
249 int SubmenuView::OnPerformDrop(const ui::DropTargetEvent
& event
) {
250 DCHECK(GetMenuItem()->GetMenuController());
251 return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event
);
254 bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent
& e
) {
255 gfx::Rect vis_bounds
= GetVisibleBounds();
256 int menu_item_count
= GetMenuItemCount();
257 if (vis_bounds
.height() == height() || !menu_item_count
) {
258 // All menu items are visible, nothing to scroll.
262 // Find the index of the first menu item whose y-coordinate is >= visible
265 while ((i
< menu_item_count
) && (GetMenuItemAt(i
)->y() < vis_bounds
.y()))
267 if (i
== menu_item_count
)
269 int first_vis_index
= std::max(0,
270 (GetMenuItemAt(i
)->y() == vis_bounds
.y()) ? i
: i
- 1);
272 // If the first item isn't entirely visible, make it visible, otherwise make
273 // the next/previous one entirely visible. If enough wasn't scrolled to show
274 // any new rows, then just scroll the amount so that smooth scrolling using
275 // the trackpad is possible.
276 int delta
= abs(e
.y_offset() / ui::MouseWheelEvent::kWheelDelta
);
278 return OnScroll(0, e
.y_offset());
279 for (bool scroll_up
= (e
.y_offset() > 0); delta
!= 0; --delta
) {
282 if (GetMenuItemAt(first_vis_index
)->y() == vis_bounds
.y()) {
283 if (first_vis_index
== 0)
287 scroll_target
= GetMenuItemAt(first_vis_index
)->y();
289 if (first_vis_index
+ 1 == menu_item_count
)
291 scroll_target
= GetMenuItemAt(first_vis_index
+ 1)->y();
292 if (GetMenuItemAt(first_vis_index
)->y() == vis_bounds
.y())
295 ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target
),
297 vis_bounds
= GetVisibleBounds();
303 void SubmenuView::OnGestureEvent(ui::GestureEvent
* event
) {
305 switch (event
->type()) {
306 case ui::ET_GESTURE_SCROLL_BEGIN
:
307 scroll_animator_
->Stop();
309 case ui::ET_GESTURE_SCROLL_UPDATE
:
310 handled
= OnScroll(0, event
->details().scroll_y());
312 case ui::ET_GESTURE_SCROLL_END
:
314 case ui::ET_SCROLL_FLING_START
:
315 if (event
->details().velocity_y() != 0.0f
)
316 scroll_animator_
->Start(0, event
->details().velocity_y());
318 case ui::ET_GESTURE_TAP_DOWN
:
319 case ui::ET_SCROLL_FLING_CANCEL
:
320 if (scroll_animator_
->is_scrolling())
321 scroll_animator_
->Stop();
333 int SubmenuView::GetRowCount() {
334 return GetMenuItemCount();
337 int SubmenuView::GetSelectedRow() {
339 for (int i
= 0; i
< child_count(); ++i
) {
340 if (child_at(i
)->id() != MenuItemView::kMenuItemViewID
)
343 if (static_cast<MenuItemView
*>(child_at(i
))->IsSelected())
352 void SubmenuView::SetSelectedRow(int row
) {
353 GetMenuItem()->GetMenuController()->SetSelection(
355 MenuController::SELECTION_DEFAULT
);
358 base::string16
SubmenuView::GetTextForRow(int row
) {
359 return GetMenuItemAt(row
)->title();
362 bool SubmenuView::IsShowing() {
363 return host_
&& host_
->IsMenuHostVisible();
366 void SubmenuView::ShowAt(Widget
* parent
,
367 const gfx::Rect
& bounds
,
370 host_
->ShowMenuHost(do_capture
);
372 host_
= new MenuHost(this);
373 // Force construction of the scroll view container.
374 GetScrollViewContainer();
375 // Force a layout since our preferred size may not have changed but our
378 host_
->InitMenuHost(parent
, bounds
, scroll_view_container_
, do_capture
);
381 GetScrollViewContainer()->NotifyAccessibilityEvent(
382 ui::AX_EVENT_MENU_START
,
384 NotifyAccessibilityEvent(
385 ui::AX_EVENT_MENU_POPUP_START
,
389 void SubmenuView::Reposition(const gfx::Rect
& bounds
) {
391 host_
->SetMenuHostBounds(bounds
);
394 void SubmenuView::Close() {
396 NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END
, true);
397 GetScrollViewContainer()->NotifyAccessibilityEvent(
398 ui::AX_EVENT_MENU_END
, true);
400 host_
->DestroyMenuHost();
405 void SubmenuView::Hide() {
407 host_
->HideMenuHost();
408 if (scroll_animator_
->is_scrolling())
409 scroll_animator_
->Stop();
412 void SubmenuView::ReleaseCapture() {
414 host_
->ReleaseMenuHostCapture();
417 bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent
& e
) {
418 return views::FocusManager::IsTabTraversalKeyEvent(e
);
421 MenuItemView
* SubmenuView::GetMenuItem() const {
422 return parent_menu_item_
;
425 void SubmenuView::SetDropMenuItem(MenuItemView
* item
,
426 MenuDelegate::DropPosition position
) {
427 if (drop_item_
== item
&& drop_position_
== position
)
429 SchedulePaintForDropIndicator(drop_item_
, drop_position_
);
431 drop_position_
= position
;
432 SchedulePaintForDropIndicator(drop_item_
, drop_position_
);
435 bool SubmenuView::GetShowSelection(MenuItemView
* item
) {
436 if (drop_item_
== NULL
)
438 // Something is being dropped on one of this menus items. Show the
439 // selection if the drop is on the passed in item and the drop position is
441 return (drop_item_
== item
&& drop_position_
== MenuDelegate::DROP_ON
);
444 MenuScrollViewContainer
* SubmenuView::GetScrollViewContainer() {
445 if (!scroll_view_container_
) {
446 scroll_view_container_
= new MenuScrollViewContainer(this);
447 // Otherwise MenuHost would delete us.
448 scroll_view_container_
->set_owned_by_client();
450 return scroll_view_container_
;
453 void SubmenuView::MenuHostDestroyed() {
455 GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED
);
458 const char* SubmenuView::GetClassName() const {
459 return kViewClassName
;
462 void SubmenuView::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
466 void SubmenuView::SchedulePaintForDropIndicator(
468 MenuDelegate::DropPosition position
) {
472 if (position
== MenuDelegate::DROP_ON
) {
473 item
->SchedulePaint();
474 } else if (position
!= MenuDelegate::DROP_NONE
) {
475 SchedulePaintInRect(CalculateDropIndicatorBounds(item
, position
));
479 gfx::Rect
SubmenuView::CalculateDropIndicatorBounds(
481 MenuDelegate::DropPosition position
) {
482 DCHECK(position
!= MenuDelegate::DROP_NONE
);
483 gfx::Rect item_bounds
= item
->bounds();
485 case MenuDelegate::DROP_BEFORE
:
486 item_bounds
.Offset(0, -kDropIndicatorHeight
/ 2);
487 item_bounds
.set_height(kDropIndicatorHeight
);
490 case MenuDelegate::DROP_AFTER
:
491 item_bounds
.Offset(0, item_bounds
.height() - kDropIndicatorHeight
/ 2);
492 item_bounds
.set_height(kDropIndicatorHeight
);
496 // Don't render anything for on.
501 bool SubmenuView::OnScroll(float dx
, float dy
) {
502 const gfx::Rect
& vis_bounds
= GetVisibleBounds();
503 const gfx::Rect
& full_bounds
= bounds();
504 int x
= vis_bounds
.x();
505 float y_f
= vis_bounds
.y() - dy
- roundoff_error_
;
506 int y
= gfx::ToRoundedInt(y_f
);
507 roundoff_error_
= y
- y_f
;
508 // clamp y to [0, full_height - vis_height)
509 y
= std::min(y
, full_bounds
.height() - vis_bounds
.height() - 1);
511 gfx::Rect
new_vis_bounds(x
, y
, vis_bounds
.width(), vis_bounds
.height());
512 if (new_vis_bounds
!= vis_bounds
) {
513 ScrollRectToVisible(new_vis_bounds
);