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