diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 45f671618e..66ba908879 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -8,6 +8,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Testing; @@ -57,11 +58,36 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0); } + [Test] + public void TestSelectAllButtonUpdatesStateWhenSearchTermChanged() + { + createFreeModSelect(); + + AddStep("apply search term", () => freeModSelectOverlay.SearchTerm = "ea"); + + AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click select all button", navigateAndClick); + AddAssert("select all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("change search term", () => freeModSelectOverlay.SearchTerm = "e"); + + AddAssert("select all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + void navigateAndClick() where T : Drawable + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + } + } + [Test] public void TestSelectDeselectAllViaKeyboard() { createFreeModSelect(); + AddStep("kill search bar focus", () => freeModSelectOverlay.SearchTextBox.KillFocus()); + AddStep("press ctrl+a", () => InputManager.Keys(PlatformAction.SelectAll)); AddUntilStep("all mods selected", assertAllAvailableModsSelected); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 9b130071cc..947b7e5be6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -94,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. 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 {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. AddUntilStep("wait for only one freemod overlay", () => this.ChildrenOfType().Count() == 1); - assertHasFreeModButton(allowedMod, false); - assertHasFreeModButton(requiredMod, false); + assertFreeModNotShown(allowedMod); + 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() .Single() .ChildrenOfType() - .Where(panel => !panel.Filtered.Value) + .Where(panel => panel.Visible) .All(b => b.Mod.GetType() != type)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8816787ceb..a41eff067b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -203,7 +203,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("mod select contains only double time mod", () => this.ChildrenOfType().Single().UserModsSelectOverlay .ChildrenOfType() - .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); + .SingleOrDefault(panel => panel.Visible)?.Mod is OsuModDoubleTime); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs index a11000214c..18739c0275 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModColumn.cs @@ -106,26 +106,26 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2); clickToggle(); AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning); - AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value)); + AddAssert("only visible items selected", () => column.ChildrenOfType().Where(panel => panel.Active.Value).All(panel => panel.Visible)); AddStep("unset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible)); AddAssert("checkbox not selected", () => !column.ChildrenOfType().Single().Current.Value); AddStep("set filter", () => setFilter(mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase))); - AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => !panel.Filtered.Value) == 2); + AddUntilStep("two panels visible", () => column.ChildrenOfType().Count(panel => panel.Visible) == 2); AddAssert("checkbox selected", () => column.ChildrenOfType().Single().Current.Value); AddStep("filter out everything", () => setFilter(_ => false)); - AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => panel.Filtered.Value)); + AddUntilStep("no panels visible", () => column.ChildrenOfType().All(panel => !panel.Visible)); AddUntilStep("checkbox hidden", () => !column.ChildrenOfType().Single().IsPresent); AddStep("inset filter", () => setFilter(null)); - AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => !panel.Filtered.Value)); + AddUntilStep("all panels visible", () => column.ChildrenOfType().All(panel => panel.Visible)); AddUntilStep("checkbox visible", () => column.ChildrenOfType().Single().IsPresent); void clickToggle() => AddStep("click toggle", () => @@ -288,10 +288,53 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("no change", () => this.ChildrenOfType().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().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().All(panel => panel.Visible)); + } + } + private void setFilter(Func? filter) { foreach (var modState in this.ChildrenOfType().Single().AvailableMods) - modState.Filtered.Value = filter?.Invoke(modState.Mod) == false; + modState.ValidForSelection.Value = filter?.Invoke(modState.Mod) != false; } private partial class TestModColumn : ModColumn diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs index 3efdba8754..2d54a4e566 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModPresetColumn.cs @@ -392,6 +392,28 @@ namespace osu.Game.Tests.Visual.UserInterface new HashSet(this.ChildrenOfType().First().Preset.Value.Mods).SetEquals(mods)); } + [Test] + public void TestTextFiltering() + { + ModPresetColumn modPresetColumn = null!; + + AddStep("clear mods", () => SelectedMods.Value = Array.Empty()); + 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().Count(panel => panel.IsPresent), () => Is.EqualTo(1)); + + AddStep("set mania ruleset", () => Ruleset.Value = rulesets.GetRuleset(3)); + AddUntilStep("no panels visible", () => modPresetColumn.ChildrenOfType().Count(panel => panel.IsPresent), () => Is.EqualTo(0)); + } + private ICollection createTestPresets() => new[] { new ModPreset diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 5cf24c1960..d566a04261 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -490,15 +490,15 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); - AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + AddAssert("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible)); AddStep("make double time invalid", () => modSelectOverlay.IsValidMod = m => !(m is OsuModDoubleTime)); - AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + AddUntilStep("double time not visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => !panel.Visible)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => panel.Visible)); AddStep("make double time valid again", () => modSelectOverlay.IsValidMod = _ => true); - AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); - AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + AddUntilStep("double time visible", () => modSelectOverlay.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => panel.Visible)); + AddAssert("nightcore still visible", () => modSelectOverlay.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => panel.Visible)); } [Test] @@ -524,7 +524,57 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); 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); + AddAssert("focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("click on mod column", navigateAndClick); + AddAssert("lost focus", () => !modSelectOverlay.SearchTextBox.HasFocus); + + void navigateAndClick() where T : Drawable + { + InputManager.MoveMouseTo(modSelectOverlay.ChildrenOfType().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] @@ -533,6 +583,8 @@ namespace osu.Game.Tests.Visual.UserInterface createScreen(); changeRuleset(0); + AddStep("kill search bar focus", () => modSelectOverlay.SearchTextBox.KillFocus()); + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); AddAssert("DT + HD selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value) == 2); @@ -540,6 +592,26 @@ namespace osu.Game.Tests.Visual.UserInterface 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().Count(panel => !panel.Visible && panel.Active.Value) == 2); + + AddStep("press backspace", () => InputManager.Key(Key.BackSpace)); + AddAssert("DT + HD still selected", () => modSelectOverlay.ChildrenOfType().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] public void TestDeselectAllViaButton() { @@ -561,6 +633,31 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); } + [Test] + public void TestDeselectAllViaButton_WithSearchApplied() + { + createScreen(); + changeRuleset(0); + + AddAssert("deselect all button disabled", () => !this.ChildrenOfType().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().Count(panel => panel.Active.Value) == 3); + AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("apply search", () => modSelectOverlay.SearchTerm = "Easy"); + AddAssert("DT + HD + RD are hidden and selected", () => modSelectOverlay.ChildrenOfType().Count(panel => !panel.Visible && panel.Active.Value) == 3); + AddAssert("deselect all button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + AddAssert("deselect all button disabled", () => !this.ChildrenOfType().Single().Enabled.Value); + } + [Test] public void TestCloseViaBackButton() { @@ -580,8 +677,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden); } + /// + /// Covers columns hiding/unhiding on changes of . + /// [Test] - public void TestColumnHiding() + public void TestColumnHidingOnIsValidChange() { AddStep("create screen", () => Child = modSelectOverlay = new TestModSelectOverlay { @@ -610,6 +710,56 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3); } + /// + /// Covers columns hiding/unhiding on changes of . + /// + [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().All(col => col.IsPresent)); + + AddStep("set search", () => modSelectOverlay.SearchTerm = "HD"); + AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 1); + + AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches"); + AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent)); + + AddStep("clear search bar", () => modSelectOverlay.SearchTerm = ""); + AddAssert("all columns visible", () => this.ChildrenOfType().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().All(col => col.IsPresent)); + + AddStep("set search", () => modSelectOverlay.SearchTerm = "fail"); + AddAssert("one column visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2); + + AddStep("hide", () => modSelectOverlay.Hide()); + AddStep("show", () => modSelectOverlay.Show()); + + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + } + [Test] public void TestColumnHidingOnRulesetChange() { @@ -688,12 +838,10 @@ namespace osu.Game.Tests.Visual.UserInterface { public override string ShortName => "unimplemented"; - public override IEnumerable GetModsFor(ModType type) - { - if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); - - return base.GetModsFor(type); - } + public override IEnumerable GetModsFor(ModType type) => + type == ModType.Conversion + ? base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }) + : base.GetModsFor(type); } } } diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index 7bd083f9d5..fb0a66cb8d 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.Mods; @@ -37,6 +38,14 @@ namespace osu.Game.Graphics.UserInterface 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 KillFocus() => textBox.KillFocus(); diff --git a/osu.Game/Localisation/ModSelectOverlayStrings.cs b/osu.Game/Localisation/ModSelectOverlayStrings.cs index f11c52ee20..05dcf138d7 100644 --- a/osu.Game/Localisation/ModSelectOverlayStrings.cs +++ b/osu.Game/Localisation/ModSelectOverlayStrings.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; @@ -39,6 +39,11 @@ namespace osu.Game.Localisation /// public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods"); + /// + /// "tab to search..." + /// + public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search..."); + private static string getKey(string key) => $@"{prefix}:{key}"; } -} +} \ No newline at end of file diff --git a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs index 3e5a3b12d1..817b6beac3 100644 --- a/osu.Game/Overlays/Mods/DeselectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/DeselectAllModsButton.cs @@ -6,16 +6,13 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public partial class DeselectAllModsButton : ShearedButton, IKeyBindingHandler + public partial class DeselectAllModsButton : ShearedButton { private readonly Bindable> selectedMods = new Bindable>(); @@ -39,18 +36,5 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = selectedMods.Value.Any(); } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != GlobalAction.DeselectAllMods) - return false; - - TriggerClick(); - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { - } } } diff --git a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs index 4f3c18fc43..59a631a7b5 100644 --- a/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/ClassicModHotkeyHandler.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Mods.Input if (!mod_type_lookup.TryGetValue(e.Key, out var typesToMatch)) 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) return false; diff --git a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs index dedb556304..e638063438 100644 --- a/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs +++ b/osu.Game/Overlays/Mods/Input/SequentialModHotkeyHandler.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods.Input if (index < 0) return false; - var modState = availableMods.Where(modState => !modState.Filtered.Value).ElementAtOrDefault(index); + var modState = availableMods.Where(modState => modState.Visible).ElementAtOrDefault(index); if (modState == null) return false; diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index fe42cc0abf..d65c94d14d 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -46,7 +46,8 @@ namespace osu.Game.Overlays.Mods foreach (var mod in availableMods) { mod.Active.BindValueChanged(_ => updateState()); - mod.Filtered.BindValueChanged(_ => updateState()); + mod.MatchingTextFilter.BindValueChanged(_ => updateState()); + mod.ValidForSelection.BindValueChanged(_ => updateState()); } updateState(); @@ -145,12 +146,17 @@ namespace osu.Game.Overlays.Mods private void updateState() { - Alpha = availableMods.All(mod => mod.Filtered.Value) ? 0 : 1; + Alpha = availableMods.All(mod => !mod.Visible) ? 0 : 1; if (toggleAllCheckbox != null && !SelectionAnimationRunning) { - toggleAllCheckbox.Alpha = availableMods.Any(panel => !panel.Filtered.Value) ? 1 : 0; - toggleAllCheckbox.Current.Value = availableMods.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + bool anyPanelsVisible = availableMods.Any(panel => panel.Visible); + + 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(); - 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); } @@ -206,8 +212,13 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in availableMods.Where(b => b.Active.Value && !b.Filtered.Value)) - pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + foreach (var button in availableMods.Where(b => b.Active.Value)) + { + if (!button.Visible) + button.Active.Value = false; + else + pendingSelectionOperations.Enqueue(() => button.Active.Value = false); + } } /// diff --git a/osu.Game/Overlays/Mods/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index b5fee9d116..f294b1892d 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -11,11 +14,10 @@ using osuTK; namespace osu.Game.Overlays.Mods { - public partial class ModPanel : ModSelectPanel + public partial class ModPanel : ModSelectPanel, IFilterable { public Mod Mod => modState.Mod; public override BindableBool Active => modState.Active; - public BindableBool Filtered => modState.Filtered; protected override float IdleSwitchWidth => 54; protected override float ExpandedSwitchWidth => 70; @@ -54,7 +56,8 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - Filtered.BindValueChanged(_ => updateFilterState(), true); + modState.ValidForSelection.BindValueChanged(_ => updateFilterState()); + modState.MatchingTextFilter.BindValueChanged(_ => updateFilterState(), true); } protected override void Select() @@ -71,9 +74,25 @@ namespace osu.Game.Overlays.Mods #region Filtering support + /// + public bool Visible => modState.Visible; + + public override IEnumerable FilterTerms => new[] + { + Mod.Name, + Mod.Acronym, + Mod.Description + }; + + public override bool MatchingFilter + { + get => modState.MatchingTextFilter.Value; + set => modState.MatchingTextFilter.Value = value; + } + private void updateFilterState() { - this.FadeTo(Filtered.Value ? 0 : 1); + this.FadeTo(Visible ? 1 : 0); } #endregion diff --git a/osu.Game/Overlays/Mods/ModPresetPanel.cs b/osu.Game/Overlays/Mods/ModPresetPanel.cs index 8bcb5e4e4e..00f6e36972 100644 --- a/osu.Game/Overlays/Mods/ModPresetPanel.cs +++ b/osu.Game/Overlays/Mods/ModPresetPanel.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Graphics; @@ -81,6 +82,27 @@ namespace osu.Game.Overlays.Mods Active.Value = new HashSet(Preset.Value.Mods).SetEquals(selectedMods.Value); } + #region Filtering support + + public override IEnumerable FilterTerms => getFilterTerms(); + + private IEnumerable 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 public ModPreset TooltipContent => Preset.Value; diff --git a/osu.Game/Overlays/Mods/ModSearchContainer.cs b/osu.Game/Overlays/Mods/ModSearchContainer.cs new file mode 100644 index 0000000000..8787530d5c --- /dev/null +++ b/osu.Game/Overlays/Mods/ModSearchContainer.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . 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(); + } + } + } +} diff --git a/osu.Game/Overlays/Mods/ModSelectColumn.cs b/osu.Game/Overlays/Mods/ModSelectColumn.cs index e6d7bcd97d..338ebdaef4 100644 --- a/osu.Game/Overlays/Mods/ModSelectColumn.cs +++ b/osu.Game/Overlays/Mods/ModSelectColumn.cs @@ -43,10 +43,15 @@ namespace osu.Game.Overlays.Mods /// public readonly Bindable Active = new BindableBool(true); + public string SearchTerm + { + set => ItemsFlow.SearchTerm = value; + } + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected readonly Container ControlContainer; - protected readonly FillFlowContainer ItemsFlow; + protected readonly ModSearchContainer ItemsFlow; private readonly TextFlowContainer headerText; private readonly Box headerBackground; @@ -150,7 +155,7 @@ namespace osu.Game.Overlays.Mods RelativeSizeAxes = Axes.Both, ClampExtension = 100, ScrollbarOverlapsContent = false, - Child = ItemsFlow = new FillFlowContainer + Child = ItemsFlow = new ModSearchContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 38ae8c68cb..9035503723 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -12,6 +12,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; @@ -25,10 +27,11 @@ using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; +using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler + public abstract partial class ModSelectOverlay : ShearedOverlayContainer, ISamplePlaybackDisabler, IKeyBindingHandler { 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!; + /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// @@ -94,7 +105,7 @@ namespace osu.Game.Overlays.Mods }; } - yield return new DeselectAllModsButton(this); + yield return deselectAllModsButton = new DeselectAllModsButton(this); } private readonly Bindable>> globalAvailableMods = new Bindable>>(); @@ -107,11 +118,14 @@ namespace osu.Game.Overlays.Mods private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; private FillFlowContainer footerButtonFlow = null!; + private DeselectAllModsButton deselectAllModsButton = null!; + private Container aboveColumnsContent = null!; private DifficultyMultiplierDisplay? multiplierDisplay; protected ShearedButton BackButton { get; private set; } = null!; protected ShearedToggleButton? CustomisationButton { get; private set; } + protected SelectAllModsButton? SelectAllModsButton { get; set; } private Sample? columnAppearSample; @@ -146,6 +160,17 @@ namespace osu.Game.Overlays.Mods 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 { RelativeSizeAxes = Axes.Both, @@ -153,7 +178,7 @@ namespace osu.Game.Overlays.Mods { Padding = new MarginPadding { - Top = (ShowTotalMultiplier ? ModsEffectDisplay.HEIGHT : 0) + PADDING, + Top = ModsEffectDisplay.HEIGHT + PADDING, Bottom = PADDING }, RelativeSizeAxes = Axes.Both, @@ -186,18 +211,10 @@ namespace osu.Game.Overlays.Mods if (ShowTotalMultiplier) { - MainAreaContent.Add(new Container + aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay { Anchor = 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 - }, + Origin = Anchor.TopRight }); } @@ -226,6 +243,14 @@ namespace osu.Game.Overlays.Mods 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; protected override void LoadComplete() @@ -263,6 +288,12 @@ namespace osu.Game.Overlays.Mods 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 // there is more horizontal content available. 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; + } + /// /// Select all visible mods in all columns. /// @@ -344,7 +382,7 @@ namespace osu.Game.Overlays.Mods private void filterMods() { 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() @@ -469,7 +507,7 @@ namespace osu.Game.Overlays.Mods base.PopIn(); - multiplierDisplay? + aboveColumnsContent .FadeIn(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; - 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 duration = allFiltered ? 0 : fade_in_duration; @@ -527,7 +565,7 @@ namespace osu.Game.Overlays.Mods base.PopOut(); - multiplierDisplay? + aboveColumnsContent .FadeOut(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) { - allFiltered = modColumn.AvailableMods.All(modState => modState.Filtered.Value); + allFiltered = modColumn.AvailableMods.All(modState => !modState.Visible); 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 // and therefore takes away keyboard focus from the screen stack. 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: { - // Pressing toggle or select should completely hide the overlay in one shot. - hideOverlay(true); + // Pressing select should select first filtered mod or completely hide the overlay in one shot if search term is empty. + if (string.IsNullOrEmpty(SearchTerm)) + { + hideOverlay(true); + return true; + } + + ModState? firstMod = columnFlow.Columns.OfType().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible); + + if (firstMod is not null) + firstMod.Active.Value = !firstMod.Active.Value; + return true; } } @@ -603,6 +669,39 @@ namespace osu.Game.Overlays.Mods } } + /// + /// + /// 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. + /// > + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null) + return false; + + SelectAllModsButton.TriggerClick(); + return true; + } + + public void OnReleased(KeyBindingReleaseEvent 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 #region Sample playback control @@ -743,6 +842,9 @@ namespace osu.Game.Overlays.Mods if (!Active.Value) 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; } diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs index 1f42292590..29f4c93e88 100644 --- a/osu.Game/Overlays/Mods/ModSelectPanel.cs +++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -25,7 +26,7 @@ using osuTK.Input; 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; } @@ -199,6 +200,9 @@ namespace osu.Game.Overlays.Mods if (samplePlaybackDisabled.Value) return; + if (!IsPresent) + return; + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY; if (enoughTimePassedSinceLastPlayback) @@ -277,5 +281,28 @@ namespace osu.Game.Overlays.Mods TextBackground.FadeColour(foregroundColour, transitionDuration, Easing.OutQuint); TextFlow.FadeColour(textColour, transitionDuration, Easing.OutQuint); } + + #region IFilterable + + public abstract IEnumerable 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 } } diff --git a/osu.Game/Overlays/Mods/ModState.cs b/osu.Game/Overlays/Mods/ModState.cs index 3ee890e876..7a5bc0f3ae 100644 --- a/osu.Game/Overlays/Mods/ModState.cs +++ b/osu.Game/Overlays/Mods/ModState.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using osu.Framework.Bindables; using osu.Game.Rulesets.Mods; @@ -32,9 +30,21 @@ namespace osu.Game.Overlays.Mods public bool PendingConfiguration { get; set; } /// - /// Whether the mod is currently filtered out due to not matching imposed criteria. + /// Whether the mod is currently valid for selection. + /// This can be in scenarios such as the free mod select overlay, where not all mods are selectable + /// regardless of search criteria imposed by the user selecting. /// - public BindableBool Filtered { get; } = new BindableBool(); + public BindableBool ValidForSelection { get; } = new BindableBool(true); + + /// + /// Whether the mod is matching the current textual filter. + /// + public BindableBool MatchingTextFilter { get; } = new BindableBool(true); + + /// + /// Whether the matches all applicable filters and visible for the user to select. + /// + public bool Visible => MatchingTextFilter.Value && ValidForSelection.Value; public ModState(Mod mod) { diff --git a/osu.Game/Overlays/Mods/SelectAllModsButton.cs b/osu.Game/Overlays/Mods/SelectAllModsButton.cs index f4b8025227..bb61cdc35d 100644 --- a/osu.Game/Overlays/Mods/SelectAllModsButton.cs +++ b/osu.Game/Overlays/Mods/SelectAllModsButton.cs @@ -1,14 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System.Collections.Generic; using System.Linq; 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.Localisation; using osu.Game.Rulesets.Mods; @@ -16,10 +11,11 @@ using osu.Game.Screens.OnlinePlay; namespace osu.Game.Overlays.Mods { - public partial class SelectAllModsButton : ShearedButton, IKeyBindingHandler + public partial class SelectAllModsButton : ShearedButton { private readonly Bindable> selectedMods = new Bindable>(); private readonly Bindable>> availableMods = new Bindable>>(); + private readonly Bindable searchTerm = new Bindable(); public SelectAllModsButton(FreeModSelectOverlay modSelectOverlay) : base(ModSelectOverlay.BUTTON_WIDTH) @@ -29,6 +25,7 @@ namespace osu.Game.Overlays.Mods selectedMods.BindTo(modSelectOverlay.SelectedMods); availableMods.BindTo(modSelectOverlay.AvailableMods); + searchTerm.BindTo(modSelectOverlay.SearchTextBox.Current); } protected override void LoadComplete() @@ -37,6 +34,7 @@ namespace osu.Game.Overlays.Mods selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); + searchTerm.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledState)); updateEnabledState(); } @@ -44,20 +42,7 @@ namespace osu.Game.Overlays.Mods { Enabled.Value = availableMods.Value .SelectMany(pair => pair.Value) - .Any(modState => !modState.Active.Value && !modState.Filtered.Value); - } - - public bool OnPressed(KeyBindingPressEvent e) - { - if (e.Repeat || e.Action != PlatformAction.SelectAll) - return false; - - TriggerClick(); - return true; - } - - public void OnReleased(KeyBindingReleaseEvent e) - { + .Any(modState => !modState.Active.Value && modState.Visible); } } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 6313d907a5..4d5d724089 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using osu.Game.Overlays; 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 IEnumerable CreateFooterButtons() => base.CreateFooterButtons().Prepend( - new SelectAllModsButton(this) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - }); + protected override IEnumerable CreateFooterButtons() + => base.CreateFooterButtons() + .Prepend(SelectAllModsButton = new SelectAllModsButton(this) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4b9f37270b..d94c4a2df9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -1,4 +1,4 @@ - + net6.0 Library