submenu_view.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
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_scroll_view_container.h"
18#include "ui/views/widget/root_view.h"
19#include "ui/views/widget/widget.h"
20
21namespace {
22
23// Height of the drop indicator. This should be an even number.
24const int kDropIndicatorHeight = 2;
25
26// Color of the drop indicator.
27const SkColor kDropIndicatorColor = SK_ColorBLACK;
28
29}  // namespace
30
31namespace views {
32
33// static
34const char SubmenuView::kViewClassName[] = "SubmenuView";
35
36SubmenuView::SubmenuView(MenuItemView* parent)
37    : parent_menu_item_(parent),
38      host_(NULL),
39      drop_item_(NULL),
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)),
46      roundoff_error_(0),
47      prefix_selector_(this) {
48  DCHECK(parent);
49  // We'll delete ourselves, otherwise the ScrollView would delete us on close.
50  set_owned_by_client();
51}
52
53SubmenuView::~SubmenuView() {
54  // The menu may not have been closed yet (it will be hidden, but not
55  // necessarily closed).
56  Close();
57
58  delete scroll_view_container_;
59}
60
61int SubmenuView::GetMenuItemCount() {
62  int count = 0;
63  for (int i = 0; i < child_count(); ++i) {
64    if (child_at(i)->id() == MenuItemView::kMenuItemViewID)
65      count++;
66  }
67  return count;
68}
69
70MenuItemView* SubmenuView::GetMenuItemAt(int index) {
71  for (int i = 0, count = 0; i < child_count(); ++i) {
72    if (child_at(i)->id() == MenuItemView::kMenuItemViewID &&
73        count++ == index) {
74      return static_cast<MenuItemView*>(child_at(i));
75    }
76  }
77  NOTREACHED();
78  return NULL;
79}
80
81void SubmenuView::ChildPreferredSizeChanged(View* child) {
82  if (!resize_open_menu_)
83    return;
84
85  MenuItemView *item = GetMenuItem();
86  MenuController* controller = item->GetMenuController();
87
88  if (controller) {
89    bool dir;
90    gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir);
91    Reposition(bounds);
92  }
93}
94
95void SubmenuView::Layout() {
96  // We're in a ScrollView, and need to set our width/height ourselves.
97  if (!parent())
98    return;
99
100  // Use our current y, unless it means part of the menu isn't visible anymore.
101  int pref_height = GetPreferredSize().height();
102  int new_y;
103  if (pref_height > parent()->height())
104    new_y = std::max(parent()->height() - pref_height, y());
105  else
106    new_y = 0;
107  SetBounds(x(), new_y, parent()->width(), pref_height);
108
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();
119    }
120  }
121}
122
123gfx::Size SubmenuView::GetPreferredSize() {
124  if (!has_children())
125    return gfx::Size();
126
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;
132  int height = 0;
133  for (int i = 0; i < child_count(); ++i) {
134    View* child = child_at(i);
135    if (!child->visible())
136      continue;
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;
148    } else {
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();
153    }
154  }
155  if (max_minor_text_width_ > 0) {
156    max_minor_text_width_ +=
157        GetMenuItem()->GetMenuConfig().label_to_minor_text_padding;
158  }
159  gfx::Insets insets = GetInsets();
160  return gfx::Size(
161      std::max(max_complex_width,
162               std::max(max_simple_width + max_minor_text_width_ +
163                        insets.width(),
164               minimum_preferred_width_ - 2 * insets.width())),
165      height + insets.height());
166}
167
168void SubmenuView::GetAccessibleState(ui::AXViewState* state) {
169  // Inherit most of the state from the parent menu item, except the role.
170  if (GetMenuItem())
171    GetMenuItem()->GetAccessibleState(state);
172  state->role = ui::AX_ROLE_MENU_LIST_POPUP;
173}
174
175ui::TextInputClient* SubmenuView::GetTextInputClient() {
176  return &prefix_selector_;
177}
178
179void SubmenuView::PaintChildren(gfx::Canvas* canvas) {
180  View::PaintChildren(canvas);
181
182  if (drop_item_ && drop_position_ != MenuDelegate::DROP_ON)
183    PaintDropIndicator(canvas, drop_item_, drop_position_);
184}
185
186bool SubmenuView::GetDropFormats(
187      int* formats,
188      std::set<OSExchangeData::CustomFormat>* custom_formats) {
189  DCHECK(GetMenuItem()->GetMenuController());
190  return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats,
191                                                            custom_formats);
192}
193
194bool SubmenuView::AreDropTypesRequired() {
195  DCHECK(GetMenuItem()->GetMenuController());
196  return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
197}
198
199bool SubmenuView::CanDrop(const OSExchangeData& data) {
200  DCHECK(GetMenuItem()->GetMenuController());
201  return GetMenuItem()->GetMenuController()->CanDrop(this, data);
202}
203
204void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) {
205  DCHECK(GetMenuItem()->GetMenuController());
206  GetMenuItem()->GetMenuController()->OnDragEntered(this, event);
207}
208
209int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) {
210  DCHECK(GetMenuItem()->GetMenuController());
211  return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event);
212}
213
214void SubmenuView::OnDragExited() {
215  DCHECK(GetMenuItem()->GetMenuController());
216  GetMenuItem()->GetMenuController()->OnDragExited(this);
217}
218
219int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event) {
220  DCHECK(GetMenuItem()->GetMenuController());
221  return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event);
222}
223
224bool 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.
229    return true;
230  }
231
232  // Find the index of the first menu item whose y-coordinate is >= visible
233  // y-coordinate.
234  int i = 0;
235  while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y()))
236    ++i;
237  if (i == menu_item_count)
238    return true;
239  int first_vis_index = std::max(0,
240      (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1);
241
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);
247  if (delta == 0)
248    return OnScroll(0, e.y_offset());
249  for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
250    int scroll_target;
251    if (scroll_up) {
252      if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) {
253        if (first_vis_index == 0)
254          break;
255        first_vis_index--;
256      }
257      scroll_target = GetMenuItemAt(first_vis_index)->y();
258    } else {
259      if (first_vis_index + 1 == menu_item_count)
260        break;
261      scroll_target = GetMenuItemAt(first_vis_index + 1)->y();
262      if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y())
263        first_vis_index++;
264    }
265    ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target),
266                                  vis_bounds.size()));
267    vis_bounds = GetVisibleBounds();
268  }
269
270  return true;
271}
272
273void SubmenuView::OnGestureEvent(ui::GestureEvent* event) {
274  bool handled = true;
275  switch (event->type()) {
276    case ui::ET_GESTURE_SCROLL_BEGIN:
277      scroll_animator_->Stop();
278      break;
279    case ui::ET_GESTURE_SCROLL_UPDATE:
280      handled = OnScroll(0, event->details().scroll_y());
281      break;
282    case ui::ET_GESTURE_SCROLL_END:
283      break;
284    case ui::ET_SCROLL_FLING_START:
285      if (event->details().velocity_y() != 0.0f)
286        scroll_animator_->Start(0, event->details().velocity_y());
287      break;
288    case ui::ET_GESTURE_TAP_DOWN:
289    case ui::ET_SCROLL_FLING_CANCEL:
290      if (scroll_animator_->is_scrolling())
291        scroll_animator_->Stop();
292      else
293        handled = false;
294      break;
295    default:
296      handled = false;
297      break;
298  }
299  if (handled)
300    event->SetHandled();
301}
302
303int SubmenuView::GetRowCount() {
304  return GetMenuItemCount();
305}
306
307int SubmenuView::GetSelectedRow() {
308  int row = 0;
309  for (int i = 0; i < child_count(); ++i) {
310    if (child_at(i)->id() != MenuItemView::kMenuItemViewID)
311      continue;
312
313    if (static_cast<MenuItemView*>(child_at(i))->IsSelected())
314      return row;
315
316    row++;
317  }
318
319  return -1;
320}
321
322void SubmenuView::SetSelectedRow(int row) {
323  GetMenuItem()->GetMenuController()->SetSelection(
324      GetMenuItemAt(row),
325      MenuController::SELECTION_DEFAULT);
326}
327
328base::string16 SubmenuView::GetTextForRow(int row) {
329  return GetMenuItemAt(row)->title();
330}
331
332bool SubmenuView::IsShowing() {
333  return host_ && host_->IsMenuHostVisible();
334}
335
336void SubmenuView::ShowAt(Widget* parent,
337                         const gfx::Rect& bounds,
338                         bool do_capture) {
339  if (host_) {
340    host_->ShowMenuHost(do_capture);
341  } else {
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
346    // content may have.
347    InvalidateLayout();
348    host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
349  }
350
351  GetScrollViewContainer()->NotifyAccessibilityEvent(
352      ui::AX_EVENT_MENU_START,
353      true);
354  NotifyAccessibilityEvent(
355      ui::AX_EVENT_MENU_POPUP_START,
356      true);
357}
358
359void SubmenuView::Reposition(const gfx::Rect& bounds) {
360  if (host_)
361    host_->SetMenuHostBounds(bounds);
362}
363
364void SubmenuView::Close() {
365  if (host_) {
366    NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true);
367    GetScrollViewContainer()->NotifyAccessibilityEvent(
368        ui::AX_EVENT_MENU_END, true);
369
370    host_->DestroyMenuHost();
371    host_ = NULL;
372  }
373}
374
375void SubmenuView::Hide() {
376  if (host_)
377    host_->HideMenuHost();
378  if (scroll_animator_->is_scrolling())
379    scroll_animator_->Stop();
380}
381
382void SubmenuView::ReleaseCapture() {
383  if (host_)
384    host_->ReleaseMenuHostCapture();
385}
386
387bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
388  return views::FocusManager::IsTabTraversalKeyEvent(e);
389}
390
391MenuItemView* SubmenuView::GetMenuItem() const {
392  return parent_menu_item_;
393}
394
395void SubmenuView::SetDropMenuItem(MenuItemView* item,
396                                  MenuDelegate::DropPosition position) {
397  if (drop_item_ == item && drop_position_ == position)
398    return;
399  SchedulePaintForDropIndicator(drop_item_, drop_position_);
400  drop_item_ = item;
401  drop_position_ = position;
402  SchedulePaintForDropIndicator(drop_item_, drop_position_);
403}
404
405bool SubmenuView::GetShowSelection(MenuItemView* item) {
406  if (drop_item_ == NULL)
407    return true;
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
410  // ON.
411  return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON);
412}
413
414MenuScrollViewContainer* 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();
419  }
420  return scroll_view_container_;
421}
422
423void SubmenuView::MenuHostDestroyed() {
424  host_ = NULL;
425  GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED);
426}
427
428const char* SubmenuView::GetClassName() const {
429  return kViewClassName;
430}
431
432void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
433  SchedulePaint();
434}
435
436void SubmenuView::PaintDropIndicator(gfx::Canvas* canvas,
437                                     MenuItemView* item,
438                                     MenuDelegate::DropPosition position) {
439  if (position == MenuDelegate::DROP_NONE)
440    return;
441
442  gfx::Rect bounds = CalculateDropIndicatorBounds(item, position);
443  canvas->FillRect(bounds, kDropIndicatorColor);
444}
445
446void SubmenuView::SchedulePaintForDropIndicator(
447    MenuItemView* item,
448    MenuDelegate::DropPosition position) {
449  if (item == NULL)
450    return;
451
452  if (position == MenuDelegate::DROP_ON) {
453    item->SchedulePaint();
454  } else if (position != MenuDelegate::DROP_NONE) {
455    SchedulePaintInRect(CalculateDropIndicatorBounds(item, position));
456  }
457}
458
459gfx::Rect SubmenuView::CalculateDropIndicatorBounds(
460    MenuItemView* item,
461    MenuDelegate::DropPosition position) {
462  DCHECK(position != MenuDelegate::DROP_NONE);
463  gfx::Rect item_bounds = item->bounds();
464  switch (position) {
465    case MenuDelegate::DROP_BEFORE:
466      item_bounds.Offset(0, -kDropIndicatorHeight / 2);
467      item_bounds.set_height(kDropIndicatorHeight);
468      return item_bounds;
469
470    case MenuDelegate::DROP_AFTER:
471      item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
472      item_bounds.set_height(kDropIndicatorHeight);
473      return item_bounds;
474
475    default:
476      // Don't render anything for on.
477      return gfx::Rect();
478  }
479}
480
481bool 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);
490  y = std::max(y, 0);
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);
494    return true;
495  }
496  return false;
497}
498
499}  // namespace views
500