From a4b14b48beda3356010ba6bd2ec7261171f358f5 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:53:02 +0100 Subject: [PATCH 1/5] Add saved beatmap filter functionality with popover UI --- osu.Game/Database/RealmAccess.cs | 7 +- .../UserInterface/ShearedSearchTextBox.cs | 22 +- osu.Game/Screens/SelectV2/FilterControl.cs | 133 +++++++++- .../Screens/SelectV2/SavedBeatmapFilter.cs | 29 +++ .../Screens/SelectV2/SavedFiltersPopover.cs | 239 ++++++++++++++++++ .../Screens/SelectV2/SearchFilterButton.cs | 80 ++++++ 6 files changed, 503 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Screens/SelectV2/SavedBeatmapFilter.cs create mode 100644 osu.Game/Screens/SelectV2/SavedFiltersPopover.cs create mode 100644 osu.Game/Screens/SelectV2/SearchFilterButton.cs diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index fa54ed538a49..cadc649f0ce3 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -101,8 +101,9 @@ public class RealmAccess : IDisposable /// 49 2025-06-10 Reset the LegacyOnlineID to -1 for all scores that have it set to 0 (which is semantically the same) for consistency of handling with OnlineID. /// 50 2025-07-11 Add UserTags to BeatmapMetadata. /// 51 2025-07-22 Add ScoreInfo.Pauses. + /// 52 2025-12-13 Added SavedBeatmapFilter. /// - private const int schema_version = 51; + private const int schema_version = 52; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1326,6 +1327,10 @@ void remapKeyBinding(int oldAction, int newAction) score.LegacyOnlineID = -1; break; + + case 52: + // SavedBeatmapFilter added. + break; } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs index b1b93dcbca09..3e89f3288076 100644 --- a/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs @@ -22,6 +22,8 @@ public partial class ShearedSearchTextBox : CompositeDrawable, IHasCurrentValue< private readonly Box background; protected readonly InnerSearchTextBox TextBox; + protected readonly Container BackgroundContent; + protected readonly Container RightInterface; public Bindable Current { @@ -62,6 +64,10 @@ public ShearedSearchTextBox() { RelativeSizeAxes = Axes.Both }, + BackgroundContent = new Container + { + RelativeSizeAxes = Axes.Both + }, new GridContainer { RelativeSizeAxes = Axes.Both, @@ -70,13 +76,19 @@ public ShearedSearchTextBox() new Drawable[] { TextBox = CreateInnerTextBox(), - new SpriteIcon + RightInterface = new Container { - Icon = FontAwesome.Solid.Search, - Origin = Anchor.Centre, Anchor = Anchor.Centre, - Size = new Vector2(16), - Shear = -Shear + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = new SpriteIcon + { + Icon = FontAwesome.Solid.Search, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(16), + Shear = -Shear + } } } }, diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index a90ac3a4e865..bf7ae319d369 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -7,14 +7,19 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -111,6 +116,9 @@ private void load(IAPIProvider api) { RelativeSizeAxes = Axes.X, HoldFocus = true, + ApplyFilter = applyFilter, + SaveFilter = saveFilter, + Ruleset = ruleset, }, }, new GridContainer @@ -249,7 +257,49 @@ protected override void Dispose(bool isDisposing) base.Dispose(isDisposing); collectionsSubscription?.Dispose(); } + private void applyFilter(SavedBeatmapFilter filter) + { + searchTextBox.Current.Value = filter.SearchQuery; + + sortDropdown.Current.Value = Enum.IsDefined(typeof(SortMode), filter.SortMode) + ? (SortMode)filter.SortMode + : SortMode.Title; + + groupDropdown.Current.Value = Enum.IsDefined(typeof(GroupMode), filter.GroupMode) + ? (GroupMode)filter.GroupMode + : GroupMode.None; + + showConvertedBeatmapsButton.Active.Value = filter.ShowConverted; + + var lowerBound = (BindableNumber)difficultyRangeSlider.LowerBound; + var upperBound = (BindableNumber)difficultyRangeSlider.UpperBound; + + double min = Math.Clamp(filter.MinStars, lowerBound.MinValue, lowerBound.MaxValue); + double max = Math.Clamp(filter.MaxStars, upperBound.MinValue, upperBound.MaxValue); + + if (min > max) + min = max; + + difficultyRangeSlider.LowerBound.Value = min; + difficultyRangeSlider.UpperBound.Value = max; + } + private void saveFilter(string name) + { + if (string.IsNullOrEmpty(ruleset.Value?.ShortName)) + return; + realm.Write(r => r.Add(new SavedBeatmapFilter + { + Name = name.Trim(), + SearchQuery = searchTextBox.Current.Value, + SortMode = (int)sortDropdown.Current.Value, + GroupMode = (int)groupDropdown.Current.Value, + ShowConverted = showConvertedBeatmapsButton.Active.Value, + MinStars = difficultyRangeSlider.LowerBound.Value, + MaxStars = difficultyRangeSlider.UpperBound.Value, + RulesetShortName = ruleset.Value.ShortName + })); + } /// /// Creates a based on the current state of the controls. /// @@ -308,9 +358,84 @@ protected override void PopOut() this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint) .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); } - internal partial class SongSelectSearchTextBox : ShearedFilterTextBox { + public Action? ApplyFilter { get; set; } + public Action? SaveFilter { get; set; } + public IBindable Ruleset { get; set; } = null!; + + private readonly Box hoverBox; + private readonly PopoverTarget popoverTarget; + private readonly BindableBool popoverVisible = new BindableBool(); + private readonly SearchFilterButton filterButton; + + public SongSelectSearchTextBox() + { + filterButton = new SearchFilterButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + RightInterface.Clear(); + RightInterface.Add(filterButton); + + // Create hover box + hoverBox = new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Y, + Width = 55, + Alpha = 0, + }; + + BackgroundContent.Add(hoverBox); + + AddInternal(popoverTarget = new PopoverTarget + { + Anchor = Anchor.BottomRight, + Origin = Anchor.TopRight, + RelativePositionAxes = Axes.Y, + Y = 1, + Size = Vector2.Zero, + CreatePopover = createPopover + }); + + filterButton.HoverTarget = hoverBox; + filterButton.IsPopoverVisible = () => popoverVisible.Value; + filterButton.SetPopoverVisible = visible => popoverVisible.Value = visible; + filterButton.SetIconShear(-Shear); + + popoverVisible.BindValueChanged(visible => + { + if (visible.NewValue) + popoverTarget.ShowPopover(); + else + popoverTarget.HidePopover(); + }); + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [BackgroundDependencyLoader] + private void load() + { + hoverBox.Colour = colours.Blue; + } + + private Popover createPopover() + { + var popover = new SavedFiltersPopover(f => ApplyFilter?.Invoke(f), n => SaveFilter?.Invoke(n), Ruleset.Value); + popover.State.BindValueChanged(state => + { + if (state.NewValue == Visibility.Hidden) + Schedule(() => popoverVisible.Value = false); + }); + return popover; + } + protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox(); private partial class InnerTextBox : InnerFilterTextBox @@ -330,6 +455,12 @@ public override bool OnPressed(KeyBindingPressEvent e) return base.OnPressed(e); } } + + private partial class PopoverTarget : Container, IHasPopover + { + public Func? CreatePopover { get; set; } + public Popover GetPopover() => CreatePopover?.Invoke()!; + } } } } diff --git a/osu.Game/Screens/SelectV2/SavedBeatmapFilter.cs b/osu.Game/Screens/SelectV2/SavedBeatmapFilter.cs new file mode 100644 index 000000000000..6a5d4fbb90ea --- /dev/null +++ b/osu.Game/Screens/SelectV2/SavedBeatmapFilter.cs @@ -0,0 +1,29 @@ +// 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 osu.Game.Database; +using Realms; + +namespace osu.Game.Screens.SelectV2 +{ + public class SavedBeatmapFilter : RealmObject, IHasGuidPrimaryKey + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public string Name { get; set; } = string.Empty; + + public string SearchQuery { get; set; } = string.Empty; + + public int SortMode { get; set; } + public int GroupMode { get; set; } + + public bool ShowConverted { get; set; } + + public double MinStars { get; set; } + public double MaxStars { get; set; } + + public string RulesetShortName { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs b/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs new file mode 100644 index 000000000000..980bf36ce38f --- /dev/null +++ b/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs @@ -0,0 +1,239 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Rulesets; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class SavedFiltersPopover : OsuPopover + { + private readonly Action onSelect; + private readonly Action onSave; + + [Resolved] + private RealmAccess? realm { get; set; } + + private readonly RulesetInfo ruleset; + + public SavedFiltersPopover(Action onSelect, Action onSave, RulesetInfo ruleset) + { + this.onSelect = onSelect; + this.onSave = onSave; + this.ruleset = ruleset; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Content.Padding = new MarginPadding(5); + + var flow = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Width = 250, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + }; + + Child = flow; + + // Add saved filters + var savedFilters = realm?.Realm.All().Where(f => f.RulesetShortName == ruleset.ShortName); + + var filtersFlow = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + + bool hasFilters = false; + + if (savedFilters != null) + { + foreach (var filter in savedFilters) + { + hasFilters = true; + + filtersFlow.Add(new SavedFilterItem(filter, onSelect, () => + { + realm?.Write(r => r.Remove(filter)); + Hide(); + })); + } + } + + if (hasFilters) + { + flow.Add(filtersFlow); + + flow.Add(new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + Colour = colours.Gray4, + }); + } + else + { + flow.Add(new OsuSpriteText + { + Text = "No saved filters", + Colour = colours.Gray8, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }); + } + + // Add save section + var nameTextBox = new OsuTextBox + { + PlaceholderText = "Filter name", + RelativeSizeAxes = Axes.X, + Height = 30 + }; + + flow.Add(nameTextBox); + + flow.Add(new RoundedButton + { + Text = "Save current filter", + RelativeSizeAxes = Axes.X, + Action = () => + { + string name = nameTextBox.Text.Trim(); + + if (string.IsNullOrWhiteSpace(name)) + { + nameTextBox.FlashColour(Color4.Red, 500); + nameTextBox.Shake(); + return; + } + + if (string.IsNullOrEmpty(ruleset.ShortName)) + { + nameTextBox.FlashColour(Color4.Red, 500); + nameTextBox.Shake(); + return; + } + + if (name.Length > 40) + { + nameTextBox.FlashColour(Color4.Red, 500); + nameTextBox.Shake(); + return; + } + + if (realm?.Realm.All().Any(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase) && f.RulesetShortName == ruleset.ShortName) == true) + { + nameTextBox.FlashColour(Color4.Red, 500); + nameTextBox.Shake(); + return; + } + + onSave(name); + Hide(); + } + }); + } + + private partial class SavedFilterItem : ClickableContainer + { + private Box background; + private SpriteIcon arrow; + private OsuSpriteText text; + + public SavedFilterItem(SavedBeatmapFilter filter, Action action, Action onDelete) + { + RelativeSizeAxes = Axes.X; + Height = 20; + Action = () => action(filter); + CornerRadius = 5; + Masking = true; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + arrow = new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronRight, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(8), + X = 0, + Alpha = 0, + Margin = new MarginPadding { Left = 3, Right = 3 }, + }, + text = new OsuSpriteText + { + Text = filter.Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 15, + }, + new IconButton + { + Icon = FontAwesome.Solid.Trash, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Action = onDelete, + Scale = new Vector2(0.55f), + X = -5, + } + }; + } + + [Resolved] + private OsuColour colours { get; set; } = null!; + + [Resolved(CanBeNull = true)] + private OverlayColourProvider? colourProvider { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + background.Colour = colourProvider?.Light4 ?? colours.BlueDark; + arrow.Colour = colourProvider?.Background5 ?? Color4.Black; + + AddInternal(new HoverSounds()); + } + + protected override bool OnHover(HoverEvent e) + { + background.FadeIn(100, Easing.OutQuint); + arrow.FadeIn(400, Easing.OutQuint); + arrow.MoveToX(3, 400, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + background.FadeOut(600, Easing.OutQuint); + arrow.FadeOut(200); + arrow.MoveToX(0, 200, Easing.In); + base.OnHoverLost(e); + } + } + } +} diff --git a/osu.Game/Screens/SelectV2/SearchFilterButton.cs b/osu.Game/Screens/SelectV2/SearchFilterButton.cs new file mode 100644 index 000000000000..2804e663fbc8 --- /dev/null +++ b/osu.Game/Screens/SelectV2/SearchFilterButton.cs @@ -0,0 +1,80 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osuTK; +using System; + +namespace osu.Game.Screens.SelectV2 +{ + public partial class SearchFilterButton : ClickableContainer + { + public Drawable? HoverTarget { get; set; } + + public Func? IsPopoverVisible { get; set; } + public Action? SetPopoverVisible { get; set; } + + private SpriteIcon icon = null!; + private Vector2 pendingShear; + + private bool popoverVisibleOnMouseDown; + + public SearchFilterButton() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = icon = new SpriteIcon + { + Icon = FontAwesome.Solid.Search, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(16), + Shear = pendingShear + }; + } + + protected override bool OnHover(HoverEvent e) + { + HoverTarget?.FadeIn(200); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + HoverTarget?.FadeOut(200); + base.OnHoverLost(e); + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + popoverVisibleOnMouseDown = IsPopoverVisible?.Invoke() == true; + return base.OnMouseDown(e); + } + + protected override bool OnClick(ClickEvent e) + { + if (SetPopoverVisible != null) + { + SetPopoverVisible(!popoverVisibleOnMouseDown); + return true; + } + + return base.OnClick(e); + } + + public void SetIconShear(Vector2 shear) + { + pendingShear = shear; + if (icon != null) + icon.Shear = shear; + } + } +} From 35e455c63913fafa6b586719a63e9ef27624b713 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:23:49 +0100 Subject: [PATCH 2/5] Added blank lines and fixed nvika warnings --- osu.Game/Screens/SelectV2/FilterControl.cs | 14 +++++++++----- osu.Game/Screens/SelectV2/SavedFiltersPopover.cs | 7 +++---- osu.Game/Screens/SelectV2/SearchFilterButton.cs | 6 +++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index bf7ae319d369..8030e1f7381b 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -257,6 +257,7 @@ protected override void Dispose(bool isDisposing) base.Dispose(isDisposing); collectionsSubscription?.Dispose(); } + private void applyFilter(SavedBeatmapFilter filter) { searchTextBox.Current.Value = filter.SearchQuery; @@ -283,6 +284,7 @@ private void applyFilter(SavedBeatmapFilter filter) difficultyRangeSlider.LowerBound.Value = min; difficultyRangeSlider.UpperBound.Value = max; } + private void saveFilter(string name) { if (string.IsNullOrEmpty(ruleset.Value?.ShortName)) @@ -300,6 +302,7 @@ private void saveFilter(string name) RulesetShortName = ruleset.Value.ShortName })); } + /// /// Creates a based on the current state of the controls. /// @@ -357,6 +360,7 @@ protected override void PopOut() { this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint) .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); + } internal partial class SongSelectSearchTextBox : ShearedFilterTextBox { @@ -365,13 +369,11 @@ internal partial class SongSelectSearchTextBox : ShearedFilterTextBox public IBindable Ruleset { get; set; } = null!; private readonly Box hoverBox; - private readonly PopoverTarget popoverTarget; private readonly BindableBool popoverVisible = new BindableBool(); - private readonly SearchFilterButton filterButton; public SongSelectSearchTextBox() { - filterButton = new SearchFilterButton + var filterButton = new SearchFilterButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -392,7 +394,7 @@ public SongSelectSearchTextBox() BackgroundContent.Add(hoverBox); - AddInternal(popoverTarget = new PopoverTarget + var popoverTarget = new PopoverTarget { Anchor = Anchor.BottomRight, Origin = Anchor.TopRight, @@ -400,7 +402,9 @@ public SongSelectSearchTextBox() Y = 1, Size = Vector2.Zero, CreatePopover = createPopover - }); + }; + + AddInternal(popoverTarget); filterButton.HoverTarget = hoverBox; filterButton.IsPopoverVisible = () => popoverVisible.Value; diff --git a/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs b/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs index 980bf36ce38f..09714fded663 100644 --- a/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs +++ b/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs @@ -156,9 +156,8 @@ private void load(OsuColour colours) private partial class SavedFilterItem : ClickableContainer { - private Box background; - private SpriteIcon arrow; - private OsuSpriteText text; + private readonly Box background; + private readonly SpriteIcon arrow; public SavedFilterItem(SavedBeatmapFilter filter, Action action, Action onDelete) { @@ -185,7 +184,7 @@ public SavedFilterItem(SavedBeatmapFilter filter, Action act Alpha = 0, Margin = new MarginPadding { Left = 3, Right = 3 }, }, - text = new OsuSpriteText + new OsuSpriteText { Text = filter.Name, Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Screens/SelectV2/SearchFilterButton.cs b/osu.Game/Screens/SelectV2/SearchFilterButton.cs index 2804e663fbc8..46a65f5c41b5 100644 --- a/osu.Game/Screens/SelectV2/SearchFilterButton.cs +++ b/osu.Game/Screens/SelectV2/SearchFilterButton.cs @@ -18,7 +18,7 @@ public partial class SearchFilterButton : ClickableContainer public Func? IsPopoverVisible { get; set; } public Action? SetPopoverVisible { get; set; } - private SpriteIcon icon = null!; + private SpriteIcon? icon; private Vector2 pendingShear; private bool popoverVisibleOnMouseDown; @@ -61,9 +61,9 @@ protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnClick(ClickEvent e) { - if (SetPopoverVisible != null) + if (SetPopoverVisible is Action setPopoverVisible) { - SetPopoverVisible(!popoverVisibleOnMouseDown); + setPopoverVisible(!popoverVisibleOnMouseDown); return true; } From 8a33bf4a523239c497c8cfa47522399ca3d30e18 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:30:46 +0100 Subject: [PATCH 3/5] Fixed formatting --- osu.Game/Screens/SelectV2/FilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 8030e1f7381b..17f2f210e4d3 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -360,8 +360,8 @@ protected override void PopOut() { this.MoveToX(150, SongSelect.ENTER_DURATION, Easing.OutQuint) .FadeOut(SongSelect.ENTER_DURATION / 3, Easing.In); - } + internal partial class SongSelectSearchTextBox : ShearedFilterTextBox { public Action? ApplyFilter { get; set; } From ac5de206e16c177f3c5de3456cc8a4261edbdea0 Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Fri, 19 Dec 2025 17:10:51 +0100 Subject: [PATCH 4/5] Make the popover slanted and keep the menu open while deleting a saved filter --- .../Screens/SelectV2/SavedFiltersPopover.cs | 124 ++++++++++++++---- 1 file changed, 95 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs b/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs index 09714fded663..5f5f374553cd 100644 --- a/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs +++ b/osu.Game/Screens/SelectV2/SavedFiltersPopover.cs @@ -52,8 +52,20 @@ private void load(OsuColour colours) Spacing = new Vector2(0, 5), }; + Body.Shear = OsuGame.SHEAR; + Child = flow; + var filterSection = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5), + }; + + flow.Add(filterSection); + // Add saved filters var savedFilters = realm?.Realm.All().Where(f => f.RulesetShortName == ruleset.ShortName); @@ -64,46 +76,62 @@ private void load(OsuColour colours) AutoSizeAxes = Axes.Y, }; - bool hasFilters = false; - - if (savedFilters != null) + var separator = new Box { - foreach (var filter in savedFilters) - { - hasFilters = true; + RelativeSizeAxes = Axes.X, + Height = 1, + Colour = colours.Gray4, + }; - filtersFlow.Add(new SavedFilterItem(filter, onSelect, () => - { - realm?.Write(r => r.Remove(filter)); - Hide(); - })); - } - } + var emptyStateText = new OsuSpriteText + { + Text = "No saved filters", + Colour = colours.Gray8, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Shear = -OsuGame.SHEAR, + }; - if (hasFilters) + void refreshFilterSection() { - flow.Add(filtersFlow); + filterSection.Clear(false); - flow.Add(new Box + if (filtersFlow.Children.Count > 0) { - RelativeSizeAxes = Axes.X, - Height = 1, - Colour = colours.Gray4, - }); + filterSection.Add(filtersFlow); + filterSection.Add(separator); + } + else + filterSection.Add(emptyStateText); } - else + + if (savedFilters != null) { - flow.Add(new OsuSpriteText + foreach (var filter in savedFilters) { - Text = "No saved filters", - Colour = colours.Gray8, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }); + var localFilter = filter; + + var item = new SavedFilterItem(localFilter, onSelect, () => + { + realm?.Write(r => r.Remove(localFilter)); + + var toRemove = filtersFlow.Children.OfType().FirstOrDefault(f => System.Collections.Generic.EqualityComparer.Default.Equals(f.Filter, localFilter)); + + if (toRemove != null) + { + filtersFlow.Remove(toRemove, true); + refreshFilterSection(); + } + }); + + filtersFlow.Add(item); + } } + refreshFilterSection(); + // Add save section - var nameTextBox = new OsuTextBox + var nameTextBox = new ShearedTextBox { PlaceholderText = "Filter name", RelativeSizeAxes = Axes.X, @@ -112,7 +140,7 @@ private void load(OsuColour colours) flow.Add(nameTextBox); - flow.Add(new RoundedButton + flow.Add(new ShearedRoundedButton { Text = "Save current filter", RelativeSizeAxes = Axes.X, @@ -154,13 +182,48 @@ private void load(OsuColour colours) }); } + private partial class ShearedTextBox : OsuTextBox + { + protected override void LoadComplete() + { + base.LoadComplete(); + + Schedule(() => + { + TextContainer.Shear = -OsuGame.SHEAR; + }); + } + + protected override SpriteText CreatePlaceholder() + { + var placeholder = base.CreatePlaceholder(); + placeholder.Shear = Vector2.Zero; + return placeholder; + } + } + + private partial class ShearedRoundedButton : RoundedButton + { + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Shear = -OsuGame.SHEAR, + }; + } + private partial class SavedFilterItem : ClickableContainer { + public SavedBeatmapFilter Filter { get; } + private readonly Box background; private readonly SpriteIcon arrow; public SavedFilterItem(SavedBeatmapFilter filter, Action action, Action onDelete) { + Filter = filter; RelativeSizeAxes = Axes.X; Height = 20; Action = () => action(filter); @@ -182,6 +245,7 @@ public SavedFilterItem(SavedBeatmapFilter filter, Action act Size = new Vector2(8), X = 0, Alpha = 0, + Shear = -OsuGame.SHEAR, Margin = new MarginPadding { Left = 3, Right = 3 }, }, new OsuSpriteText @@ -190,6 +254,7 @@ public SavedFilterItem(SavedBeatmapFilter filter, Action act Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, X = 15, + Shear = -OsuGame.SHEAR, }, new IconButton { @@ -199,6 +264,7 @@ public SavedFilterItem(SavedBeatmapFilter filter, Action act Action = onDelete, Scale = new Vector2(0.55f), X = -5, + Shear = -OsuGame.SHEAR, } }; } From 5169eaab5ab5b763b6e154ab56ebd0b072ffc26c Mon Sep 17 00:00:00 2001 From: Sayrix <43046854+Sayrix@users.noreply.github.com> Date: Tue, 23 Dec 2025 01:31:56 +0100 Subject: [PATCH 5/5] Follow the correct popover button state behavior --- osu.Game/Screens/SelectV2/FilterControl.cs | 24 ++----------------- .../Screens/SelectV2/SearchFilterButton.cs | 22 +++++++---------- 2 files changed, 11 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 17f2f210e4d3..56b262dcc515 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -7,7 +7,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -369,7 +368,6 @@ internal partial class SongSelectSearchTextBox : ShearedFilterTextBox public IBindable Ruleset { get; set; } = null!; private readonly Box hoverBox; - private readonly BindableBool popoverVisible = new BindableBool(); public SongSelectSearchTextBox() { @@ -407,17 +405,8 @@ public SongSelectSearchTextBox() AddInternal(popoverTarget); filterButton.HoverTarget = hoverBox; - filterButton.IsPopoverVisible = () => popoverVisible.Value; - filterButton.SetPopoverVisible = visible => popoverVisible.Value = visible; + filterButton.PopoverTarget = popoverTarget; filterButton.SetIconShear(-Shear); - - popoverVisible.BindValueChanged(visible => - { - if (visible.NewValue) - popoverTarget.ShowPopover(); - else - popoverTarget.HidePopover(); - }); } [Resolved] @@ -429,16 +418,7 @@ private void load() hoverBox.Colour = colours.Blue; } - private Popover createPopover() - { - var popover = new SavedFiltersPopover(f => ApplyFilter?.Invoke(f), n => SaveFilter?.Invoke(n), Ruleset.Value); - popover.State.BindValueChanged(state => - { - if (state.NewValue == Visibility.Hidden) - Schedule(() => popoverVisible.Value = false); - }); - return popover; - } + private Popover createPopover() => new SavedFiltersPopover(f => ApplyFilter?.Invoke(f), n => SaveFilter?.Invoke(n), Ruleset.Value); protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox(); diff --git a/osu.Game/Screens/SelectV2/SearchFilterButton.cs b/osu.Game/Screens/SelectV2/SearchFilterButton.cs index 46a65f5c41b5..00bd823fb376 100644 --- a/osu.Game/Screens/SelectV2/SearchFilterButton.cs +++ b/osu.Game/Screens/SelectV2/SearchFilterButton.cs @@ -2,12 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osuTK; -using System; namespace osu.Game.Screens.SelectV2 { @@ -15,14 +16,11 @@ public partial class SearchFilterButton : ClickableContainer { public Drawable? HoverTarget { get; set; } - public Func? IsPopoverVisible { get; set; } - public Action? SetPopoverVisible { get; set; } + public IHasPopover? PopoverTarget { get; set; } private SpriteIcon? icon; private Vector2 pendingShear; - private bool popoverVisibleOnMouseDown; - public SearchFilterButton() { RelativeSizeAxes = Axes.Both; @@ -55,19 +53,17 @@ protected override void OnHoverLost(HoverLostEvent e) protected override bool OnMouseDown(MouseDownEvent e) { - popoverVisibleOnMouseDown = IsPopoverVisible?.Invoke() == true; + if (PopoverTarget is Drawable drawable) + drawable.HidePopover(); return base.OnMouseDown(e); } - protected override bool OnClick(ClickEvent e) + protected override void OnMouseUp(MouseUpEvent e) { - if (SetPopoverVisible is Action setPopoverVisible) - { - setPopoverVisible(!popoverVisibleOnMouseDown); - return true; - } + if (IsHovered && PopoverTarget is IHasPopover hasPopover) + hasPopover.ShowPopover(); - return base.OnClick(e); + base.OnMouseUp(e); } public void SetIconShear(Vector2 shear)