From 8e26cf4e1b8f2bd09e0e69f9f290cef906598480 Mon Sep 17 00:00:00 2001 From: Krzysztof Gutkowski Date: Mon, 16 Feb 2026 16:40:29 +0100 Subject: [PATCH] Restore previous beatmap when leaving scoped mode (#36582) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../TestSceneSongSelectFiltering.cs | 192 ++++++++++++++++-- .../FilterControl.ScopedBeatmapSetDisplay.cs | 19 +- osu.Game/Screens/SelectV2/FilterControl.cs | 31 +-- osu.Game/Screens/SelectV2/ISongSelect.cs | 17 +- .../SelectV2/PanelBeatmapSet.SpreadDisplay.cs | 9 +- .../PanelBeatmapStandalone.SpreadDisplay.cs | 9 +- osu.Game/Screens/SelectV2/SongSelect.cs | 25 ++- 7 files changed, 248 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs index 0e195b4742..2e7e08b331 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/TestSceneSongSelectFiltering.cs @@ -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().Any(d => d.Enabled.Value)); - AddStep("click spread indicator", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().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().Any(d => d.Enabled.Value)); - AddStep("click spread indicator", () => this.ChildrenOfType().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().Any(d => d.Enabled.Value)); - AddStep("click spread indicator", () => this.ChildrenOfType().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().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().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(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(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().Any(d => d.Enabled.Value)); + AddStep("click spread indicator", () => this.ChildrenOfType().Single(d => d.Enabled.Value).TriggerClick()); + } + else + { + AddUntilStep("wait for spread indicator", () => this.ChildrenOfType().Any(d => d.Enabled.Value)); + AddStep("click spread indicator", () => this.ChildrenOfType().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().FirstOrDefault(); private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); diff --git a/osu.Game/Screens/SelectV2/FilterControl.ScopedBeatmapSetDisplay.cs b/osu.Game/Screens/SelectV2/FilterControl.ScopedBeatmapSetDisplay.cs index 650ef4a1a9..378db5cb2d 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.ScopedBeatmapSetDisplay.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.ScopedBeatmapSetDisplay.cs @@ -22,13 +22,8 @@ namespace osu.Game.Screens.SelectV2 { public partial class ScopedBeatmapSetDisplay : OsuClickableContainer, IKeyBindingHandler { - public Bindable ScopedBeatmapSet - { - get => scopedBeatmapSet.Current; - set => scopedBeatmapSet.Current = value; - } + public IBindable ScopedBeatmapSet { get; } = new Bindable(); - private readonly BindableWithCurrent scopedBeatmapSet = new BindableWithCurrent(); 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 e) { - if (scopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat) + if (ScopedBeatmapSet.Value != null && e.Action == GlobalAction.Back && !e.Repeat) { TriggerClick(); return true; diff --git a/osu.Game/Screens/SelectV2/FilterControl.cs b/osu.Game/Screens/SelectV2/FilterControl.cs index 268e937167..be2d472e6b 100644 --- a/osu.Game/Screens/SelectV2/FilterControl.cs +++ b/osu.Game/Screens/SelectV2/FilterControl.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.SelectV2 private const float corner_radius = 10; - public Bindable ScopedBeatmapSet { get; } = new Bindable(); + public IBindable ScopedBeatmapSet { get; } = new Bindable(); private SongSelectSearchTextBox searchTextBox = null!; private ShearedToggleButton showConvertedBeatmapsButton = null!; @@ -48,6 +48,9 @@ namespace osu.Game.Screens.SelectV2 private ShearedDropdown groupDropdown = null!; private CollectionDropdown collectionDropdown = null!; + [Resolved] + private ISongSelect? songSelect { get; set; } + [Resolved] private IBindable 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 ScopedBeatmapSet - { - get => scopedBeatmapSet.Current; - set => scopedBeatmapSet.Current = value; - } - - private readonly BindableWithCurrent scopedBeatmapSet = new BindableWithCurrent(); + public IBindable ScopedBeatmapSet { get; } = new Bindable(); protected override InnerSearchTextBox CreateInnerTextBox() => new InnerTextBox { - ScopedBeatmapSet = ScopedBeatmapSet, + ScopedBeatmapSet = { BindTarget = ScopedBeatmapSet }, }; private partial class InnerTextBox : InnerFilterTextBox { - public Bindable ScopedBeatmapSet - { - get => scopedBeatmapSet.Current; - set => scopedBeatmapSet.Current = value; - } - - private readonly BindableWithCurrent scopedBeatmapSet = new BindableWithCurrent(); + public IBindable ScopedBeatmapSet { get; } = new Bindable(); public override bool HandleLeftRightArrows => false; public override bool OnPressed(KeyBindingPressEvent e) { - if (e.Action == GlobalAction.Back && scopedBeatmapSet.Value != null) + if (e.Action == GlobalAction.Back && ScopedBeatmapSet.Value != null) return false; return base.OnPressed(e); diff --git a/osu.Game/Screens/SelectV2/ISongSelect.cs b/osu.Game/Screens/SelectV2/ISongSelect.cs index 6280e4048a..5de1d4e75e 100644 --- a/osu.Game/Screens/SelectV2/ISongSelect.cs +++ b/osu.Game/Screens/SelectV2/ISongSelect.cs @@ -48,8 +48,21 @@ namespace osu.Game.Screens.SelectV2 IEnumerable GetForwardActions(BeatmapInfo beatmap); /// - /// Set this to a non- 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. /// - Bindable ScopedBeatmapSet { get; } + /// The beatmapset. + void ScopeToBeatmapSet(BeatmapSetInfo beatmapSet); + + /// + /// Removes the beatmapset scope and reverts the previously selected filters. + /// + void UnscopeBeatmapSet(); + + /// + /// 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 + /// or . + /// + IBindable ScopedBeatmapSet { get; } } } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapSet.SpreadDisplay.cs b/osu.Game/Screens/SelectV2/PanelBeatmapSet.SpreadDisplay.cs index db34630c48..08ea270e1f 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapSet.SpreadDisplay.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapSet.SpreadDisplay.cs @@ -34,11 +34,14 @@ namespace osu.Game.Screens.SelectV2 protected override Colour4 DimColour => Colour4.White; - private readonly Bindable scopedBeatmapSet = new Bindable(); + private readonly IBindable scopedBeatmapSet = new Bindable(); private readonly Bindable showConvertedBeatmaps = new Bindable(); private const double transition_duration = 200; + [Resolved] + private ISongSelect? songSelect { get; set; } + [Resolved] private Bindable 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(); } diff --git a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.SpreadDisplay.cs b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.SpreadDisplay.cs index b6b0bac077..f8f5883c99 100644 --- a/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.SpreadDisplay.cs +++ b/osu.Game/Screens/SelectV2/PanelBeatmapStandalone.SpreadDisplay.cs @@ -29,11 +29,14 @@ namespace osu.Game.Screens.SelectV2 protected override Colour4 DimColour => Colour4.White; - private readonly Bindable scopedBeatmapSet = new Bindable(); + private readonly IBindable scopedBeatmapSet = new Bindable(); private readonly Bindable showConvertedBeatmaps = new Bindable(); private const double transition_duration = 200; + [Resolved] + private ISongSelect? songSelect { get; set; } + [Resolved] private Bindable 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 { diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ecf8b3301b..00590336ce 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -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 ScopedBeatmapSet => filterControl.ScopedBeatmapSet; + private GroupedBeatmap? beforeScopedSelection; + + private readonly Bindable scopedBeatmapSet = new Bindable(); + public IBindable 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