// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osuTK; using osuTK.Input; namespace osu.Framework.Tests.Visual { public class TestCaseNestedMenus : ManualInputManagerTestCase { private const int max_depth = 5; private const int max_count = 5; public override IReadOnlyList RequiredTypes => new[] { typeof(Menu) }; private Random rng; private MenuStructure menus; [SetUp] public override void SetUp() => Schedule(() => { base.SetUp(); Clear(); rng = new Random(1337); Menu menu; Children = new Drawable[] { new CursorContainer(), new Container { RelativeSizeAxes = Axes.Both, Child = menu = createMenu() } }; menus = new MenuStructure(menu); }); private Menu createMenu() => new ClickOpenMenu(TimePerAction) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Items = new[] { generateRandomMenuItem("First"), generateRandomMenuItem("Second"), generateRandomMenuItem("Third"), } }; private class ClickOpenMenu : Menu { protected override Menu CreateSubMenu() => new ClickOpenMenu(HoverOpenDelay, false); public ClickOpenMenu(double timePerAction, bool topLevel = true) : base(Direction.Vertical, topLevel) { HoverOpenDelay = timePerAction; } } #region Test Cases /// /// Tests if the respects = true, by not alowing it to be closed /// when a click happens outside the . /// [Test] public void TestAlwaysOpen() { AddStep("Click outside", () => InputManager.Click(MouseButton.Left)); AddAssert("Check AlwaysOpen = true", () => menus.GetSubMenu(0).State == MenuState.Open); } /// /// Tests if the hover state on s is valid. /// [Test] public void TestHoverState() { AddAssert("Check submenu closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetMenuItems()[0])); AddAssert("Check item hovered", () => menus.GetMenuItems()[0].IsHovered); } /// /// Tests if the respects = true. /// [Test] public void TestTopLevelMenu() { AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(0).GetMenuItems()[0])); AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); AddStep("Click item", () => InputManager.Click(MouseButton.Left)); AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); } /// /// Tests if clicking once on a menu that has opens it, and clicking a second time /// closes it. /// [Test] public void TestDoubleClick() { AddStep("Click item", () => clickItem(0, 0)); AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); AddStep("Click item", () => clickItem(0, 0)); AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); } /// /// Tests whether click on s causes sub-menus to instantly appear. /// [Test] public void TestInstantOpen() { AddStep("Click item", () => clickItem(0, 1)); AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); AddStep("Click item", () => clickItem(1, 0)); AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); } /// /// Tests if clicking on an item that has no sub-menu causes the menu to close. /// [Test] public void TestActionClick() { AddStep("Click item", () => clickItem(0, 0)); AddStep("Click item", () => clickItem(1, 0)); AddAssert("Check closed", () => menus.GetSubMenu(1)?.State != MenuState.Open); } /// /// Tests if hovering over menu items respects the . /// [Test] public void TestHoverOpen() { AddStep("Click item", () => clickItem(0, 1)); AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[0])); AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(2).GetMenuItems()[0])); AddAssert("Check closed", () => menus.GetSubMenu(3)?.State != MenuState.Open); AddAssert("Check open", () => menus.GetSubMenu(3).State == MenuState.Open); } /// /// Tests if hovering over a different item on the main will instantly open another menu /// and correctly changes the sub-menu items to the new items from the hovered item. /// [Test] public void TestHoverChange() { IReadOnlyList currentItems = null; AddStep("Click item", () => { clickItem(0, 0); }); AddStep("Get items", () => { currentItems = menus.GetSubMenu(1).Items; }); AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(0).GetMenuItems()[1])); AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); AddAssert("Check new items", () => !menus.GetSubMenu(1).Items.SequenceEqual(currentItems)); AddAssert("Check closed", () => { int currentSubMenu = 3; while (true) { var subMenu = menus.GetSubMenu(currentSubMenu); if (subMenu == null) break; if (subMenu.State == MenuState.Open) return false; currentSubMenu++; } return true; }); } /// /// Tests whether hovering over a different item on a sub-menu opens a new sub-menu in a delayed fashion /// and correctly changes the sub-menu items to the new items from the hovered item. /// [Test] public void TestDelayedHoverChange() { AddStep("Click item", () => clickItem(0, 2)); AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[0])); AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); AddStep("Hover item", () => { InputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[1]); }); AddAssert("Check closed", () => menus.GetSubMenu(2)?.State != MenuState.Open); AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); AddAssert("Check closed", () => { int currentSubMenu = 3; while (true) { var subMenu = menus.GetSubMenu(currentSubMenu); if (subMenu == null) break; if (subMenu.State == MenuState.Open) return false; currentSubMenu++; } return true; }); } /// /// Tests whether clicking on s that have opened sub-menus don't close the sub-menus. /// Then tests hovering in reverse order to make sure only the lower level menus close. /// [Test] public void TestMenuClicksDontClose() { AddStep("Click item", () => clickItem(0, 1)); AddStep("Click item", () => clickItem(1, 0)); AddStep("Click item", () => clickItem(2, 0)); AddStep("Click item", () => clickItem(3, 0)); for (int i = 3; i >= 1; i--) { int menuIndex = i; AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(menuIndex).GetMenuItems()[0])); AddAssert("Check submenu open", () => menus.GetSubMenu(menuIndex + 1).State == MenuState.Open); AddStep("Click item", () => InputManager.Click(MouseButton.Left)); AddAssert("Check all open", () => { for (int j = 0; j <= menuIndex; j++) { int menuIndex2 = j; if (menus.GetSubMenu(menuIndex2)?.State != MenuState.Open) return false; } return true; }); } } /// /// Tests whether clicking on the that has closes all sub menus. /// [Test] public void TestMenuClickClosesSubMenus() { AddStep("Click item", () => clickItem(0, 1)); AddStep("Click item", () => clickItem(1, 0)); AddStep("Click item", () => clickItem(2, 0)); AddStep("Click item", () => clickItem(3, 0)); AddStep("Click item", () => clickItem(0, 1)); AddAssert("Check submenus closed", () => { for (int j = 1; j <= 3; j++) { int menuIndex2 = j; if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) return false; } return true; }); } /// /// Tests whether clicking on an action in a sub-menu closes all s. /// [Test] public void TestActionClickClosesMenus() { AddStep("Click item", () => clickItem(0, 1)); AddStep("Click item", () => clickItem(1, 0)); AddStep("Click item", () => clickItem(2, 0)); AddStep("Click item", () => clickItem(3, 0)); AddStep("Click item", () => clickItem(4, 0)); AddAssert("Check submenus closed", () => { for (int j = 1; j <= 3; j++) { int menuIndex2 = j; if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) return false; } return true; }); } /// /// Tests whether clicking outside the structure closes all sub-menus. /// /// Whether the previous menu should first be hovered before clicking outside. [TestCase(false)] [TestCase(true)] public void TestClickingOutsideClosesMenus(bool hoverPrevious) { for (int i = 0; i <= 3; i++) { int i2 = i; for (int j = 0; j <= i; j++) { int menuToOpen = j; int itemToOpen = menuToOpen == 0 ? 1 : 0; AddStep("Click item", () => clickItem(menuToOpen, itemToOpen)); } if (hoverPrevious && i > 0) AddStep("Hover previous", () => InputManager.MoveMouseTo(menus.GetSubStructure(i2 - 1).GetMenuItems()[i2 > 1 ? 0 : 1])); AddStep("Remove hover", () => InputManager.MoveMouseTo(Vector2.Zero)); AddStep("Click outside", () => InputManager.Click(MouseButton.Left)); AddAssert("Check submenus closed", () => { for (int j = 1; j <= i2 + 1; j++) { int menuIndex2 = j; if (menus.GetSubMenu(menuIndex2).State == MenuState.Open) return false; } return true; }); } } /// /// Opens some menus and then changes the selected item. /// [Test] public void TestSelectedState() { AddStep("Click item", () => clickItem(0, 2)); AddAssert("Check open", () => menus.GetSubMenu(1).State == MenuState.Open); AddStep("Hover item", () => InputManager.MoveMouseTo(menus.GetSubStructure(1).GetMenuItems()[1])); AddAssert("Check closed 1", () => menus.GetSubMenu(2)?.State != MenuState.Open); AddAssert("Check open", () => menus.GetSubMenu(2).State == MenuState.Open); AddAssert("Check selected index 1", () => menus.GetSubStructure(1).GetSelectedIndex() == 1); AddStep("Change selection", () => menus.GetSubStructure(1).SetSelectedState(0, MenuItemState.Selected)); AddAssert("Check selected index", () => menus.GetSubStructure(1).GetSelectedIndex() == 0); AddStep("Change selection", () => menus.GetSubStructure(1).SetSelectedState(2, MenuItemState.Selected)); AddAssert("Check selected index 2", () => menus.GetSubStructure(1).GetSelectedIndex() == 2); AddStep("Close menus", () => menus.GetSubMenu(0).Close()); AddAssert("Check selected index 4", () => menus.GetSubStructure(1).GetSelectedIndex() == -1); } #endregion /// /// Click an item in a menu. /// /// The level of menu our click targets. /// The item to click in the menu. private void clickItem(int menuIndex, int itemIndex) { InputManager.MoveMouseTo(menus.GetSubStructure(menuIndex).GetMenuItems()[itemIndex]); InputManager.Click(MouseButton.Left); } private MenuItem generateRandomMenuItem(string name = "Menu Item", int currDepth = 1) { var item = new MenuItem(name); if (currDepth == max_depth) return item; int subCount = rng.Next(0, max_count); var subItems = new List(); for (int i = 0; i < subCount; i++) subItems.Add(generateRandomMenuItem(item.Text + $" #{i + 1}", currDepth + 1)); item.Items = subItems; return item; } /// /// Helper class used to retrieve various internal properties/items from a . /// private class MenuStructure { private readonly Menu menu; public MenuStructure(Menu menu) { this.menu = menu; } /// /// Retrieves the s of the represented by this . /// public IReadOnlyList GetMenuItems() { var contents = (CompositeDrawable)menu.InternalChildren[0]; var contentContainer = (CompositeDrawable)contents.InternalChildren[1]; return ((CompositeDrawable)((CompositeDrawable)contentContainer.InternalChildren[0]).InternalChildren[0]).InternalChildren; } /// /// Finds the index in the represented by this that /// has set to . /// public int GetSelectedIndex() { var items = GetMenuItems(); for (int i = 0; i < items.Count; i++) { var state = (MenuItemState)(items[i]?.GetType().GetProperty("State")?.GetValue(items[i]) ?? MenuItemState.NotSelected); if (state == MenuItemState.Selected) return i; } return -1; } /// /// Sets the at the specified index to a specified state. /// /// The index of the to set the state of. /// The state to be set. public void SetSelectedState(int index, MenuItemState state) { var item = GetMenuItems()[index]; item.GetType().GetProperty("State")?.SetValue(item, state); } /// /// Retrieves the sub- at an index-offset from the current . /// /// The sub- index. An index of 0 is the represented by this . public Menu GetSubMenu(int index) { var currentMenu = menu; for (int i = 0; i < index; i++) { if (currentMenu == null) break; var container = (CompositeDrawable)currentMenu.InternalChildren[1]; // ReSharper disable once ArrangeRedundantParentheses // Broken resharper inspection (https://youtrack.jetbrains.com/issue/RIDER-19843) currentMenu = (container.InternalChildren.Count > 0 ? container.InternalChildren[0] : null) as Menu; } return currentMenu; } /// /// Generates a new for the a sub-. /// /// The sub- index to generate the for. An index of 0 is the represented by this . public MenuStructure GetSubStructure(int index) => new MenuStructure(GetSubMenu(index)); } } }