1 // Copyright 2014 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.
5 #include "ui/views/controls/menu/menu_controller.h"
7 #include "base/strings/utf_string_conversions.h"
8 #include "ui/aura/scoped_window_targeter.h"
9 #include "ui/aura/window.h"
10 #include "ui/events/event_handler.h"
11 #include "ui/events/null_event_targeter.h"
12 #include "ui/events/test/event_generator.h"
13 #include "ui/views/controls/menu/menu_item_view.h"
14 #include "ui/views/controls/menu/submenu_view.h"
15 #include "ui/views/test/views_test_base.h"
18 #include "ui/aura/scoped_window_targeter.h"
19 #include "ui/aura/window.h"
26 #include "ui/events/test/events_test_utils_x11.h"
34 class SubmenuViewShown
: public SubmenuView
{
36 SubmenuViewShown(MenuItemView
* parent
) : SubmenuView(parent
) {}
37 ~SubmenuViewShown() override
{}
38 bool IsShowing() override
{ return true; }
41 DISALLOW_COPY_AND_ASSIGN(SubmenuViewShown
);
44 class TestEventHandler
: public ui::EventHandler
{
46 TestEventHandler() : outstanding_touches_(0) {}
48 void OnTouchEvent(ui::TouchEvent
* event
) override
{
49 switch(event
->type()) {
50 case ui::ET_TOUCH_PRESSED
:
51 outstanding_touches_
++;
53 case ui::ET_TOUCH_RELEASED
:
54 case ui::ET_TOUCH_CANCELLED
:
55 outstanding_touches_
--;
62 int outstanding_touches() const { return outstanding_touches_
; }
65 int outstanding_touches_
;
66 DISALLOW_COPY_AND_ASSIGN(TestEventHandler
);
71 class TestMenuItemViewShown
: public MenuItemView
{
73 TestMenuItemViewShown() : MenuItemView(nullptr) {
74 submenu_
= new SubmenuViewShown(this);
76 ~TestMenuItemViewShown() override
{}
79 DISALLOW_COPY_AND_ASSIGN(TestMenuItemViewShown
);
82 class MenuControllerTest
: public ViewsTestBase
{
84 MenuControllerTest() : menu_controller_(nullptr) {
87 ~MenuControllerTest() override
{}
90 void SetUp() override
{
91 ViewsTestBase::SetUp();
95 void TearDown() override
{
98 menu_controller_
->showing_
= false;
99 menu_controller_
->owner_
= nullptr;
100 delete menu_controller_
;
101 menu_controller_
= nullptr;
103 ViewsTestBase::TearDown();
106 void ReleaseTouchId(int id
) {
107 event_generator_
->ReleaseTouchId(id
);
110 void PressKey(ui::KeyboardCode key_code
) {
111 event_generator_
->PressKey(key_code
, 0);
114 #if defined(OS_LINUX) && defined(USE_X11)
115 void TestEventTargeter() {
117 // With the |ui::NullEventTargeter| instantiated and assigned we expect
118 // the menu to not handle the key event.
119 aura::ScopedWindowTargeter
scoped_targeter(
120 owner()->GetNativeWindow()->GetRootWindow(),
121 scoped_ptr
<ui::EventTargeter
>(new ui::NullEventTargeter
));
122 event_generator_
->PressKey(ui::VKEY_ESCAPE
, 0);
123 EXPECT_EQ(MenuController::EXIT_NONE
, menu_exit_type());
125 // Now that the targeter has been destroyed, expect to exit the menu
126 // normally when hitting escape.
127 event_generator_
->PressKey(ui::VKEY_ESCAPE
, 0);
128 EXPECT_EQ(MenuController::EXIT_OUTERMOST
, menu_exit_type());
130 #endif // defined(OS_LINUX) && defined(USE_X11)
133 void SetPendingStateItem(MenuItemView
* item
) {
134 menu_controller_
->pending_state_
.item
= item
;
135 menu_controller_
->pending_state_
.submenu_open
= true;
138 void ResetSelection() {
139 menu_controller_
->SetSelection(
141 MenuController::SELECTION_EXIT
|
142 MenuController::SELECTION_UPDATE_IMMEDIATELY
);
145 void IncrementSelection() {
146 menu_controller_
->IncrementSelection(
147 MenuController::INCREMENT_SELECTION_DOWN
);
150 void DecrementSelection() {
151 menu_controller_
->IncrementSelection(
152 MenuController::INCREMENT_SELECTION_UP
);
155 MenuItemView
* FindInitialSelectableMenuItemDown(MenuItemView
* parent
) {
156 return menu_controller_
->FindInitialSelectableMenuItem(
157 parent
, MenuController::INCREMENT_SELECTION_DOWN
);
160 MenuItemView
* FindInitialSelectableMenuItemUp(MenuItemView
* parent
) {
161 return menu_controller_
->FindInitialSelectableMenuItem(
162 parent
, MenuController::INCREMENT_SELECTION_UP
);
165 MenuItemView
* FindNextSelectableMenuItem(MenuItemView
* parent
,
168 return menu_controller_
->FindNextSelectableMenuItem(
169 parent
, index
, MenuController::INCREMENT_SELECTION_DOWN
);
172 MenuItemView
* FindPreviousSelectableMenuItem(MenuItemView
* parent
,
174 return menu_controller_
->FindNextSelectableMenuItem(
175 parent
, index
, MenuController::INCREMENT_SELECTION_UP
);
179 menu_controller_
->RunMessageLoop(false);
182 Widget
* owner() { return owner_
.get(); }
183 ui::test::EventGenerator
* event_generator() { return event_generator_
.get(); }
184 TestMenuItemViewShown
* menu_item() { return menu_item_
.get(); }
185 MenuController
* menu_controller() { return menu_controller_
; }
186 const MenuItemView
* pending_state_item() const {
187 return menu_controller_
->pending_state_
.item
;
189 MenuController::ExitType
menu_exit_type() const {
190 return menu_controller_
->exit_type_
;
195 owner_
.reset(new Widget
);
196 Widget::InitParams params
= CreateParams(Widget::InitParams::TYPE_POPUP
);
197 params
.ownership
= Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET
;
198 owner_
->Init(params
);
199 event_generator_
.reset(
200 new ui::test::EventGenerator(GetContext(), owner_
->GetNativeWindow()));
205 SetupMenuController();
208 void SetupMenuItem() {
209 menu_item_
.reset(new TestMenuItemViewShown
);
210 menu_item_
->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
211 menu_item_
->AppendMenuItemWithLabel(2, base::ASCIIToUTF16("Two"));
212 menu_item_
->AppendMenuItemWithLabel(3, base::ASCIIToUTF16("Three"));
213 menu_item_
->AppendMenuItemWithLabel(4, base::ASCIIToUTF16("Four"));
216 void SetupMenuController() {
217 menu_controller_
= new MenuController(nullptr, true, nullptr);
218 menu_controller_
->owner_
= owner_
.get();
219 menu_controller_
->showing_
= true;
220 menu_controller_
->SetSelection(
221 menu_item_
.get(), MenuController::SELECTION_UPDATE_IMMEDIATELY
);
224 scoped_ptr
<Widget
> owner_
;
225 scoped_ptr
<ui::test::EventGenerator
> event_generator_
;
226 scoped_ptr
<TestMenuItemViewShown
> menu_item_
;
227 MenuController
* menu_controller_
;
229 DISALLOW_COPY_AND_ASSIGN(MenuControllerTest
);
232 #if defined(OS_LINUX) && defined(USE_X11)
233 // Tests that an event targeter which blocks events will be honored by the menu
235 TEST_F(MenuControllerTest
, EventTargeter
) {
236 base::MessageLoopForUI::current()->PostTask(
238 base::Bind(&MenuControllerTest::TestEventTargeter
,
239 base::Unretained(this)));
242 #endif // defined(OS_LINUX) && defined(USE_X11)
245 // Tests that touch event ids are released correctly. See crbug.com/439051 for
246 // details. When the ids aren't managed correctly, we get stuck down touches.
247 TEST_F(MenuControllerTest
, TouchIdsReleasedCorrectly
) {
248 TestEventHandler test_event_handler
;
249 owner()->GetNativeWindow()->GetRootWindow()->AddPreTargetHandler(
250 &test_event_handler
);
252 std::vector
<int> devices
;
253 devices
.push_back(1);
254 ui::SetUpTouchDevicesForTest(devices
);
256 event_generator()->PressTouchId(0);
257 event_generator()->PressTouchId(1);
258 event_generator()->ReleaseTouchId(0);
260 base::MessageLoopForUI::current()->PostTask(
262 base::Bind(&MenuControllerTest::ReleaseTouchId
,
263 base::Unretained(this),
266 base::MessageLoopForUI::current()->PostTask(
268 base::Bind(&MenuControllerTest::PressKey
,
269 base::Unretained(this),
274 EXPECT_EQ(MenuController::EXIT_OUTERMOST
, menu_exit_type());
275 EXPECT_EQ(0, test_event_handler
.outstanding_touches());
277 owner()->GetNativeWindow()->GetRootWindow()->RemovePreTargetHandler(
278 &test_event_handler
);
280 #endif // defined(USE_X11)
282 // Tests that initial selected menu items are correct when items are enabled or
284 TEST_F(MenuControllerTest
, InitialSelectedItem
) {
285 // Leave items "Two", "Three", and "Four" enabled.
286 menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
287 // The first selectable item should be item "Two".
288 MenuItemView
* first_selectable
=
289 FindInitialSelectableMenuItemDown(menu_item());
290 ASSERT_NE(nullptr, first_selectable
);
291 EXPECT_EQ(2, first_selectable
->GetCommand());
292 // The last selectable item should be item "Four".
293 MenuItemView
* last_selectable
=
294 FindInitialSelectableMenuItemUp(menu_item());
295 ASSERT_NE(nullptr, last_selectable
);
296 EXPECT_EQ(4, last_selectable
->GetCommand());
298 // Leave items "One" and "Two" enabled.
299 menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(true);
300 menu_item()->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(true);
301 menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
302 menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false);
303 // The first selectable item should be item "One".
304 first_selectable
= FindInitialSelectableMenuItemDown(menu_item());
305 ASSERT_NE(nullptr, first_selectable
);
306 EXPECT_EQ(1, first_selectable
->GetCommand());
307 // The last selectable item should be item "Two".
308 last_selectable
= FindInitialSelectableMenuItemUp(menu_item());
309 ASSERT_NE(nullptr, last_selectable
);
310 EXPECT_EQ(2, last_selectable
->GetCommand());
312 // Leave only a single item "One" enabled.
313 menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(true);
314 menu_item()->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(false);
315 menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
316 menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false);
317 // The first selectable item should be item "One".
318 first_selectable
= FindInitialSelectableMenuItemDown(menu_item());
319 ASSERT_NE(nullptr, first_selectable
);
320 EXPECT_EQ(1, first_selectable
->GetCommand());
321 // The last selectable item should be item "One".
322 last_selectable
= FindInitialSelectableMenuItemUp(menu_item());
323 ASSERT_NE(nullptr, last_selectable
);
324 EXPECT_EQ(1, last_selectable
->GetCommand());
326 // Leave only a single item "Three" enabled.
327 menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
328 menu_item()->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(false);
329 menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(true);
330 menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false);
331 // The first selectable item should be item "Three".
332 first_selectable
= FindInitialSelectableMenuItemDown(menu_item());
333 ASSERT_NE(nullptr, first_selectable
);
334 EXPECT_EQ(3, first_selectable
->GetCommand());
335 // The last selectable item should be item "Three".
336 last_selectable
= FindInitialSelectableMenuItemUp(menu_item());
337 ASSERT_NE(nullptr, last_selectable
);
338 EXPECT_EQ(3, last_selectable
->GetCommand());
340 // Leave only a single item ("Two") selected. It should be the first and the
341 // last selectable item.
342 menu_item()->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
343 menu_item()->GetSubmenu()->GetMenuItemAt(1)->SetEnabled(true);
344 menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
345 menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false);
346 first_selectable
= FindInitialSelectableMenuItemDown(menu_item());
347 ASSERT_NE(nullptr, first_selectable
);
348 EXPECT_EQ(2, first_selectable
->GetCommand());
349 last_selectable
= FindInitialSelectableMenuItemUp(menu_item());
350 ASSERT_NE(nullptr, last_selectable
);
351 EXPECT_EQ(2, last_selectable
->GetCommand());
353 // There should be no next or previous selectable item since there is only a
354 // single enabled item in the menu.
355 EXPECT_EQ(nullptr, FindNextSelectableMenuItem(menu_item(), 1));
356 EXPECT_EQ(nullptr, FindPreviousSelectableMenuItem(menu_item(), 1));
358 // Clear references in menu controller to the menu item that is going away.
362 // Tests that opening menu and pressing 'Down' and 'Up' iterates over enabled
364 TEST_F(MenuControllerTest
, NextSelectedItem
) {
365 // Disabling the item "Three" gets it skipped when using keyboard to navigate.
366 menu_item()->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
368 // Fake initial hot selection.
369 SetPendingStateItem(menu_item()->GetSubmenu()->GetMenuItemAt(0));
370 EXPECT_EQ(1, pending_state_item()->GetCommand());
372 // Move down in the menu.
374 IncrementSelection();
375 EXPECT_EQ(2, pending_state_item()->GetCommand());
377 // Skip disabled item.
378 IncrementSelection();
379 EXPECT_EQ(4, pending_state_item()->GetCommand());
382 IncrementSelection();
383 EXPECT_EQ(1, pending_state_item()->GetCommand());
385 // Move up in the menu.
387 DecrementSelection();
388 EXPECT_EQ(4, pending_state_item()->GetCommand());
390 // Skip disabled item.
391 DecrementSelection();
392 EXPECT_EQ(2, pending_state_item()->GetCommand());
394 // Select previous item.
395 DecrementSelection();
396 EXPECT_EQ(1, pending_state_item()->GetCommand());
398 // Clear references in menu controller to the menu item that is going away.
402 // Tests that opening menu and pressing 'Up' selects the last enabled menu item.
403 TEST_F(MenuControllerTest
, PreviousSelectedItem
) {
404 // Disabling the item "Four" gets it skipped when using keyboard to navigate.
405 menu_item()->GetSubmenu()->GetMenuItemAt(3)->SetEnabled(false);
407 // Fake initial root item selection and submenu showing.
408 SetPendingStateItem(menu_item());
409 EXPECT_EQ(0, pending_state_item()->GetCommand());
411 // Move up and select a previous (in our case the last enabled) item.
412 DecrementSelection();
413 EXPECT_EQ(3, pending_state_item()->GetCommand());
415 // Clear references in menu controller to the menu item that is going away.