1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 14:33:01 +08:00

Merge pull request #23414 from Cootz/add-mod-search-option

Add mod search
This commit is contained in:
Dean Herbert 2023-06-19 02:07:00 +09:00 committed by GitHub
commit f8b94ed033
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 559 additions and 114 deletions

View File

@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -57,11 +58,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0); AddAssert("customisation area not expanded", () => this.ChildrenOfType<ModSettingsArea>().Single().Height == 0);
} }
[Test]
public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged()
{
createFreeModSelect();
AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea");
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddStep("click select all button", navigateAndClick<SelectAllModsButton>);
AddAssert("select all button disabled", () => !this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e");
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
void navigateAndClick<T>() where T : Drawable
{
InputManager.MoveMouseTo(this.ChildrenOfType<T>().Single());
InputManager.Click(MouseButton.Left);
}
}
[Test] [Test]
public void TestSelectDeselectAllViaKeyboard() public void TestSelectDeselectAllViaKeyboard()
{ {
createFreeModSelect(); createFreeModSelect();
AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus());
AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll)); AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll));
AddUntilStep("all mods selected", assertAllAvailableModsSelected); AddUntilStep("all mods selected", assertAllAvailableModsSelected);

View File

@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
{ {
AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) }); AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
@ -102,17 +103,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
// A previous test's mod overlay could still be fading out. // A previous test's mod overlay could still be fading out.
AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1); AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType<FreeModSelectOverlay>().Count() == 1);
assertHasFreeModButton(allowedMod, false); assertFreeModNotShown(allowedMod);
assertHasFreeModButton(requiredMod, false); assertFreeModNotShown(requiredMod);
} }
private void assertHasFreeModButton(Type type, bool hasButton = true) private void assertFreeModNotShown(Type type)
{ {
AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
() => this.ChildrenOfType<FreeModSelectOverlay>() () => this.ChildrenOfType<FreeModSelectOverlay>()
.Single() .Single()
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.Where(panel => !panel.Filtered.Value) .Where(panel => panel.Visible)
.All(b => b.Mod.GetType() != type)); .All(b => b.Mod.GetType() != type));
} }

View File

@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("mod select contains only double time mod", AddUntilStep("mod select contains only double time mod",
() => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay () => this.ChildrenOfType<RoomSubScreen>().Single().UserModsSelectOverlay
.ChildrenOfType<ModPanel>() .ChildrenOfType<ModPanel>()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime);
} }
[Test] [Test]

View File

@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2); AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => panel.Visible) == 2);
clickToggle(); clickToggle();
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => panel.Visible));
AddStep("unset filter", () => setFilter(null)); AddStep("unset filter", () => setFilter(null));
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value)); AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value); AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase)));
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2); AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => panel.Visible) == 2);
AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value); AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
AddStep("filter out everything", () => setFilter(_ => false)); AddStep("filter out everything", () => setFilter(_ => false));
AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Filtered.Value)); AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Visible));
AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
AddStep("inset filter", () => setFilter(null)); AddStep("inset filter", () => setFilter(null));
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value)); AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent); AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
void clickToggle() => AddStep("click toggle", () => void clickToggle() => AddStep("click toggle", () =>
@ -288,10 +288,53 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2); AddAssert("no change", () => this.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
} }
[Test]
public void TestApplySearchTerms()
{
Mod hidden = getExampleModsFor(ModType.DifficultyIncrease).Where(modState => modState.Mod is ModHidden).Select(modState => modState.Mod).Single();
ModColumn column = null!;
AddStep("create content", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30),
Child = column = new ModColumn(ModType.DifficultyIncrease, false)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AvailableMods = getExampleModsFor(ModType.DifficultyIncrease)
}
});
applySearchAndAssert(hidden.Name);
clearSearch();
applySearchAndAssert(hidden.Acronym);
clearSearch();
applySearchAndAssert(hidden.Description.ToString());
void applySearchAndAssert(string searchTerm)
{
AddStep("search by mod name", () => column.SearchTerm = searchTerm);
AddAssert("only hidden is visible", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Visible).All(panel => panel.Mod is ModHidden));
}
void clearSearch()
{
AddStep("clear search", () => column.SearchTerm = string.Empty);
AddAssert("all mods are visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Visible));
}
}
private void setFilter(Func<Mod, bool>? filter) private void setFilter(Func<Mod, bool>? filter)
{ {
foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods) foreach (var modState in this.ChildrenOfType<ModColumn>().Single().AvailableMods)
modState.Filtered.Value = filter?.Invoke(modState.Mod) == false; modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false;
} }
private partial class TestModColumn : ModColumn private partial class TestModColumn : ModColumn

View File

@ -392,6 +392,28 @@ namespace osu.Game.Tests.Visual.UserInterface
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods)); new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods));
} }
[Test]
public void TestTextFiltering()
{
ModPresetColumn modPresetColumn = null!;
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
AddStep("set text filter", () => modPresetColumn.SearchTerm = "First");
AddUntilStep("one panel visible", () => modPresetColumn.ChildrenOfType<ModPresetPanel>().Count(panel => panel.IsPresent), () => Is.EqualTo(1));
AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3));
AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType<ModPresetPanel>().Count(panel => panel.IsPresent), () => Is.EqualTo(0));
}
private ICollection<ModPreset> createTestPresets() => new[] private ICollection<ModPreset> createTestPresets() => new[]
{ {
new ModPreset new ModPreset

View File

@ -490,15 +490,15 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen(); createScreen();
changeRuleset(0); changeRuleset(0);
AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime));
AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible));
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible));
AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true);
AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible));
AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible));
} }
[Test] [Test]
@ -524,7 +524,57 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
waitForColumnLoad(); waitForColumnLoad();
AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); AddAssert("unimplemented mod panel is filtered", () => !getPanelForMod(typeof(TestUnimplementedMod)).Visible);
}
[Test]
public void TestFirstModSelectDeselect()
{
createScreen();
AddStep("apply search", () => modSelectOverlay.SearchTerm = "HD");
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value);
AddStep("press enter again", () => InputManager.Key(Key.Enter));
AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value);
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestSearchFocusChangeViaClick()
{
createScreen();
AddStep("click on search", navigateAndClick<ShearedSearchTextBox>);
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("click on mod column", navigateAndClick<ModColumn>);
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
void navigateAndClick<T>() where T : Drawable
{
InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType<T>().FirstOrDefault());
InputManager.Click(MouseButton.Left);
}
}
[Test]
public void TestSearchFocusChangeViaKey()
{
createScreen();
const Key focus_switch_key = Key.Tab;
AddStep("press tab", () => InputManager.Key(focus_switch_key));
AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("press tab", () => InputManager.Key(focus_switch_key));
AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus);
} }
[Test] [Test]
@ -533,6 +583,8 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen(); createScreen();
changeRuleset(0); changeRuleset(0);
AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus());
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
@ -540,6 +592,26 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
} }
[Test]
public void TestDeselectAllViaKey_WithSearchApplied()
{
createScreen();
changeRuleset(0);
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
AddStep("focus on search", () => modSelectOverlay.SearchTextBox.TakeFocus());
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
AddAssert("DT + HD selected and hidden", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 2);
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("search term changed", () => modSelectOverlay.SearchTerm == "Eas");
AddStep("kill focus", () => modSelectOverlay.SearchTextBox.KillFocus());
AddStep("press backspace", () => InputManager.Key(Key.BackSpace));
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
}
[Test] [Test]
public void TestDeselectAllViaButton() public void TestDeselectAllViaButton()
{ {
@ -561,6 +633,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value); AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
} }
[Test]
public void TestDeselectAllViaButton_WithSearchApplied()
{
createScreen();
changeRuleset(0);
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("select DT + HD + RD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModRandom() });
AddAssert("DT + HD + RD selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 3);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy");
AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => !panel.Visible && panel.Active.Value) == 3);
AddAssert("deselect all button enabled", () => this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any());
AddAssert("deselect all button disabled", () => !this.ChildrenOfType<DeselectAllModsButton>().Single().Enabled.Value);
}
[Test] [Test]
public void TestCloseViaBackButton() public void TestCloseViaBackButton()
{ {
@ -580,8 +677,11 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
} }
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.IsValidMod"/>.
/// </summary>
[Test] [Test]
public void TestColumnHiding() public void TestColumnHidingOnIsValidChange()
{ {
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{ {
@ -610,6 +710,56 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3); AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
} }
/// <summary>
/// Covers columns hiding/unhiding on changes of <see cref="ModSelectOverlay.SearchTerm"/>.
/// </summary>
[Test]
public void TestColumnHidingOnTextFilterChange()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "HD");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches");
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("clear search bar", () => modSelectOverlay.SearchTerm = "");
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
}
[Test]
public void TestHidingOverlayClearsTextSearch()
{
AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods }
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "fail");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("hide", () => modSelectOverlay.Hide());
AddStep("show", () => modSelectOverlay.Show());
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
}
[Test] [Test]
public void TestColumnHidingOnRulesetChange() public void TestColumnHidingOnRulesetChange()
{ {
@ -688,12 +838,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public override string ShortName => "unimplemented"; public override string ShortName => "unimplemented";
public override IEnumerable<Mod> GetModsFor(ModType type) public override IEnumerable<Mod> GetModsFor(ModType type) =>
{ type == ModType.Conversion
if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); ? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() })
: base.GetModsFor(type);
return base.GetModsFor(type);
}
} }
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -37,6 +38,14 @@ namespace osu.Game.Graphics.UserInterface
set => textBox.HoldFocus = value; set => textBox.HoldFocus = value;
} }
public LocalisableString PlaceholderText
{
get => textBox.PlaceholderText;
set => textBox.PlaceholderText = value;
}
public new bool HasFocus => textBox.HasFocus;
public void TakeFocus() => textBox.TakeFocus(); public void TakeFocus() => textBox.TakeFocus();
public void KillFocus() => textBox.KillFocus(); public void KillFocus() => textBox.KillFocus();

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -39,6 +39,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods"); public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods");
/// <summary>
/// "tab to search..."
/// </summary>
public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -6,16 +6,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler<GlobalAction> public partial class DeselectAllModsButton : ShearedButton
{ {
private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>(); private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>();
@ -39,18 +36,5 @@ namespace osu.Game.Overlays.Mods
{ {
Enabled.Value = selectedMods.Value.Any(); Enabled.Value = selectedMods.Value.Any();
} }
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat || e.Action != GlobalAction.DeselectAllMods)
return false;
TriggerClick();
return true;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input
if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch))
return false; return false;
var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && !modState.Filtered.Value).ToArray(); var matchingMods = availableMods.Where(modState => matches(modState, typesToMatch) && modState.Visible).ToArray();
if (matchingMods.Length == 0) if (matchingMods.Length == 0)
return false; return false;

View File

@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input
if (index < 0) if (index < 0)
return false; return false;
var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index); var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index);
if (modState == null) if (modState == null)
return false; return false;

View File

@ -46,7 +46,8 @@ namespace osu.Game.Overlays.Mods
foreach (var mod in availableMods) foreach (var mod in availableMods)
{ {
mod.Active.BindValueChanged(_ => updateState()); mod.Active.BindValueChanged(_ => updateState());
mod.Filtered.BindValueChanged(_ => updateState()); mod.MatchingTextFilter.BindValueChanged(_ => updateState());
mod.ValidForSelection.BindValueChanged(_ => updateState());
} }
updateState(); updateState();
@ -145,12 +146,17 @@ namespace osu.Game.Overlays.Mods
private void updateState() private void updateState()
{ {
Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1; Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1;
if (toggleAllCheckbox != null && !SelectionAnimationRunning) if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{ {
toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0; bool anyPanelsVisible = availableMods.Any(panel => panel.Visible);
toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value);
toggleAllCheckbox.Alpha = anyPanelsVisible ? 1 : 0;
// checking `anyPanelsVisible` is important since `.All()` returns `true` for empty enumerables.
if (anyPanelsVisible)
toggleAllCheckbox.Current.Value = availableMods.Where(panel => panel.Visible).All(panel => panel.Active.Value);
} }
} }
@ -195,7 +201,7 @@ namespace osu.Game.Overlays.Mods
{ {
pendingSelectionOperations.Clear(); pendingSelectionOperations.Clear();
foreach (var button in availableMods.Where(b => !b.Active.Value && !b.Filtered.Value)) foreach (var button in availableMods.Where(b => !b.Active.Value && b.Visible))
pendingSelectionOperations.Enqueue(() => button.Active.Value = true); pendingSelectionOperations.Enqueue(() => button.Active.Value = true);
} }
@ -206,8 +212,13 @@ namespace osu.Game.Overlays.Mods
{ {
pendingSelectionOperations.Clear(); pendingSelectionOperations.Clear();
foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value)) foreach (var button in availableMods.Where(b => b.Active.Value))
pendingSelectionOperations.Enqueue(() => button.Active.Value = false); {
if (!button.Visible)
button.Active.Value = false;
else
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
}
} }
/// <summary> /// <summary>

View File

@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -11,11 +14,10 @@ using osuTK;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class ModPanel : ModSelectPanel public partial class ModPanel : ModSelectPanel, IFilterable
{ {
public Mod Mod => modState.Mod; public Mod Mod => modState.Mod;
public override BindableBool Active => modState.Active; public override BindableBool Active => modState.Active;
public BindableBool Filtered => modState.Filtered;
protected override float IdleSwitchWidth => 54; protected override float IdleSwitchWidth => 54;
protected override float ExpandedSwitchWidth => 70; protected override float ExpandedSwitchWidth => 70;
@ -54,7 +56,8 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
Filtered.BindValueChanged(_ => updateFilterState(), true); modState.ValidForSelection.BindValueChanged(_ => updateFilterState());
modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true);
} }
protected override void Select() protected override void Select()
@ -71,9 +74,25 @@ namespace osu.Game.Overlays.Mods
#region Filtering support #region Filtering support
/// <seealso cref="ModState.Visible"/>
public bool Visible => modState.Visible;
public override IEnumerable<LocalisableString> FilterTerms => new[]
{
Mod.Name,
Mod.Acronym,
Mod.Description
};
public override bool MatchingFilter
{
get => modState.MatchingTextFilter.Value;
set => modState.MatchingTextFilter.Value = value;
}
private void updateFilterState() private void updateFilterState()
{ {
this.FadeTo(Filtered.Value ? 0 : 1); this.FadeTo(Visible ? 1 : 0);
} }
#endregion #endregion

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -81,6 +82,27 @@ namespace osu.Game.Overlays.Mods
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value); Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value);
} }
#region Filtering support
public override IEnumerable<LocalisableString> FilterTerms => getFilterTerms();
private IEnumerable<LocalisableString> getFilterTerms()
{
var preset = Preset.Value;
yield return preset.Name;
yield return preset.Description;
foreach (Mod mod in preset.Mods)
{
yield return mod.Name;
yield return mod.Acronym;
yield return mod.Description;
}
}
#endregion
#region IHasCustomTooltip #region IHasCustomTooltip
public ModPreset TooltipContent => Preset.Value; public ModPreset TooltipContent => Preset.Value;

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Containers;
namespace osu.Game.Overlays.Mods
{
public partial class ModSearchContainer : SearchContainer
{
public new string SearchTerm
{
get => base.SearchTerm;
set
{
if (value == SearchTerm)
return;
base.SearchTerm = value;
// Manual filtering here is required because ModColumn can be hidden when search term applied,
// causing the whole SearchContainer to become non-present and never actually perform a subsequent
// filter.
Filter();
}
}
}
}

View File

@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods
/// </summary> /// </summary>
public readonly Bindable<bool> Active = new BindableBool(true); public readonly Bindable<bool> Active = new BindableBool(true);
public string SearchTerm
{
set => ItemsFlow.SearchTerm = value;
}
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
protected readonly Container ControlContainer; protected readonly Container ControlContainer;
protected readonly FillFlowContainer ItemsFlow; protected readonly ModSearchContainer ItemsFlow;
private readonly TextFlowContainer headerText; private readonly TextFlowContainer headerText;
private readonly Box headerBackground; private readonly Box headerBackground;
@ -150,7 +155,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ClampExtension = 100, ClampExtension = 100,
ScrollbarOverlapsContent = false, ScrollbarOverlapsContent = false,
Child = ItemsFlow = new FillFlowContainer Child = ItemsFlow = new ModSearchContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -12,6 +12,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
@ -25,10 +27,11 @@ using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler<PlatformAction>
{ {
public const int BUTTON_WIDTH = 200; public const int BUTTON_WIDTH = 200;
@ -64,6 +67,14 @@ namespace osu.Game.Overlays.Mods
} }
} }
public string SearchTerm
{
get => SearchTextBox.Current.Value;
set => SearchTextBox.Current.Value = value;
}
public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
/// <summary> /// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// Whether the total score multiplier calculated from the current selected set of mods should be shown.
/// </summary> /// </summary>
@ -94,7 +105,7 @@ namespace osu.Game.Overlays.Mods
}; };
} }
yield return new DeselectAllModsButton(this); yield return deselectAllModsButton = new DeselectAllModsButton(this);
} }
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(); private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> globalAvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
@ -107,11 +118,14 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!; private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!; private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!; private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private DeselectAllModsButton deselectAllModsButton = null!;
private Container aboveColumnsContent = null!;
private DifficultyMultiplierDisplay? multiplierDisplay; private DifficultyMultiplierDisplay? multiplierDisplay;
protected ShearedButton BackButton { get; private set; } = null!; protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; } protected ShearedToggleButton? CustomisationButton { get; private set; }
protected SelectAllModsButton? SelectAllModsButton { get; set; }
private Sample? columnAppearSample; private Sample? columnAppearSample;
@ -146,6 +160,17 @@ namespace osu.Game.Overlays.Mods
MainAreaContent.AddRange(new Drawable[] MainAreaContent.AddRange(new Drawable[]
{ {
aboveColumnsContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = ModsEffectDisplay.HEIGHT,
Padding = new MarginPadding { Horizontal = 100 },
Child = SearchTextBox = new ShearedSearchTextBox
{
HoldFocus = false,
Width = 300
}
},
new OsuContextMenuContainer new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -153,7 +178,7 @@ namespace osu.Game.Overlays.Mods
{ {
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING, Top = ModsEffectDisplay.HEIGHT + PADDING,
Bottom = PADDING Bottom = PADDING
}, },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -186,18 +211,10 @@ namespace osu.Game.Overlays.Mods
if (ShowTotalMultiplier) if (ShowTotalMultiplier)
{ {
MainAreaContent.Add(new Container aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight
AutoSizeAxes = Axes.X,
Height = ModsEffectDisplay.HEIGHT,
Margin = new MarginPadding { Horizontal = 100 },
Child = multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
}); });
} }
@ -226,6 +243,14 @@ namespace osu.Game.Overlays.Mods
globalAvailableMods.BindTo(game.AvailableMods); globalAvailableMods.BindTo(game.AvailableMods);
} }
public override void Hide()
{
base.Hide();
// clear search for next user interaction with mod overlay
SearchTextBox.Current.Value = string.Empty;
}
private ModSettingChangeTracker? modSettingChangeTracker; private ModSettingChangeTracker? modSettingChangeTracker;
protected override void LoadComplete() protected override void LoadComplete()
@ -263,6 +288,12 @@ namespace osu.Game.Overlays.Mods
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
SearchTextBox.Current.BindValueChanged(query =>
{
foreach (var column in columnFlow.Columns)
column.SearchTerm = query.NewValue;
}, true);
// Start scrolled slightly to the right to give the user a sense that // Start scrolled slightly to the right to give the user a sense that
// there is more horizontal content available. // there is more horizontal content available.
ScheduleAfterChildren(() => ScheduleAfterChildren(() =>
@ -272,6 +303,13 @@ namespace osu.Game.Overlays.Mods
}); });
} }
protected override void Update()
{
base.Update();
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch;
}
/// <summary> /// <summary>
/// Select all visible mods in all columns. /// Select all visible mods in all columns.
/// </summary> /// </summary>
@ -344,7 +382,7 @@ namespace osu.Game.Overlays.Mods
private void filterMods() private void filterMods()
{ {
foreach (var modState in allAvailableMods) foreach (var modState in allAvailableMods)
modState.Filtered.Value = !modState.Mod.HasImplementation || !IsValidMod.Invoke(modState.Mod); modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
} }
private void updateMultiplier() private void updateMultiplier()
@ -469,7 +507,7 @@ namespace osu.Game.Overlays.Mods
base.PopIn(); base.PopIn();
multiplierDisplay? aboveColumnsContent
.FadeIn(fade_in_duration, Easing.OutQuint) .FadeIn(fade_in_duration, Easing.OutQuint)
.MoveToY(0, fade_in_duration, Easing.OutQuint); .MoveToY(0, fade_in_duration, Easing.OutQuint);
@ -479,7 +517,7 @@ namespace osu.Game.Overlays.Mods
{ {
var column = columnFlow[i].Column; var column = columnFlow[i].Column;
bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => modState.Filtered.Value); bool allFiltered = column is ModColumn modColumn && modColumn.AvailableMods.All(modState => !modState.Visible);
double delay = allFiltered ? 0 : nonFilteredColumnCount * 30; double delay = allFiltered ? 0 : nonFilteredColumnCount * 30;
double duration = allFiltered ? 0 : fade_in_duration; double duration = allFiltered ? 0 : fade_in_duration;
@ -527,7 +565,7 @@ namespace osu.Game.Overlays.Mods
base.PopOut(); base.PopOut();
multiplierDisplay? aboveColumnsContent
.FadeOut(fade_out_duration / 2, Easing.OutQuint) .FadeOut(fade_out_duration / 2, Easing.OutQuint)
.MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint); .MoveToY(-distance, fade_out_duration / 2, Easing.OutQuint);
@ -541,7 +579,7 @@ namespace osu.Game.Overlays.Mods
if (column is ModColumn modColumn) if (column is ModColumn modColumn)
{ {
allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value); allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible);
modColumn.FlushPendingSelections(); modColumn.FlushPendingSelections();
} }
@ -578,10 +616,38 @@ namespace osu.Game.Overlays.Mods
// This is handled locally here because this overlay is being registered at the game level // This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack. // and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection: case GlobalAction.ToggleModSelection:
// Pressing toggle should completely hide the overlay in one shot.
hideOverlay(true);
return true;
// This is handled locally here due to conflicts in input handling between the search text box and the deselect all mods button.
// Attempting to handle this action locally in both places leads to a possible scenario
// wherein activating the binding will both change the contents of the search text box and deselect all mods.
case GlobalAction.DeselectAllMods:
{
if (!SearchTextBox.HasFocus)
{
deselectAllModsButton.TriggerClick();
return true;
}
break;
}
case GlobalAction.Select: case GlobalAction.Select:
{ {
// Pressing toggle or select should completely hide the overlay in one shot. // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty.
hideOverlay(true); if (string.IsNullOrEmpty(SearchTerm))
{
hideOverlay(true);
return true;
}
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);
if (firstMod is not null)
firstMod.Active.Value = !firstMod.Active.Value;
return true; return true;
} }
} }
@ -603,6 +669,39 @@ namespace osu.Game.Overlays.Mods
} }
} }
/// <inheritdoc cref="IKeyBindingHandler{PlatformAction}"/>
/// <remarks>
/// This is handled locally here due to conflicts in input handling between the search text box and the select all mods button.
/// Attempting to handle this action locally in both places leads to a possible scenario
/// wherein activating the "select all" platform binding will both select all text in the search box and select all mods.
/// </remarks>>
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
return false;
SelectAllModsButton.TriggerClick();
return true;
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat || e.Key != Key.Tab)
return false;
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
if (SearchTextBox.HasFocus)
SearchTextBox.KillFocus();
else
SearchTextBox.TakeFocus();
return true;
}
#endregion #endregion
#region Sample playback control #region Sample playback control
@ -743,6 +842,9 @@ namespace osu.Game.Overlays.Mods
if (!Active.Value) if (!Active.Value)
RequestScroll?.Invoke(this); RequestScroll?.Invoke(this);
// Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action.
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null));
return true; return true;
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -25,7 +26,7 @@ using osuTK.Input;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour public abstract partial class ModSelectPanel : OsuClickableContainer, IHasAccentColour, IFilterable
{ {
public abstract BindableBool Active { get; } public abstract BindableBool Active { get; }
@ -199,6 +200,9 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value) if (samplePlaybackDisabled.Value)
return; return;
if (!IsPresent)
return;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY; bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
if (enoughTimePassedSinceLastPlayback) if (enoughTimePassedSinceLastPlayback)
@ -277,5 +281,28 @@ namespace osu.Game.Overlays.Mods
TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint);
TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint);
} }
#region IFilterable
public abstract IEnumerable<LocalisableString> FilterTerms { get; }
private bool matchingFilter = true;
public virtual bool MatchingFilter
{
get => matchingFilter;
set
{
if (matchingFilter == value)
return;
matchingFilter = value;
this.FadeTo(value ? 1 : 0);
}
}
public bool FilteringActive { set { } }
#endregion
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -32,9 +30,21 @@ namespace osu.Game.Overlays.Mods
public bool PendingConfiguration { get; set; } public bool PendingConfiguration { get; set; }
/// <summary> /// <summary>
/// Whether the mod is currently filtered out due to not matching imposed criteria. /// Whether the mod is currently valid for selection.
/// This can be <see langword="false"/> in scenarios such as the free mod select overlay, where not all mods are selectable
/// regardless of search criteria imposed by the user selecting.
/// </summary> /// </summary>
public BindableBool Filtered { get; } = new BindableBool(); public BindableBool ValidForSelection { get; } = new BindableBool(true);
/// <summary>
/// Whether the mod is matching the current textual filter.
/// </summary>
public BindableBool MatchingTextFilter { get; } = new BindableBool(true);
/// <summary>
/// Whether the <see cref="Mod"/> matches all applicable filters and visible for the user to select.
/// </summary>
public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value;
public ModState(Mod mod) public ModState(Mod mod)
{ {

View File

@ -1,14 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -16,10 +11,11 @@ using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler<PlatformAction> public partial class SelectAllModsButton : ShearedButton
{ {
private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>(); private readonly Bindable<IReadOnlyList<Mod>> selectedMods = new Bindable<IReadOnlyList<Mod>>();
private readonly Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>(); private readonly Bindable<Dictionary<ModType, IReadOnlyList<ModState>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<ModState>>>();
private readonly Bindable<string> searchTerm = new Bindable<string>();
public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay) public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay)
: base(ModSelectOverlay.BUTTON_WIDTH) : base(ModSelectOverlay.BUTTON_WIDTH)
@ -29,6 +25,7 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindTo(modSelectOverlay.SelectedMods); selectedMods.BindTo(modSelectOverlay.SelectedMods);
availableMods.BindTo(modSelectOverlay.AvailableMods); availableMods.BindTo(modSelectOverlay.AvailableMods);
searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -37,6 +34,7 @@ namespace osu.Game.Overlays.Mods
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState));
updateEnabledState(); updateEnabledState();
} }
@ -44,20 +42,7 @@ namespace osu.Game.Overlays.Mods
{ {
Enabled.Value = availableMods.Value Enabled.Value = availableMods.Value
.SelectMany(pair => pair.Value) .SelectMany(pair => pair.Value)
.Any(modState => !modState.Active.Value && !modState.Filtered.Value); .Any(modState => !modState.Active.Value && modState.Visible);
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
if (e.Repeat || e.Action != PlatformAction.SelectAll)
return false;
TriggerClick();
return true;
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Game.Overlays; using osu.Game.Overlays;
using System.Collections.Generic; using System.Collections.Generic;
@ -34,11 +32,12 @@ namespace osu.Game.Screens.OnlinePlay
protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true); protected override ModColumn CreateModColumn(ModType modType) => new ModColumn(modType, true);
protected override IEnumerable<ShearedButton> CreateFooterButtons() => base.CreateFooterButtons().Prepend( protected override IEnumerable<ShearedButton> CreateFooterButtons()
new SelectAllModsButton(this) => base.CreateFooterButtons()
{ .Prepend(SelectAllModsButton = new SelectAllModsButton(this)
Anchor = Anchor.BottomLeft, {
Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
}); Origin = Anchor.BottomLeft,
});
} }
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>