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/run_loop.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "ui/aura/scoped_window_targeter.h"
10 #include "ui/aura/window.h"
11 #include "ui/events/event_handler.h"
12 #include "ui/events/null_event_targeter.h"
13 #include "ui/events/platform/platform_event_source.h"
14 #include "ui/views/controls/menu/menu_item_view.h"
15 #include "ui/views/controls/menu/submenu_view.h"
16 #include "ui/views/test/views_test_base.h"
17 #include "ui/wm/public/dispatcher_client.h"
20 #include "base/message_loop/message_pump_dispatcher.h"
21 #elif defined(USE_X11)
25 #include "ui/events/devices/x11/device_data_manager_x11.h"
26 #include "ui/events/test/events_test_utils_x11.h"
27 #elif defined(USE_OZONE)
28 #include "ui/events/event.h"
35 class TestMenuItemView
: public MenuItemView
{
37 TestMenuItemView() : MenuItemView(NULL
) {}
38 ~TestMenuItemView() override
{}
41 DISALLOW_COPY_AND_ASSIGN(TestMenuItemView
);
44 class TestPlatformEventSource
: public ui::PlatformEventSource
{
46 TestPlatformEventSource() {
48 ui::DeviceDataManagerX11::CreateInstance();
51 ~TestPlatformEventSource() override
{}
53 uint32_t Dispatch(const ui::PlatformEvent
& event
) {
54 return DispatchEvent(event
);
58 DISALLOW_COPY_AND_ASSIGN(TestPlatformEventSource
);
61 class TestDispatcherClient
: public aura::client::DispatcherClient
{
63 TestDispatcherClient() : dispatcher_(NULL
) {}
64 ~TestDispatcherClient() override
{}
66 base::MessagePumpDispatcher
* dispatcher() {
70 // aura::client::DispatcherClient:
71 void PrepareNestedLoopClosures(base::MessagePumpDispatcher
* dispatcher
,
72 base::Closure
* run_closure
,
73 base::Closure
* quit_closure
) override
{
74 scoped_ptr
<base::RunLoop
> run_loop(new base::RunLoop());
75 *quit_closure
= run_loop
->QuitClosure();
76 *run_closure
= base::Bind(&TestDispatcherClient::RunNestedDispatcher
,
77 base::Unretained(this),
78 base::Passed(&run_loop
),
83 void RunNestedDispatcher(scoped_ptr
<base::RunLoop
> run_loop
,
84 base::MessagePumpDispatcher
* dispatcher
) {
85 base::AutoReset
<base::MessagePumpDispatcher
*> reset_dispatcher(&dispatcher_
,
87 base::MessageLoopForUI
* loop
= base::MessageLoopForUI::current();
88 base::MessageLoop::ScopedNestableTaskAllower
allow(loop
);
92 base::MessagePumpDispatcher
* dispatcher_
;
94 DISALLOW_COPY_AND_ASSIGN(TestDispatcherClient
);
99 class MenuControllerTest
: public ViewsTestBase
{
101 MenuControllerTest() : controller_(NULL
) {}
102 ~MenuControllerTest() override
{ ResetMenuController(); }
104 // Dispatches |count| number of items, each in a separate iteration of the
105 // message-loop, by posting a task.
106 void Step3_DispatchEvents(int count
) {
107 base::MessageLoopForUI
* loop
= base::MessageLoopForUI::current();
108 base::MessageLoop::ScopedNestableTaskAllower
allow(loop
);
109 controller_
->exit_type_
= MenuController::EXIT_ALL
;
113 base::MessageLoop::current()->PostTask(
115 base::Bind(&MenuControllerTest::Step3_DispatchEvents
,
116 base::Unretained(this),
119 EXPECT_TRUE(run_loop_
->running());
124 // Runs a nested message-loop that does not involve the menu itself.
125 void Step2_RunNestedLoop() {
126 base::MessageLoopForUI
* loop
= base::MessageLoopForUI::current();
127 base::MessageLoop::ScopedNestableTaskAllower
allow(loop
);
128 base::MessageLoop::current()->PostTask(
130 base::Bind(&MenuControllerTest::Step3_DispatchEvents
,
131 base::Unretained(this),
133 run_loop_
.reset(new base::RunLoop());
137 void Step1_RunMenu() {
138 base::MessageLoop::current()->PostTask(
140 base::Bind(&MenuControllerTest::Step2_RunNestedLoop
,
141 base::Unretained(this)));
142 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
143 RunMenu(owner
.get());
146 scoped_ptr
<Widget
> CreateOwnerWidget() {
147 scoped_ptr
<Widget
> widget(new Widget
);
148 Widget::InitParams params
= CreateParams(Widget::InitParams::TYPE_POPUP
);
149 params
.ownership
= Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET
;
150 widget
->Init(params
);
153 aura::client::SetDispatcherClient(
154 widget
->GetNativeWindow()->GetRootWindow(), &dispatcher_client_
);
155 return widget
.Pass();
158 const MenuItemView
* pending_state_item() const {
159 return controller_
->pending_state_
.item
;
162 void SetPendingStateItem(MenuItemView
* item
) {
163 controller_
->pending_state_
.item
= item
;
166 void IncrementSelection(int delta
) {
167 controller_
->IncrementSelection(delta
);
170 MenuItemView
* FindFirstSelectableMenuItem(MenuItemView
* parent
) {
171 return controller_
->FindFirstSelectableMenuItem(parent
);
174 MenuItemView
* FindNextSelectableMenuItem(MenuItemView
* parent
,
177 return controller_
->FindNextSelectableMenuItem(parent
, index
, delta
);
179 void SetupMenu(views::Widget
* owner
, views::MenuItemView
* item
) {
180 ResetMenuController();
181 controller_
= new MenuController(NULL
, true, NULL
);
182 controller_
->owner_
= owner
;
183 controller_
->showing_
= true;
184 controller_
->SetSelection(item
,
185 MenuController::SELECTION_UPDATE_IMMEDIATELY
);
188 void RunMenu(views::Widget
* owner
) {
189 scoped_ptr
<TestMenuItemView
> menu_item(new TestMenuItemView
);
190 SetupMenu(owner
, menu_item
.get());
191 controller_
->RunMessageLoop(false);
195 void DispatchEscapeAndExpect(MenuController::ExitType exit_type
) {
196 ui::ScopedXI2Event key_event
;
197 key_event
.InitKeyEvent(ui::ET_KEY_PRESSED
, ui::VKEY_ESCAPE
, 0);
198 event_source_
.Dispatch(key_event
);
199 EXPECT_EQ(exit_type
, controller_
->exit_type());
200 controller_
->exit_type_
= MenuController::EXIT_ALL
;
204 void DispatchTouch(int evtype
, int id
) {
205 ui::ScopedXI2Event touch_event
;
206 std::vector
<ui::Valuator
> valuators
;
207 touch_event
.InitTouchEvent(1, evtype
, id
, gfx::Point(10, 10), valuators
);
208 event_source_
.Dispatch(touch_event
);
213 void DispatchEvent() {
216 memset(&xevent
, 0, sizeof(xevent
));
217 event_source_
.Dispatch(&xevent
);
218 #elif defined(OS_WIN)
220 memset(&msg
, 0, sizeof(MSG
));
221 dispatcher_client_
.dispatcher()->Dispatch(msg
);
222 #elif defined(USE_OZONE)
223 ui::KeyEvent
event(' ', ui::VKEY_SPACE
, ui::EF_NONE
);
224 event_source_
.Dispatch(&event
);
226 #error Unsupported platform
231 void ResetMenuController() {
233 // These properties are faked by RunMenu for the purposes of testing and
234 // need to be undone before we call the destructor.
235 controller_
->owner_
= NULL
;
236 controller_
->showing_
= false;
242 // A weak pointer to the MenuController owned by this class.
243 MenuController
* controller_
;
244 scoped_ptr
<base::RunLoop
> run_loop_
;
245 TestPlatformEventSource event_source_
;
246 TestDispatcherClient dispatcher_client_
;
248 DISALLOW_COPY_AND_ASSIGN(MenuControllerTest
);
251 TEST_F(MenuControllerTest
, Basic
) {
252 base::MessageLoop::ScopedNestableTaskAllower
allow_nested(
253 base::MessageLoop::current());
254 message_loop()->PostTask(
256 base::Bind(&MenuControllerTest::Step1_RunMenu
, base::Unretained(this)));
259 #if defined(OS_LINUX) && defined(USE_X11)
260 // Tests that an event targeter which blocks events will be honored by the menu
262 TEST_F(MenuControllerTest
, EventTargeter
) {
264 // Verify that the menu handles the escape key under normal circumstances.
265 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
266 message_loop()->PostTask(
268 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect
,
269 base::Unretained(this),
270 MenuController::EXIT_OUTERMOST
));
271 RunMenu(owner
.get());
275 // With the NULL targeter instantiated and assigned we expect the menu to
276 // not handle the key event.
277 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
278 aura::ScopedWindowTargeter
scoped_targeter(
279 owner
->GetNativeWindow()->GetRootWindow(),
280 scoped_ptr
<ui::EventTargeter
>(new ui::NullEventTargeter
));
281 message_loop()->PostTask(
283 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect
,
284 base::Unretained(this),
285 MenuController::EXIT_NONE
));
286 RunMenu(owner
.get());
293 class TestEventHandler
: public ui::EventHandler
{
295 TestEventHandler() : outstanding_touches_(0) {}
297 void OnTouchEvent(ui::TouchEvent
* event
) override
{
298 switch(event
->type()) {
299 case ui::ET_TOUCH_PRESSED
:
300 outstanding_touches_
++;
302 case ui::ET_TOUCH_RELEASED
:
303 case ui::ET_TOUCH_CANCELLED
:
304 outstanding_touches_
--;
311 int outstanding_touches() const { return outstanding_touches_
; }
314 int outstanding_touches_
;
317 // Tests that touch event ids are released correctly. See
318 // crbug.com/439051 for details. When the ids aren't managed
319 // correctly, we get stuck down touches.
320 TEST_F(MenuControllerTest
, TouchIdsReleasedCorrectly
) {
321 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
322 TestEventHandler test_event_handler
;
323 owner
->GetNativeWindow()->GetRootWindow()->AddPreTargetHandler(
324 &test_event_handler
);
326 std::vector
<int> devices
;
327 devices
.push_back(1);
328 ui::SetUpTouchDevicesForTest(devices
);
330 DispatchTouch(XI_TouchBegin
, 0);
331 DispatchTouch(XI_TouchBegin
, 1);
332 DispatchTouch(XI_TouchEnd
, 0);
334 message_loop()->PostTask(FROM_HERE
,
335 base::Bind(&MenuControllerTest::DispatchTouch
,
336 base::Unretained(this), XI_TouchEnd
, 1));
338 message_loop()->PostTask(
340 base::Bind(&MenuControllerTest::DispatchEscapeAndExpect
,
341 base::Unretained(this), MenuController::EXIT_OUTERMOST
));
343 RunMenu(owner
.get());
344 EXPECT_EQ(0, test_event_handler
.outstanding_touches());
346 owner
->GetNativeWindow()->GetRootWindow()->RemovePreTargetHandler(
347 &test_event_handler
);
349 #endif // defined(USE_X11)
351 TEST_F(MenuControllerTest
, FirstSelectedItem
) {
352 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
353 scoped_ptr
<TestMenuItemView
> menu_item(new TestMenuItemView
);
354 menu_item
->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
355 menu_item
->AppendMenuItemWithLabel(2, base::ASCIIToUTF16("Two"));
356 // Disabling the item "One" gets it skipped when a menu is first opened.
357 menu_item
->GetSubmenu()->GetMenuItemAt(0)->SetEnabled(false);
359 SetupMenu(owner
.get(), menu_item
.get());
361 // First selectable item should be item "Two".
362 MenuItemView
* first_selectable
= FindFirstSelectableMenuItem(menu_item
.get());
363 EXPECT_EQ(2, first_selectable
->GetCommand());
365 // There should be no next or previous selectable item since there is only a
366 // single enabled item in the menu.
367 SetPendingStateItem(first_selectable
);
368 EXPECT_EQ(NULL
, FindNextSelectableMenuItem(menu_item
.get(), 1, 1));
369 EXPECT_EQ(NULL
, FindNextSelectableMenuItem(menu_item
.get(), 1, -1));
372 TEST_F(MenuControllerTest
, NextSelectedItem
) {
373 scoped_ptr
<Widget
> owner(CreateOwnerWidget());
374 scoped_ptr
<TestMenuItemView
> menu_item(new TestMenuItemView
);
375 menu_item
->AppendMenuItemWithLabel(1, base::ASCIIToUTF16("One"));
376 menu_item
->AppendMenuItemWithLabel(2, base::ASCIIToUTF16("Two"));
377 menu_item
->AppendMenuItemWithLabel(3, base::ASCIIToUTF16("Three"));
378 menu_item
->AppendMenuItemWithLabel(4, base::ASCIIToUTF16("Four"));
379 // Disabling the item "Three" gets it skipped when using keyboard to navigate.
380 menu_item
->GetSubmenu()->GetMenuItemAt(2)->SetEnabled(false);
382 SetupMenu(owner
.get(), menu_item
.get());
384 // Fake initial hot selection.
385 SetPendingStateItem(menu_item
->GetSubmenu()->GetMenuItemAt(0));
386 EXPECT_EQ(1, pending_state_item()->GetCommand());
388 // Move down in the menu.
390 IncrementSelection(1);
391 EXPECT_EQ(2, pending_state_item()->GetCommand());
393 // Skip disabled item.
394 IncrementSelection(1);
395 EXPECT_EQ(4, pending_state_item()->GetCommand());
398 IncrementSelection(1);
399 EXPECT_EQ(1, pending_state_item()->GetCommand());
401 // Move up in the menu.
403 IncrementSelection(-1);
404 EXPECT_EQ(4, pending_state_item()->GetCommand());
406 // Skip disabled item.
407 IncrementSelection(-1);
408 EXPECT_EQ(2, pending_state_item()->GetCommand());
410 // Select previous item.
411 IncrementSelection(-1);
412 EXPECT_EQ(1, pending_state_item()->GetCommand());