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.
4
5#include "ui/views/controls/menu/submenu_view.h"
6
7#include <algorithm>
8
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_item_view.h"
18#include "ui/views/controls/menu/menu_scroll_view_container.h"
19#include "ui/views/widget/root_view.h"
20#include "ui/views/widget/widget.h"
21
22namespace {
23
24// Height of the drop indicator. This should be an even number.
25const int kDropIndicatorHeight = 2;
26
27// Color of the drop indicator.
28const SkColor kDropIndicatorColor = SK_ColorBLACK;
29
30}  // namespace
31
32namespace views {
33
34// static
35const char SubmenuView::kViewClassName[] = "SubmenuView";
36
37SubmenuView::SubmenuView(MenuItemView* parent)
38    : parent_menu_item_(parent),
39      host_(NULL),
40      drop_item_(NULL),
41      drop_position_(MenuDelegate::DROP_NONE),
42      scroll_view_container_(NULL),
43      max_minor_text_width_(0),
44      minimum_preferred_width_(0),
45      resize_open_menu_(false),
46      scroll_animator_(new ScrollAnimator(this)),
47      roundoff_error_(0),
48      prefix_selector_(this) {
49  DCHECK(parent);
50  // We'll delete ourselves, otherwise the ScrollView would delete us on close.
51  set_owned_by_client();
52}
53
54SubmenuView::~SubmenuView() {
55  // The menu may not have been closed yet (it will be hidden, but not
56  // necessarily closed).
57  Close();
58
59  delete scroll_view_container_;
60}
61
62int SubmenuView::GetMenuItemCount() {
63  int count = 0;
64  for (int i = 0; i < child_count(); ++i) {
65    if (child_at(i)->id() == MenuItemView::kMenuItemViewID)
66      count++;
67  }
68  return count;
69}
70
71MenuItemView* SubmenuView::GetMenuItemAt(int index) {
72  for (int i = 0, count = 0; i < child_count(); ++i) {
73    if (child_at(i)->id() == MenuItemView::kMenuItemViewID &&
74        count++ == index) {
75      return static_cast<MenuItemView*>(child_at(i));
76    }
77  }
78  NOTREACHED();
79  return NULL;
80}
81
82void SubmenuView::ChildPreferredSizeChanged(View* child) {
83  if (!resize_open_menu_)
84    return;
85
86  MenuItemView *item = GetMenuItem();
87  MenuController* controller = item->GetMenuController();
88
89  if (controller) {
90    bool dir;
91    gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir);
92    Reposition(bounds);
93  }
94}
95
96void SubmenuView::Layout() {
97  // We're in a ScrollView, and need to set our width/height ourselves.
98  if (!parent())
99    return;
100
101  // Use our current y, unless it means part of the menu isn't visible anymore.
102  int pref_height = GetPreferredSize().height();
103  int new_y;
104  if (pref_height > parent()->height())
105    new_y = std::max(parent()->height() - pref_height, y());
106  else
107    new_y = 0;
108  SetBounds(x(), new_y, parent()->width(), pref_height);
109
110  gfx::Insets insets = GetInsets();
111  int x = insets.left();
112  int y = insets.top();
113  int menu_item_width = width() - insets.width();
114  for (int i = 0; i < child_count(); ++i) {
115    View* child = child_at(i);
116    if (child->visible()) {
117      int child_height = child->GetHeightForWidth(menu_item_width);
118      child->SetBounds(x, y, menu_item_width, child_height);
119      y += child_height;
120    }
121  }
122}
123
124gfx::Size SubmenuView::GetPreferredSize() const {
125  if (!has_children())
126    return gfx::Size();
127
128  max_minor_text_width_ = 0;
129  // The maximum width of items which contain maybe a label and multiple views.
130  int max_complex_width = 0;
131  // The max. width of items which contain a label and maybe an accelerator.
132  int max_simple_width = 0;
133
134  // We perform the size calculation in two passes. In the first pass, we
135  // calculate the width of the menu. In the second, we calculate the height
136  // using that width. This allows views that have flexible widths to adjust
137  // accordingly.
138  for (int i = 0; i < child_count(); ++i) {
139    const View* child = child_at(i);
140    if (!child->visible())
141      continue;
142    if (child->id() == MenuItemView::kMenuItemViewID) {
143      const MenuItemView* menu = static_cast<const MenuItemView*>(child);
144      const MenuItemView::MenuItemDimensions& dimensions =
145          menu->GetDimensions();
146      max_simple_width = std::max(
147          max_simple_width, dimensions.standard_width);
148      max_minor_text_width_ =
149          std::max(max_minor_text_width_, dimensions.minor_text_width);
150      max_complex_width = std::max(max_complex_width,
151          dimensions.standard_width + dimensions.children_width);
152    } else {
153      max_complex_width = std::max(max_complex_width,
154                                   child->GetPreferredSize().width());
155    }
156  }
157  if (max_minor_text_width_ > 0) {
158    max_minor_text_width_ +=
159        GetMenuItem()->GetMenuConfig().label_to_minor_text_padding;
160  }
161  // Finish calculating our optimum width.
162  gfx::Insets insets = GetInsets();
163  int width = std::max(max_complex_width,
164                       std::max(max_simple_width + max_minor_text_width_ +
165                                    insets.width(),
166                                minimum_preferred_width_ - 2 * insets.width()));
167
168  // Then, the height for that width.
169  int height = 0;
170  int menu_item_width = width - insets.width();
171  for (int i = 0; i < child_count(); ++i) {
172    const View* child = child_at(i);
173    height += child->visible() ? child->GetHeightForWidth(menu_item_width) : 0;
174  }
175
176  return gfx::Size(width, height + insets.height());
177}
178
179void SubmenuView::GetAccessibleState(ui::AXViewState* state) {
180  // Inherit most of the state from the parent menu item, except the role.
181  if (GetMenuItem())
182    GetMenuItem()->GetAccessibleState(state);
183  state->role = ui::AX_ROLE_MENU_LIST_POPUP;
184}
185
186ui::TextInputClient* SubmenuView::GetTextInputClient() {
187  return &prefix_selector_;
188}
189
190void SubmenuView::PaintChildren(gfx::Canvas* canvas,
191                                const views::CullSet& cull_set) {
192  View::PaintChildren(canvas, cull_set);
193
194  if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON)
195    PaintDropIndicator(canvas, drop_item_, drop_position_);
196}
197
198bool SubmenuView::GetDropFormats(
199      int* formats,
200      std::set<OSExchangeData::CustomFormat>* custom_formats) {
201  DCHECK(GetMenuItem()->GetMenuController());
202  return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats,
203                                                            custom_formats);
204}
205
206bool SubmenuView::AreDropTypesRequired() {
207  DCHECK(GetMenuItem()->GetMenuController());
208  return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
209}
210
211bool SubmenuView::CanDrop(const OSExchangeData& data) {
212  DCHECK(GetMenuItem()->GetMenuController());
213  return GetMenuItem()->GetMenuController()->CanDrop(this, data);
214}
215
216void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) {
217  DCHECK(GetMenuItem()->GetMenuController());
218  GetMenuItem()->GetMenuController()->OnDragEntered(this, event);
219}
220
221int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) {
222  DCHECK(GetMenuItem()->GetMenuController());
223  return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event);
224}
225
226void SubmenuView::OnDragExited() {
227  DCHECK(GetMenuItem()->GetMenuController());
228  GetMenuItem()->GetMenuController()->OnDragExited(this);
229}
230
231int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) {
232  DCHECK(GetMenuItem()->GetMenuController());
233  return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event);
234}
235
236bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent& e) {
237  gfx::Rect vis_bounds = GetVisibleBounds();
238  int menu_item_count = GetMenuItemCount();
239  if (vis_bounds.height() == height() || !menu_item_count) {
240    // All menu items are visible, nothing to scroll.
241    return true;
242  }
243
244  // Find the index of the first menu item whose y-coordinate is >= visible
245  // y-coordinate.
246  int i = 0;
247  while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y()))
248    ++i;
249  if (i == menu_item_count)
250    return true;
251  int first_vis_index = std::max(0,
252      (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1);
253
254  // If the first item isn't entirely visible, make it visible, otherwise make
255  // the next/previous one entirely visible. If enough wasn't scrolled to show
256  // any new rows, then just scroll the amount so that smooth scrolling using
257  // the trackpad is possible.
258  int delta = abs(e.y_offset() / ui::MouseWheelEvent::kWheelDelta);
259  if (delta == 0)
260    return OnScroll(0, e.y_offset());
261  for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
262    int scroll_target;
263    if (scroll_up) {
264      if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) {
265        if (first_vis_index == 0)
266          break;
267        first_vis_index--;
268      }
269      scroll_target = GetMenuItemAt(first_vis_index)->y();
270    } else {
271      if (first_vis_index + 1 == menu_item_count)
272        break;
273      scroll_target = GetMenuItemAt(first_vis_index + 1)->y();
274      if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y())
275        first_vis_index++;
276    }
277    ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target),
278                                  vis_bounds.size()));
279    vis_bounds = GetVisibleBounds();
280  }
281
282  return true;
283}
284
285void SubmenuView::OnGestureEvent(ui::GestureEvent* event) {
286  bool handled = true;
287  switch (event->type()) {
288    case ui::ET_GESTURE_SCROLL_BEGIN:
289      scroll_animator_->Stop();
290      break;
291    case ui::ET_GESTURE_SCROLL_UPDATE:
292      handled = OnScroll(0, event->details().scroll_y());
293      break;
294    case ui::ET_GESTURE_SCROLL_END:
295      break;
296    case ui::ET_SCROLL_FLING_START:
297      if (event->details().velocity_y() != 0.0f)
298        scroll_animator_->Start(0, event->details().velocity_y());
299      break;
300    case ui::ET_GESTURE_TAP_DOWN:
301    case ui::ET_SCROLL_FLING_CANCEL:
302      if (scroll_animator_->is_scrolling())
303        scroll_animator_->Stop();
304      else
305        handled = false;
306      break;
307    default:
308      handled = false;
309      break;
310  }
311  if (handled)
312    event->SetHandled();
313}
314
315int SubmenuView::GetRowCount() {
316  return GetMenuItemCount();
317}
318
319int SubmenuView::GetSelectedRow() {
320  int row = 0;
321  for (int i = 0; i < child_count(); ++i) {
322    if (child_at(i)->id() != MenuItemView::kMenuItemViewID)
323      continue;
324
325    if (static_cast<MenuItemView*>(child_at(i))->IsSelected())
326      return row;
327
328    row++;
329  }
330
331  return -1;
332}
333
334void SubmenuView::SetSelectedRow(int row) {
335  GetMenuItem()->GetMenuController()->SetSelection(
336      GetMenuItemAt(row),
337      MenuController::SELECTION_DEFAULT);
338}
339
340base::string16 SubmenuView::GetTextForRow(int row) {
341  return GetMenuItemAt(row)->title();
342}
343
344bool SubmenuView::IsShowing() {
345  return host_ && host_->IsMenuHostVisible();
346}
347
348void SubmenuView::ShowAt(Widget* parent,
349                         const gfx::Rect& bounds,
350                         bool do_capture) {
351  if (host_) {
352    host_->ShowMenuHost(do_capture);
353  } else {
354    host_ = new MenuHost(this);
355    // Force construction of the scroll view container.
356    GetScrollViewContainer();
357    // Force a layout since our preferred size may not have changed but our
358    // content may have.
359    InvalidateLayout();
360    host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
361  }
362
363  GetScrollViewContainer()->NotifyAccessibilityEvent(
364      ui::AX_EVENT_MENU_START,
365      true);
366  NotifyAccessibilityEvent(
367      ui::AX_EVENT_MENU_POPUP_START,
368      true);
369}
370
371void SubmenuView::Reposition(const gfx::Rect& bounds) {
372  if (host_)
373    host_->SetMenuHostBounds(bounds);
374}
375
376void SubmenuView::Close() {
377  if (host_) {
378    NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true);
379    GetScrollViewContainer()->NotifyAccessibilityEvent(
380        ui::AX_EVENT_MENU_END, true);
381
382    host_->DestroyMenuHost();
383    host_ = NULL;
384  }
385}
386
387void SubmenuView::Hide() {
388  if (host_)
389    host_->HideMenuHost();
390  if (scroll_animator_->is_scrolling())
391    scroll_animator_->Stop();
392}
393
394void SubmenuView::ReleaseCapture() {
395  if (host_)
396    host_->ReleaseMenuHostCapture();
397}
398
399bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
400  return views::FocusManager::IsTabTraversalKeyEvent(e);
401}
402
403MenuItemView* SubmenuView::GetMenuItem() const {
404  return parent_menu_item_;
405}
406
407void SubmenuView::SetDropMenuItem(MenuItemView* item,
408                                  MenuDelegate::DropPosition position) {
409  if (drop_item_ == item && drop_position_ == position)
410    return;
411  SchedulePaintForDropIndicator(drop_item_, drop_position_);
412  drop_item_ = item;
413  drop_position_ = position;
414  SchedulePaintForDropIndicator(drop_item_, drop_position_);
415}
416
417bool SubmenuView::GetShowSelection(MenuItemView* item) {
418  if (drop_item_ == NULL)
419    return true;
420  // Something is being dropped on one of this menus items. Show the
421  // selection if the drop is on the passed in item and the drop position is
422  // ON.
423  return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON);
424}
425
426MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() {
427  if (!scroll_view_container_) {
428    scroll_view_container_ = new MenuScrollViewContainer(this);
429    // Otherwise MenuHost would delete us.
430    scroll_view_container_->set_owned_by_client();
431  }
432  return scroll_view_container_;
433}
434
435void SubmenuView::MenuHostDestroyed() {
436  host_ = NULL;
437  GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED);
438}
439
440const char* SubmenuView::GetClassName() const {
441  return kViewClassName;
442}
443
444void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
445  SchedulePaint();
446}
447
448void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas,
449                                     MenuItemView* item,
450                                     MenuDelegate::DropPosition position) {
451  if (position == MenuDelegate::DROP_NONE)
452    return;
453
454  gfx::Rect bounds = CalculateDropIndicatorBounds(item, position);
455  canvas->FillRect(bounds, kDropIndicatorColor);
456}
457
458void SubmenuView::SchedulePaintForDropIndicator(
459    MenuItemView* item,
460    MenuDelegate::DropPosition position) {
461  if (item == NULL)
462    return;
463
464  if (position == MenuDelegate::DROP_ON) {
465    item->SchedulePaint();
466  } else if (position != MenuDelegate::DROP_NONE) {
467    SchedulePaintInRect(CalculateDropIndicatorBounds(item, position));
468  }
469}
470
471gfx::Rect SubmenuView::CalculateDropIndicatorBounds(
472    MenuItemView* item,
473    MenuDelegate::DropPosition position) {
474  DCHECK(position != MenuDelegate::DROP_NONE);
475  gfx::Rect item_bounds = item->bounds();
476  switch (position) {
477    case MenuDelegate::DROP_BEFORE:
478      item_bounds.Offset(0, -kDropIndicatorHeight / 2);
479      item_bounds.set_height(kDropIndicatorHeight);
480      return item_bounds;
481
482    case MenuDelegate::DROP_AFTER:
483      item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
484      item_bounds.set_height(kDropIndicatorHeight);
485      return item_bounds;
486
487    default:
488      // Don't render anything for on.
489      return gfx::Rect();
490  }
491}
492
493bool SubmenuView::OnScroll(float dx, float dy) {
494  const gfx::Rect& vis_bounds = GetVisibleBounds();
495  const gfx::Rect& full_bounds = bounds();
496  int x = vis_bounds.x();
497  float y_f = vis_bounds.y() - dy - roundoff_error_;
498  int y = gfx::ToRoundedInt(y_f);
499  roundoff_error_ = y - y_f;
500  // clamp y to [0, full_height - vis_height)
501  y = std::min(y, full_bounds.height() - vis_bounds.height() - 1);
502  y = std::max(y, 0);
503  gfx::Rect new_vis_bounds(x, y, vis_bounds.width(), vis_bounds.height());
504  if (new_vis_bounds != vis_bounds) {
505    ScrollRectToVisible(new_vis_bounds);
506    return true;
507  }
508  return false;
509}
510
511}  // namespace views
512