Skip to content

Commit

Permalink
Merge pull request ppy#6072 from frenzibyte/dropdown-search
Browse files Browse the repository at this point in the history
Implement dropdown searching
  • Loading branch information
peppy authored Dec 13, 2023
2 parents 67e11cc + d5c2e65 commit ba569bf
Show file tree
Hide file tree
Showing 15 changed files with 632 additions and 168 deletions.
11 changes: 11 additions & 0 deletions osu.Framework.Tests/Visual/Layout/TestSceneFillFlowContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,17 @@ public TestSceneDropdownHeader()
label = new SpriteText(),
};
}

protected override DropdownSearchBar CreateSearchBar() => new BasicDropdownSearchBar();

private partial class BasicDropdownSearchBar : DropdownSearchBar
{
protected override void PopIn() => this.FadeIn();

protected override void PopOut() => this.FadeOut();

protected override TextBox CreateTextBox() => new BasicTextBox();
}
}

private partial class AnchorDropdown : BasicDropdown<Anchor>
Expand Down
140 changes: 130 additions & 10 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneDropdown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osuTK;
using osuTK.Input;

Expand All @@ -24,6 +25,16 @@ public partial class TestSceneDropdown : ManualInputManagerTestScene
{
private const int items_to_add = 10;

[Test]
public void TestBasic()
{
AddStep("setup dropdowns", () =>
{
TestDropdown[] dropdowns = createDropdowns(2);
dropdowns[1].AlwaysShowSearchBar = true;
});
}

[Test]
public void TestSelectByUserInteraction()
{
Expand Down Expand Up @@ -143,10 +154,10 @@ public void TestKeyboardSelection(bool cleanSelection)
AddAssert("previous item is selected", () => testDropdown.SelectedIndex == Math.Max(0, previousIndex - 1));

AddStep("select last item", () => InputManager.Keys(PlatformAction.MoveToListEnd));
AddAssert("last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.Last().Item);
AddAssert("last item selected", () => testDropdown.SelectedItem == testDropdown.Menu.VisibleMenuItems.Last().Item);

AddStep("select last item", () => InputManager.Keys(PlatformAction.MoveToListStart));
AddAssert("first item selected", () => testDropdown.SelectedItem == testDropdown.Menu.DrawableMenuItems.First().Item);
AddAssert("first item selected", () => testDropdown.SelectedItem == testDropdown.Menu.VisibleMenuItems.First().Item);

AddStep("select next item when empty", () => InputManager.Key(Key.Up));
AddStep("select previous item when empty", () => InputManager.Key(Key.Down));
Expand Down Expand Up @@ -330,7 +341,7 @@ public void TestClearItemsInBindableWhileNotPresent()
AddStep("hide dropdown", () => testDropdown.Hide());
AddStep("clear items", () => bindableList.Clear());
AddStep("show dropdown", () => testDropdown.Show());
AddAssert("dropdown menu empty", () => !testDropdown.Menu.DrawableMenuItems.Any());
AddAssert("dropdown menu empty", () => !testDropdown.Menu.Children.Any());
}

/// <summary>
Expand All @@ -355,7 +366,7 @@ public void TestAddItemBeforeDropdownLoad()
};
});

AddAssert("text is expected", () => dropdown.Menu.DrawableMenuItems.First().ChildrenOfType<SpriteText>().First().Text.ToString(), () => Is.EqualTo("loaded: test"));
AddAssert("text is expected", () => dropdown.Menu.VisibleMenuItems.First().ChildrenOfType<SpriteText>().First().Text.ToString(), () => Is.EqualTo("loaded: test"));
}

/// <summary>
Expand All @@ -377,7 +388,7 @@ public void TestAddItemWhileDropdownIsInReadyState()
dropdown.Items = new TestModel("test").Yield();
});

AddAssert("text is expected", () => dropdown.Menu.DrawableMenuItems.First(d => d.IsSelected).ChildrenOfType<SpriteText>().First().Text.ToString(), () => Is.EqualTo("loaded: test"));
AddAssert("text is expected", () => dropdown.Menu.VisibleMenuItems.First(d => d.IsSelected).ChildrenOfType<SpriteText>().First().Text.ToString(), () => Is.EqualTo("loaded: test"));
}

/// <summary>
Expand Down Expand Up @@ -418,19 +429,122 @@ public void TestSetNonExistentItem([Values] bool afterBdl)
AddAssert("text is expected", () => dropdown.SelectedItem.Text.Value.ToString(), () => Is.EqualTo("loaded: non-existent item"));
}

#region Searching

[Test]
public void TestSearching()
{
ManualTextDropdown dropdown = null!;

AddStep("setup dropdown", () => dropdown = createDropdowns<ManualTextDropdown>(1)[0]);
AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));

toggleDropdownViaClick(() => dropdown);

AddAssert("search bar still hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));

AddStep("trigger text", () => dropdown.TextInput.Text("test 4"));
AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("items filtered", () =>
{
var drawableItem = dropdown.Menu.VisibleMenuItems.Single(i => i.IsPresent);
return drawableItem.Item.Text.Value == "test 4";
});
AddAssert("item preselected", () => dropdown.Menu.VisibleMenuItems.Single().IsPreSelected);

AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("item selected", () => dropdown.SelectedItem.Text.Value == "test 4");
}

[Test]
public void TestReleaseFocusAfterSearching()
{
ManualTextDropdown dropdown = null!;

AddStep("setup dropdown", () => dropdown = createDropdowns<ManualTextDropdown>(1)[0]);
toggleDropdownViaClick(() => dropdown);

AddStep("trigger text", () => dropdown.TextInput.Text("test 4"));
AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

AddStep("press escape", () => InputManager.Key(Key.Escape));
AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
AddAssert("dropdown still open", () => dropdown.Menu.State == MenuState.Open);

AddStep("press escape again", () => InputManager.Key(Key.Escape));
AddAssert("dropdown closed", () => dropdown.Menu.State == MenuState.Closed);

toggleDropdownViaClick(() => dropdown);
AddStep("trigger text", () => dropdown.TextInput.Text("test 4"));
AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

AddStep("click away", () =>
{
InputManager.MoveMouseTo(Vector2.Zero);
InputManager.Click(MouseButton.Left);
});

AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
}

[Test]
public void TestAlwaysShowSearchBar()
{
ManualTextDropdown dropdown = null!;

AddStep("setup dropdown", () =>
{
dropdown = createDropdowns<ManualTextDropdown>(1)[0];
dropdown.AlwaysShowSearchBar = true;
});

AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
toggleDropdownViaClick(() => dropdown);

AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

AddStep("trigger text", () => dropdown.TextInput.Text("test 4"));
AddAssert("search bar still visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

AddStep("press escape", () => InputManager.Key(Key.Escape));
AddAssert("search bar still visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddAssert("dropdown still open", () => dropdown.Menu.State == MenuState.Open);

AddStep("press escape again", () => InputManager.Key(Key.Escape));
AddAssert("dropdown closed", () => dropdown.Menu.State == MenuState.Closed);
AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));

toggleDropdownViaClick(() => dropdown);
AddStep("trigger text", () => dropdown.TextInput.Text("test 4"));
AddAssert("search bar visible", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));

AddStep("click away", () =>
{
InputManager.MoveMouseTo(Vector2.Zero);
InputManager.Click(MouseButton.Left);
});

AddAssert("search bar hidden", () => dropdown.ChildrenOfType<DropdownSearchBar>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
}

#endregion

private TestDropdown createDropdown() => createDropdowns(1).Single();

private TestDropdown[] createDropdowns(int count)
private TestDropdown[] createDropdowns(int count) => createDropdowns<TestDropdown>(count);

private TDropdown[] createDropdowns<TDropdown>(int count)
where TDropdown : TestDropdown, new()
{
TestDropdown[] dropdowns = new TestDropdown[count];
TDropdown[] dropdowns = new TDropdown[count];

for (int dropdownIndex = 0; dropdownIndex < count; dropdownIndex++)
{
var testItems = new TestModel[10];
for (int itemIndex = 0; itemIndex < items_to_add; itemIndex++)
testItems[itemIndex] = "test " + itemIndex;

dropdowns[dropdownIndex] = new TestDropdown
dropdowns[dropdownIndex] = new TDropdown
{
Position = new Vector2(50f, 50f),
Width = 150,
Expand Down Expand Up @@ -488,8 +602,14 @@ private partial class TestDropdown : BasicDropdown<TestModel?>
{
internal new DropdownMenuItem<TestModel?> SelectedItem => base.SelectedItem;

public int SelectedIndex => Menu.DrawableMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem);
public int PreselectedIndex => Menu.DrawableMenuItems.ToList().IndexOf(Menu.PreselectedItem);
public int SelectedIndex => Menu.VisibleMenuItems.Select(d => d.Item).ToList().IndexOf(SelectedItem);
public int PreselectedIndex => Menu.VisibleMenuItems.ToList().IndexOf(Menu.PreselectedItem);
}

private partial class ManualTextDropdown : TestDropdown
{
[Cached(typeof(TextInputSource))]
public readonly ManualTextInputSource TextInput = new ManualTextInputSource();
}

/// <summary>
Expand Down
11 changes: 11 additions & 0 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneTabControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,17 @@ public StyledDropdownHeader()
new Box { Width = 20, Height = 20 }
};
}

protected override DropdownSearchBar CreateSearchBar() => new BasicDropdownSearchBar();

private partial class BasicDropdownSearchBar : DropdownSearchBar
{
protected override void PopIn() => this.FadeIn();

protected override void PopOut() => this.FadeOut();

protected override TextBox CreateTextBox() => new BasicTextBox();
}
}

private partial class TabControlWithNoDropdown : BasicTabControl<TestEnum>
Expand Down
22 changes: 12 additions & 10 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneTextBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using osu.Framework.Testing;
using osu.Framework.Utils;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;

namespace osu.Framework.Tests.Visual.UserInterface
Expand Down Expand Up @@ -878,7 +877,7 @@ private partial class NumberTextBox : BasicTextBox

private partial class CustomTextBox : BasicTextBox
{
protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, CalculatedTextSize);
protected override Drawable GetDrawableCharacter(char c) => new ScalingText(c, FontSize);

private partial class ScalingText : CompositeDrawable
{
Expand Down Expand Up @@ -923,16 +922,19 @@ private partial class BorderCaret : Caret

public BorderCaret()
{
RelativeSizeAxes = Axes.Y;

Masking = true;
BorderColour = Color4.White;
BorderThickness = 3;

InternalChild = new Box
InternalChild = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Colour = Color4.Transparent
Masking = true,
BorderColour = Colour4.White,
BorderThickness = 3f,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Transparent,
},
};
}

Expand Down
52 changes: 4 additions & 48 deletions osu.Framework.Tests/Visual/UserInterface/TestSceneTextBoxEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osuTK;
using osuTK.Input;

Expand All @@ -20,7 +21,7 @@ namespace osu.Framework.Tests.Visual.UserInterface
public partial class TestSceneTextBoxEvents : ManualInputManagerTestScene
{
private EventQueuesTextBox textBox;
private ManualTextInput textInput;
private ManualTextInputSource textInput;
private ManualTextInputContainer textInputContainer;

private const string default_text = "some default text";
Expand Down Expand Up @@ -618,57 +619,12 @@ protected override void OnImeResult(string result, bool successful) =>
public partial class ManualTextInputContainer : Container
{
[Cached(typeof(TextInputSource))]
public readonly ManualTextInput TextInput;
public readonly ManualTextInputSource TextInput;

public ManualTextInputContainer()
{
RelativeSizeAxes = Axes.Both;
TextInput = new ManualTextInput();
}
}

public class ManualTextInput : TextInputSource
{
public void Text(string text) => TriggerTextInput(text);

public new void TriggerImeComposition(string text, int start, int length)
{
base.TriggerImeComposition(text, start, length);
}

public new void TriggerImeResult(string text)
{
base.TriggerImeResult(text);
}

public override void ResetIme()
{
base.ResetIme();

// this call will be somewhat delayed in a real world scenario, but let's run it immediately for simplicity.
base.TriggerImeComposition(string.Empty, 0, 0);
}

public readonly Queue<bool> ActivationQueue = new Queue<bool>();
public readonly Queue<bool> EnsureActivatedQueue = new Queue<bool>();
public readonly Queue<bool> DeactivationQueue = new Queue<bool>();

protected override void ActivateTextInput(bool allowIme)
{
base.ActivateTextInput(allowIme);
ActivationQueue.Enqueue(allowIme);
}

protected override void EnsureTextInputActivated(bool allowIme)
{
base.EnsureTextInputActivated(allowIme);
EnsureActivatedQueue.Enqueue(allowIme);
}

protected override void DeactivateTextInput()
{
base.DeactivateTextInput();
DeactivationQueue.Enqueue(true);
TextInput = new ManualTextInputSource();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Testing.Input;
using osuTK;
using osuTK.Input;

Expand All @@ -20,7 +21,7 @@ public partial class TestSceneTextBoxKeyEvents : ManualInputManagerTestScene
{
private KeyEventQueuesTextBox textBox;

private TestSceneTextBoxEvents.ManualTextInput textInput;
private ManualTextInputSource textInput;

[Resolved]
private GameHost host { get; set; }
Expand Down
Loading

0 comments on commit ba569bf

Please sign in to comment.