From 81b07dee563b9ef01da5b8571061c09ccf0b417d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:41:33 +0300 Subject: [PATCH 01/21] Introduce `IExpandable` interface --- osu.Game/Overlays/IExpandable.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 osu.Game/Overlays/IExpandable.cs diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs new file mode 100644 index 0000000000..770ac97847 --- /dev/null +++ b/osu.Game/Overlays/IExpandable.cs @@ -0,0 +1,19 @@ +// 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.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Overlays +{ + /// + /// An interface for drawables with ability to expand/contract. + /// + public interface IExpandable : IDrawable + { + /// + /// Whether this drawable is in an expanded state. + /// + BindableBool Expanded { get; } + } +} From 62a2bccd7655f78a66d74a7d77d4dec73f311c3a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:41:45 +0300 Subject: [PATCH 02/21] Abstractify expansion logic from `ExpandingButtonContainer` --- osu.Game/Overlays/ExpandingButtonContainer.cs | 136 ++---------------- .../Overlays/ExpandingControlContainer.cs | 124 ++++++++++++++++ osu.Game/Overlays/IExpandingContainer.cs | 16 +++ osu.Game/Overlays/SettingsPanel.cs | 2 +- 4 files changed, 150 insertions(+), 128 deletions(-) create mode 100644 osu.Game/Overlays/ExpandingControlContainer.cs create mode 100644 osu.Game/Overlays/IExpandingContainer.cs diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index 4eb8c47a1f..d7ff285707 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,141 +1,23 @@ // 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; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Overlays { - public abstract class ExpandingButtonContainer : Container, IStateful + /// + /// An with a long hover expansion delay for buttons. + /// + /// + /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". + /// + public class ExpandingButtonContainer : ExpandingControlContainer { - private readonly float contractedWidth; - private readonly float expandedWidth; - - public event Action StateChanged; - - protected override Container Content => FillFlow; - - protected FillFlowContainer FillFlow { get; } - protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) + : base(contractedWidth, expandedWidth) { - this.contractedWidth = contractedWidth; - this.expandedWidth = expandedWidth; - - RelativeSizeAxes = Axes.Y; - Width = contractedWidth; - - InternalChildren = new Drawable[] - { - new SidebarScrollContainer - { - Children = new[] - { - FillFlow = new FillFlowContainer - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - } - } - }, - }; } - private ScheduledDelegate expandEvent; - private ExpandedState state; - - protected override bool OnHover(HoverEvent e) - { - queueExpandIfHovering(); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - expandEvent?.Cancel(); - hoveredButton = null; - State = ExpandedState.Contracted; - - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - queueExpandIfHovering(); - return base.OnMouseMove(e); - } - - private class SidebarScrollContainer : OsuScrollContainer - { - public SidebarScrollContainer() - { - RelativeSizeAxes = Axes.Both; - ScrollbarVisible = false; - } - } - - public ExpandedState State - { - get => state; - set - { - expandEvent?.Cancel(); - - if (state == value) return; - - state = value; - - switch (state) - { - default: - this.ResizeTo(new Vector2(contractedWidth, Height), 500, Easing.OutQuint); - break; - - case ExpandedState.Expanded: - this.ResizeTo(new Vector2(expandedWidth, Height), 500, Easing.OutQuint); - break; - } - - StateChanged?.Invoke(State); - } - } - - private Drawable hoveredButton; - - private void queueExpandIfHovering() - { - // if the same button is hovered, let the scheduled expand play out.. - if (hoveredButton?.IsHovered == true) - return; - - // ..otherwise check whether a new button is hovered, and if so, queue a new hover operation. - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the buttons may be nested within a child hierarchy. - hoveredButton = FillFlow.ChildrenOfType().FirstOrDefault(c => c.IsHovered); - - expandEvent?.Cancel(); - - if (hoveredButton?.IsHovered == true && State != ExpandedState.Expanded) - expandEvent = Scheduler.AddDelayed(() => State = ExpandedState.Expanded, 750); - } - } - - public enum ExpandedState - { - Contracted, - Expanded, + protected override double HoverExpansionDelay => 750; } } diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs new file mode 100644 index 0000000000..8e02cab923 --- /dev/null +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Overlays +{ + /// + /// Represents a with the ability to expand/contract when hovering the controls within it. + /// + /// The type of UI control to lookup for hover expansion. + public class ExpandingControlContainer : Container, IExpandingContainer + where TControl : class, IDrawable + { + private readonly float contractedWidth; + private readonly float expandedWidth; + + public BindableBool Expanded { get; } = new BindableBool(); + + /// + /// Delay before the container switches to expanded state from hover. + /// + protected virtual double HoverExpansionDelay => 0; + + protected override Container Content => FillFlow; + + protected FillFlowContainer FillFlow { get; } + + protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + { + this.contractedWidth = contractedWidth; + this.expandedWidth = expandedWidth; + + RelativeSizeAxes = Axes.Y; + Width = contractedWidth; + + InternalChild = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = FillFlow = new FillFlowContainer + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, + }; + } + + private ScheduledDelegate hoverExpandEvent; + private TControl activeControl; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Expanded.BindValueChanged(v => + { + this.ResizeWidthTo(v.NewValue ? expandedWidth : contractedWidth, 500, Easing.OutQuint); + }, true); + } + + protected override void Update() + { + base.Update(); + + // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. + // that's because contracting the container depends not only on whether it's no longer hovered, + // but also on whether the hovered control is no longer in a dragged state (if it was). + if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) + { + hoverExpandEvent?.Cancel(); + + Expanded.Value = false; + hoverExpandEvent = null; + activeControl = null; + } + } + + protected override bool OnHover(HoverEvent e) + { + queueExpandIfHovering(); + return true; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + queueExpandIfHovering(); + return base.OnMouseMove(e); + } + + private void queueExpandIfHovering() + { + // if the same control is hovered or dragged, let the scheduled expand play out.. + if (activeControl != null && isControlActive(activeControl)) + return; + + // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. + hoverExpandEvent?.Cancel(); + + // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way + // to handle cases like the editor where the controls may be nested within a child hierarchy. + activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); + + if (activeControl != null && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + } + + /// + /// Whether the given control is currently active, by checking whether it's hovered or dragged. + /// + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + } +} diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Overlays/IExpandingContainer.cs new file mode 100644 index 0000000000..ec5f0c90f4 --- /dev/null +++ b/osu.Game/Overlays/IExpandingContainer.cs @@ -0,0 +1,16 @@ +// 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.Containers; + +namespace osu.Game.Overlays +{ + /// + /// A target expanding container that should be resolved by children s to propagate state changes. + /// + [Cached(typeof(IExpandingContainer))] + public interface IExpandingContainer : IContainer, IExpandable + { + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index ba7118cffe..b11b6fde27 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -265,7 +265,7 @@ namespace osu.Game.Overlays return; SectionsContainer.ScrollTo(section); - Sidebar.State = ExpandedState.Contracted; + Sidebar.Expanded.Value = false; }, }; } From 326f12f8477da2838a368e921329d1ee726e4c44 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:43:05 +0300 Subject: [PATCH 03/21] Add `IExpandable` support for `SettingsToolboxGroup` --- osu.Game/Overlays/SettingsToolboxGroup.cs | 80 +++++++++++-------- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 2 +- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index ca0980a9c9..9e7223df9d 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; @@ -18,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class SettingsToolboxGroup : Container + public class SettingsToolboxGroup : Container, IExpandable { private const float transition_duration = 250; private const int container_width = 270; @@ -34,30 +35,7 @@ namespace osu.Game.Overlays private readonly FillFlowContainer content; private readonly IconButton button; - private bool expanded = true; - - public bool Expanded - { - get => expanded; - set - { - if (expanded == value) return; - - expanded = value; - - content.ClearTransforms(); - - if (expanded) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - updateExpanded(); - } - } + public BindableBool Expanded { get; } = new BindableBool(true); private Color4 expandedColour; @@ -67,7 +45,7 @@ namespace osu.Game.Overlays /// Create a new instance. /// /// The title to be displayed in the header of this group. - protected SettingsToolboxGroup(string title) + public SettingsToolboxGroup(string title) { AutoSizeAxes = Axes.Y; Width = container_width; @@ -115,7 +93,7 @@ namespace osu.Game.Overlays Position = new Vector2(-15, 0), Icon = FontAwesome.Solid.Bars, Scale = new Vector2(0.75f), - Action = () => Expanded = !Expanded, + Action = () => Expanded.Toggle(), }, } }, @@ -155,23 +133,58 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); } + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } + + private bool expandedByContainer; + protected override void LoadComplete() { base.LoadComplete(); - this.Delay(600).FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); - updateExpanded(); + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + if (containerExpanded.NewValue && !Expanded.Value) + { + Expanded.Value = true; + expandedByContainer = true; + } + else if (!containerExpanded.NewValue && expandedByContainer) + { + Expanded.Value = false; + expandedByContainer = false; + } + + updateActiveState(); + }, true); + + Expanded.BindValueChanged(v => + { + content.ClearTransforms(); + + if (v.NewValue) + content.AutoSizeAxes = Axes.Y; + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } + + button.FadeColour(Expanded.Value ? expandedColour : Color4.White, 200, Easing.InOutQuint); + }, true); + + this.Delay(600).Schedule(updateActiveState); } protected override bool OnHover(HoverEvent e) { - this.FadeIn(fade_duration, Easing.OutQuint); + updateActiveState(); return false; } protected override void OnHoverLost(HoverLostEvent e) { - this.FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); + updateActiveState(); base.OnHoverLost(e); } @@ -181,7 +194,10 @@ namespace osu.Game.Overlays expandedColour = colours.Yellow; } - private void updateExpanded() => button.FadeColour(expanded ? expandedColour : Color4.White, 200, Easing.InOutQuint); + private void updateActiveState() + { + this.FadeTo(IsHovered || expandingContainer?.Expanded.Value == true ? 1 : inactive_alpha, fade_duration, Easing.OutQuint); + } protected override Container Content => content; diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index ffcbb06fb3..807b4989c7 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), - VisualSettings = new VisualSettings { Expanded = false } + VisualSettings = new VisualSettings { Expanded = { Value = false } } } }; } From f4c7a332c343b1d23df835914150fe85992ef7ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:45:08 +0300 Subject: [PATCH 04/21] Add `IExpandable` support for `SettingsItem` --- osu.Game/Overlays/Settings/ISettingsItem.cs | 9 +- osu.Game/Overlays/Settings/SettingsItem.cs | 91 ++++++++++++++++----- 2 files changed, 78 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index e7afa48502..fe21f0664a 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -2,12 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings { - public interface ISettingsItem : IDrawable, IDisposable + /// + /// A non-generic interface for s. + /// + public interface ISettingsItem : IExpandable, IDisposable { + /// + /// Invoked when the setting value has changed. + /// event Action SettingChanged; } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index e709be1343..cc8d5b36d0 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText labelText; + private SpriteText label; private OsuTextFlowContainer warningText; @@ -42,21 +42,34 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } + private LocalisableString labelText; + public virtual LocalisableString LabelText { - get => labelText?.Text ?? string.Empty; + get => labelText; set { - if (labelText == null) - { - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, labelText = new OsuSpriteText()); + ensureLabelCreated(); - updateDisabled(); - } + labelText = value; + updateLabelText(); + } + } - labelText.Text = value; - updateLayout(); + private LocalisableString? contractedLabelText; + + /// + /// Text to be displayed in place of when this is in a contracted state. + /// + public LocalisableString? ContractedLabelText + { + get => contractedLabelText; + set + { + ensureLabelCreated(); + + contractedLabelText = value; + updateLabelText(); } } @@ -90,6 +103,10 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } + public BindableBool Expanded { get; } = new BindableBool(true); + + public event Action SettingChanged; + public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } @@ -101,8 +118,6 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } - public event Action SettingChanged; - protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -151,23 +166,59 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre }); - updateLayout(); } } - private void updateLayout() - { - bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } - // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => Expanded.Value = containerExpanded.NewValue, true); + + Expanded.BindValueChanged(v => + { + updateLabelText(); + + Control.FadeTo(v.NewValue ? 1 : 0, 500, Easing.OutQuint); + Control.BypassAutoSizeAxes = v.NewValue ? Axes.None : Axes.Both; + }, true); + + FinishTransforms(true); + } + + private void ensureLabelCreated() + { + if (label != null) + return; + + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Insert(-1, label = new OsuSpriteText()); + + updateDisabled(); + } + + private void updateLabelText() + { + if (label != null) + { + if (contractedLabelText is LocalisableString contractedText) + label.Text = Expanded.Value ? labelText : contractedText; + else + label.Text = labelText; + } + + // if the settings item is providing a non-empty label, the default value indicator should be centred vertically to the left of the label. // otherwise, it should be centred vertically to the left of the main control of the settings item. - defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; + defaultValueIndicatorContainer.Height = !string.IsNullOrEmpty(label?.Text.ToString()) ? label.DrawHeight : Control.DrawHeight; } private void updateDisabled() { - if (labelText != null) - labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + if (label != null) + label.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } } From 648e7f6bbc4883d591ed7c01994f511006caa770 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 17:47:11 +0300 Subject: [PATCH 05/21] Handle controls with dragging state wrapped in underlying components I'm not 100% sure about this approach but it'll do for now. --- osu.Game/Overlays/ExpandingControlContainer.cs | 2 +- osu.Game/Overlays/Settings/ISettingsItem.cs | 5 +++++ osu.Game/Overlays/Settings/SettingsItem.cs | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index 8e02cab923..2accd63fb9 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -119,6 +119,6 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged; + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is ISettingsItem item && item.IsControlDragged); } } diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index fe21f0664a..20e2f48f96 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -14,5 +14,10 @@ namespace osu.Game.Overlays.Settings /// Invoked when the setting value has changed. /// event Action SettingChanged; + + /// + /// Returns whether the UI control is currently in a dragged state. + /// + bool IsControlDragged { get; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index cc8d5b36d0..29980dc5a8 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -105,6 +105,8 @@ namespace osu.Game.Overlays.Settings public BindableBool Expanded { get; } = new BindableBool(true); + public bool IsControlDragged => Control.IsDragged; + public event Action SettingChanged; public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); From 6b35c0fe01795d664fc3599085a4ec2aa17d614d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 21 Jan 2022 18:39:58 +0300 Subject: [PATCH 06/21] Add test scene for `ExpandingControlContainer` --- .../TestSceneExpandingControlContainer.cs | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs new file mode 100644 index 0000000000..d75089ceac --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs @@ -0,0 +1,183 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneExpandingControlContainer : OsuManualInputManagerTestScene + { + private TestExpandingContainer container; + private SettingsToolboxGroup toolboxGroup; + + private SettingsSlider slider1; + private SettingsSlider slider2; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = container = new TestExpandingContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Height = 0.33f, + Child = toolboxGroup = new SettingsToolboxGroup("sliders") + { + RelativeSizeAxes = Axes.X, + Width = 1, + Children = new Drawable[] + { + slider1 = new SettingsSlider + { + Current = new BindableFloat + { + Default = 1.0f, + MinValue = 1.0f, + MaxValue = 10.0f, + Precision = 0.01f, + }, + }, + slider2 = new SettingsSlider + { + Current = new BindableDouble + { + Default = 1.0, + MinValue = 1.0, + MaxValue = 10.0, + Precision = 0.01, + }, + }, + } + } + }; + + slider1.Current.BindValueChanged(v => + { + slider1.LabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; + }, true); + + slider2.Current.BindValueChanged(v => + { + slider2.LabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; + }, true); + }); + + [Test] + public void TestDisplay() + { + AddStep("switch to contracted", () => container.Expanded.Value = false); + AddStep("switch to expanded", () => container.Expanded.Value = true); + AddStep("set left origin", () => container.Origin = Anchor.CentreLeft); + AddStep("set centre origin", () => container.Origin = Anchor.Centre); + AddStep("set right origin", () => container.Origin = Anchor.CentreRight); + } + + /// + /// Tests hovering over controls expands the parenting container appropriately and does not contract until hover is lost from container. + /// + [Test] + public void TestHoveringControlExpandsContainer() + { + AddAssert("ensure container contracted", () => !container.Expanded.Value); + + AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container expanded", () => container.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("hover group top", () => InputManager.MoveMouseTo(toolboxGroup.ScreenSpaceDrawQuad.TopLeft + new Vector2(5))); + AddAssert("container still expanded", () => container.Expanded.Value); + AddAssert("controls still expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container contracted", () => !container.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests dragging a UI control (e.g. ) outside its parenting container does not contract it until dragging is finished. + /// + [Test] + public void TestDraggingControlOutsideDoesntContractContainer() + { + AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container expanded", () => container.Expanded.Value); + + AddStep("hover slider nub", () => InputManager.MoveMouseTo(slider1.ChildrenOfType().Single())); + AddStep("hold slider nub", () => InputManager.PressButton(MouseButton.Left)); + AddStep("drag outside container", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container still expanded", () => container.Expanded.Value); + + AddStep("release slider nub", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("container contracted", () => !container.Expanded.Value); + } + + /// + /// Tests expanding a container will expand underlying groups if contracted. + /// + [Test] + public void TestExpandingContainerExpandsContractedGroup() + { + AddStep("contract group", () => toolboxGroup.Expanded.Value = false); + + AddStep("expand container", () => container.Expanded.Value = true); + AddAssert("group expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("contract container", () => container.Expanded.Value = false); + AddAssert("group contracted", () => !toolboxGroup.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests contracting a container does not contract underlying groups if expanded by user (i.e. by setting directly). + /// + [Test] + public void TestContractingContainerDoesntContractUserExpandedGroup() + { + AddAssert("ensure group expanded", () => toolboxGroup.Expanded.Value); + + AddStep("expand container", () => container.Expanded.Value = true); + AddAssert("group still expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); + + AddStep("contract container", () => container.Expanded.Value = false); + AddAssert("group still expanded", () => toolboxGroup.Expanded.Value); + AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); + } + + /// + /// Tests expanding a container via does not get contracted by losing hover. + /// + [Test] + public void TestExpandingContainerDoesntGetContractedByHover() + { + AddStep("expand container", () => container.Expanded.Value = true); + + AddStep("hover control", () => InputManager.MoveMouseTo(slider1)); + AddAssert("container still expanded", () => container.Expanded.Value); + + AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); + AddAssert("container still expanded", () => container.Expanded.Value); + } + + private class TestExpandingContainer : ExpandingControlContainer + { + public TestExpandingContainer() + : base(120, 250) + { + } + } + } +} From b5e6352137d4254323d2888f308c81dc2ced3208 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 09:31:29 +0300 Subject: [PATCH 07/21] Revert `SettingsItem`-related changes --- osu.Game/Overlays/Settings/ISettingsItem.cs | 14 +--- osu.Game/Overlays/Settings/SettingsItem.cs | 91 +++++---------------- 2 files changed, 21 insertions(+), 84 deletions(-) diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs index 20e2f48f96..e7afa48502 100644 --- a/osu.Game/Overlays/Settings/ISettingsItem.cs +++ b/osu.Game/Overlays/Settings/ISettingsItem.cs @@ -2,22 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings { - /// - /// A non-generic interface for s. - /// - public interface ISettingsItem : IExpandable, IDisposable + public interface ISettingsItem : IDrawable, IDisposable { - /// - /// Invoked when the setting value has changed. - /// event Action SettingChanged; - - /// - /// Returns whether the UI control is currently in a dragged state. - /// - bool IsControlDragged { get; } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 29980dc5a8..e709be1343 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Settings protected readonly FillFlowContainer FlowContent; - private SpriteText label; + private SpriteText labelText; private OsuTextFlowContainer warningText; @@ -42,34 +42,21 @@ namespace osu.Game.Overlays.Settings [Resolved] private OsuColour colours { get; set; } - private LocalisableString labelText; - public virtual LocalisableString LabelText { - get => labelText; + get => labelText?.Text ?? string.Empty; set { - ensureLabelCreated(); + if (labelText == null) + { + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Insert(-1, labelText = new OsuSpriteText()); - labelText = value; - updateLabelText(); - } - } + updateDisabled(); + } - private LocalisableString? contractedLabelText; - - /// - /// Text to be displayed in place of when this is in a contracted state. - /// - public LocalisableString? ContractedLabelText - { - get => contractedLabelText; - set - { - ensureLabelCreated(); - - contractedLabelText = value; - updateLabelText(); + labelText.Text = value; + updateLayout(); } } @@ -103,12 +90,6 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } - public BindableBool Expanded { get; } = new BindableBool(true); - - public bool IsControlDragged => Control.IsDragged; - - public event Action SettingChanged; - public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } @@ -120,6 +101,8 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } + public event Action SettingChanged; + protected SettingsItem() { RelativeSizeAxes = Axes.X; @@ -168,59 +151,23 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.Centre, Origin = Anchor.Centre }); + updateLayout(); } } - [Resolved(canBeNull: true)] - private IExpandingContainer expandingContainer { get; set; } - - protected override void LoadComplete() + private void updateLayout() { - base.LoadComplete(); + bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString()); - expandingContainer?.Expanded.BindValueChanged(containerExpanded => Expanded.Value = containerExpanded.NewValue, true); - - Expanded.BindValueChanged(v => - { - updateLabelText(); - - Control.FadeTo(v.NewValue ? 1 : 0, 500, Easing.OutQuint); - Control.BypassAutoSizeAxes = v.NewValue ? Axes.None : Axes.Both; - }, true); - - FinishTransforms(true); - } - - private void ensureLabelCreated() - { - if (label != null) - return; - - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Insert(-1, label = new OsuSpriteText()); - - updateDisabled(); - } - - private void updateLabelText() - { - if (label != null) - { - if (contractedLabelText is LocalisableString contractedText) - label.Text = Expanded.Value ? labelText : contractedText; - else - label.Text = labelText; - } - - // if the settings item is providing a non-empty label, the default value indicator should be centred vertically to the left of the label. + // if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label. // otherwise, it should be centred vertically to the left of the main control of the settings item. - defaultValueIndicatorContainer.Height = !string.IsNullOrEmpty(label?.Text.ToString()) ? label.DrawHeight : Control.DrawHeight; + defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight; } private void updateDisabled() { - if (label != null) - label.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; + if (labelText != null) + labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } } } From 699826677006b6e7a230f061c505c53b2fbd73c6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:18:06 +0300 Subject: [PATCH 08/21] Add simplified implementation of an expandable slider --- osu.Game/Overlays/ExpandableSlider.cs | 123 ++++++++++++++++++++++++ osu.Game/Overlays/IExpandableControl.cs | 16 +++ 2 files changed, 139 insertions(+) create mode 100644 osu.Game/Overlays/ExpandableSlider.cs create mode 100644 osu.Game/Overlays/IExpandableControl.cs diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs new file mode 100644 index 0000000000..38faf44148 --- /dev/null +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -0,0 +1,123 @@ +// 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.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + /// + /// An implementation for the UI slider bar control. + /// + public class ExpandableSlider : CompositeDrawable, IExpandableControl, IHasCurrentValue + where T : struct, IEquatable, IComparable, IConvertible + where TSlider : OsuSliderBar, new() + { + private readonly OsuSpriteText label; + private readonly TSlider slider; + + private LocalisableString contractedLabelText; + + /// + /// The label text to display when this slider is in a contracted state. + /// + public LocalisableString ContractedLabelText + { + get => contractedLabelText; + set + { + if (value == contractedLabelText) + return; + + contractedLabelText = value; + + if (!Expanded.Value) + label.Text = value; + } + } + + private LocalisableString expandedLabelText; + + /// + /// The label text to display when this slider is in an expanded state. + /// + public LocalisableString ExpandedLabelText + { + get => expandedLabelText; + set + { + if (value == expandedLabelText) + return; + + expandedLabelText = value; + + if (Expanded.Value) + label.Text = value; + } + } + + public Bindable Current + { + get => slider.Current; + set => slider.Current = value; + } + + public BindableBool Expanded { get; } = new BindableBool(); + + public bool IsControlDragged => slider.IsDragged; + + public override bool HandlePositionalInput => true; + + public ExpandableSlider() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + label = new OsuSpriteText(), + slider = new TSlider(), + } + }; + } + + [Resolved(canBeNull: true)] + private IExpandingContainer expandingContainer { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + expandingContainer?.Expanded.BindValueChanged(containerExpanded => + { + Expanded.Value = containerExpanded.NewValue; + }, true); + + Expanded.BindValueChanged(v => + { + label.Text = v.NewValue ? expandedLabelText : contractedLabelText; + slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); + slider.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + }, true); + } + } + + /// + /// An implementation for the UI slider bar control. + /// + public class ExpandableSlider : ExpandableSlider> + where T : struct, IEquatable, IComparable, IConvertible + { + } +} diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs new file mode 100644 index 0000000000..fae07ae23b --- /dev/null +++ b/osu.Game/Overlays/IExpandableControl.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays +{ + /// + /// An interface for UI controls with the ability to expand/contract. + /// + public interface IExpandableControl : IExpandable + { + /// + /// Returns whether the UI control is currently in a dragged state. + /// + bool IsControlDragged { get; } + } +} From eb83b7fe0a912130641da4e56a9808ed18ce099b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:18:17 +0300 Subject: [PATCH 09/21] Update existing implementation with changes --- .../TestSceneExpandingControlContainer.cs | 15 +++++++-------- osu.Game/Overlays/ExpandingControlContainer.cs | 3 +-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs index d75089ceac..48089566cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings.Sections; using osuTK; using osuTK.Input; @@ -20,8 +19,8 @@ namespace osu.Game.Tests.Visual.UserInterface private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; - private SettingsSlider slider1; - private SettingsSlider slider2; + private ExpandableSlider slider1; + private ExpandableSlider slider2; [SetUp] public void SetUp() => Schedule(() => @@ -37,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface Width = 1, Children = new Drawable[] { - slider1 = new SettingsSlider + slider1 = new ExpandableSlider { Current = new BindableFloat { @@ -47,7 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface Precision = 0.01f, }, }, - slider2 = new SettingsSlider + slider2 = new ExpandableSlider { Current = new BindableDouble { @@ -63,13 +62,13 @@ namespace osu.Game.Tests.Visual.UserInterface slider1.Current.BindValueChanged(v => { - slider1.LabelText = $"Slider One ({v.NewValue:0.##x})"; + slider1.ExpandedLabelText = $"Slider One ({v.NewValue:0.##x})"; slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})"; }, true); slider2.Current.BindValueChanged(v => { - slider2.LabelText = $"Slider Two ({v.NewValue:N2})"; + slider2.ExpandedLabelText = $"Slider Two ({v.NewValue:N2})"; slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})"; }, true); }); @@ -172,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("container still expanded", () => container.Expanded.Value); } - private class TestExpandingContainer : ExpandingControlContainer + private class TestExpandingContainer : ExpandingControlContainer { public TestExpandingContainer() : base(120, 250) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index 2accd63fb9..fb6a71ba97 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -9,7 +9,6 @@ using osu.Framework.Input.Events; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Containers; -using osu.Game.Overlays.Settings; namespace osu.Game.Overlays { @@ -119,6 +118,6 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is ISettingsItem item && item.IsControlDragged); + private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is IExpandableControl expandable && expandable.IsControlDragged); } } From 161ff45f8cb5e93d7cf2a02c5601344d9d35064c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 26 Jan 2022 10:35:42 +0300 Subject: [PATCH 10/21] Resolve further UI-related issues --- osu.Game/Overlays/ExpandableSlider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index 38faf44148..524485d806 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays { @@ -84,10 +85,14 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), Children = new Drawable[] { label = new OsuSpriteText(), - slider = new TSlider(), + slider = new TSlider + { + RelativeSizeAxes = Axes.X, + }, } }; } @@ -108,7 +113,7 @@ namespace osu.Game.Overlays { label.Text = v.NewValue ? expandedLabelText : contractedLabelText; slider.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); - slider.BypassAutoSizeAxes = v.NewValue ? Axes.Y : Axes.None; + slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; }, true); } } From 2cc69d6b19b2682db3a7c6ec8dd79ccc1421a0d1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 27 Jan 2022 15:57:56 +0300 Subject: [PATCH 11/21] Replace `IsControlDragged` with an abstract `ShouldBeExpanded` --- osu.Game/Overlays/ExpandableSlider.cs | 2 +- osu.Game/Overlays/ExpandingControlContainer.cs | 8 +++++++- osu.Game/Overlays/IExpandable.cs | 11 +++++++++++ osu.Game/Overlays/IExpandableControl.cs | 4 ---- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index 524485d806..d346b9b22c 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -72,7 +72,7 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(); - public bool IsControlDragged => slider.IsDragged; + bool IExpandable.ShouldBeExpanded => IsHovered || slider.IsDragged; public override bool HandlePositionalInput => true; diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingControlContainer.cs index fb6a71ba97..859e4bcd25 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingControlContainer.cs @@ -118,6 +118,12 @@ namespace osu.Game.Overlays /// /// Whether the given control is currently active, by checking whether it's hovered or dragged. /// - private bool isControlActive(TControl control) => control.IsHovered || control.IsDragged || (control is IExpandableControl expandable && expandable.IsControlDragged); + private bool isControlActive(TControl control) + { + if (control is IExpandable expandable) + return expandable.ShouldBeExpanded; + + return control.IsHovered || control.IsDragged; + } } } diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs index 770ac97847..f998fc7b9f 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Overlays/IExpandable.cs @@ -3,6 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { @@ -15,5 +16,15 @@ namespace osu.Game.Overlays /// Whether this drawable is in an expanded state. /// BindableBool Expanded { get; } + + /// + /// Whether this drawable should be/stay expanded by a parenting . + /// By default, this is when this drawable is in a hovered or dragged state. + /// + /// + /// This is defined for certain controls which may have a child handling dragging instead. + /// (e.g. in which dragging is handled by their underlying control). + /// + bool ShouldBeExpanded => IsHovered || IsDragged; } } diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs index fae07ae23b..2cda6f467b 100644 --- a/osu.Game/Overlays/IExpandableControl.cs +++ b/osu.Game/Overlays/IExpandableControl.cs @@ -8,9 +8,5 @@ namespace osu.Game.Overlays /// public interface IExpandableControl : IExpandable { - /// - /// Returns whether the UI control is currently in a dragged state. - /// - bool IsControlDragged { get; } } } From bbef12e72c7bbaaf41c9a9b34b9d90ce5b4cfc2e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Feb 2022 05:45:12 +0300 Subject: [PATCH 12/21] Refactor `ExpandingControlContainer` to no longer rely on controls --- ...iner.cs => TestSceneExpandingContainer.cs} | 40 ++---------- osu.Game/Overlays/ExpandableSlider.cs | 4 +- osu.Game/Overlays/ExpandingButtonContainer.cs | 6 +- ...trolContainer.cs => ExpandingContainer.cs} | 64 ++++++------------- osu.Game/Overlays/IExpandable.cs | 11 ---- osu.Game/Overlays/IExpandableControl.cs | 12 ---- 6 files changed, 28 insertions(+), 109 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneExpandingControlContainer.cs => TestSceneExpandingContainer.cs} (74%) rename osu.Game/Overlays/{ExpandingControlContainer.cs => ExpandingContainer.cs} (54%) delete mode 100644 osu.Game/Overlays/IExpandableControl.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs similarity index 74% rename from osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index 48089566cc..f63591311f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingControlContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -1,20 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections; using osuTK; -using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneExpandingControlContainer : OsuManualInputManagerTestScene + public class TestSceneExpandingContainer : OsuManualInputManagerTestScene { private TestExpandingContainer container; private SettingsToolboxGroup toolboxGroup; @@ -84,44 +80,22 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests hovering over controls expands the parenting container appropriately and does not contract until hover is lost from container. + /// Tests hovering expands the container and does not contract until hover is lost. /// [Test] - public void TestHoveringControlExpandsContainer() + public void TestHoveringExpandsContainer() { AddAssert("ensure container contracted", () => !container.Expanded.Value); - AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); + AddStep("hover container", () => InputManager.MoveMouseTo(container)); AddAssert("container expanded", () => container.Expanded.Value); AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); - AddStep("hover group top", () => InputManager.MoveMouseTo(toolboxGroup.ScreenSpaceDrawQuad.TopLeft + new Vector2(5))); - AddAssert("container still expanded", () => container.Expanded.Value); - AddAssert("controls still expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); - AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("container contracted", () => !container.Expanded.Value); AddAssert("controls contracted", () => !slider1.Expanded.Value && !slider2.Expanded.Value); } - /// - /// Tests dragging a UI control (e.g. ) outside its parenting container does not contract it until dragging is finished. - /// - [Test] - public void TestDraggingControlOutsideDoesntContractContainer() - { - AddStep("hover slider", () => InputManager.MoveMouseTo(slider1)); - AddAssert("container expanded", () => container.Expanded.Value); - - AddStep("hover slider nub", () => InputManager.MoveMouseTo(slider1.ChildrenOfType().Single())); - AddStep("hold slider nub", () => InputManager.PressButton(MouseButton.Left)); - AddStep("drag outside container", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddAssert("container still expanded", () => container.Expanded.Value); - - AddStep("release slider nub", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("container contracted", () => !container.Expanded.Value); - } - /// /// Tests expanding a container will expand underlying groups if contracted. /// @@ -157,21 +131,21 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests expanding a container via does not get contracted by losing hover. + /// Tests expanding a container via does not get contracted by losing hover. /// [Test] public void TestExpandingContainerDoesntGetContractedByHover() { AddStep("expand container", () => container.Expanded.Value = true); - AddStep("hover control", () => InputManager.MoveMouseTo(slider1)); + AddStep("hover container", () => InputManager.MoveMouseTo(container)); AddAssert("container still expanded", () => container.Expanded.Value); AddStep("hover away", () => InputManager.MoveMouseTo(Vector2.Zero)); AddAssert("container still expanded", () => container.Expanded.Value); } - private class TestExpandingContainer : ExpandingControlContainer + private class TestExpandingContainer : ExpandingContainer { public TestExpandingContainer() : base(120, 250) diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Overlays/ExpandableSlider.cs index d346b9b22c..062de98659 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Overlays/ExpandableSlider.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays /// /// An implementation for the UI slider bar control. /// - public class ExpandableSlider : CompositeDrawable, IExpandableControl, IHasCurrentValue + public class ExpandableSlider : CompositeDrawable, IExpandable, IHasCurrentValue where T : struct, IEquatable, IComparable, IConvertible where TSlider : OsuSliderBar, new() { @@ -72,8 +72,6 @@ namespace osu.Game.Overlays public BindableBool Expanded { get; } = new BindableBool(); - bool IExpandable.ShouldBeExpanded => IsHovered || slider.IsDragged; - public override bool HandlePositionalInput => true; public ExpandableSlider() diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Overlays/ExpandingButtonContainer.cs index d7ff285707..8fb3e1b550 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Overlays/ExpandingButtonContainer.cs @@ -1,17 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Graphics.UserInterface; - namespace osu.Game.Overlays { /// - /// An with a long hover expansion delay for buttons. + /// An with a long hover expansion delay. /// /// /// Mostly used for buttons with explanatory labels, in which the label would display after a "long hover". /// - public class ExpandingButtonContainer : ExpandingControlContainer + public class ExpandingButtonContainer : ExpandingContainer { protected ExpandingButtonContainer(float contractedWidth, float expandedWidth) : base(contractedWidth, expandedWidth) diff --git a/osu.Game/Overlays/ExpandingControlContainer.cs b/osu.Game/Overlays/ExpandingContainer.cs similarity index 54% rename from osu.Game/Overlays/ExpandingControlContainer.cs rename to osu.Game/Overlays/ExpandingContainer.cs index 859e4bcd25..ea3fffcb78 100644 --- a/osu.Game/Overlays/ExpandingControlContainer.cs +++ b/osu.Game/Overlays/ExpandingContainer.cs @@ -1,23 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { /// - /// Represents a with the ability to expand/contract when hovering the controls within it. + /// Represents a with the ability to expand/contract on hover. /// - /// The type of UI control to lookup for hover expansion. - public class ExpandingControlContainer : Container, IExpandingContainer - where TControl : class, IDrawable + public class ExpandingContainer : Container, IExpandingContainer { private readonly float contractedWidth; private readonly float expandedWidth; @@ -33,7 +29,7 @@ namespace osu.Game.Overlays protected FillFlowContainer FillFlow { get; } - protected ExpandingControlContainer(float contractedWidth, float expandedWidth) + protected ExpandingContainer(float contractedWidth, float expandedWidth) { this.contractedWidth = contractedWidth; this.expandedWidth = expandedWidth; @@ -57,7 +53,6 @@ namespace osu.Game.Overlays } private ScheduledDelegate hoverExpandEvent; - private TControl activeControl; protected override void LoadComplete() { @@ -69,61 +64,38 @@ namespace osu.Game.Overlays }, true); } - protected override void Update() - { - base.Update(); - - // if the container was expanded from hovering over a control, we have to check per-frame whether we can contract it back. - // that's because contracting the container depends not only on whether it's no longer hovered, - // but also on whether the hovered control is no longer in a dragged state (if it was). - if (hoverExpandEvent != null && !IsHovered && (activeControl == null || !isControlActive(activeControl))) - { - hoverExpandEvent?.Cancel(); - - Expanded.Value = false; - hoverExpandEvent = null; - activeControl = null; - } - } - protected override bool OnHover(HoverEvent e) { - queueExpandIfHovering(); + updateHoverExpansion(); return true; } protected override bool OnMouseMove(MouseMoveEvent e) { - queueExpandIfHovering(); + updateHoverExpansion(); return base.OnMouseMove(e); } - private void queueExpandIfHovering() + protected override void OnHoverLost(HoverLostEvent e) { - // if the same control is hovered or dragged, let the scheduled expand play out.. - if (activeControl != null && isControlActive(activeControl)) + if (hoverExpandEvent != null) + { + hoverExpandEvent?.Cancel(); + hoverExpandEvent = null; + + Expanded.Value = false; return; + } - // ..otherwise check whether a new control is hovered, and if so, queue a new hover operation. - hoverExpandEvent?.Cancel(); - - // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way - // to handle cases like the editor where the controls may be nested within a child hierarchy. - activeControl = FillFlow.ChildrenOfType().FirstOrDefault(isControlActive); - - if (activeControl != null && !Expanded.Value) - hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); + base.OnHoverLost(e); } - /// - /// Whether the given control is currently active, by checking whether it's hovered or dragged. - /// - private bool isControlActive(TControl control) + private void updateHoverExpansion() { - if (control is IExpandable expandable) - return expandable.ShouldBeExpanded; + hoverExpandEvent?.Cancel(); - return control.IsHovered || control.IsDragged; + if (IsHovered && !Expanded.Value) + hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay); } } } diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Overlays/IExpandable.cs index f998fc7b9f..770ac97847 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Overlays/IExpandable.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { @@ -16,15 +15,5 @@ namespace osu.Game.Overlays /// Whether this drawable is in an expanded state. /// BindableBool Expanded { get; } - - /// - /// Whether this drawable should be/stay expanded by a parenting . - /// By default, this is when this drawable is in a hovered or dragged state. - /// - /// - /// This is defined for certain controls which may have a child handling dragging instead. - /// (e.g. in which dragging is handled by their underlying control). - /// - bool ShouldBeExpanded => IsHovered || IsDragged; } } diff --git a/osu.Game/Overlays/IExpandableControl.cs b/osu.Game/Overlays/IExpandableControl.cs deleted file mode 100644 index 2cda6f467b..0000000000 --- a/osu.Game/Overlays/IExpandableControl.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Overlays -{ - /// - /// An interface for UI controls with the ability to expand/contract. - /// - public interface IExpandableControl : IExpandable - { - } -} From b9d9fc56afa5f579389201f8d06bfa41b3c3980b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 17:51:39 +0900 Subject: [PATCH 13/21] Move files to UI namespace --- .../Visual/UserInterface/TestSceneExpandingContainer.cs | 2 ++ .../Containers}/ExpandingButtonContainer.cs | 2 +- .../{Overlays => Graphics/Containers}/ExpandingContainer.cs | 3 +-- osu.Game/{Overlays => Graphics/Containers}/IExpandable.cs | 2 +- .../{Overlays => Graphics/Containers}/IExpandingContainer.cs | 3 ++- .../{Overlays => Graphics/UserInterface}/ExpandableSlider.cs | 4 ++-- osu.Game/Overlays/Settings/SettingsSidebar.cs | 1 + osu.Game/Overlays/SettingsToolboxGroup.cs | 1 + osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 9 files changed, 12 insertions(+), 8 deletions(-) rename osu.Game/{Overlays => Graphics/Containers}/ExpandingButtonContainer.cs (94%) rename osu.Game/{Overlays => Graphics/Containers}/ExpandingContainer.cs (97%) rename osu.Game/{Overlays => Graphics/Containers}/IExpandable.cs (93%) rename osu.Game/{Overlays => Graphics/Containers}/IExpandingContainer.cs (88%) rename osu.Game/{Overlays => Graphics/UserInterface}/ExpandableSlider.cs (97%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index f63591311f..f4920b4412 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections; using osuTK; diff --git a/osu.Game/Overlays/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs similarity index 94% rename from osu.Game/Overlays/ExpandingButtonContainer.cs rename to osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index 8fb3e1b550..b79af22bd2 100644 --- a/osu.Game/Overlays/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// An with a long hover expansion delay. diff --git a/osu.Game/Overlays/ExpandingContainer.cs b/osu.Game/Graphics/Containers/ExpandingContainer.cs similarity index 97% rename from osu.Game/Overlays/ExpandingContainer.cs rename to osu.Game/Graphics/Containers/ExpandingContainer.cs index ea3fffcb78..b50e008362 100644 --- a/osu.Game/Overlays/ExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingContainer.cs @@ -6,9 +6,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Threading; -using osu.Game.Graphics.Containers; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// Represents a with the ability to expand/contract on hover. diff --git a/osu.Game/Overlays/IExpandable.cs b/osu.Game/Graphics/Containers/IExpandable.cs similarity index 93% rename from osu.Game/Overlays/IExpandable.cs rename to osu.Game/Graphics/Containers/IExpandable.cs index 770ac97847..593564a2f9 100644 --- a/osu.Game/Overlays/IExpandable.cs +++ b/osu.Game/Graphics/Containers/IExpandable.cs @@ -4,7 +4,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// An interface for drawables with ability to expand/contract. diff --git a/osu.Game/Overlays/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs similarity index 88% rename from osu.Game/Overlays/IExpandingContainer.cs rename to osu.Game/Graphics/Containers/IExpandingContainer.cs index ec5f0c90f4..a82faa3cd1 100644 --- a/osu.Game/Overlays/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -3,8 +3,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.Containers { /// /// A target expanding container that should be resolved by children s to propagate state changes. diff --git a/osu.Game/Overlays/ExpandableSlider.cs b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs similarity index 97% rename from osu.Game/Overlays/ExpandableSlider.cs rename to osu.Game/Graphics/UserInterface/ExpandableSlider.cs index 062de98659..60e83f9c81 100644 --- a/osu.Game/Overlays/ExpandableSlider.cs +++ b/osu.Game/Graphics/UserInterface/ExpandableSlider.cs @@ -8,11 +8,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osuTK; -namespace osu.Game.Overlays +namespace osu.Game.Graphics.UserInterface { /// /// An implementation for the UI slider bar control. diff --git a/osu.Game/Overlays/Settings/SettingsSidebar.cs b/osu.Game/Overlays/Settings/SettingsSidebar.cs index e6ce90c33e..4e6a1eb914 100644 --- a/osu.Game/Overlays/Settings/SettingsSidebar.cs +++ b/osu.Game/Overlays/Settings/SettingsSidebar.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Settings { diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index 9e7223df9d..08321f68fe 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 92ea2db338..39783cc8bb 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Overlays; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; From 3aa5908de8d0bd691c5a5839130e3ce66fabe107 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 18:01:56 +0900 Subject: [PATCH 14/21] Remove unused using statement --- osu.Game/Graphics/Containers/IExpandingContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/IExpandingContainer.cs b/osu.Game/Graphics/Containers/IExpandingContainer.cs index a82faa3cd1..eb186c96a8 100644 --- a/osu.Game/Graphics/Containers/IExpandingContainer.cs +++ b/osu.Game/Graphics/Containers/IExpandingContainer.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { From e324287f796128a722a4d94435b02b90f356076e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Feb 2022 18:08:16 +0900 Subject: [PATCH 15/21] Reduce expansion delay on `ExpandingButtonContainer` Felt too long. --- osu.Game/Graphics/Containers/ExpandingButtonContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs index b79af22bd2..859850e771 100644 --- a/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs +++ b/osu.Game/Graphics/Containers/ExpandingButtonContainer.cs @@ -16,6 +16,6 @@ namespace osu.Game.Graphics.Containers { } - protected override double HoverExpansionDelay => 750; + protected override double HoverExpansionDelay => 400; } } From db74a226c0b3e5d59493075b124b4dc1e8fdb0a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 02:54:45 +0900 Subject: [PATCH 16/21] Fix test regression due to mouse overlapping settings overlay --- osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs index e34ec6c46a..bbab6380ba 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSideOverlays.cs @@ -19,6 +19,10 @@ namespace osu.Game.Tests.Visual.Menus base.SetUpSteps(); AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f); + + // avoids mouse interacting with settings overlay. + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded); } From efeba30b9fe0976fd99c89841029323f88c2f243 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 16:01:14 +0900 Subject: [PATCH 17/21] Remove ruleset and mod bindables from PlaylistItem --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- .../Visual/Multiplayer/QueueModeTestScene.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 41 +++++++------- .../TestSceneMatchBeatmapDetailArea.cs | 11 ++-- .../Multiplayer/TestSceneMultiplayer.cs | 49 ++++++++-------- .../TestSceneMultiplayerMatchSubScreen.cs | 11 ++-- .../Multiplayer/TestSceneMultiplayerPlayer.cs | 2 +- .../TestSceneMultiplayerPlaylist.cs | 4 +- .../TestSceneMultiplayerReadyButton.cs | 2 +- .../TestSceneMultiplayerSpectateButton.cs | 2 +- .../TestScenePlaylistsRoomSettingsPlaylist.cs | 11 ++-- .../TestScenePlaylistsSongSelect.cs | 19 ++++++- .../Visual/Multiplayer/TestSceneTeamVersus.cs | 8 +-- .../TestScenePlaylistsResultsScreen.cs | 2 +- .../TestScenePlaylistsRoomCreation.cs | 8 +-- .../Online/Multiplayer/MultiplayerClient.cs | 34 ++++------- .../Online/Rooms/MultiplayerPlaylistItem.cs | 4 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 6 +- osu.Game/Online/Rooms/PlaylistItem.cs | 56 ++----------------- .../OnlinePlay/Components/ModeTypeInfo.cs | 11 +++- .../OnlinePlay/Components/RoomManager.cs | 6 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 19 ++++--- .../Lounge/Components/RoomsContainer.cs | 3 +- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 5 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../OnlinePlay/OnlinePlaySongSelect.cs | 27 +++++---- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 6 +- .../Playlists/PlaylistsSongSelect.cs | 24 +++----- .../Multiplayer/MultiplayerTestScene.cs | 2 +- .../Visual/OnlinePlay/TestRoomManager.cs | 2 +- 31 files changed, 178 insertions(+), 207 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 343fc7e6e0..9aa04dda92 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Online selectedItem.Value = new PlaylistItem { Beatmap = { Value = testBeatmapInfo }, - Ruleset = { Value = testBeatmapInfo.Ruleset }, + RulesetID = testBeatmapInfo.Ruleset.OnlineID, }; Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker diff --git a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs index c79395b343..36d6c6a306 100644 --- a/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/QueueModeTestScene.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = InitialBeatmap }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } })); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 5c8c90e166..659cc22350 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -215,25 +216,25 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = 0, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, Expired = true, - RequiredMods = + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }, new PlaylistItem { ID = 1, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } } } @@ -314,12 +315,12 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapSet = new BeatmapSetInfo() } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } @@ -348,12 +349,12 @@ namespace osu.Game.Tests.Visual.Multiplayer ID = index++, OwnerID = 2, Beatmap = { Value = b }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 1d61a5d496..6144824ba0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -35,12 +36,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = SelectedRoom.Value.Playlist.Count, Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8f6ba6375f..41715f6cfb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -19,6 +19,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -99,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -235,7 +236,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -257,7 +258,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -287,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -340,7 +341,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -373,7 +374,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -393,7 +394,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -415,7 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -454,7 +455,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -493,7 +494,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -532,7 +533,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -566,7 +567,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -606,7 +607,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -626,8 +627,8 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - AllowedMods = { new OsuModHidden() } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new OsuModHidden()) } } } }); @@ -666,7 +667,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -697,7 +698,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }, API.LocalUser.Value); @@ -715,7 +716,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { ID = 2, Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -743,7 +744,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -779,7 +780,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -818,7 +819,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -849,7 +850,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -882,7 +883,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 869fb17317..a6151198cf 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -10,6 +10,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -72,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -89,8 +90,8 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new TaikoRuleset().RulesetInfo }, - AllowedMods = { new TaikoModSwap() } + RulesetID = new TaikoRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new TaikoModSwap()) } }); }); @@ -112,7 +113,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -127,7 +128,7 @@ namespace osu.Game.Tests.Visual.Multiplayer SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs index 73f2ed5b39..010e9dc078 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlayer.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }, Client.Room?.Users.ToArray())); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs index 936798e6b4..361178bfe4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs @@ -146,12 +146,12 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } + RulesetID = Ruleset.Value.OnlineID, }, new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value }, + RulesetID = Ruleset.Value.OnlineID, Expired = true } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 9867e5225e..7834226f15 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer selectedItem.Value = new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID }; if (button != null) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs index 42ae279667..70d4d9dd55 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer selectedItem.Value = new PlaylistItem { Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, }; Child = new FillFlowContainer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs index e63e58824f..8bfdda29d5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSettingsPlaylist.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Models; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; @@ -161,12 +162,12 @@ namespace osu.Game.Tests.Visual.Multiplayer BeatmapSet = new BeatmapSetInfo() } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, - RequiredMods = + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + RequiredMods = new[] { - new OsuModHardRock(), - new OsuModDoubleTime(), - new OsuModAutoplay() + new APIMod(new OsuModHardRock()), + new APIMod(new OsuModDoubleTime()), + new APIMod(new OsuModAutoplay()) } }); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index d933491ab6..3333afc88b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -115,8 +115,17 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2); AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); - AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); - AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 1 has rate 1.5", () => + { + var mod = (OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(1.5, mod.SpeedChange.Value); + }); + + AddAssert("item 2 has rate 2", () => + { + var mod = (OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(2, mod.SpeedChange.Value); + }); } /// @@ -138,7 +147,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); - AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item has rate 1.5", () => + { + var m = (OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset()); + return Precision.AlmostEquals(1.5, m.SpeedChange.Value); + }); } private class TestPlaylistsSongSelect : PlaylistsSongSelect diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index 781f0a1824..513c1413fa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID } } }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 11df115b1a..a05d01613c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Playlists LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID })); }); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs index 68225f6d64..578ea63b4e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomCreation.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Playlists room.Playlist.Add(new PlaylistItem { Beatmap = { Value = importedBeatmap.Beatmaps.First() }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Playlists } } }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } + RulesetID = new OsuRuleset().RulesetInfo.OnlineID }); }); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 903aaa89e3..9d45229961 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -727,30 +727,18 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); } - private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) + private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem { - var ruleset = Rulesets.GetRuleset(item.RulesetID); - - Debug.Assert(ruleset != null); - - var rulesetInstance = ruleset.CreateInstance(); - - var playlistItem = new PlaylistItem - { - ID = item.ID, - BeatmapID = item.BeatmapID, - OwnerID = item.OwnerID, - Ruleset = { Value = ruleset }, - Expired = item.Expired, - PlaylistOrder = item.PlaylistOrder, - PlayedAt = item.PlayedAt - }; - - playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); - playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance))); - - return playlistItem; - } + ID = item.ID, + BeatmapID = item.BeatmapID, + OwnerID = item.OwnerID, + RulesetID = item.RulesetID, + Expired = item.Expired, + PlaylistOrder = item.PlaylistOrder, + PlayedAt = item.PlayedAt, + RequiredMods = item.RequiredMods.ToArray(), + AllowedMods = item.AllowedMods.ToArray() + }; /// /// Retrieves a from an online source. diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs index 8ec073ff1e..d74cdd8c34 100644 --- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs +++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs @@ -66,8 +66,8 @@ namespace osu.Game.Online.Rooms BeatmapID = item.BeatmapID; BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty; RulesetID = item.RulesetID; - RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(); - AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray(); + RequiredMods = item.RequiredMods.ToArray(); + AllowedMods = item.AllowedMods.ToArray(); Expired = item.Expired; PlaylistOrder = item.PlaylistOrder ?? 0; PlayedAt = item.PlayedAt; diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index f1bb57bd9d..85327be037 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -65,7 +65,11 @@ namespace osu.Game.Online.Rooms public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) { - var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance(); + var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); + if (ruleset == null) + throw new InvalidOperationException($"Couldn't create score with unknown ruleset: {playlistItem.RulesetID}"); + + var rulesetInstance = ruleset.CreateInstance(); var scoreInfo = new ScoreInfo { diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 83a70c405b..c082babb01 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; @@ -10,8 +9,6 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Rooms { @@ -49,68 +46,25 @@ namespace osu.Game.Online.Rooms [JsonIgnore] public readonly Bindable Beatmap = new Bindable(); - [JsonIgnore] - public readonly Bindable Ruleset = new Bindable(); - - [JsonIgnore] - public readonly BindableList AllowedMods = new BindableList(); - - [JsonIgnore] - public readonly BindableList RequiredMods = new BindableList(); - [JsonProperty("beatmap")] private APIBeatmap apiBeatmap { get; set; } - private APIMod[] allowedModsBacking; - [JsonProperty("allowed_mods")] - private APIMod[] allowedMods - { - get => AllowedMods.Select(m => new APIMod(m)).ToArray(); - set => allowedModsBacking = value; - } - - private APIMod[] requiredModsBacking; + public APIMod[] AllowedMods { get; set; } = Array.Empty(); [JsonProperty("required_mods")] - private APIMod[] requiredMods - { - get => RequiredMods.Select(m => new APIMod(m)).ToArray(); - set => requiredModsBacking = value; - } + public APIMod[] RequiredMods { get; set; } = Array.Empty(); public PlaylistItem() { Beatmap.BindValueChanged(beatmap => BeatmapID = beatmap.NewValue?.OnlineID ?? -1); - Ruleset.BindValueChanged(ruleset => RulesetID = ruleset.NewValue?.OnlineID ?? 0); } public void MarkInvalid() => valid.Value = false; - public void MapObjects(IRulesetStore rulesets) + public void MapObjects() { Beatmap.Value ??= apiBeatmap; - Ruleset.Value ??= rulesets.GetRuleset(RulesetID); - - Debug.Assert(Ruleset.Value != null); - - Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); - - if (allowedModsBacking != null) - { - AllowedMods.Clear(); - AllowedMods.AddRange(allowedModsBacking.Select(m => m.ToMod(rulesetInstance))); - - allowedModsBacking = null; - } - - if (requiredModsBacking != null) - { - RequiredMods.Clear(); - RequiredMods.AddRange(requiredModsBacking.Select(m => m.ToMod(rulesetInstance))); - - requiredModsBacking = null; - } } #region Newtonsoft.Json implicit ShouldSerialize() methods @@ -133,7 +87,7 @@ namespace osu.Game.Online.Rooms && BeatmapID == other.BeatmapID && RulesetID == other.RulesetID && Expired == other.Expired - && allowedMods.SequenceEqual(other.allowedMods) - && requiredMods.SequenceEqual(other.requiredMods); + && AllowedMods.SequenceEqual(other.AllowedMods) + && RequiredMods.SequenceEqual(other.RequiredMods); } } diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index 2026106c42..d534a1e374 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables; +using osu.Game.Rulesets; using osuTK; namespace osu.Game.Screens.OnlinePlay.Components @@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Components private const float height = 28; private const float transition_duration = 100; + [Resolved] + private RulesetStore rulesets { get; set; } + private Container drawableRuleset; public ModeTypeInfo() @@ -56,11 +60,14 @@ namespace osu.Game.Screens.OnlinePlay.Components private void updateBeatmap() { var item = Playlist.FirstOrDefault(); + var ruleset = item == null ? null : rulesets.GetRuleset(item.RulesetID)?.CreateInstance(); - if (item?.Beatmap != null) + if (item?.Beatmap != null && ruleset != null) { + var mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray(); + drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, ruleset.RulesetInfo, mods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 238aa4059d..21b64b61bb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Rulesets; namespace osu.Game.Screens.OnlinePlay.Components { @@ -27,9 +26,6 @@ namespace osu.Game.Screens.OnlinePlay.Components protected IBindable JoinedRoom => joinedRoom; private readonly Bindable joinedRoom = new Bindable(); - [Resolved] - private IRulesetStore rulesets { get; set; } - [Resolved] private IAPIProvider api { get; set; } @@ -117,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Components try { foreach (var pi in room.Playlist) - pi.MapObjects(rulesets); + pi.MapObjects(); var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); if (existing == null) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e1f7ea5e92..dcf2a5a480 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -69,8 +69,9 @@ namespace osu.Game.Screens.OnlinePlay private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both }; private readonly IBindable valid = new Bindable(); private readonly Bindable beatmap = new Bindable(); - private readonly Bindable ruleset = new Bindable(); - private readonly BindableList requiredMods = new BindableList(); + + private IRulesetInfo ruleset; + private Mod[] requiredMods; private Container maskingContainer; private Container difficultyIconContainer; @@ -86,6 +87,9 @@ namespace osu.Game.Screens.OnlinePlay private PanelBackground panelBackground; private FillFlowContainer mainFillFlow; + [Resolved] + private RulesetStore rulesets { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -108,8 +112,6 @@ namespace osu.Game.Screens.OnlinePlay beatmap.BindTo(item.Beatmap); valid.BindTo(item.Valid); - ruleset.BindTo(item.Ruleset); - requiredMods.BindTo(item.RequiredMods); if (item.Expired) Colour = OsuColour.Gray(0.5f); @@ -119,6 +121,11 @@ namespace osu.Game.Screens.OnlinePlay private void load() { maskingContainer.BorderColour = colours.Yellow; + + ruleset = rulesets.GetRuleset(Item.RulesetID); + var rulesetInstance = ruleset?.CreateInstance(); + + requiredMods = Item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); } protected override void LoadComplete() @@ -145,9 +152,7 @@ namespace osu.Game.Screens.OnlinePlay }, true); beatmap.BindValueChanged(_ => Scheduler.AddOnce(refresh)); - ruleset.BindValueChanged(_ => Scheduler.AddOnce(refresh)); valid.BindValueChanged(_ => Scheduler.AddOnce(refresh)); - requiredMods.CollectionChanged += (_, __) => Scheduler.AddOnce(refresh); onScreenLoader.DelayedLoadStarted += _ => { @@ -276,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay } if (Item.Beatmap.Value != null) - difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; + difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) }; else difficultyIconContainer.Clear(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index f4d7823fcc..9f917c978c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; @@ -78,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { bool matchingFilter = true; - matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.MatchesOnlineID(criteria.Ruleset)); + matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.RulesetID == criteria.Ruleset.OnlineID); if (!string.IsNullOrEmpty(criteria.SearchString)) matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2d5225639f..02e1b115a0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -386,7 +386,9 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null || !this.IsCurrentScreen()) return; - Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); + var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToList(); } private void updateRuleset() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 073497e1ce..12caf1fde1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -11,7 +11,6 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -71,8 +70,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer BeatmapID = item.BeatmapID, BeatmapChecksum = item.Beatmap.Value.MD5Hash, RulesetID = item.RulesetID, - RequiredMods = item.RequiredMods.Select(m => new APIMod(m)).ToArray(), - AllowedMods = item.AllowedMods.Select(m => new APIMod(m)).ToArray() + RequiredMods = item.RequiredMods.ToArray(), + AllowedMods = item.AllowedMods.ToArray() }; Task task = itemToEdit != null ? client.EditPlaylistItem(multiplayerItem) : client.AddPlaylistItem(multiplayerItem); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a397493bab..cb50a56052 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -247,7 +247,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // update local mods based on room's reported status for the local user (omitting the base call implementation). // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). var ruleset = Ruleset.Value.CreateInstance(); - Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods.Select(m => m.ToMod(ruleset))).ToList(); } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 63957caee3..eab1f83967 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; @@ -37,6 +38,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] protected IBindable SelectedItem { get; private set; } + [Resolved] + private RulesetStore rulesets { get; set; } + protected override UserActivity InitialActivity => new UserActivity.InLobby(room); protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); @@ -78,10 +82,15 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. - // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = SelectedItem?.Value?.RequiredMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); - FreeMods.Value = SelectedItem?.Value?.AllowedMods.Select(m => m.DeepClone()).ToArray() ?? Array.Empty(); + var rulesetInstance = SelectedItem?.Value?.RulesetID == null ? null : rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + + if (rulesetInstance != null) + { + // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. + // Similarly, freeMods is currently empty but should only contain the allowed mods. + Mods.Value = SelectedItem.Value.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + FreeMods.Value = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + } Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); @@ -110,15 +119,11 @@ namespace osu.Game.Screens.OnlinePlay { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = - { - Value = Ruleset.Value - } + RulesetID = Ruleset.Value.OnlineID, + RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); - SelectItem(item); return true; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 35d417520e..2b071175d5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Extensions; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; @@ -36,10 +37,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (!Beatmap.Value.BeatmapInfo.MatchesOnlineID(PlaylistItem.Beatmap.Value)) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); - if (!ruleset.Value.MatchesOnlineID(PlaylistItem.Ruleset.Value)) + if (ruleset.Value.OnlineID != PlaylistItem.RulesetID) throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset"); - if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) + var localMods = Mods.Value.Select(m => new APIMod(m)).ToArray(); + if (!PlaylistItem.RequiredMods.All(m => localMods.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 0fd76f7e25..3ac576b18e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Screens; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; @@ -30,7 +31,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists break; case 1: - populateItemFromCurrent(Playlist.Single()); + Playlist.Clear(); + createNewItem(); break; } @@ -41,24 +43,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { PlaylistItem item = new PlaylistItem { - ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1 + ID = Playlist.Count == 0 ? 0 : Playlist.Max(p => p.ID) + 1, + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + RulesetID = Ruleset.Value.OnlineID, + RequiredMods = Mods.Value.Select(m => new APIMod(m)).ToArray(), + AllowedMods = FreeMods.Value.Select(m => new APIMod(m)).ToArray() }; - populateItemFromCurrent(item); - Playlist.Add(item); } - - private void populateItemFromCurrent(PlaylistItem item) - { - item.Beatmap.Value = Beatmap.Value.BeatmapInfo; - item.Ruleset.Value = Ruleset.Value; - - item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value.Select(m => m.DeepClone())); - - item.AllowedMods.Clear(); - item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.DeepClone())); - } } } diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 7607122ef0..ed86d572b9 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer new PlaylistItem { Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, - Ruleset = { Value = Ruleset.Value } + RulesetID = Ruleset.Value.OnlineID } } }; diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs index 4cbc6174c9..73d0df2c36 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay { room.Playlist.Add(new PlaylistItem { - Ruleset = { Value = ruleset }, + RulesetID = ruleset.OnlineID, Beatmap = { Value = new BeatmapInfo From 5b765581d83b9d7613e507697d68392c034d8ebe Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 16:18:40 +0900 Subject: [PATCH 18/21] Fix free mod selection not showing allowed mods --- .../TestSceneMultiplayerMatchSubScreen.cs | 25 +++++++++++++++++++ .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index a6151198cf..4dd3427bee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -14,11 +14,14 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; @@ -150,5 +153,27 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad); } + + [Test] + public void TestFreeModSelectionHasAllowedMods() + { + AddStep("add playlist item with allowed mod", () => + { + SelectedRoom.Value.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + AllowedMods = new[] { new APIMod(new OsuModDoubleTime()) } + }); + }); + + ClickButtonWhenEnabled(); + + AddUntilStep("wait for join", () => RoomJoined); + + ClickButtonWhenEnabled(); + + AddAssert("mod select contains only double time mod", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Mod is OsuModDoubleTime); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 02e1b115a0..836629ada0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -350,10 +351,12 @@ namespace osu.Game.Screens.OnlinePlay.Match if (selected == null) return; + var rulesetInstance = rulesets.GetRuleset(SelectedItem.Value.RulesetID)?.CreateInstance(); + Debug.Assert(rulesetInstance != null); + var allowedMods = SelectedItem.Value.AllowedMods.Select(m => m.ToMod(rulesetInstance)); + // Remove any user mods that are no longer allowed. - UserMods.Value = UserMods.Value - .Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType())) - .ToList(); + UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToList(); UpdateMods(); updateRuleset(); @@ -367,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); - userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType()); + userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType()); } } From f03de16ee5a46deac3b5f2ca1edfba5c4c5dca7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 16:27:10 +0900 Subject: [PATCH 19/21] Add a test EF database Created on d8a23aad4 (just before skins were migrated to realm). This contains: - 2 beatmap sets (intro and disco prince) - 1 score (set on disco prince using autopilot/DT) - 1 skin (haxwell) - 322 named files (from skin) - 5 named files (from beatmaps) - 270 total file infos --- osu.Game.Tests/Resources/client.db | Bin 0 -> 266240 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/client.db diff --git a/osu.Game.Tests/Resources/client.db b/osu.Game.Tests/Resources/client.db new file mode 100644 index 0000000000000000000000000000000000000000..079d5af3b7454bcd1c3acfdd199ce1a15c33567f GIT binary patch literal 266240 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%0AIS*{2p$_Webaed|cdzP^yMTaF-b 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 literal 0 HcmV?d00001 From b1cf014dc21a2dd61040522df015de5e4b6ed02a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Feb 2022 16:18:39 +0900 Subject: [PATCH 20/21] Add test coverage of EF to Realm migration process --- .../Navigation/TestEFToRealmMigration.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs new file mode 100644 index 0000000000..00a06d420e --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestEFToRealmMigration.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Scoring; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestEFToRealmMigration : OsuGameTestScene + { + public override void RecycleLocalStorage(bool isDisposing) + { + base.RecycleLocalStorage(isDisposing); + + if (isDisposing) + return; + + using (var outStream = LocalStorage.GetStream(DatabaseContextFactory.DATABASE_NAME, FileAccess.Write, FileMode.Create)) + using (var stream = TestResources.OpenResource(DatabaseContextFactory.DATABASE_NAME)) + stream.CopyTo(outStream); + } + + [Test] + public void TestMigration() + { + // Numbers are taken from the test database (see commit f03de16ee5a46deac3b5f2ca1edfba5c4c5dca7d). + AddAssert("Check beatmaps", () => Game.Dependencies.Get().Run(r => r.All().Count(s => !s.Protected) == 1)); + AddAssert("Check skins", () => Game.Dependencies.Get().Run(r => r.All().Count(s => !s.Protected) == 1)); + AddAssert("Check scores", () => Game.Dependencies.Get().Run(r => r.All().Count() == 1)); + + // One extra file is created during realm migration / startup due to the circles intro import. + AddAssert("Check files", () => Game.Dependencies.Get().Run(r => r.All().Count() == 271)); + } + } +} From 03106e846c6a6a70c944253a333aa922c1c2bde7 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Feb 2022 17:13:31 +0900 Subject: [PATCH 21/21] Fix test failures due to async mod icon loads --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 4dd3427bee..7c18ed2572 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -173,7 +173,8 @@ namespace osu.Game.Tests.Visual.Multiplayer ClickButtonWhenEnabled(); - AddAssert("mod select contains only double time mod", () => this.ChildrenOfType().Single().ChildrenOfType().Single().Mod is OsuModDoubleTime); + AddUntilStep("mod select contains only double time mod", + () => this.ChildrenOfType().SingleOrDefault()?.ChildrenOfType().SingleOrDefault()?.Mod is OsuModDoubleTime); } } }