1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-24 02:09:54 +08:00

Restore previous beatmap when leaving scoped mode (#36582)

Resolves #36288.

If the current selection is still available after leaving scoped mode,
it's left as is. If it's not, the selection from before entering scoped
mode is restored.


https://github.com/user-attachments/assets/b1ac3de1-7c7f-4949-82a9-1dd0459f3f61

---------

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
This commit is contained in:
Krzysztof Gutkowski
2026-02-16 16:40:29 +01:00
committed by GitHub
Unverified
parent 810edebe87
commit 8e26cf4e1b
7 changed files with 248 additions and 54 deletions
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@@ -266,6 +267,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddAssert("still has selection", () => Beatmap.IsDefault, () => Is.False);
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMinimum, 0.0));
}
[Test]
@@ -365,13 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortBy(SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value));
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
scopeBeatmap(false);
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -389,9 +386,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortBy(SortMode.Artist);
checkMatchedBeatmaps(6);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
scopeBeatmap(true);
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -413,9 +408,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForFiltering();
checkMatchedBeatmaps(3);
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
WaitForFiltering();
scopeBeatmap(true);
checkMatchedBeatmaps(3);
AddStep("press Escape", () => InputManager.Key(Key.Escape));
@@ -424,6 +417,179 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("text filter not emptied", () => filterTextBox.Current.Value, () => Is.Not.Empty);
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeRevertsToOriginalSelection(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("select normal difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Normal")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("exit scoped view", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FilterControl.ScopedBeatmapSetDisplay>().First());
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
checkMatchedBeatmaps(6);
AddAssert("normal difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeWhenSelectedBeatmapHiddenByFilters(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("set star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, findBeatmap("Hard").StarRating + 0.1));
WaitForFiltering();
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("exit scoped view", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<FilterControl.ScopedBeatmapSetDisplay>().First());
InputManager.Click(MouseButton.Left);
});
WaitForFiltering();
AddAssert("hard difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByChangingRuleset(bool grouped)
{
bool showConverts = Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps);
AddStep("hide converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0, 2);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(2);
scopeBeatmap(grouped);
checkMatchedBeatmaps(2);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("change ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
WaitForFiltering();
AddAssert("hard catch difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("revert convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, showConverts));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByShowingConverts(bool grouped)
{
bool showConverts = Config.Get<bool>(OsuSetting.ShowConvertedBeatmaps);
AddStep("hide converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("set star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(1).StarRating + 0.1));
WaitForFiltering();
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("select insane difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Insane")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Insane")));
AddStep("show converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
WaitForFiltering();
AddAssert("hard difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
AddStep("revert convert setting", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, showConverts));
AddStep("reset star difficulty filter", () => Config.SetValue(OsuSetting.DisplayStarsMaximum, 10.1));
}
[TestCase(false)]
[TestCase(true)]
public void TestUnscopeByChangingFilterText(bool grouped)
{
ImportBeatmapForRuleset(0);
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortBy(grouped ? SortMode.Title : SortMode.Difficulty);
checkMatchedBeatmaps(6);
AddStep("select hard difficulty", () => Beatmap.Value = Beatmaps.GetWorkingBeatmap(findBeatmap("Hard")));
AddUntilStep("selection changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Hard")));
scopeBeatmap(grouped);
checkMatchedBeatmaps(3);
AddStep("set filter text", () => filterTextBox.Current.Value = findBeatmap("Normal").DifficultyName);
WaitForFiltering();
AddAssert("normal difficulty is selected", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(findBeatmap("Normal")));
}
private void scopeBeatmap(bool grouped)
{
if (grouped)
{
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapSet.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
}
else
{
AddUntilStep("wait for spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Any(d => d.Enabled.Value));
AddStep("click spread indicator", () => this.ChildrenOfType<PanelBeatmapStandalone.SpreadDisplay>().Single(d => d.Enabled.Value).TriggerClick());
}
WaitForFiltering();
}
private BeatmapInfo findBeatmap(string difficultySubstring) => Beatmap.Value.BeatmapSetInfo.Beatmaps.First(b => b.DifficultyName.Contains(difficultySubstring));
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));
@@ -22,13 +22,8 @@ namespace osu.Game.Screens.SelectV2
{
public partial class ScopedBeatmapSetDisplay : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
private Box flashLayer = null!;
private Container content = null!;
private OsuTextFlowContainer text = null!;
@@ -44,7 +39,7 @@ namespace osu.Game.Screens.SelectV2
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
private void load(ISongSelect? songSelect, OverlayColourProvider colourProvider)
{
Content.AutoSizeEasing = Easing.OutQuint;
Content.AutoSizeDuration = transition_duration;
@@ -97,25 +92,25 @@ namespace osu.Game.Screens.SelectV2
Alpha = 0,
},
});
Action = () => scopedBeatmapSet.Value = null;
Action = () => songSelect?.UnscopeBeatmapSet();
}
protected override void LoadComplete()
{
base.LoadComplete();
scopedBeatmapSet.BindValueChanged(_ => updateState(), true);
ScopedBeatmapSet.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
if (scopedBeatmapSet.Value != null)
if (ScopedBeatmapSet.Value != null)
{
content.BypassAutoSizeAxes = Axes.None;
text.Clear();
text.AddText(SongSelectStrings.TemporarilyShowingAllBeatmapsIn);
text.AddText(@" ");
text.AddText(scopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold));
text.AddText(ScopedBeatmapSet.Value.Metadata.GetDisplayTitleRomanisable(), t => t.Font = OsuFont.Style.Body.With(weight: FontWeight.Bold));
}
else
{
@@ -126,7 +121,7 @@ namespace osu.Game.Screens.SelectV2
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (scopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat)
if (ScopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat)
{
TriggerClick();
return true;
+11 -20
View File
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.SelectV2
private const float corner_radius = 10;
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
private SongSelectSearchTextBox searchTextBox = null!;
private ShearedToggleButton showConvertedBeatmapsButton = null!;
@@ -48,6 +48,9 @@ namespace osu.Game.Screens.SelectV2
private ShearedDropdown<GroupMode> groupDropdown = null!;
private CollectionDropdown collectionDropdown = null!;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -115,7 +118,7 @@ namespace osu.Game.Screens.SelectV2
{
RelativeSizeAxes = Axes.X,
HoldFocus = true,
ScopedBeatmapSet = ScopedBeatmapSet,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
},
},
new GridContainer
@@ -190,7 +193,7 @@ namespace osu.Game.Screens.SelectV2
},
new ScopedBeatmapSetDisplay
{
ScopedBeatmapSet = ScopedBeatmapSet,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
}
},
}
@@ -298,7 +301,7 @@ namespace osu.Game.Screens.SelectV2
{
if (clearScopedSet && ScopedBeatmapSet.Value != null)
{
ScopedBeatmapSet.Value = null;
songSelect?.UnscopeBeatmapSet();
// because `ScopedBeatmapSet` has a value change callback bound to it that calls `updateCriteria()` again,
// we can just do nothing other than clear it to avoid extra work and duplicated `CriteriaChanged` invocations
return;
@@ -331,34 +334,22 @@ namespace osu.Game.Screens.SelectV2
internal partial class SongSelectSearchTextBox : ShearedFilterTextBox
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox
{
ScopedBeatmapSet = ScopedBeatmapSet,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
};
private partial class InnerTextBox : InnerFilterTextBox
{
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet
{
get => scopedBeatmapSet.Current;
set => scopedBeatmapSet.Current = value;
}
private readonly BindableWithCurrent<BeatmapSetInfo?> scopedBeatmapSet = new BindableWithCurrent<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; } = new Bindable<BeatmapSetInfo?>();
public override bool HandleLeftRightArrows => false;
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Back && scopedBeatmapSet.Value != null)
if (e.Action == GlobalAction.Back && ScopedBeatmapSet.Value != null)
return false;
return base.OnPressed(e);
+15 -2
View File
@@ -48,8 +48,21 @@ namespace osu.Game.Screens.SelectV2
IEnumerable<OsuMenuItem> GetForwardActions(BeatmapInfo beatmap);
/// <summary>
/// Set this to a non-<see langword="null"/> value in order to temporarily bypass filter and show all difficulties of the given beatmap set.
/// Temporarily bypasses filters and shows all difficulties of the given beatmapset.
/// </summary>
Bindable<BeatmapSetInfo?> ScopedBeatmapSet { get; }
/// <param name="beatmapSet">The beatmapset.</param>
void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet);
/// <summary>
/// Removes the beatmapset scope and reverts the previously selected filters.
/// </summary>
void UnscopeBeatmapSet();
/// <summary>
/// Contains the currently scoped beatmapset. Used by external consumers for displaying its state.
/// Cannot be used to change the value, any changes must be done through <see cref="ScopeToBeatmapSet"/>
/// or <see cref="UnscopeBeatmapSet"/>.
/// </summary>
IBindable<BeatmapSetInfo?> ScopedBeatmapSet { get; }
}
}
@@ -34,11 +34,14 @@ namespace osu.Game.Screens.SelectV2
protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly IBindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -59,7 +62,7 @@ namespace osu.Game.Screens.SelectV2
}
[BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager)
private void load(OsuConfigManager configManager)
{
Add(new FillFlowContainer
{
@@ -201,7 +204,7 @@ namespace osu.Game.Screens.SelectV2
}
}
Action = () => scopedBeatmapSet.Value = BeatmapSet.Value;
Action = () => songSelect?.ScopeToBeatmapSet(BeatmapSet.Value);
updateEnabled();
}
@@ -29,11 +29,14 @@ namespace osu.Game.Screens.SelectV2
protected override Colour4 DimColour => Colour4.White;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly IBindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
private readonly Bindable<bool> showConvertedBeatmaps = new Bindable<bool>();
private const double transition_duration = 200;
[Resolved]
private ISongSelect? songSelect { get; set; }
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -56,12 +59,12 @@ namespace osu.Game.Screens.SelectV2
Action = () =>
{
if (Beatmap.Value != null)
scopedBeatmapSet.Value = Beatmap.Value.BeatmapSet!;
songSelect?.ScopeToBeatmapSet(Beatmap.Value.BeatmapSet!);
};
}
[BackgroundDependencyLoader]
private void load(ISongSelect? songSelect, OsuConfigManager configManager)
private void load(OsuConfigManager configManager)
{
Add(new FillFlowContainer
{
+24 -1
View File
@@ -284,6 +284,7 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet },
},
}
},
@@ -1242,7 +1243,29 @@ namespace osu.Game.Screens.SelectV2
beatmaps.Restore(b);
}
public Bindable<BeatmapSetInfo?> ScopedBeatmapSet => filterControl.ScopedBeatmapSet;
private GroupedBeatmap? beforeScopedSelection;
private readonly Bindable<BeatmapSetInfo?> scopedBeatmapSet = new Bindable<BeatmapSetInfo?>();
public IBindable<BeatmapSetInfo?> ScopedBeatmapSet => scopedBeatmapSet;
public void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet)
{
beforeScopedSelection = carousel.CurrentGroupedBeatmap;
scopedBeatmapSet.Value = beatmapSet;
}
public void UnscopeBeatmapSet()
{
if (scopedBeatmapSet.Value == null)
return;
if (beforeScopedSelection != null)
queueBeatmapSelection(beforeScopedSelection);
scopedBeatmapSet.Value = null;
beforeScopedSelection = null;
}
#endregion