diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs index 4f91e355c004..70319e02e7d9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneUserTagControl.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; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -30,6 +31,26 @@ public partial class TestSceneUserTagControl : OsuManualInputManagerTestScene private int writeRequestCount; + private Container content = null!; + + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.Child = new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Child = content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 700, + AutoSizeAxes = Axes.Y, + }, + }; + } + [SetUpSteps] public void SetUpSteps() { @@ -61,6 +82,7 @@ public void SetUpSteps() }, new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", }, new APITag { Id = 5, Name = "style/mono-heavy", Description = "Features monos used in large amounts.", RulesetId = 1, }, + new APITag { Id = 6, Name = "style/freeform", Description = "An unrestrained and loose approach towards visual structure." }, ] }), 500); return true; @@ -138,6 +160,40 @@ public void TestNotWritable() AddAssert("no vote requests send", () => writeRequestCount, () => Is.Zero); } + [Test] + public void TestFilter() + { + AddStep("show", () => + { + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.BeatmapInfo.OnlineID = 42; + Beatmap.Value = working; + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) + { + Filter = t => (t.GroupName ?? "").StartsWith("style", StringComparison.Ordinal), + Writable = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + }; + }); + AddUntilStep("two tags are shown", () => getTagFlow().Count, () => Is.EqualTo(2)); + AddAssert("add new button is not present", () => getTagFlow().ChildrenOfType().Where(t => t.UserTag.DisplayName == "add"), () => Is.Empty); + AddStep("vote for tag 6", () => + { + InputManager.MoveMouseTo(getDrawableTagById(6)); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("tag 6 voted for", () => getDrawableTagById(6).UserTag.VoteCount.Value, () => Is.EqualTo(1)); + AddStep("remove vote for tag 6", () => + { + InputManager.MoveMouseTo(getDrawableTagById(6)); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("tag 6 not voted for", () => getDrawableTagById(6).UserTag.VoteCount.Value, () => Is.EqualTo(0)); + AddAssert("two tags are shown", () => getTagFlow().Count, () => Is.EqualTo(2)); + } + [Test] public void TestTagsDoNotMoveUntilMouseMovesAway() { @@ -173,24 +229,19 @@ public void TestTagsDoNotMoveUntilMouseMovesAway() AddUntilStep("tag 2 voted for", () => getDrawableTagById(2).UserTag.VoteCount.Value, () => Is.EqualTo(4)); AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("tag 2 reordered to first", () => getTagFlow().GetLayoutPosition(getDrawableTagById(2)), () => Is.EqualTo(0)); - - FillFlowContainer getTagFlow() => this.ChildrenOfType>().Single(); - - UserTagControl.DrawableUserTag getDrawableTagById(long id) => getTagFlow().Single(t => t.UserTag.Id == id); } + private FillFlowContainer getTagFlow() => this.ChildrenOfType>().Single(); + private UserTagControl.DrawableUserTag getDrawableTagById(long id) => getTagFlow().Single(t => t.UserTag.Id == id); + private void recreateControl(bool writable = true) { - Child = new PopoverContainer + Child = new UserTagControl(Beatmap.Value.BeatmapInfo) { - RelativeSizeAxes = Axes.Both, - Child = new UserTagControl(Beatmap.Value.BeatmapInfo) - { - Writable = writable, - Width = 700, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Writable = writable, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, }; } } diff --git a/osu.Game/Screens/Ranking/UserTagControl.cs b/osu.Game/Screens/Ranking/UserTagControl.cs index f293f228af68..83226e0e1d4f 100644 --- a/osu.Game/Screens/Ranking/UserTagControl.cs +++ b/osu.Game/Screens/Ranking/UserTagControl.cs @@ -51,6 +51,15 @@ public partial class UserTagControl : CompositeDrawable /// public bool Writable { private get; init; } + /// + /// Allows to override the list of displayed tags using a provided filter function. + /// + /// + /// Tags matching the filter function will always be displayed, irrespective of the + /// amount of votes. Additionally, the 'add new tag' button will not be displayed. + /// + public Func? Filter { private get; init; } + private InputManager inputManager = null!; [Resolved] @@ -93,7 +102,7 @@ private void load(SessionStatics sessionStatics) } }; - if (Writable) + if (Writable && Filter == null) { tagFlow.Add(addNewTagUserTag = new AddNewTagUserTag { @@ -135,9 +144,15 @@ private void updateTags() return; relevantTagsById.Clear(); - relevantTagsById.AddRange(apiTags.Value - .Where(t => t.RulesetId == null || t.RulesetId == beatmapInfo.Ruleset.OnlineID) - .Select(t => new KeyValuePair(t.Id, new UserTag(t)))); + + var relevantTags = apiTags.Value + .Where(t => t.RulesetId == null || t.RulesetId == beatmapInfo.Ruleset.OnlineID) + .Select(t => new KeyValuePair(t.Id, new UserTag(t))); + + if (Filter != null) + relevantTags = relevantTags.Where(kvp => Filter(kvp.Value)); + + relevantTagsById.AddRange(relevantTags); foreach (var topTag in apiBeatmap.Value.TopTags ?? []) { @@ -145,7 +160,6 @@ private void updateTags() { tag.VoteCount.Value = topTag.VoteCount; tag.Updating.Value = false; - displayedTags.Add(tag); } } @@ -157,6 +171,19 @@ private void updateTags() tag.Updating.Value = false; } } + + if (Filter == null) + { + foreach (var topTag in apiBeatmap.Value.TopTags ?? []) + { + if (relevantTagsById.TryGetValue(topTag.TagId, out var tag)) + displayedTags.Add(tag); + } + } + else + { + displayedTags.AddRange(relevantTagsById.Values); + } } private void displayTags(object? sender, NotifyCollectionChangedEventArgs e) @@ -194,7 +221,10 @@ private void displayTags(object? sender, NotifyCollectionChangedEventArgs e) case NotifyCollectionChangedAction.Reset: { tagFlow.Clear(); - if (Writable) tagFlow.Add(addNewTagUserTag!); + + if (Writable && Filter == null) + tagFlow.Add(addNewTagUserTag!); + break; } } @@ -230,7 +260,7 @@ private void toggleVote(UserTag tag) { tag.VoteCount.Value += 1; tag.Voted.Value = true; - if (!displayedTags.Contains(tag)) + if (!displayedTags.Contains(tag) && Filter == null) displayedTags.Add(tag); }; request = addReq; @@ -245,6 +275,9 @@ private void toggleVote(UserTag tag) private void voteCountChanged(ValueChangedEvent _) { + if (Filter != null) + return; + var tagsWithNoVotes = displayedTags.Where(t => t.VoteCount.Value == 0).ToArray(); foreach (var tag in tagsWithNoVotes)