A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Linq;
6using NUnit.Framework;
7using osu.Framework.Bindables;
8using osu.Framework.Graphics;
9using osu.Framework.Graphics.UserInterface;
10using osu.Framework.Input;
11using osu.Framework.Input.Events;
12using osu.Framework.Input.States;
13using osu.Framework.Testing;
14using osuTK;
15using osuTK.Input;
16
17namespace osu.Framework.Tests.Visual.UserInterface
18{
19 public class TestSceneDropdown : ManualInputManagerTestScene
20 {
21 private const int items_to_add = 10;
22 private const float explicit_height = 100;
23 private float calculatedHeight;
24 private readonly TestDropdown testDropdown, testDropdownMenu, bindableDropdown, emptyDropdown, disabledDropdown;
25 private readonly PlatformActionContainer platformActionContainerKeyboardSelection, platformActionContainerKeyboardPreselection, platformActionContainerEmptyDropdown;
26 private readonly BindableList<TestModel> bindableList = new BindableList<TestModel>();
27
28 private int previousIndex;
29 private int lastVisibleIndexOnTheCurrentPage, lastVisibleIndexOnTheNextPage;
30 private int firstVisibleIndexOnTheCurrentPage, firstVisibleIndexOnThePreviousPage;
31
32 public TestSceneDropdown()
33 {
34 var testItems = new TestModel[10];
35 int i = 0;
36 while (i < items_to_add)
37 testItems[i] = @"test " + i++;
38
39 Add(platformActionContainerKeyboardSelection = new PlatformActionContainer
40 {
41 Child = testDropdown = new TestDropdown
42 {
43 Width = 150,
44 Position = new Vector2(50, 50),
45 Items = testItems
46 }
47 });
48
49 Add(platformActionContainerKeyboardPreselection = new PlatformActionContainer
50 {
51 Child = testDropdownMenu = new TestDropdown
52 {
53 Width = 150,
54 Position = new Vector2(250, 50),
55 Items = testItems
56 }
57 });
58 testDropdownMenu.Menu.MaxHeight = explicit_height;
59
60 Add(bindableDropdown = new TestDropdown
61 {
62 Width = 150,
63 Position = new Vector2(450, 50),
64 ItemSource = bindableList
65 });
66
67 Add(platformActionContainerEmptyDropdown = new PlatformActionContainer
68 {
69 Child = emptyDropdown = new TestDropdown
70 {
71 Width = 150,
72 Position = new Vector2(650, 50),
73 }
74 });
75
76 Add(disabledDropdown = new TestDropdown
77 {
78 Width = 150,
79 Position = new Vector2(50, 350),
80 Items = testItems,
81 Current =
82 {
83 Value = testItems[3],
84 Disabled = true
85 }
86 });
87 }
88
89 [Test]
90 public void TestExternalBindableChangeKeepsSelection()
91 {
92 toggleDropdownViaClick(testDropdown, "dropdown1");
93 AddStep("click item 4", () => testDropdown.SelectItem(testDropdown.Menu.Items[4]));
94
95 AddAssert("item 4 is selected", () => testDropdown.Current.Value.Identifier == "test 4");
96
97 AddStep("replace items", () =>
98 {
99 testDropdown.Items = testDropdown.Items.Select(i => new TestModel(i.ToString())).ToArray();
100 });
101
102 AddAssert("item 4 is selected", () => testDropdown.Current.Value.Identifier == "test 4");
103 }
104
105 [Test]
106 public void TestBasic()
107 {
108 var i = items_to_add;
109
110 toggleDropdownViaClick(testDropdown, "dropdown1");
111 AddAssert("dropdown is open", () => testDropdown.Menu.State == MenuState.Open);
112
113 AddRepeatStep("add item", () => testDropdown.AddDropdownItem("test " + i++), items_to_add);
114 AddAssert("item count is correct", () => testDropdown.Items.Count() == items_to_add * 2);
115
116 AddStep($"Set dropdown1 height to {explicit_height}", () =>
117 {
118 calculatedHeight = testDropdown.Menu.Height;
119 testDropdown.Menu.MaxHeight = explicit_height;
120 });
121 AddAssert($"dropdown1 height is {explicit_height}", () => testDropdown.Menu.Height == explicit_height);
122
123 AddStep($"Set dropdown1 height to {float.PositiveInfinity}", () => testDropdown.Menu.MaxHeight = float.PositiveInfinity);
124 AddAssert("dropdown1 height is calculated automatically", () => testDropdown.Menu.Height == calculatedHeight);
125
126 AddStep("click item 13", () => testDropdown.SelectItem(testDropdown.Menu.Items[13]));
127
128 AddAssert("dropdown1 is closed", () => testDropdown.Menu.State == MenuState.Closed);
129 AddAssert("item 13 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(13)));
130
131 AddStep("select item 15", () => testDropdown.Current.Value = testDropdown.Items.ElementAt(15));
132 AddAssert("item 15 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(15)));
133
134 toggleDropdownViaClick(testDropdown, "dropdown1");
135 AddAssert("dropdown1 is open", () => testDropdown.Menu.State == MenuState.Open);
136
137 toggleDropdownViaClick(testDropdownMenu, "dropdown2");
138
139 AddAssert("dropdown1 is closed", () => testDropdown.Menu.State == MenuState.Closed);
140 AddAssert("dropdown2 is open", () => testDropdownMenu.Menu.State == MenuState.Open);
141
142 AddStep("select 'invalid'", () => testDropdown.Current.Value = "invalid");
143
144 AddAssert("'invalid' is selected", () => testDropdown.Current.Value.Identifier == "invalid");
145 AddAssert("label shows 'invalid'", () => testDropdown.Header.Label.ToString() == "invalid");
146
147 AddStep("select item 2", () => testDropdown.Current.Value = testDropdown.Items.ElementAt(2));
148 AddAssert("item 2 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(2)));
149
150 AddStep("clear bindable list", () => bindableList.Clear());
151 toggleDropdownViaClick(bindableDropdown, "dropdown3");
152 AddAssert("no elements in bindable dropdown", () => !bindableDropdown.Items.Any());
153
154 AddStep("add items to bindable", () => bindableList.AddRange(new[] { "one", "two", "three" }.Select(s => new TestModel(s))));
155 AddStep("select three", () => bindableDropdown.Current.Value = "three");
156 AddStep("remove first item from bindable", () => bindableList.RemoveAt(0));
157 AddAssert("two items in dropdown", () => bindableDropdown.Items.Count() == 2);
158 AddAssert("current value still three", () => bindableDropdown.Current.Value.Identifier == "three");
159
160 AddStep("remove three", () => bindableList.Remove("three"));
161 AddAssert("current value should be two", () => bindableDropdown.Current.Value.Identifier == "two");
162 }
163
164 private void performKeypress(Drawable drawable, Key key)
165 {
166 drawable.TriggerEvent(new KeyDownEvent(new InputState(), key));
167 drawable.TriggerEvent(new KeyUpEvent(new InputState(), key));
168 }
169
170 private void performPlatformAction(PlatformAction action, PlatformActionContainer platformActionContainer, Drawable drawable)
171 {
172 var tempIsHovered = drawable.IsHovered;
173 var tempHasFocus = drawable.HasFocus;
174
175 drawable.IsHovered = true;
176 drawable.HasFocus = true;
177
178 platformActionContainer.TriggerPressed(action);
179 platformActionContainer.TriggerReleased(action);
180
181 drawable.IsHovered = tempIsHovered;
182 drawable.HasFocus = tempHasFocus;
183 }
184
185 [TestCase(false)]
186 [TestCase(true)]
187 public void TestKeyboardSelection(bool cleanSelection)
188 {
189 if (cleanSelection)
190 AddStep("Clean selection", () => testDropdown.Current.Value = null);
191
192 AddStep("Select next item", () =>
193 {
194 previousIndex = testDropdown.SelectedIndex;
195 performKeypress(testDropdown.Header, Key.Down);
196 });
197 AddAssert("Next item is selected", () => testDropdown.SelectedIndex == previousIndex + 1);
198
199 AddStep("Select previous item", () =>
200 {
201 previousIndex = testDropdown.SelectedIndex;
202 performKeypress(testDropdown.Header, Key.Up);
203 });
204 AddAssert("Previous item is selected", () => testDropdown.SelectedIndex == Math.Max(0, previousIndex - 1));
205
206 AddStep("Select last item",
207 () => performPlatformAction(PlatformAction.MoveToListEnd, platformActionContainerKeyboardSelection, testDropdown.Header));
208 AddAssert("Last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.Last().Item);
209
210 AddStep("Select first item",
211 () => performPlatformAction(PlatformAction.MoveToListStart, platformActionContainerKeyboardSelection, testDropdown.Header));
212 AddAssert("First item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.First().Item);
213
214 AddStep("Select next item when empty", () => performKeypress(emptyDropdown.Header, Key.Up));
215 AddStep("Select previous item when empty", () => performKeypress(emptyDropdown.Header, Key.Down));
216 AddStep("Select last item when empty", () => performKeypress(emptyDropdown.Header, Key.PageUp));
217 AddStep("Select first item when empty", () => performKeypress(emptyDropdown.Header, Key.PageDown));
218 }
219
220 [TestCase(false)]
221 [TestCase(true)]
222 public void TestKeyboardPreselection(bool cleanSelection)
223 {
224 if (cleanSelection)
225 AddStep("Clean selection", () => testDropdownMenu.Current.Value = null);
226
227 toggleDropdownViaClick(testDropdownMenu);
228 assertDropdownIsOpen(testDropdownMenu);
229
230 AddStep("Preselect next item", () =>
231 {
232 previousIndex = testDropdownMenu.PreselectedIndex;
233 performKeypress(testDropdownMenu.Menu, Key.Down);
234 });
235 AddAssert("Next item is preselected", () => testDropdownMenu.PreselectedIndex == previousIndex + 1);
236
237 AddStep("Preselect previous item", () =>
238 {
239 previousIndex = testDropdownMenu.PreselectedIndex;
240 performKeypress(testDropdownMenu.Menu, Key.Up);
241 });
242 AddAssert("Previous item is preselected", () => testDropdownMenu.PreselectedIndex == Math.Max(0, previousIndex - 1));
243
244 AddStep("Preselect last visible item", () =>
245 {
246 lastVisibleIndexOnTheCurrentPage = testDropdownMenu.Menu.DrawableMenuItems.ToList().IndexOf(testDropdownMenu.Menu.VisibleMenuItems.Last());
247 performKeypress(testDropdownMenu.Menu, Key.PageDown);
248 });
249 AddAssert("Last visible item preselected", () => testDropdownMenu.PreselectedIndex == lastVisibleIndexOnTheCurrentPage);
250
251 AddStep("Preselect last visible item on the next page", () =>
252 {
253 lastVisibleIndexOnTheNextPage =
254 Math.Clamp(lastVisibleIndexOnTheCurrentPage + testDropdownMenu.Menu.VisibleMenuItems.Count(), 0, testDropdownMenu.Menu.Items.Count - 1);
255
256 performKeypress(testDropdownMenu.Menu, Key.PageDown);
257 });
258 AddAssert("Last visible item on the next page preselected", () => testDropdownMenu.PreselectedIndex == lastVisibleIndexOnTheNextPage);
259
260 AddStep("Preselect first visible item", () =>
261 {
262 firstVisibleIndexOnTheCurrentPage = testDropdownMenu.Menu.DrawableMenuItems.ToList().IndexOf(testDropdownMenu.Menu.VisibleMenuItems.First());
263 performKeypress(testDropdownMenu.Menu, Key.PageUp);
264 });
265 AddAssert("First visible item preselected", () => testDropdownMenu.PreselectedIndex == firstVisibleIndexOnTheCurrentPage);
266
267 AddStep("Preselect first visible item on the previous page", () =>
268 {
269 firstVisibleIndexOnThePreviousPage = Math.Clamp(firstVisibleIndexOnTheCurrentPage - testDropdownMenu.Menu.VisibleMenuItems.Count(), 0,
270 testDropdownMenu.Menu.Items.Count - 1);
271 performKeypress(testDropdownMenu.Menu, Key.PageUp);
272 });
273 AddAssert("First visible item on the previous page selected", () => testDropdownMenu.PreselectedIndex == firstVisibleIndexOnThePreviousPage);
274 AddAssert("First item is preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.First().Item);
275
276 AddStep("Preselect last item",
277 () => performPlatformAction(PlatformAction.MoveToListEnd, platformActionContainerKeyboardPreselection, testDropdownMenu));
278 AddAssert("Last item preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.Last().Item);
279
280 AddStep("Finalize selection", () => performKeypress(testDropdownMenu.Menu, Key.Enter));
281 assertLastItemSelected();
282 assertDropdownIsClosed(testDropdownMenu);
283
284 toggleDropdownViaClick(testDropdownMenu);
285 assertDropdownIsOpen(testDropdownMenu);
286
287 AddStep("Preselect first item",
288 () => performPlatformAction(PlatformAction.MoveToListStart, platformActionContainerKeyboardPreselection, testDropdownMenu));
289 AddAssert("First item preselected", () => testDropdownMenu.Menu.PreselectedItem.Item == testDropdownMenu.Menu.DrawableMenuItems.First().Item);
290
291 AddStep("Discard preselection", () => performKeypress(testDropdownMenu.Menu, Key.Escape));
292 assertDropdownIsClosed(testDropdownMenu);
293 assertLastItemSelected();
294
295 toggleDropdownViaClick(emptyDropdown, "empty dropdown");
296 AddStep("Preselect next item when empty", () => performKeypress(emptyDropdown.Menu, Key.Down));
297 AddStep("Preselect previous item when empty", () => performKeypress(emptyDropdown.Menu, Key.Up));
298 AddStep("Preselect first visible item when empty", () => performKeypress(emptyDropdown.Menu, Key.PageUp));
299 AddStep("Preselect last visible item when empty", () => performKeypress(emptyDropdown.Menu, Key.PageDown));
300 AddStep("Preselect first item when empty",
301 () => performPlatformAction(PlatformAction.MoveToListStart, platformActionContainerEmptyDropdown, emptyDropdown));
302 AddStep("Preselect last item when empty",
303 () => performPlatformAction(PlatformAction.MoveToListEnd, platformActionContainerEmptyDropdown, emptyDropdown));
304
305 void assertLastItemSelected() => AddAssert("Last item selected", () => testDropdownMenu.SelectedItem == testDropdownMenu.Menu.DrawableMenuItems.Last().Item);
306 }
307
308 [Test]
309 public void TestSelectNull()
310 {
311 AddStep("select item 1", () => testDropdown.Current.Value = testDropdown.Items.ElementAt(1));
312 AddAssert("item 1 is selected", () => testDropdown.Current.Value.Equals(testDropdown.Items.ElementAt(1)));
313 AddStep("select item null", () => testDropdown.Current.Value = null);
314 AddAssert("null is selected", () => testDropdown.Current.Value == null);
315 }
316
317 [Test]
318 public void TestDisabledCurrent()
319 {
320 TestModel originalValue = null;
321
322 AddStep("store original value", () => originalValue = disabledDropdown.Current.Value);
323
324 toggleDropdownViaClick(disabledDropdown);
325 assertDropdownIsClosed(disabledDropdown);
326
327 AddStep("attempt to select next", () => performKeypress(disabledDropdown, Key.Down));
328 valueIsUnchanged();
329
330 AddStep("attempt to select previous", () => performKeypress(disabledDropdown, Key.Up));
331 valueIsUnchanged();
332
333 AddStep("attempt to select first", () => InputManager.Keys(PlatformAction.MoveToListStart));
334 valueIsUnchanged();
335
336 AddStep("attempt to select last", () => InputManager.Keys(PlatformAction.MoveToListEnd));
337 valueIsUnchanged();
338
339 AddStep("enable current", () => disabledDropdown.Current.Disabled = false);
340 toggleDropdownViaClick(disabledDropdown);
341 assertDropdownIsOpen(disabledDropdown);
342
343 AddStep("disable current", () => disabledDropdown.Current.Disabled = true);
344 assertDropdownIsClosed(disabledDropdown);
345
346 void valueIsUnchanged() => AddAssert("value is unchanged", () => disabledDropdown.Current.Value.Equals(originalValue));
347 }
348
349 private void toggleDropdownViaClick(TestDropdown dropdown, string dropdownName = null) => AddStep($"click {dropdownName ?? "dropdown"}", () =>
350 {
351 InputManager.MoveMouseTo(dropdown.Header);
352 InputManager.Click(MouseButton.Left);
353 });
354
355 private void assertDropdownIsOpen(TestDropdown dropdown) => AddAssert("dropdown is open", () => dropdown.Menu.State == MenuState.Open);
356
357 private void assertDropdownIsClosed(TestDropdown dropdown) => AddAssert("dropdown is closed", () => dropdown.Menu.State == MenuState.Closed);
358
359 private class TestModel : IEquatable<TestModel>
360 {
361 public readonly string Identifier;
362
363 public TestModel(string identifier)
364 {
365 Identifier = identifier;
366 }
367
368 public bool Equals(TestModel other)
369 {
370 if (other == null)
371 return false;
372
373 return other.Identifier == Identifier;
374 }
375
376 public override string ToString() => Identifier;
377
378 public static implicit operator TestModel(string str) => new TestModel(str);
379 }
380
381 private class TestDropdown : BasicDropdown<TestModel>
382 {
383 public new DropdownMenu Menu => base.Menu;
384
385 protected override DropdownMenu CreateMenu() => new TestDropdownMenu();
386
387 protected override DropdownHeader CreateHeader() => new BasicDropdownHeader();
388
389 public void SelectItem(MenuItem item) => ((TestDropdownMenu)Menu).SelectItem(item);
390
391 private class TestDropdownMenu : BasicDropdownMenu
392 {
393 public void SelectItem(MenuItem item) => Children.FirstOrDefault(c => c.Item == item)?
394 .TriggerEvent(new ClickEvent(GetContainingInputManager().CurrentState, MouseButton.Left));
395 }
396
397 internal new DropdownMenuItem<TestModel> SelectedItem => base.SelectedItem;
398
399 public int SelectedIndex => Menu.DrawableMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem);
400 public int PreselectedIndex => Menu.DrawableMenuItems.ToList().IndexOf(Menu.PreselectedItem);
401 }
402 }
403}