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.Collections.Generic;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Bindables;
9using osu.Framework.Extensions.IEnumerableExtensions;
10using osu.Framework.Graphics;
11using osu.Framework.Graphics.Containers;
12using osu.Framework.Graphics.Shapes;
13using osu.Framework.Graphics.UserInterface;
14using osu.Framework.Input;
15using osu.Framework.Localisation;
16using osuTK;
17
18namespace osu.Framework.Tests.Visual.UserInterface
19{
20 public class TestSceneTabControl : FrameworkTestScene
21 {
22 private readonly TestEnum[] items;
23
24 private FillFlowContainer tabControlContainer;
25
26 private StyledTabControl pinnedAndAutoSort;
27 private StyledTabControl switchingTabControl;
28 private PlatformActionContainer platformActionContainer;
29 private StyledTabControlWithoutDropdown withoutDropdownTabControl;
30 private StyledTabControl removeAllTabControl;
31 private StyledMultilineTabControl multilineTabControl;
32 private StyledTabControl simpleTabcontrol;
33 private StyledTabControl simpleTabcontrolNoSwitchOnRemove;
34 private BasicTabControl<TestEnum?> basicTabControl;
35
36 public TestSceneTabControl()
37 {
38 items = (TestEnum[])Enum.GetValues(typeof(TestEnum));
39 }
40
41 [SetUp]
42 public void Setup() => Schedule(() =>
43 {
44 Clear();
45
46 Add(tabControlContainer = new FillFlowContainer
47 {
48 RelativeSizeAxes = Axes.Both,
49 Direction = FillDirection.Full,
50 Spacing = new Vector2(50),
51 Children = new Drawable[]
52 {
53 simpleTabcontrol = new StyledTabControl
54 {
55 Size = new Vector2(200, 30),
56 },
57 simpleTabcontrolNoSwitchOnRemove = new StyledTabControl
58 {
59 Size = new Vector2(200, 30),
60 SwitchTabOnRemove = false
61 },
62 multilineTabControl = new StyledMultilineTabControl
63 {
64 Size = new Vector2(200, 60),
65 },
66 pinnedAndAutoSort = new StyledTabControl
67 {
68 Size = new Vector2(200, 30),
69 AutoSort = true
70 },
71 platformActionContainer = new PlatformActionContainer
72 {
73 RelativeSizeAxes = Axes.None,
74 Size = new Vector2(200, 30),
75 Child = switchingTabControl = new StyledTabControl
76 {
77 RelativeSizeAxes = Axes.Both,
78 IsSwitchable = true,
79 }
80 },
81 removeAllTabControl = new StyledTabControl
82 {
83 Size = new Vector2(200, 30)
84 },
85 withoutDropdownTabControl = new StyledTabControlWithoutDropdown
86 {
87 Size = new Vector2(200, 30)
88 },
89 basicTabControl = new BasicTabControl<TestEnum?>
90 {
91 Size = new Vector2(200, 20)
92 }
93 }
94 });
95
96 foreach (var item in items)
97 {
98 simpleTabcontrol.AddItem(item);
99 simpleTabcontrolNoSwitchOnRemove.AddItem(item);
100 multilineTabControl.AddItem(item);
101 switchingTabControl.AddItem(item);
102 withoutDropdownTabControl.AddItem(item);
103 basicTabControl.AddItem(item);
104 }
105
106 items.Take(7).ForEach(item => pinnedAndAutoSort.AddItem(item));
107 pinnedAndAutoSort.PinItem(TestEnum.Test5);
108 });
109
110 [Test]
111 public void Basic()
112 {
113 var nextTest = new Func<TestEnum>(() => items.FirstOrDefault(test => !pinnedAndAutoSort.Items.Contains(test)));
114
115 Stack<TestEnum> pinned = new Stack<TestEnum>();
116
117 AddStep("AddItem", () =>
118 {
119 var item = nextTest.Invoke();
120 if (!pinnedAndAutoSort.Items.Contains(item))
121 pinnedAndAutoSort.AddItem(item);
122 });
123
124 AddStep("RemoveItem", () =>
125 {
126 if (pinnedAndAutoSort.Items.Any())
127 {
128 pinnedAndAutoSort.RemoveItem(pinnedAndAutoSort.Items.First());
129 }
130 });
131
132 AddStep("PinItem", () =>
133 {
134 var item = nextTest.Invoke();
135
136 if (!pinnedAndAutoSort.Items.Contains(item))
137 {
138 pinned.Push(item);
139 pinnedAndAutoSort.AddItem(item);
140 pinnedAndAutoSort.PinItem(item);
141 }
142 });
143
144 AddStep("UnpinItem", () =>
145 {
146 if (pinned.Count > 0) pinnedAndAutoSort.UnpinItem(pinned.Pop());
147 });
148
149 AddStep("Set first tab", () => switchingTabControl.Current.Value = switchingTabControl.Items.First());
150 AddStep("Switch forward", () => platformActionContainer.TriggerPressed(PlatformAction.DocumentNext));
151 AddAssert("Ensure second tab", () => switchingTabControl.Current.Value == switchingTabControl.Items.ElementAt(1));
152
153 AddStep("Switch backward", () => platformActionContainer.TriggerPressed(PlatformAction.DocumentPrevious));
154 AddAssert("Ensure first Tab", () => switchingTabControl.Current.Value == switchingTabControl.Items.First());
155
156 AddStep("Switch backward", () => platformActionContainer.TriggerPressed(PlatformAction.DocumentPrevious));
157 AddAssert("Ensure last tab", () => switchingTabControl.Current.Value == switchingTabControl.Items.Last());
158
159 AddStep("Switch forward", () => platformActionContainer.TriggerPressed(PlatformAction.DocumentNext));
160 AddAssert("Ensure first tab", () => switchingTabControl.Current.Value == switchingTabControl.Items.First());
161
162 AddStep("Add all items", () => items.ForEach(item => removeAllTabControl.AddItem(item)));
163 AddAssert("Ensure all items", () => removeAllTabControl.Items.Count == items.Length);
164
165 AddStep("Remove all items", () => removeAllTabControl.Clear());
166 AddAssert("Ensure no items", () => !removeAllTabControl.Items.Any());
167
168 AddAssert("Ensure any items", () => withoutDropdownTabControl.Items.Any());
169 AddStep("Remove all items", () => withoutDropdownTabControl.Clear());
170 AddAssert("Ensure no items", () => !withoutDropdownTabControl.Items.Any());
171
172 AddAssert("Ensure not all items visible on singleline", () => simpleTabcontrol.VisibleItems.Count() < items.Length);
173 AddAssert("Ensure all items visible on multiline", () => multilineTabControl.VisibleItems.Count() == items.Length);
174 }
175
176 [Test]
177 public void TestLeasedBindable()
178 {
179 LeasedBindable<TestEnum?> leased = null;
180
181 AddStep("change value to test0", () => simpleTabcontrol.Current.Value = TestEnum.Test0);
182 AddStep("lease bindable", () => leased = simpleTabcontrol.Current.BeginLease(true));
183 AddStep("change value to test1", () => leased.Value = TestEnum.Test1);
184 AddAssert("value changed", () => simpleTabcontrol.Current.Value == TestEnum.Test1);
185 AddAssert("tab changed", () => simpleTabcontrol.SelectedTab.Value == TestEnum.Test1);
186 AddStep("end lease", () => leased.UnbindAll());
187 }
188
189 [Test]
190 public void TestTabSelectedWhenDisabledBindableIsBound()
191 {
192 Bindable<TestEnum?> bindable;
193
194 AddStep("add tabcontrol", () =>
195 {
196 bindable = new Bindable<TestEnum?> { Value = TestEnum.Test2 };
197
198 simpleTabcontrol = new StyledTabControl
199 {
200 Size = new Vector2(200, 30)
201 };
202
203 foreach (var item in items)
204 simpleTabcontrol.AddItem(item);
205
206 bindable.Disabled = true;
207 simpleTabcontrol.Current = bindable;
208
209 Child = simpleTabcontrol;
210 });
211
212 AddAssert("test2 selected", () => simpleTabcontrol.SelectedTab.Value == TestEnum.Test2);
213 }
214
215 [Test]
216 public void TestClicksBlockedWhenBindableDisabled()
217 {
218 AddStep("add tabcontrol", () =>
219 {
220 Child = simpleTabcontrol = new StyledTabControl { Size = new Vector2(200, 30) };
221
222 foreach (var item in items)
223 simpleTabcontrol.AddItem(item);
224
225 simpleTabcontrol.Current = new Bindable<TestEnum?>
226 {
227 Value = TestEnum.Test0,
228 Disabled = true
229 };
230 });
231
232 AddStep("click a tab", () => simpleTabcontrol.TabMap[TestEnum.Test2].TriggerClick());
233 AddAssert("test0 still selected", () => simpleTabcontrol.SelectedTab.Value == TestEnum.Test0);
234 }
235
236 [TestCase(true)]
237 [TestCase(false)]
238 public void SelectNull(bool autoSort)
239 {
240 AddStep($"Set autosort to {autoSort}", () => simpleTabcontrol.AutoSort = autoSort);
241 AddStep("select item 1", () => simpleTabcontrol.Current.Value = simpleTabcontrol.Items.ElementAt(1));
242 AddAssert("item 1 is selected", () => simpleTabcontrol.Current.Value == simpleTabcontrol.Items.ElementAt(1));
243 AddStep("select item null", () => simpleTabcontrol.Current.Value = null);
244 AddAssert("null is selected", () => simpleTabcontrol.Current.Value == null);
245 }
246
247 [Test]
248 public void TestRemovingTabMovesOutFromDropdown()
249 {
250 AddStep("Remove test3", () => simpleTabcontrol.RemoveItem(TestEnum.Test3));
251 AddAssert("Test 4 is visible", () => simpleTabcontrol.TabMap[TestEnum.Test4].IsPresent);
252
253 AddUntilStep("Remove all visible items", () =>
254 {
255 simpleTabcontrol.RemoveItem(simpleTabcontrol.Items.First(d => simpleTabcontrol.TabMap[d].IsPresent));
256 return !simpleTabcontrol.Dropdown.Items.Any();
257 });
258 }
259
260 [Test]
261 public void TestRemovingSelectedTabSwitchesSelection()
262 {
263 AddStep("Select tab 2", () => simpleTabcontrol.Current.Value = TestEnum.Test2);
264 AddStep("Remove tab 2", () => simpleTabcontrol.RemoveItem(TestEnum.Test2));
265 AddAssert("Ensure selection switches to next tab", () => simpleTabcontrol.SelectedTab.Value == TestEnum.Test3);
266
267 AddStep("Select last tab", () => simpleTabcontrol.Current.Value = simpleTabcontrol.Items.Last());
268 AddStep("Remove selected tab", () => simpleTabcontrol.RemoveItem(simpleTabcontrol.SelectedTab.Value));
269 AddAssert("Ensure selection switches to previous tab", () => simpleTabcontrol.SelectedTab.Value == simpleTabcontrol.Items.Last());
270
271 AddStep("Remove all tabs", () =>
272 {
273 var itemsForDelete = new List<TestEnum?>(simpleTabcontrol.Items);
274 itemsForDelete.ForEach(item => simpleTabcontrol.RemoveItem(item));
275 });
276 AddAssert("Ensure selected tab is null", () => simpleTabcontrol.SelectedTab == null);
277 }
278
279 /// <summary>
280 /// Tests that the selection is not switched on a <see cref="TabControl{T}"/> that has <see cref="TabControl{T}.SwitchTabOnRemove"/> set to <c>false</c>.
281 /// </summary>
282 [Test]
283 public void TestRemovingSelectedTabDoesNotSwitchSelectionIfNotSwitchTabOnRemove()
284 {
285 AddStep("Select tab 2", () => simpleTabcontrolNoSwitchOnRemove.Current.Value = TestEnum.Test2);
286 AddStep("Remove tab 2", () => simpleTabcontrolNoSwitchOnRemove.RemoveItem(TestEnum.Test2));
287 AddAssert("Ensure has not switched", () => simpleTabcontrolNoSwitchOnRemove.SelectedTab.Value == TestEnum.Test2);
288 }
289
290 [Test]
291 public void TestRemovingUnswitchableTab()
292 {
293 AddStep("Set last tab unswitchable", () => ((StyledTabControl.TestTabItem)simpleTabcontrol.SwitchableTabs.Last()).SetSwitchable(false));
294
295 AddStep("Select last tab", () => simpleTabcontrol.Current.Value = simpleTabcontrol.Items.Last());
296 AddStep("Remove selected tab", () => simpleTabcontrol.RemoveItem(simpleTabcontrol.SelectedTab.Value));
297 AddAssert("Ensure selection switches to previous tab", () => simpleTabcontrol.SelectedTab.Value == simpleTabcontrol.Items.Last());
298
299 AddStep("Set first tab unswitchable", () => ((StyledTabControl.TestTabItem)simpleTabcontrol.SwitchableTabs.First()).SetSwitchable(false));
300
301 AddStep("Select first tab", () => simpleTabcontrol.Current.Value = simpleTabcontrol.Items.First());
302 AddStep("Remove selected tab", () => simpleTabcontrol.RemoveItem(simpleTabcontrol.SelectedTab.Value));
303 AddAssert("Ensure selection switches to next tab", () => simpleTabcontrol.SelectedTab.Value == simpleTabcontrol.Items.First());
304 }
305
306 [Test]
307 public void TestUnswitchableNotSelectedOnRemoveAll()
308 {
309 AddStep("Set middle tab unswitchable", () => ((StyledTabControl.TestTabItem)simpleTabcontrol.SwitchableTabs.Skip(5).First()).SetSwitchable(false));
310
311 AddStep("Remove all switchable tabs", () =>
312 {
313 var itemsForDelete = new List<TestEnum?>();
314
315 foreach (var kvp in simpleTabcontrol.TabMap)
316 {
317 if (kvp.Value.IsSwitchable)
318 itemsForDelete.Add(kvp.Key);
319 }
320
321 itemsForDelete.ForEach(item => simpleTabcontrol.RemoveItem(item));
322 });
323
324 AddAssert("Unswitchable tab still present", () => simpleTabcontrol.Items.Count == 1);
325 AddAssert("Ensure selected tab is null", () => simpleTabcontrol.SelectedTab == null);
326 }
327
328 [Test]
329 public void TestItemsImmediatelyUpdatedAfterAdd()
330 {
331 TabControlWithNoDropdown tabControl = null;
332
333 AddStep("create tab control", () =>
334 {
335 tabControl = new TabControlWithNoDropdown { Size = new Vector2(200, 30) };
336
337 foreach (var item in items)
338 tabControl.AddItem(item);
339 });
340
341 AddAssert("contained items match added items", () => tabControl.Items.SequenceEqual(items));
342 }
343
344 [Test]
345 public void TestItemsAddedWhenSet()
346 {
347 TabControlWithNoDropdown tabControl = null;
348
349 AddStep("create tab control", () =>
350 {
351 tabControl = new TabControlWithNoDropdown
352 {
353 Size = new Vector2(200, 30),
354 Items = items
355 };
356 });
357
358 AddAssert("contained items match added items", () => tabControl.Items.SequenceEqual(items));
359 }
360
361 [TestCase(false, null)]
362 [TestCase(true, TestEnum.Test0)]
363 public void TestInitialSelection(bool selectFirstByDefault, TestEnum? expectedInitialSelection)
364 {
365 StyledTabControl tabControl = null;
366
367 AddStep("create tab control", () =>
368 {
369 tabControlContainer.Add(tabControl = new StyledTabControl
370 {
371 Size = new Vector2(200, 30),
372 Items = items.Cast<TestEnum?>().ToList(),
373 SelectFirstTabByDefault = selectFirstByDefault
374 });
375 });
376
377 AddUntilStep("wait for loaded", () => tabControl.IsLoaded);
378 AddAssert("initial selection is correct", () => tabControl.Current.Value == expectedInitialSelection);
379 }
380
381 [TestCase(true, TestEnum.Test1, true)]
382 [TestCase(false, TestEnum.Test1, true)]
383 [TestCase(true, TestEnum.Test9, true)]
384 [TestCase(false, TestEnum.Test9, false)]
385 public void TestInitialSort(bool autoSort, TestEnum? initialItem, bool expected)
386 {
387 StyledTabControl tabControlWithBindable = null;
388 Bindable<TestEnum?> testBindable = new Bindable<TestEnum?> { Value = initialItem };
389
390 AddStep("create tab control", () =>
391 {
392 tabControlContainer.Add(tabControlWithBindable = new StyledTabControl
393 {
394 Size = new Vector2(200, 20),
395 Items = items.Cast<TestEnum?>().ToList(),
396 AutoSort = autoSort,
397 Current = { BindTarget = testBindable }
398 });
399 });
400
401 AddUntilStep("wait for loaded", () => tabControlWithBindable.IsLoaded);
402 AddAssert($"Current selection {(expected ? "visible" : "not visible")}", () => tabControlWithBindable.SelectedTab.IsPresent == expected);
403 }
404
405 private class StyledTabControlWithoutDropdown : TabControl<TestEnum>
406 {
407 protected override Dropdown<TestEnum> CreateDropdown() => null;
408
409 protected override TabItem<TestEnum> CreateTabItem(TestEnum value)
410 => new BasicTabControl<TestEnum>.BasicTabItem(value);
411 }
412
413 private class StyledMultilineTabControl : TabControl<TestEnum>
414 {
415 protected override Dropdown<TestEnum> CreateDropdown() => null;
416
417 protected override TabItem<TestEnum> CreateTabItem(TestEnum value)
418 => new BasicTabControl<TestEnum>.BasicTabItem(value);
419
420 protected override TabFillFlowContainer CreateTabFlow() => base.CreateTabFlow().With(f => { f.AllowMultiline = true; });
421 }
422
423 public class StyledTabControl : TabControl<TestEnum?>
424 {
425 public new IReadOnlyDictionary<TestEnum?, TabItem<TestEnum?>> TabMap => base.TabMap;
426
427 public new TabItem<TestEnum?> SelectedTab => base.SelectedTab;
428
429 public new Dropdown<TestEnum?> Dropdown => base.Dropdown;
430
431 protected override Dropdown<TestEnum?> CreateDropdown() => new StyledDropdown();
432
433 public TabItem<TestEnum?> CreateTabItem(TestEnum? value, bool isSwitchable)
434 => new TestTabItem(value);
435
436 protected override TabItem<TestEnum?> CreateTabItem(TestEnum? value) => CreateTabItem(value, true);
437
438 public class TestTabItem : BasicTabControl<TestEnum?>.BasicTabItem
439 {
440 public TestTabItem(TestEnum? value)
441 : base(value)
442 {
443 }
444
445 private bool switchable = true;
446
447 public void SetSwitchable(bool isSwitchable) => switchable = isSwitchable;
448
449 public override bool IsSwitchable => switchable;
450 }
451 }
452
453 private class StyledDropdown : Dropdown<TestEnum?>
454 {
455 protected override DropdownMenu CreateMenu() => new StyledDropdownMenu();
456
457 protected override DropdownHeader CreateHeader() => new StyledDropdownHeader();
458
459 public StyledDropdown()
460 {
461 Menu.Anchor = Anchor.TopRight;
462 Menu.Origin = Anchor.TopRight;
463 Header.Anchor = Anchor.TopRight;
464 Header.Origin = Anchor.TopRight;
465 }
466
467 private class StyledDropdownMenu : BasicDropdown<TestEnum?>.BasicDropdownMenu
468 {
469 public StyledDropdownMenu()
470 {
471 ScrollbarVisible = false;
472 CornerRadius = 4;
473 }
474 }
475 }
476
477 private class StyledDropdownHeader : DropdownHeader
478 {
479 protected internal override LocalisableString Label { get; set; }
480
481 public StyledDropdownHeader()
482 {
483 Background.Hide(); // don't need a background
484
485 RelativeSizeAxes = Axes.None;
486 AutoSizeAxes = Axes.X;
487
488 Foreground.RelativeSizeAxes = Axes.None;
489 Foreground.AutoSizeAxes = Axes.Both;
490
491 Foreground.Children = new[]
492 {
493 new Box { Width = 20, Height = 20 }
494 };
495 }
496 }
497
498 private class TabControlWithNoDropdown : BasicTabControl<TestEnum>
499 {
500 protected override Dropdown<TestEnum> CreateDropdown() => null;
501 }
502
503 public enum TestEnum
504 {
505 Test0,
506 Test1,
507 Test2,
508 Test3,
509 Test4,
510 Test5,
511 Test6,
512 Test7,
513 Test8,
514 Test9,
515 Test10,
516 Test11,
517 Test12
518 }
519 }
520}