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