Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / views / controls / menu / submenu_view.cc
blob1c5461a84524b8b9bba99e4a7da01e45e30ae54b
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"
7 #include <algorithm>
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"
24 namespace {
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;
32 } // namespace
34 namespace views {
36 // static
37 const char SubmenuView::kViewClassName[] = "SubmenuView";
39 SubmenuView::SubmenuView(MenuItemView* parent)
40 : parent_menu_item_(parent),
41 host_(NULL),
42 drop_item_(NULL),
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)),
49 roundoff_error_(0),
50 prefix_selector_(this) {
51 DCHECK(parent);
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).
59 Close();
61 delete scroll_view_container_;
64 int SubmenuView::GetMenuItemCount() {
65 int count = 0;
66 for (int i = 0; i < child_count(); ++i) {
67 if (child_at(i)->id() == MenuItemView::kMenuItemViewID)
68 count++;
70 return count;
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 &&
76 count++ == index) {
77 return static_cast<MenuItemView*>(child_at(i));
80 NOTREACHED();
81 return NULL;
84 PrefixSelector* SubmenuView::GetPrefixSelector() {
85 return &prefix_selector_;
88 void SubmenuView::ChildPreferredSizeChanged(View* child) {
89 if (!resize_open_menu_)
90 return;
92 MenuItemView *item = GetMenuItem();
93 MenuController* controller = item->GetMenuController();
95 if (controller) {
96 bool dir;
97 gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir);
98 Reposition(bounds);
102 void SubmenuView::Layout() {
103 // We're in a ScrollView, and need to set our width/height ourselves.
104 if (!parent())
105 return;
107 // Use our current y, unless it means part of the menu isn't visible anymore.
108 int pref_height = GetPreferredSize().height();
109 int new_y;
110 if (pref_height > parent()->height())
111 new_y = std::max(parent()->height() - pref_height, y());
112 else
113 new_y = 0;
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);
125 y += child_height;
130 gfx::Size SubmenuView::GetPreferredSize() const {
131 if (!has_children())
132 return gfx::Size();
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
143 // accordingly.
144 for (int i = 0; i < child_count(); ++i) {
145 const View* child = child_at(i);
146 if (!child->visible())
147 continue;
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);
158 } else {
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_ +
171 insets.width(),
172 minimum_preferred_width_ - 2 * insets.width()));
174 // Then, the height for that width.
175 int height = 0;
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.
187 if (GetMenuItem())
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;
196 if (drop_item_) {
197 switch (drop_position_) {
198 case MenuDelegate::DROP_NONE:
199 case MenuDelegate::DROP_ON:
200 break;
201 case MenuDelegate::DROP_UNKNOWN:
202 case MenuDelegate::DROP_BEFORE:
203 case MenuDelegate::DROP_AFTER:
204 paint_drop_indicator = true;
205 break;
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(
217 int* formats,
218 std::set<OSExchangeData::CustomFormat>* custom_formats) {
219 DCHECK(GetMenuItem()->GetMenuController());
220 return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats,
221 custom_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.
259 return true;
262 // Find the index of the first menu item whose y-coordinate is >= visible
263 // y-coordinate.
264 int i = 0;
265 while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y()))
266 ++i;
267 if (i == menu_item_count)
268 return true;
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);
277 if (delta == 0)
278 return OnScroll(0, e.y_offset());
279 for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
280 int scroll_target;
281 if (scroll_up) {
282 if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) {
283 if (first_vis_index == 0)
284 break;
285 first_vis_index--;
287 scroll_target = GetMenuItemAt(first_vis_index)->y();
288 } else {
289 if (first_vis_index + 1 == menu_item_count)
290 break;
291 scroll_target = GetMenuItemAt(first_vis_index + 1)->y();
292 if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y())
293 first_vis_index++;
295 ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target),
296 vis_bounds.size()));
297 vis_bounds = GetVisibleBounds();
300 return true;
303 void SubmenuView::OnGestureEvent(ui::GestureEvent* event) {
304 bool handled = true;
305 switch (event->type()) {
306 case ui::ET_GESTURE_SCROLL_BEGIN:
307 scroll_animator_->Stop();
308 break;
309 case ui::ET_GESTURE_SCROLL_UPDATE:
310 handled = OnScroll(0, event->details().scroll_y());
311 break;
312 case ui::ET_GESTURE_SCROLL_END:
313 break;
314 case ui::ET_SCROLL_FLING_START:
315 if (event->details().velocity_y() != 0.0f)
316 scroll_animator_->Start(0, event->details().velocity_y());
317 break;
318 case ui::ET_GESTURE_TAP_DOWN:
319 case ui::ET_SCROLL_FLING_CANCEL:
320 if (scroll_animator_->is_scrolling())
321 scroll_animator_->Stop();
322 else
323 handled = false;
324 break;
325 default:
326 handled = false;
327 break;
329 if (handled)
330 event->SetHandled();
333 int SubmenuView::GetRowCount() {
334 return GetMenuItemCount();
337 int SubmenuView::GetSelectedRow() {
338 int row = 0;
339 for (int i = 0; i < child_count(); ++i) {
340 if (child_at(i)->id() != MenuItemView::kMenuItemViewID)
341 continue;
343 if (static_cast<MenuItemView*>(child_at(i))->IsSelected())
344 return row;
346 row++;
349 return -1;
352 void SubmenuView::SetSelectedRow(int row) {
353 GetMenuItem()->GetMenuController()->SetSelection(
354 GetMenuItemAt(row),
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,
368 bool do_capture) {
369 if (host_) {
370 host_->ShowMenuHost(do_capture);
371 } else {
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
376 // content may have.
377 InvalidateLayout();
378 host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
381 GetScrollViewContainer()->NotifyAccessibilityEvent(
382 ui::AX_EVENT_MENU_START,
383 true);
384 NotifyAccessibilityEvent(
385 ui::AX_EVENT_MENU_POPUP_START,
386 true);
389 void SubmenuView::Reposition(const gfx::Rect& bounds) {
390 if (host_)
391 host_->SetMenuHostBounds(bounds);
394 void SubmenuView::Close() {
395 if (host_) {
396 NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true);
397 GetScrollViewContainer()->NotifyAccessibilityEvent(
398 ui::AX_EVENT_MENU_END, true);
400 host_->DestroyMenuHost();
401 host_ = NULL;
405 void SubmenuView::Hide() {
406 if (host_)
407 host_->HideMenuHost();
408 if (scroll_animator_->is_scrolling())
409 scroll_animator_->Stop();
412 void SubmenuView::ReleaseCapture() {
413 if (host_)
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)
428 return;
429 SchedulePaintForDropIndicator(drop_item_, drop_position_);
430 drop_item_ = item;
431 drop_position_ = position;
432 SchedulePaintForDropIndicator(drop_item_, drop_position_);
435 bool SubmenuView::GetShowSelection(MenuItemView* item) {
436 if (drop_item_ == NULL)
437 return true;
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
440 // ON.
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() {
454 host_ = NULL;
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) {
463 SchedulePaint();
466 void SubmenuView::SchedulePaintForDropIndicator(
467 MenuItemView* item,
468 MenuDelegate::DropPosition position) {
469 if (item == NULL)
470 return;
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(
480 MenuItemView* item,
481 MenuDelegate::DropPosition position) {
482 DCHECK(position != MenuDelegate::DROP_NONE);
483 gfx::Rect item_bounds = item->bounds();
484 switch (position) {
485 case MenuDelegate::DROP_BEFORE:
486 item_bounds.Offset(0, -kDropIndicatorHeight / 2);
487 item_bounds.set_height(kDropIndicatorHeight);
488 return item_bounds;
490 case MenuDelegate::DROP_AFTER:
491 item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
492 item_bounds.set_height(kDropIndicatorHeight);
493 return item_bounds;
495 default:
496 // Don't render anything for on.
497 return gfx::Rect();
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);
510 y = std::max(y, 0);
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);
514 return true;
516 return false;
519 } // namespace views