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