diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml index 8ca9f38234..442b97c473 100644 --- a/.github/workflows/sentry-release.yml +++ b/.github/workflows/sentry-release.yml @@ -23,4 +23,4 @@ jobs: SENTRY_URL: https://sentry.ppy.sh/ with: environment: production - version: ${{ github.ref }} + version: osu@${{ github.ref_name }} diff --git a/osu.Android.props b/osu.Android.props index d5a77c6349..116c7dbfcd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index a36f07ff7b..496d495b43 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -17,11 +17,11 @@ using osu.Framework.Testing.Input; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; using osu.Game.Skinning; +using osu.Game.Tests.Gameplay; using osuTK; namespace osu.Game.Rulesets.Osu.Tests @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneGameplayCursor() { var ruleset = new OsuRuleset(); - gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty()); + gameplayState = TestGameplayState.Create(ruleset); AddStep("change background colour", () => { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 23500f5da6..79ff222a89 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (positionInfo == positionInfos.First()) { - positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); + positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2); positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); } else diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs index da73c2addb..266f7d1251 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs @@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (!(osuObject is Slider slider)) return; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); @@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (!(osuObject is Slider slider)) return; + // No need to update the head and tail circles, since slider handles that when the new slider path is set slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); @@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); } + + /// + /// Rotate a slider about its start position by the specified angle. + /// + /// The slider to be rotated. + /// The angle, measured in radians, to rotate the slider by. + public static void RotateSlider(Slider slider, float rotation) + { + void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position; + + // No need to update the head and tail circles, since slider handles that when the new slider path is set + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + slider.NestedHitObjects.OfType().ForEach(rotateNestedObject); + + var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(); + foreach (var point in controlPoints) + point.Position = rotateVector(point.Position, rotation); + + slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); + } + + /// + /// Rotate a vector by the specified angle. + /// + /// The vector to be rotated. + /// The angle, measured in radians, to rotate the vector by. + /// The rotated vector. + private static Vector2 rotateVector(Vector2 vector, float rotation) + { + float angle = MathF.Atan2(vector.Y, vector.X) + rotation; + float length = vector.Length; + return new Vector2( + length * MathF.Cos(angle), + length * MathF.Sin(angle) + ); + } } } diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs index d1bc3b45df..a77d1f8b0f 100644 --- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs +++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Primitives; +using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; @@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils foreach (OsuHitObject hitObject in hitObjects) { Vector2 relativePosition = hitObject.Position - previousPosition; - float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X); float relativeAngle = absoluteAngle - previousAngle; - positionInfos.Add(new ObjectPositionInfo(hitObject) + ObjectPositionInfo positionInfo; + positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject) { RelativeAngle = relativeAngle, DistanceFromPrevious = relativePosition.Length }); + if (hitObject is Slider slider) + { + float absoluteRotation = getSliderRotation(slider); + positionInfo.Rotation = absoluteRotation - absoluteAngle; + absoluteAngle = absoluteRotation; + } + previousPosition = hitObject.EndPosition; previousAngle = absoluteAngle; } @@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils if (hitObject is Spinner) { - previous = null; + previous = current; continue; } @@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils if (previous != null) { - Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; - Vector2 relativePosition = previous.HitObject.Position - earliestPosition; - previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X); + if (previous.HitObject is Slider s) + { + previousAbsoluteAngle = getSliderRotation(s); + } + else + { + Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre; + Vector2 relativePosition = previous.HitObject.Position - earliestPosition; + previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X); + } } float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle; var posRelativeToPrev = new Vector2( - current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle), - current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle) + current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle), + current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle) ); Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre; @@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev); current.PositionModified = lastEndPosition + posRelativeToPrev; + + if (!(current.HitObject is Slider slider)) + return; + + absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X); + + Vector2 centreOfMassOriginal = calculateCentreOfMass(slider); + Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider)); + centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified); + + float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X); + if (!Precision.AlmostEquals(relativeRotation, 0)) + RotateSlider(slider, relativeRotation); } /// @@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils var previousPosition = workingObject.PositionModified; // Clamp slider position to the placement area - // If the slider is larger than the playfield, force it to stay at the original position + // If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield float newX = possibleMovementBounds.Width < 0 - ? workingObject.PositionOriginal.X + ? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X) : Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right); float newY = possibleMovementBounds.Height < 0 - ? workingObject.PositionOriginal.Y + ? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y) : Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom); slider.Position = workingObject.PositionModified = new Vector2(newX, newY); @@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils ); } + /// + /// Estimate the centre of mass of a slider relative to its start position. + /// + /// The slider to process. + /// The centre of mass of the slider. + private static Vector2 calculateCentreOfMass(Slider slider) + { + const double sample_step = 50; + + // just sample the start and end positions if the slider is too short + if (slider.Distance <= sample_step) + { + return Vector2.Divide(slider.Path.PositionAt(1), 2); + } + + int count = 0; + Vector2 sum = Vector2.Zero; + double pathDistance = slider.Distance; + + for (double i = 0; i < pathDistance; i += sample_step) + { + sum += slider.Path.PositionAt(i / pathDistance); + count++; + } + + return sum / count; + } + + /// + /// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path. + /// + /// The slider to process. + /// The angle in radians. + private static float getSliderRotation(Slider slider) + { + var endPositionVector = slider.Path.PositionAt(1); + return MathF.Atan2(endPositionVector.Y, endPositionVector.X); + } + public class ObjectPositionInfo { /// @@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils /// public float DistanceFromPrevious { get; set; } + /// + /// The rotation of the hit object, relative to its jump angle. + /// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle. + /// For hit circles and spinners, this property is ignored. + /// + public float Rotation { get; set; } + /// /// The hit object associated with this . /// diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs index 4b9be77471..393d3886e7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components; +using osu.Game.Tests.Beatmaps; using osuTK; namespace osu.Game.Tests.Visual.Editing @@ -13,6 +16,9 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneEditorClock : EditorClockTestScene { + [Cached] + private EditorBeatmap editorBeatmap = new EditorBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)); + public TestSceneEditorClock() { Add(new FillFlowContainer diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs index 6aa884a197..bf0a7876a9 100644 --- a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Editing { [TestFixture] - public class TestScenePlaybackControl : OsuTestScene + public class TestScenePlaybackControl : EditorClockTestScene { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs index 46b45979ea..8dd368f2a9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTapTimingControl.cs @@ -77,6 +77,12 @@ namespace osu.Game.Tests.Visual.Editing timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType().First().BPM:N2}"; } + [Test] + public void TestNoop() + { + AddStep("do nothing", () => { }); + } + [Test] public void TestTapThenReset() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index e6fad33a51..d55852ec43 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -4,7 +4,9 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -18,6 +20,28 @@ namespace osu.Game.Tests.Visual.Editing { public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer); + [Test] + public void TestContextMenu() + { + TimelineHitObjectBlueprint blueprint; + + AddStep("add object", () => + { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new HitCircle { StartTime = 3000 }); + }); + + AddStep("click object", () => + { + blueprint = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(blueprint); + InputManager.Click(MouseButton.Left); + }); + + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("context menu open", () => this.ChildrenOfType().SingleOrDefault()?.State == MenuState.Open); + } + [Test] public void TestDisallowZeroDurationObjects() { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 17b8189fc7..a358166477 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -1,14 +1,18 @@ // 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.Allocation; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; +using osu.Game.Screens.Edit.Timing.RowAttributes; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { @@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private TimingScreen timingScreen; + protected override bool ScrollUsingMouseWheel => false; public TestSceneTimingScreen() @@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); Beatmap.Disabled = true; - Child = new TimingScreen + Child = timingScreen = new TimingScreen { State = { Value = Visibility.Visible }, }; } + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Stop clock", () => Clock.Stop()); + + AddUntilStep("wait for rows to load", () => Child.ChildrenOfType().Any()); + } + + [Test] + public void TestTrackingCurrentTimeWhileRunning() + { + AddStep("Select first effect point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); + AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + + AddStep("Seek to just before next point", () => Clock.Seek(69000)); + AddStep("Start clock", () => Clock.Start()); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); + } + + [Test] + public void TestTrackingCurrentTimeWhilePaused() + { + AddStep("Select first effect point", () => + { + InputManager.MoveMouseTo(Child.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670); + AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670); + + AddStep("Seek to later", () => Clock.Seek(80000)); + AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670); + } + protected override void Dispose(bool isDisposing) { Beatmap.Disabled = false; diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 4aed445d9d..93bfb288d2 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; @@ -38,25 +39,29 @@ namespace osu.Game.Tests.Visual.Editing Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0); - AddRange(new Drawable[] + Add(new OsuContextMenuContainer { - EditorBeatmap, - Composer, - new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new Drawable[] + EditorBeatmap, + Composer, + new FillFlowContainer { - new StartStopButton(), - new AudioVisualiser(), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new StartStopButton(), + new AudioVisualiser(), + } + }, + TimelineArea = new TimelineArea(CreateTestComponent()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, } - }, - TimelineArea = new TimelineArea(CreateTestComponent()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, } }); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 53364b6d89..e9aa85f4ce 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Graphics.Containers; using osu.Framework.Lists; using osu.Framework.Testing; using osu.Framework.Timing; @@ -22,7 +21,6 @@ using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Storyboards; -using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Gameplay { @@ -33,18 +31,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SkinManager skinManager { get; set; } - [Cached] - private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset()); - - [Cached(typeof(HealthProcessor))] - private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); - - [Cached] - private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()); - - [Cached] - private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); - protected override bool HasCustomSteps => true; [Test] @@ -81,11 +67,19 @@ namespace osu.Game.Tests.Visual.Gameplay if (expectedComponentsContainer == null) return false; - var expectedComponentsAdjustmentContainer = new Container + var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer { Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content), Size = actualComponentsContainer.DrawSize, Child = expectedComponentsContainer, + // proxy the same required dependencies that `actualComponentsContainer` is using. + CachedDependencies = new (Type, object)[] + { + (typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get()), + (typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get()), + (typeof(GameplayState), actualComponentsContainer.Dependencies.Get()), + (typeof(GameplayClock), actualComponentsContainer.Dependencies.Get()) + }, }; Add(expectedComponentsAdjustmentContainer); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 2d12645811..83c557ee51 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -15,7 +15,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Skinning; -using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Gameplay; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); [Cached] - private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()); + private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 81763564fa..8362739d3b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -1,7 +1,6 @@ // 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.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -14,16 +13,15 @@ using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; -using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Tests.Gameplay; using osu.Game.Tests.Mods; using osuTK; using osuTK.Graphics; @@ -41,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; [Cached] - private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); + private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [SetUpSteps] public void SetUpSteps() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs index 8150252d45..5f838b8813 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditorMultipleSkins.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Skinning.Editor; -using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Gameplay; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); [Cached] - private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()); + private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs index ac5e408d90..5f2d9ee9e8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Gameplay; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay private HealthProcessor healthProcessor = new DrainingHealthProcessor(0); [Cached] - private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()); + private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset()); [Cached] private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 8b420cebc8..b5cdd61ee5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -18,8 +18,8 @@ using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Play; -using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps.IO; +using osu.Game.Tests.Gameplay; using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Spectator; using osuTK; @@ -259,12 +259,15 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestFinalFramesPurgedBeforeEndingPlay() { - AddStep("begin playing", () => spectatorClient.BeginPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()), new Score())); + AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score())); AddStep("send frames and finish play", () => { spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero)); - spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true }); + + var completedGameplayState = TestGameplayState.Create(new OsuRuleset()); + completedGameplayState.HasPassed = true; + spectatorClient.EndPlaying(completedGameplayState); }); // We can't access API because we're an "online" test. diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index f8748922cf..2d2e05c4c9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -20,13 +20,13 @@ using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Screens.Play; +using osu.Game.Tests.Gameplay; using osu.Game.Tests.Mods; using osu.Game.Tests.Visual.Spectator; using osuTK; @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay CachedDependencies = new[] { (typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())), - (typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty())) + (typeof(GameplayState), TestGameplayState.Create(new OsuRuleset())) }, Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 7d010592ae..3172a68b81 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -124,13 +124,19 @@ namespace osu.Game.Tests.Visual.Multiplayer Status = { Value = new RoomStatusOpen() }, Category = { Value = RoomCategory.Spotlight }, }), + createLoungeRoom(new Room + { + Name = { Value = "Featured artist room" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.FeaturedArtist }, + }), } }; }); - AddUntilStep("wait for panel load", () => rooms.Count == 5); + AddUntilStep("wait for panel load", () => rooms.Count == 6); AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2); - AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3); + AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4); } [Test] diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 859727e632..9d206af40e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -10,7 +10,10 @@ using osu.Game.Rulesets; using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Drawables; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.BeatmapSet.Scores; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; namespace osu.Game.Tests.Visual.Online @@ -101,6 +104,14 @@ namespace osu.Game.Tests.Visual.Online AddStep("show many difficulties", () => overlay.ShowBeatmapSet(createManyDifficultiesBeatmapSet())); downloadAssert(true); + + AddAssert("status is loved", () => overlay.ChildrenOfType().Single().Status == BeatmapOnlineStatus.Loved); + AddAssert("scores container is visible", () => overlay.ChildrenOfType().Single().Alpha == 1); + + AddStep("go to second beatmap", () => overlay.ChildrenOfType().ElementAt(1).TriggerClick()); + + AddAssert("status is graveyard", () => overlay.ChildrenOfType().Single().Status == BeatmapOnlineStatus.Graveyard); + AddAssert("scores container is hidden", () => overlay.ChildrenOfType().Single().Alpha == 0); } [Test] @@ -232,6 +243,7 @@ namespace osu.Game.Tests.Visual.Online Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(), Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(), }, + Status = i % 2 == 0 ? BeatmapOnlineStatus.Graveyard : BeatmapOnlineStatus.Loved, }); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs index e4bc5645b6..39a4f1a8a1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online { leaveText.Text = $"OnRequestLeave: {channel.Name}"; leaveText.FadeOutFromOne(1000, Easing.InQuint); - selected.Value = null; + selected.Value = channelList.ChannelListingChannel; channelList.RemoveChannel(channel); }; @@ -112,6 +112,12 @@ namespace osu.Game.Tests.Visual.Online for (int i = 0; i < 10; i++) channelList.AddChannel(createRandomPrivateChannel()); }); + + AddStep("Add Announce Channels", () => + { + for (int i = 0; i < 2; i++) + channelList.AddChannel(createRandomAnnounceChannel()); + }); } [Test] @@ -170,5 +176,16 @@ namespace osu.Game.Tests.Visual.Online Username = $"test user {id}", }); } + + private Channel createRandomAnnounceChannel() + { + int id = RNG.Next(0, 10000); + return new Channel + { + Name = $"Announce {id}", + Type = ChannelType.Announce, + Id = id, + }; + } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs deleted file mode 100644 index e6eaffc4c1..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelTabControl.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Utils; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Chat; -using osu.Game.Overlays.Chat.Tabs; -using osuTK.Graphics; - -namespace osu.Game.Tests.Visual.Online -{ - public class TestSceneChannelTabControl : OsuTestScene - { - private readonly TestTabControl channelTabControl; - - public TestSceneChannelTabControl() - { - SpriteText currentText; - Add(new Container - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Children = new Drawable[] - { - channelTabControl = new TestTabControl - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Height = 50 - }, - new Box - { - Colour = Color4.Black.Opacity(0.1f), - RelativeSizeAxes = Axes.X, - Height = 50, - Depth = -1, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - } - }); - - Add(new Container - { - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Children = new Drawable[] - { - currentText = new OsuSpriteText - { - Text = "Currently selected channel:" - } - } - }); - - channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel); - channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue; - - AddStep("Add random private channel", addRandomPrivateChannel); - AddAssert("There is only one channels", () => channelTabControl.Items.Count == 2); - AddRepeatStep("Add 3 random private channels", addRandomPrivateChannel, 3); - AddAssert("There are four channels", () => channelTabControl.Items.Count == 5); - AddStep("Add random public channel", () => addChannel(RNG.Next().ToString())); - - AddRepeatStep("Select a random channel", () => - { - List validChannels = channelTabControl.Items.Where(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)).ToList(); - channelTabControl.SelectChannel(validChannels[RNG.Next(0, validChannels.Count)]); - }, 20); - - Channel channelBefore = null; - AddStep("set first channel", () => channelTabControl.SelectChannel(channelBefore = channelTabControl.Items.First(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)))); - - AddStep("select selector tab", () => channelTabControl.SelectChannel(channelTabControl.Items.Single(c => c is ChannelSelectorTabItem.ChannelSelectorTabChannel))); - AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value); - - AddAssert("check channel unchanged", () => channelBefore == channelTabControl.Current.Value); - - AddStep("set second channel", () => channelTabControl.SelectChannel(channelTabControl.Items.GetNext(channelBefore))); - AddAssert("selector tab is inactive", () => !channelTabControl.ChannelSelectorActive.Value); - - AddUntilStep("remove all channels", () => - { - foreach (var item in channelTabControl.Items.ToList()) - { - if (item is ChannelSelectorTabItem.ChannelSelectorTabChannel) - continue; - - channelTabControl.RemoveChannel(item); - return false; - } - - return true; - }); - - AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value); - } - - private void addRandomPrivateChannel() => - channelTabControl.AddChannel(new Channel(new APIUser - { - Id = RNG.Next(1000, 10000000), - Username = "Test User " + RNG.Next(1000) - })); - - private void addChannel(string name) => - channelTabControl.AddChannel(new Channel - { - Type = ChannelType.Public, - Name = name - }); - - private class TestTabControl : ChannelTabControl - { - public void SelectChannel(Channel channel) => base.SelectTab(TabMap[channel]); - } - } -} diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 6818147da4..a28de3be1e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; -using osu.Game.Overlays; using osu.Game.Overlays.Chat; using osuTK.Graphics; @@ -22,12 +21,10 @@ namespace osu.Game.Tests.Visual.Online public class TestSceneChatLink : OsuTestScene { private readonly TestChatLineContainer textContainer; - private readonly DialogOverlay dialogOverlay; private Color4 linkColour; public TestSceneChatLink() { - Add(dialogOverlay = new DialogOverlay { Depth = float.MinValue }); Add(textContainer = new TestChatLineContainer { Padding = new MarginPadding { Left = 20, Right = 20 }, @@ -47,9 +44,6 @@ namespace osu.Game.Tests.Visual.Online availableChannels.Add(new Channel { Name = "#english" }); availableChannels.Add(new Channel { Name = "#japanese" }); Dependencies.Cache(chatManager); - - Dependencies.Cache(new ChatOverlay()); - Dependencies.CacheAs(dialogOverlay); } [SetUp] diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 4d1dee1650..2cf1114f30 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -1,19 +1,22 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// 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.Collections.Generic; using System.Linq; +using System.Collections.Generic; using System.Net; +using System.Threading; using JetBrains.Annotations; using NUnit.Framework; 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.Input; +using osu.Framework.Logging; using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -21,387 +24,223 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Overlays; using osu.Game.Overlays.Chat; -using osu.Game.Overlays.Chat.Selection; -using osu.Game.Overlays.Chat.Tabs; +using osu.Game.Overlays.Chat.Listing; +using osu.Game.Overlays.Chat.ChannelList; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Online { + [TestFixture] public class TestSceneChatOverlay : OsuManualInputManagerTestScene { private TestChatOverlay chatOverlay; private ChannelManager channelManager; - private IEnumerable visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+"); - private IEnumerable joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+"); - private readonly List channels; + private APIUser testUser; + private Channel testPMChannel; + private Channel[] testChannels; - private Channel currentChannel => channelManager.CurrentChannel.Value; - private Channel nextChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) + 1); - private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); - private Channel channel1 => channels[0]; - private Channel channel2 => channels[1]; - private Channel channel3 => channels[2]; + private Channel testChannel1 => testChannels[0]; + private Channel testChannel2 => testChannels[1]; - [CanBeNull] - private Func> onGetMessages; - - public TestSceneChatOverlay() - { - channels = Enumerable.Range(1, 10) - .Select(index => new Channel(new APIUser()) - { - Name = $"Channel no. {index}", - Topic = index == 3 ? null : $"We talk about the number {index} here", - Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary, - Id = index - }) - .ToList(); - } + [Resolved] + private OsuConfigManager config { get; set; } = null!; [SetUp] - public void Setup() + public void SetUp() => Schedule(() => { - Schedule(() => + testUser = new APIUser { Username = "test user", Id = 5071479 }; + testPMChannel = new Channel(testUser); + testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); + + Child = new DependencyProvidingContainer { - ChannelManagerContainer container; - - Child = container = new ChannelManagerContainer(channels) + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] { - RelativeSizeAxes = Axes.Both, - }; - - chatOverlay = container.ChatOverlay; - channelManager = container.ChannelManager; - }); - } + (typeof(ChannelManager), channelManager = new ChannelManager()), + }, + Children = new Drawable[] + { + channelManager, + chatOverlay = new TestChatOverlay(), + }, + }; + }); [SetUpSteps] public void SetUpSteps() { - AddStep("register request handling", () => + AddStep("Setup request handler", () => { - onGetMessages = null; - ((DummyAPIAccess)API).HandleRequest = req => { switch (req) { + case GetUpdatesRequest getUpdates: + getUpdates.TriggerFailure(new WebException()); + return true; + case JoinChannelRequest joinChannel: joinChannel.TriggerSuccess(); return true; - case GetUserRequest getUser: - if (getUser.Lookup.Equals("some body", StringComparison.OrdinalIgnoreCase)) - { - getUser.TriggerSuccess(new APIUser - { - Username = "some body", - Id = 1, - }); - } - else - { - getUser.TriggerFailure(new WebException()); - } - + case LeaveChannelRequest leaveChannel: + leaveChannel.TriggerSuccess(); return true; case GetMessagesRequest getMessages: - var messages = onGetMessages?.Invoke(getMessages.Channel); - if (messages != null) - getMessages.TriggerSuccess(messages); + getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel)); return true; - } - return false; + case GetUserRequest getUser: + if (getUser.Lookup == testUser.Username) + getUser.TriggerSuccess(testUser); + else + getUser.TriggerFailure(new WebException()); + return true; + + case PostMessageRequest postMessage: + postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000)) + { + Content = postMessage.Message.Content, + ChannelId = postMessage.Message.ChannelId, + Sender = postMessage.Message.Sender, + Timestamp = new DateTimeOffset(DateTime.Now), + }); + return true; + + default: + Logger.Log($"Unhandled Request Type: {req.GetType()}"); + return false; + } }; }); + + AddStep("Add test channels", () => + { + (channelManager.AvailableChannels as BindableList)?.AddRange(testChannels); + }); } [Test] - public void TestHideOverlay() + public void TestBasic() { - AddStep("Open chat overlay", () => chatOverlay.Show()); + AddStep("Show overlay with channel", () => + { + chatOverlay.Show(); + Channel joinedChannel = channelManager.JoinChannel(testChannel1); + channelManager.CurrentChannel.Value = joinedChannel; + }); + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + waitForChannel1Visible(); + } - AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); - AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); + [Test] + public void TestShowHide() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden); + } - AddStep("Close chat overlay", () => chatOverlay.Hide()); + [Test] + public void TestChatHeight() + { + BindableFloat configChatHeight = new BindableFloat(); - AddAssert("Chat overlay was hidden", () => chatOverlay.State.Value == Visibility.Hidden); - AddAssert("Channel selection overlay was hidden", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); + float newHeight = 0; + + AddStep("Reset config chat height", () => + { + config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight); + configChatHeight.SetDefault(); + }); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); + AddStep("Click top bar", () => + { + InputManager.MoveMouseTo(chatOverlayTopBar); + InputManager.PressButton(MouseButton.Left); + }); + AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); + AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); + AddStep("Store new height", () => newHeight = chatOverlay.Height); + AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight); } [Test] public void TestChannelSelection() { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); - AddStep("Setup get message response", () => onGetMessages = channel => - { - if (channel == channel1) - { - return new List - { - new Message(1) - { - ChannelId = channel1.Id, - Content = "hello from channel 1!", - Sender = new APIUser - { - Id = 2, - Username = "test_user" - } - } - }; - } - - return null; - }); - - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - - AddAssert("Current channel is channel 1", () => currentChannel == channel1); - AddUntilStep("Loading spinner hidden", () => chatOverlay.ChildrenOfType().All(spinner => !spinner.IsPresent)); - AddAssert("Channel message shown", () => chatOverlay.ChildrenOfType().Count() == 1); - AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Listing is visible", () => listingIsVisible); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + waitForChannel1Visible(); } [Test] - public void TestSearchInSelector() + public void TestSearchInListing() { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType().First().Text = "no. 2"); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Listing is visible", () => listingIsVisible); + AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2"); AddUntilStep("Only channel 2 visible", () => { - var listItems = chatOverlay.ChildrenOfType().Where(c => c.IsPresent); - return listItems.Count() == 1 && listItems.Single().Channel == channel2; + IEnumerable listingItems = chatOverlay.ChildrenOfType() + .Where(item => item.IsPresent); + return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2; }); } - [Test] - public void TestChannelShortcutKeys() - { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); - AddStep("Close channel selector", () => InputManager.Key(Key.Escape)); - AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - - for (int zeroBasedIndex = 0; zeroBasedIndex < 10; ++zeroBasedIndex) - { - int oneBasedIndex = zeroBasedIndex + 1; - int targetNumberKey = oneBasedIndex % 10; - var targetChannel = channels[zeroBasedIndex]; - AddStep($"Press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); - AddAssert($"Channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel); - } - } - - private Channel expectedChannel; - - [Test] - public void TestCloseChannelBehaviour() - { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddUntilStep("Join until dropdown has channels", () => - { - if (visibleChannels.Count() < joinedChannels.Count()) - return true; - - // Using temporary channels because they don't hide their names when not active - channelManager.JoinChannel(new Channel - { - Name = $"Channel no. {joinedChannels.Count() + 11}", - Type = ChannelType.Temporary - }); - - return false; - }); - - AddStep("Switch to last tab", () => clickDrawable(chatOverlay.TabMap[visibleChannels.Last()])); - AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); - - // Closing the last channel before dropdown - AddStep("Close current channel", () => - { - expectedChannel = nextChannel; - chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); - }); - AddAssert("Next channel selected", () => currentChannel == expectedChannel); - AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); - - // Depending on the window size, one more channel might need to be closed for the selectorTab to appear - AddUntilStep("Close channels until selector visible", () => - { - if (chatOverlay.ChannelTabControl.VisibleItems.Last().Name == "+") - return true; - - chatOverlay.ChannelTabControl.RemoveChannel(visibleChannels.Last()); - return false; - }); - AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last()); - - // Closing the last channel with dropdown no longer present - AddStep("Close last when selector next", () => - { - expectedChannel = previousChannel; - chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); - }); - AddAssert("Previous channel selected", () => currentChannel == expectedChannel); - - // Standard channel closing - AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1)); - AddStep("Close current channel", () => - { - expectedChannel = nextChannel; - chatOverlay.ChannelTabControl.RemoveChannel(currentChannel); - }); - AddAssert("Next channel selected", () => currentChannel == expectedChannel); - - // Selector reappearing after all channels closed - AddUntilStep("Close all channels", () => - { - if (!joinedChannels.Any()) - return true; - - chatOverlay.ChannelTabControl.RemoveChannel(joinedChannels.Last()); - return false; - }); - AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); - } - [Test] public void TestChannelCloseButton() { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join 2 channels", () => + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join PM and public channels", () => { - channelManager.JoinChannel(channel1); - channelManager.JoinChannel(channel2); + channelManager.JoinChannel(testChannel1); + channelManager.JoinChannel(testPMChannel); }); - - // PM channel close button only appears when active - AddStep("Select PM channel", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Click PM close button", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); - AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(channel2)); - - // Non-PM chat channel close button only appears when hovered - AddStep("Hover normal channel tab", () => InputManager.MoveMouseTo(chatOverlay.TabMap[channel1])); - AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); - AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any()); - } - - [Test] - public void TestCloseTabShortcut() - { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join 2 channels", () => + AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); + AddStep("Click close button", () => { - channelManager.JoinChannel(channel1); - channelManager.JoinChannel(channel2); + ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single(); + clickDrawable(closeButton); }); - - // Want to close channel 2 - AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close tab via shortcut", pressCloseDocumentKeys); - - // Channel 2 should be closed - AddAssert("Channel 1 open", () => channelManager.JoinedChannels.Contains(channel1)); - AddAssert("Channel 2 closed", () => !channelManager.JoinedChannels.Contains(channel2)); - - // Want to close channel 1 - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - - AddStep("Close tab via shortcut", pressCloseDocumentKeys); - // Channel 1 and channel 2 should be closed - AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any()); - } - - [Test] - public void TestNewTabShortcut() - { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join 2 channels", () => + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); + AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Click close button", () => { - channelManager.JoinChannel(channel1); - channelManager.JoinChannel(channel2); + ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType().Single(); + clickDrawable(closeButton); }); - - // Want to join another channel - AddStep("Press new tab shortcut", pressNewTabKeys); - - // Selector should be visible - AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); - } - - [Test] - public void TestRestoreTabShortcut() - { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join 3 channels", () => - { - channelManager.JoinChannel(channel1); - channelManager.JoinChannel(channel2); - channelManager.JoinChannel(channel3); - }); - - // Should do nothing - AddStep("Restore tab via shortcut", pressRestoreTabKeys); - AddAssert("All channels still open", () => channelManager.JoinedChannels.Count == 3); - - // Close channel 1 - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); - AddAssert("Channel 1 closed", () => !channelManager.JoinedChannels.Contains(channel1)); - AddAssert("Other channels still open", () => channelManager.JoinedChannels.Count == 2); - - // Reopen channel 1 - AddStep("Restore tab via shortcut", pressRestoreTabKeys); - AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3); - AddAssert("Current channel is channel 1", () => currentChannel == channel1); - - // Close two channels - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); - AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); - AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); - AddAssert("Only one channel open", () => channelManager.JoinedChannels.Count == 1); - AddAssert("Current channel is channel 3", () => currentChannel == channel3); - - // Should first re-open channel 2 - AddStep("Restore tab via shortcut", pressRestoreTabKeys); - AddAssert("Channel 1 still closed", () => !channelManager.JoinedChannels.Contains(channel1)); - AddAssert("Channel 2 now open", () => channelManager.JoinedChannels.Contains(channel2)); - AddAssert("Current channel is channel 2", () => currentChannel == channel2); - - // Should then re-open channel 1 - AddStep("Restore tab via shortcut", pressRestoreTabKeys); - AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3); - AddAssert("Current channel is channel 1", () => currentChannel == channel1); + AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); } [Test] public void TestChatCommand() { - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - - AddStep("Open chat with user", () => channelManager.PostCommand("chat some body")); + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); AddAssert("PM channel is selected", () => - channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body"); - - AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat nobody")); + channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); + AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist")); AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage); // Make sure no unnecessary requests are made when the PM channel is already open. - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null); - AddStep("Open chat with user", () => channelManager.PostCommand("chat some body")); + AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); AddAssert("PM channel is selected", () => - channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body"); + channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); } [Test] @@ -409,20 +248,17 @@ namespace osu.Game.Tests.Visual.Online { Channel multiplayerChannel = null; - AddStep("open chat overlay", () => chatOverlay.Show()); - - AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) { Name = "#mp_1", Type = ChannelType.Multiplayer, })); - - AddAssert("channel joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); - AddAssert("channel not present in overlay", () => !chatOverlay.TabMap.ContainsKey(multiplayerChannel)); - AddAssert("multiplayer channel is not current", () => channelManager.CurrentChannel.Value != multiplayerChannel); - - AddStep("leave channel", () => channelManager.LeaveChannel(multiplayerChannel)); - AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel)); + AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); + AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType() + .Where(item => item.IsPresent) + .Select(item => item.Channel) + .Contains(multiplayerChannel)); } [Test] @@ -430,26 +266,21 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Send message in channel 1", () => { - channel1.AddNewMessages(message = new Message + testChannel1.AddNewMessages(message = new Message { - ChannelId = channel1.Id, + ChannelId = testChannel1.Id, Content = "Message to highlight!", Timestamp = DateTimeOffset.Now, - Sender = new APIUser - { - Id = 2, - Username = "Someone", - } + Sender = testUser, }); }); - - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1)); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + waitForChannel1Visible(); } [Test] @@ -457,28 +288,22 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Open chat overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - - AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Send message in channel 2", () => { - channel2.AddNewMessages(message = new Message + testChannel2.AddNewMessages(message = new Message { - ChannelId = channel2.Id, + ChannelId = testChannel2.Id, Content = "Message to highlight!", Timestamp = DateTimeOffset.Now, - Sender = new APIUser - { - Id = 2, - Username = "Someone", - } + Sender = testUser, }); }); - - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2)); - AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); + waitForChannel2Visible(); } [Test] @@ -486,30 +311,23 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Open chat overlay", () => chatOverlay.Show()); - - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); - - AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Send message in channel 2", () => { - channel2.AddNewMessages(message = new Message + testChannel2.AddNewMessages(message = new Message { - ChannelId = channel2.Id, + ChannelId = testChannel2.Id, Content = "Message to highlight!", Timestamp = DateTimeOffset.Now, - Sender = new APIUser - { - Id = 2, - Username = "Someone", - } + Sender = testUser, }); }); - AddStep("Leave channel 2", () => channelManager.LeaveChannel(channel2)); - - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2)); - AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2); + AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2)); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); + waitForChannel2Visible(); } [Test] @@ -517,24 +335,19 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Send message in channel 1", () => { - channel1.AddNewMessages(message = new Message + testChannel1.AddNewMessages(message = new Message { - ChannelId = channel1.Id, + ChannelId = testChannel1.Id, Content = "Message to highlight!", Timestamp = DateTimeOffset.Now, - Sender = new APIUser - { - Id = 2, - Username = "Someone", - } + Sender = testUser, }); }); - - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1)); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + waitForChannel1Visible(); } [Test] @@ -542,40 +355,169 @@ namespace osu.Game.Tests.Visual.Online { Message message = null; - AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); - + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Send message in channel 1", () => { - channel1.AddNewMessages(message = new Message + testChannel1.AddNewMessages(message = new Message { - ChannelId = channel1.Id, + ChannelId = testChannel1.Id, Content = "Message to highlight!", Timestamp = DateTimeOffset.Now, - Sender = new APIUser - { - Id = 2, - Username = "Someone", - } + Sender = testUser, }); }); - AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null); - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1)); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + waitForChannel1Visible(); } - private void pressChannelHotkey(int number) + [Test] + public void TestTextBoxRetainsFocus() { - var channelKey = Key.Number0 + number; - InputManager.PressKey(Key.AltLeft); - InputManager.Key(channelKey); - InputManager.ReleaseKey(Key.AltLeft); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + waitForChannel1Visible(); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel)); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click selector", () => clickDrawable(channelSelectorButton)); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); } - private void pressCloseDocumentKeys() => InputManager.Keys(PlatformAction.DocumentClose); + [Test] + public void TestSlowLoadingChannel() + { + AddStep("Show overlay (slow-loading)", () => + { + chatOverlay.Show(); + chatOverlay.SlowLoading = true; + }); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddUntilStep("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading); - private void pressNewTabKeys() => InputManager.Keys(PlatformAction.TabNew); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2))); + AddUntilStep("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading); - private void pressRestoreTabKeys() => InputManager.Keys(PlatformAction.TabRestore); + AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set()); + AddUntilStep("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready); + AddAssert("Channel 1 not displayed", () => !channelIsVisible); + + AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set()); + AddUntilStep("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded); + waitForChannel2Visible(); + + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddUntilStep("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded); + waitForChannel1Visible(); + } + + [Test] + public void TestKeyboardCloseAndRestoreChannel() + { + AddStep("Show overlay with channel 1", () => + { + channelManager.JoinChannel(testChannel1); + chatOverlay.Show(); + }); + waitForChannel1Visible(); + AddStep("Press document close keys", () => InputManager.Keys(PlatformAction.DocumentClose)); + AddAssert("Listing is visible", () => listingIsVisible); + + AddStep("Press tab restore keys", () => InputManager.Keys(PlatformAction.TabRestore)); + waitForChannel1Visible(); + } + + [Test] + public void TestKeyboardNewChannel() + { + AddStep("Show overlay with channel 1", () => + { + channelManager.JoinChannel(testChannel1); + chatOverlay.Show(); + }); + waitForChannel1Visible(); + AddStep("Press tab new keys", () => InputManager.Keys(PlatformAction.TabNew)); + AddAssert("Listing is visible", () => listingIsVisible); + } + + [Test] + public void TestKeyboardNextChannel() + { + Channel announceChannel = createAnnounceChannel(); + Channel pmChannel1 = createPrivateChannel(); + Channel pmChannel2 = createPrivateChannel(); + + AddStep("Show overlay with channels", () => + { + channelManager.JoinChannel(testChannel1); + channelManager.JoinChannel(testChannel2); + channelManager.JoinChannel(pmChannel1); + channelManager.JoinChannel(pmChannel2); + channelManager.JoinChannel(announceChannel); + chatOverlay.Show(); + }); + + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + + waitForChannel1Visible(); + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + waitForChannel2Visible(); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddUntilStep("PM Channel 1 displayed", () => channelIsVisible && currentDrawableChannel?.Channel == pmChannel1); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddUntilStep("PM Channel 2 displayed", () => channelIsVisible && currentDrawableChannel?.Channel == pmChannel2); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + AddUntilStep("Announce channel displayed", () => channelIsVisible && currentDrawableChannel?.Channel == announceChannel); + + AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext)); + waitForChannel1Visible(); + } + + private void waitForChannel1Visible() => + AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel?.Channel == testChannel1); + + private void waitForChannel2Visible() => + AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel?.Channel == testChannel2); + + private bool listingIsVisible => + chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible; + + private bool loadingIsVisible => + chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible; + + private bool channelIsVisible => + !listingIsVisible && !loadingIsVisible; + + [CanBeNull] + private DrawableChannel currentDrawableChannel => + chatOverlay.ChildrenOfType().SingleOrDefault(); + + private ChannelListItem getChannelListItem(Channel channel) => + chatOverlay.ChildrenOfType().Single(item => item.Channel == channel); + + private ChatTextBox chatOverlayTextBox => + chatOverlay.ChildrenOfType().Single(); + + private ChatOverlayTopBar chatOverlayTopBar => + chatOverlay.ChildrenOfType().Single(); + + private ChannelListItem channelSelectorButton => + chatOverlay.ChildrenOfType().Single(item => item.Channel is ChannelListing.ChannelListingChannel); private void clickDrawable(Drawable d) { @@ -583,81 +525,75 @@ namespace osu.Game.Tests.Visual.Online InputManager.Click(MouseButton.Left); } - private class ChannelManagerContainer : Container + private List createChannelMessages(Channel channel) { - public TestChatOverlay ChatOverlay { get; private set; } - - [Cached] - public ChannelManager ChannelManager { get; } = new ChannelManager(); - - private readonly List channels; - - public ChannelManagerContainer(List channels) + var message = new Message + { + ChannelId = channel.Id, + Content = $"Hello, this is a message in {channel.Name}", + Sender = testUser, + Timestamp = new DateTimeOffset(DateTime.Now), + }; + return new List { message }; + } + + private Channel createPublicChannel(int id) => new Channel + { + Id = id, + Name = $"#channel-{id}", + Topic = $"We talk about the number {id} here", + Type = ChannelType.Public, + }; + + private Channel createPrivateChannel() + { + int id = RNG.Next(0, 10000); + return new Channel(new APIUser + { + Id = id, + Username = $"test user {id}", + }); + } + + private Channel createAnnounceChannel() + { + int id = RNG.Next(0, 10000); + return new Channel + { + Name = $"Announce {id}", + Type = ChannelType.Announce, + Id = id, + }; + } + + private class TestChatOverlay : ChatOverlay + { + public bool SlowLoading { get; set; } + + public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel); + + protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) + { + return SlowLoading + ? new SlowLoadingDrawableChannel(newChannel) + : new ChatOverlayDrawableChannel(newChannel); + } + } + + private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel + { + public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim(); + + public SlowLoadingDrawableChannel([NotNull] Channel channel) + : base(channel) { - this.channels = channels; } [BackgroundDependencyLoader] private void load() { - ((BindableList)ChannelManager.AvailableChannels).AddRange(channels); - - InternalChildren = new Drawable[] - { - ChannelManager, - ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, }, - }; + LoadEvent.Wait(10000); } } - - private class TestChatOverlay : ChatOverlay - { - public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value; - - public new ChannelTabControl ChannelTabControl => base.ChannelTabControl; - - public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay; - - protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl(); - - public IReadOnlyDictionary> TabMap => ((TestTabControl)ChannelTabControl).TabMap; - } - - private class TestTabControl : ChannelTabControl - { - protected override TabItem CreateTabItem(Channel value) - { - switch (value.Type) - { - case ChannelType.PM: - return new TestPrivateChannelTabItem(value); - - default: - return new TestChannelTabItem(value); - } - } - - public new IReadOnlyDictionary> TabMap => base.TabMap; - } - - private class TestChannelTabItem : ChannelTabItem - { - public TestChannelTabItem(Channel channel) - : base(channel) - { - } - - public new ClickableContainer CloseButton => base.CloseButton; - } - - private class TestPrivateChannelTabItem : PrivateChannelTabItem - { - public TestPrivateChannelTabItem(Channel channel) - : base(channel) - { - } - - public new ClickableContainer CloseButton => base.CloseButton; - } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs deleted file mode 100644 index e27db00003..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ /dev/null @@ -1,500 +0,0 @@ -// 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 System.Collections.Generic; -using System.Net; -using System.Threading; -using JetBrains.Annotations; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; -using osu.Framework.Testing; -using osu.Framework.Utils; -using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Chat; -using osu.Game.Overlays; -using osu.Game.Overlays.Chat; -using osu.Game.Overlays.Chat.Listing; -using osu.Game.Overlays.Chat.ChannelList; -using osuTK; -using osuTK.Input; - -namespace osu.Game.Tests.Visual.Online -{ - [TestFixture] - public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene - { - private TestChatOverlayV2 chatOverlay; - private ChannelManager channelManager; - - private APIUser testUser; - private Channel testPMChannel; - private Channel[] testChannels; - - private Channel testChannel1 => testChannels[0]; - private Channel testChannel2 => testChannels[1]; - - [Resolved] - private OsuConfigManager config { get; set; } = null!; - - [SetUp] - public void SetUp() => Schedule(() => - { - testUser = new APIUser { Username = "test user", Id = 5071479 }; - testPMChannel = new Channel(testUser); - testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); - - Child = new DependencyProvidingContainer - { - RelativeSizeAxes = Axes.Both, - CachedDependencies = new (Type, object)[] - { - (typeof(ChannelManager), channelManager = new ChannelManager()), - }, - Children = new Drawable[] - { - channelManager, - chatOverlay = new TestChatOverlayV2 { RelativeSizeAxes = Axes.Both }, - }, - }; - }); - - [SetUpSteps] - public void SetUpSteps() - { - AddStep("Setup request handler", () => - { - ((DummyAPIAccess)API).HandleRequest = req => - { - switch (req) - { - case GetUpdatesRequest getUpdates: - getUpdates.TriggerFailure(new WebException()); - return true; - - case JoinChannelRequest joinChannel: - joinChannel.TriggerSuccess(); - return true; - - case LeaveChannelRequest leaveChannel: - leaveChannel.TriggerSuccess(); - return true; - - case GetMessagesRequest getMessages: - getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel)); - return true; - - case GetUserRequest getUser: - if (getUser.Lookup == testUser.Username) - getUser.TriggerSuccess(testUser); - else - getUser.TriggerFailure(new WebException()); - return true; - - case PostMessageRequest postMessage: - postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000)) - { - Content = postMessage.Message.Content, - ChannelId = postMessage.Message.ChannelId, - Sender = postMessage.Message.Sender, - Timestamp = new DateTimeOffset(DateTime.Now), - }); - return true; - - default: - Logger.Log($"Unhandled Request Type: {req.GetType()}"); - return false; - } - }; - }); - - AddStep("Add test channels", () => - { - (channelManager.AvailableChannels as BindableList)?.AddRange(testChannels); - }); - } - - [Test] - public void TestBasic() - { - AddStep("Show overlay with channel", () => - { - chatOverlay.Show(); - Channel joinedChannel = channelManager.JoinChannel(testChannel1); - channelManager.CurrentChannel.Value = joinedChannel; - }); - AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); - AddUntilStep("Channel is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); - } - - [Test] - public void TestShowHide() - { - AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); - AddStep("Hide overlay", () => chatOverlay.Hide()); - AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden); - } - - [Test] - public void TestChatHeight() - { - BindableFloat configChatHeight = new BindableFloat(); - config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight); - float newHeight = 0; - - AddStep("Reset config chat height", () => configChatHeight.SetDefault()); - AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); - AddStep("Click top bar", () => - { - InputManager.MoveMouseTo(chatOverlayTopBar); - InputManager.PressButton(MouseButton.Left); - }); - AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); - AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); - AddStep("Store new height", () => newHeight = chatOverlay.Height); - AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight); - AddStep("Hide overlay", () => chatOverlay.Hide()); - AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight); - } - - [Test] - public void TestChannelSelection() - { - AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Listing is visible", () => listingIsVisible); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); - } - - [Test] - public void TestSearchInListing() - { - AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Listing is visible", () => listingIsVisible); - AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2"); - AddUntilStep("Only channel 2 visibile", () => - { - IEnumerable listingItems = chatOverlay.ChildrenOfType() - .Where(item => item.IsPresent); - return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2; - }); - } - - [Test] - public void TestChannelCloseButton() - { - AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join PM and public channels", () => - { - channelManager.JoinChannel(testChannel1); - channelManager.JoinChannel(testPMChannel); - }); - AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); - AddStep("Click close button", () => - { - ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single(); - clickDrawable(closeButton); - }); - AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); - AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); - AddStep("Click close button", () => - { - ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType().Single(); - clickDrawable(closeButton); - }); - AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); - } - - [Test] - public void TestChatCommand() - { - AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); - AddAssert("PM channel is selected", () => - channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); - AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist")); - AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage); - - // Make sure no unnecessary requests are made when the PM channel is already open. - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null); - AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); - AddAssert("PM channel is selected", () => - channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); - } - - [Test] - public void TestMultiplayerChannelIsNotShown() - { - Channel multiplayerChannel = null; - - AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) - { - Name = "#mp_1", - Type = ChannelType.Multiplayer, - })); - AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); - AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType() - .Where(item => item.IsPresent) - .Select(item => item.Channel) - .Contains(multiplayerChannel)); - } - - [Test] - public void TestHighlightOnCurrentChannel() - { - Message message = null; - - AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddStep("Send message in channel 1", () => - { - testChannel1.AddNewMessages(message = new Message - { - ChannelId = testChannel1.Id, - Content = "Message to highlight!", - Timestamp = DateTimeOffset.Now, - Sender = testUser, - }); - }); - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); - AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); - } - - [Test] - public void TestHighlightOnAnotherChannel() - { - Message message = null; - - AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddStep("Send message in channel 2", () => - { - testChannel2.AddNewMessages(message = new Message - { - ChannelId = testChannel2.Id, - Content = "Message to highlight!", - Timestamp = DateTimeOffset.Now, - Sender = testUser, - }); - }); - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); - AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2); - } - - [Test] - public void TestHighlightOnLeftChannel() - { - Message message = null; - - AddStep("Show overlay", () => chatOverlay.Show()); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddStep("Send message in channel 2", () => - { - testChannel2.AddNewMessages(message = new Message - { - ChannelId = testChannel2.Id, - Content = "Message to highlight!", - Timestamp = DateTimeOffset.Now, - Sender = testUser, - }); - }); - AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2)); - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); - AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2); - } - - [Test] - public void TestHighlightWhileChatNeverOpen() - { - Message message = null; - - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Send message in channel 1", () => - { - testChannel1.AddNewMessages(message = new Message - { - ChannelId = testChannel1.Id, - Content = "Message to highlight!", - Timestamp = DateTimeOffset.Now, - Sender = testUser, - }); - }); - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); - AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); - } - - [Test] - public void TestHighlightWithNullChannel() - { - Message message = null; - - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Send message in channel 1", () => - { - testChannel1.AddNewMessages(message = new Message - { - ChannelId = testChannel1.Id, - Content = "Message to highlight!", - Timestamp = DateTimeOffset.Now, - Sender = testUser, - }); - }); - AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null); - AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); - AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); - } - - [Test] - public void TestTextBoxRetainsFocus() - { - AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel)); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Click selector", () => clickDrawable(channelSelectorButton)); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); - AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); - AddStep("Hide overlay", () => chatOverlay.Hide()); - AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); - } - - [Test] - public void TestSlowLoadingChannel() - { - AddStep("Show overlay (slow-loading)", () => - { - chatOverlay.Show(); - chatOverlay.SlowLoading = true; - }); - AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading); - - AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); - AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2))); - AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading); - - AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set()); - AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready); - AddAssert("Channel 1 not displayed", () => !channelIsVisible); - - AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set()); - AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded); - AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2); - - AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); - AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded); - AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1); - } - - private bool listingIsVisible => - chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible; - - private bool loadingIsVisible => - chatOverlay.ChildrenOfType().Single().State.Value == Visibility.Visible; - - private bool channelIsVisible => - !listingIsVisible && !loadingIsVisible; - - private DrawableChannel currentDrawableChannel => - chatOverlay.ChildrenOfType().Single(); - - private ChannelListItem getChannelListItem(Channel channel) => - chatOverlay.ChildrenOfType().Single(item => item.Channel == channel); - - private ChatTextBox chatOverlayTextBox => - chatOverlay.ChildrenOfType().Single(); - - private ChatOverlayTopBar chatOverlayTopBar => - chatOverlay.ChildrenOfType().Single(); - - private ChannelListItem channelSelectorButton => - chatOverlay.ChildrenOfType().Single(item => item.Channel is ChannelListing.ChannelListingChannel); - - private void clickDrawable(Drawable d) - { - InputManager.MoveMouseTo(d); - InputManager.Click(MouseButton.Left); - } - - private List createChannelMessages(Channel channel) - { - var message = new Message - { - ChannelId = channel.Id, - Content = $"Hello, this is a message in {channel.Name}", - Sender = testUser, - Timestamp = new DateTimeOffset(DateTime.Now), - }; - return new List { message }; - } - - private Channel createPublicChannel(int id) => new Channel - { - Id = id, - Name = $"#channel-{id}", - Topic = $"We talk about the number {id} here", - Type = ChannelType.Public, - }; - - private class TestChatOverlayV2 : ChatOverlayV2 - { - public bool SlowLoading { get; set; } - - public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType().Single(c => c.Channel == channel); - - protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) - { - return SlowLoading - ? new SlowLoadingDrawableChannel(newChannel) - : new ChatOverlayDrawableChannel(newChannel); - } - } - - private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel - { - public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim(); - - public SlowLoadingDrawableChannel([NotNull] Channel channel) - : base(channel) - { - } - - [BackgroundDependencyLoader] - private void load() - { - LoadEvent.Wait(10000); - } - } - } -} diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 35a4f8cf2d..edee26c081 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; +using osu.Game.Overlays; using osu.Game.Overlays.Dashboard; using osu.Game.Tests.Visual.Spectator; using osu.Game.Users; @@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online CachedDependencies = new (Type, object)[] { (typeof(SpectatorClient), spectatorClient), - (typeof(UserLookupCache), lookupCache) + (typeof(UserLookupCache), lookupCache), + (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)), }, Child = currentlyPlaying = new CurrentlyPlayingDisplay { diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index f5fe00458a..c532e8bc05 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists { AddUntilStep("wait for scores loaded", () => requestComplete + // request handler may need to fire more than once to get scores. + && totalCount > 0 && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ScorePanelList.AllPanelsVisible); AddWaitStep("wait for display", 5); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 35281a85eb..1efe6d7380 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,6 +14,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Rulesets.Osu.Objects; @@ -114,10 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking throw new NotImplementedException(); } - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) - { - throw new NotImplementedException(); - } + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap); public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) { @@ -151,6 +150,24 @@ namespace osu.Game.Tests.Visual.Ranking } } }; + + private class TestBeatmapConverter : IBeatmapConverter + { +#pragma warning disable CS0067 // The event is never used + public event Action> ObjectConverted; +#pragma warning restore CS0067 + + public IBeatmap Beatmap { get; } + + public TestBeatmapConverter(IBeatmap beatmap) + { + Beatmap = beatmap; + } + + public bool CanConvert() => true; + + public IBeatmap Convert(CancellationToken cancellationToken = default) => Beatmap.Clone(); + } } private class TestRulesetAllStatsRequireHitEvents : TestRuleset diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 83265e13ad..3e679a7905 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("clear label", () => textBox.LabelText = default); AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); - AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator..."); + AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true)); AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1)); } @@ -129,16 +130,18 @@ namespace osu.Game.Tests.Visual.Settings SettingsNumberBox numberBox = null; AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox()); - AddAssert("warning text not created", () => !numberBox.ChildrenOfType().Any()); + AddAssert("warning text not created", () => !numberBox.ChildrenOfType().Any()); - AddStep("set warning text", () => numberBox.WarningText = "this is a warning!"); - AddAssert("warning text created", () => numberBox.ChildrenOfType().Single().Alpha == 1); + AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true)); + AddAssert("warning text created", () => numberBox.ChildrenOfType().Single().Alpha == 1); - AddStep("unset warning text", () => numberBox.WarningText = default); - AddAssert("warning text hidden", () => numberBox.ChildrenOfType().Single().Alpha == 0); + AddStep("unset warning text", () => numberBox.ClearNoticeText()); + AddAssert("warning text hidden", () => !numberBox.ChildrenOfType().Any()); - AddStep("set warning text again", () => numberBox.WarningText = "another warning!"); - AddAssert("warning text shown again", () => numberBox.ChildrenOfType().Single().Alpha == 1); + AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true)); + AddAssert("warning text shown again", () => numberBox.ChildrenOfType().Single().Alpha == 1); + + AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!")); } } } diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs index 84903d381a..5ebdee0b09 100644 --- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs +++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs @@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Drawables downloadTrackers.Add(beatmapDownloadTracker); AddInternal(beatmapDownloadTracker); + // Note that this is downloading the beatmaps even if they are already downloaded. + // We could rely more on `BeatmapDownloadTracker`'s exposed state to avoid this. beatmapDownloader.Download(beatmapSet); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 0276abc3ff..ff13e61360 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -59,7 +59,7 @@ namespace osu.Game.Beatmaps.Formats } catch (Exception e) { - Logger.Log($"Failed to process line \"{line}\" into \"{output}\": {e.Message}", LoggingTarget.Runtime, LogLevel.Important); + Logger.Log($"Failed to process line \"{line}\" into \"{output}\": {e.Message}"); } } } diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs index 2f98aef58a..506103a2c0 100644 --- a/osu.Game/Database/OnlineLookupCache.cs +++ b/osu.Game/Database/OnlineLookupCache.cs @@ -91,7 +91,7 @@ namespace osu.Game.Database } } - private void performLookup() + private async Task performLookup() { // contains at most 50 unique IDs from tasks, which is used to perform the lookup. var nextTaskBatch = new Dictionary>>(); @@ -127,7 +127,7 @@ namespace osu.Game.Database // rather than queueing, we maintain our own single-threaded request stream. // todo: we probably want retry logic here. - api.Perform(request); + await api.PerformAsync(request).ConfigureAwait(false); finishPendingTask(); diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index afedf36cad..7fd94b57f3 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -188,6 +189,24 @@ namespace osu.Game.Graphics } } + /// + /// Retrieves the main accent colour for a . + /// + public Color4? ForRoomCategory(RoomCategory roomCategory) + { + switch (roomCategory) + { + case RoomCategory.Spotlight: + return SpotlightColour; + + case RoomCategory.FeaturedArtist: + return FeaturedArtistColour; + + default: + return null; + } + } + /// /// Returns a foreground text colour that is supposed to contrast well with /// the supplied . @@ -360,5 +379,8 @@ namespace osu.Game.Graphics public readonly Color4 ChatBlue = Color4Extensions.FromHex(@"17292e"); public readonly Color4 ContextMenuGray = Color4Extensions.FromHex(@"223034"); + + public Color4 SpotlightColour => Green2; + public Color4 FeaturedArtistColour => Blue2; } } diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs index 80dfa104f3..ae2b85da51 100644 --- a/osu.Game/IO/Archives/ZipArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -1,11 +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 System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Toolkit.HighPerformance; +using osu.Framework.Extensions; using osu.Framework.IO.Stores; using SharpCompress.Archives.Zip; +using SixLabors.ImageSharp.Memory; namespace osu.Game.IO.Archives { @@ -27,15 +31,12 @@ namespace osu.Game.IO.Archives if (entry == null) throw new FileNotFoundException(); - // allow seeking - MemoryStream copy = new MemoryStream(); + var owner = MemoryAllocator.Default.Allocate((int)entry.Size); using (Stream s = entry.OpenEntryStream()) - s.CopyTo(copy); + s.ReadToFill(owner.Memory.Span); - copy.Position = 0; - - return copy; + return new MemoryOwnerMemoryStream(owner); } public override void Dispose() @@ -45,5 +46,48 @@ namespace osu.Game.IO.Archives } public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames(); + + private class MemoryOwnerMemoryStream : Stream + { + private readonly IMemoryOwner owner; + private readonly Stream stream; + + public MemoryOwnerMemoryStream(IMemoryOwner owner) + { + this.owner = owner; + + stream = owner.Memory.AsStream(); + } + + protected override void Dispose(bool disposing) + { + owner?.Dispose(); + base.Dispose(disposing); + } + + public override void Flush() => stream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count); + + public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); + + public override void SetLength(long value) => stream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count); + + public override bool CanRead => stream.CanRead; + + public override bool CanSeek => stream.CanSeek; + + public override bool CanWrite => stream.CanWrite; + + public override long Length => stream.Length; + + public override long Position + { + get => stream.Position; + set => stream.Position = value; + } + } } } diff --git a/osu.Game/Localisation/LayoutSettingsStrings.cs b/osu.Game/Localisation/LayoutSettingsStrings.cs new file mode 100644 index 0000000000..5ac28f19b3 --- /dev/null +++ b/osu.Game/Localisation/LayoutSettingsStrings.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class LayoutSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.LayoutSettings"; + + /// + /// "Checking for fullscreen capabilities..." + /// + public static LocalisableString CheckingForFullscreenCapabilities => new TranslatableString(getKey(@"checking_for_fullscreen_capabilities"), @"Checking for fullscreen capabilities..."); + + /// + /// "osu! is running exclusive fullscreen, guaranteeing low latency!" + /// + public static LocalisableString OsuIsRunningExclusiveFullscreen => new TranslatableString(getKey(@"osu_is_running_exclusive_fullscreen"), @"osu! is running exclusive fullscreen, guaranteeing low latency!"); + + /// + /// "Unable to run exclusive fullscreen. You'll still experience some input latency." + /// + public static LocalisableString UnableToRunExclusiveFullscreen => new TranslatableString(getKey(@"unable_to_run_exclusive_fullscreen"), @"Unable to run exclusive fullscreen. You'll still experience some input latency."); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index f292e95bd1..d3707d977c 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -63,12 +63,13 @@ namespace osu.Game.Online.API public virtual void Queue(APIRequest request) { - if (HandleRequest?.Invoke(request) != true) + Schedule(() => { - // this will fail due to not receiving an APIAccess, and trigger a failure on the request. - // this is intended - any request in testing that needs non-failures should use HandleRequest. - request.Perform(this); - } + if (HandleRequest?.Invoke(request) != true) + { + request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request.")); + } + }); } public void Perform(APIRequest request) => HandleRequest?.Invoke(request); diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index b7d67de04d..31f67bcecc 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -15,7 +15,6 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Chat.Listing; -using osu.Game.Overlays.Chat.Tabs; namespace osu.Game.Online.Chat { @@ -134,7 +133,7 @@ namespace osu.Game.Online.Chat private void currentChannelChanged(ValueChangedEvent e) { - bool isSelectorChannel = e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel || e.NewValue is ChannelListing.ChannelListingChannel; + bool isSelectorChannel = e.NewValue is ChannelListing.ChannelListingChannel; if (!isSelectorChannel) JoinChannel(e.NewValue); diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index ca9bf00b23..261724e315 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -20,6 +20,8 @@ namespace osu.Game.Online { public class HubClientConnector : IHubClientConnector { + public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down."; + /// /// Invoked whenever a new hub connection is built, to configure it before it's started. /// @@ -64,20 +66,28 @@ namespace osu.Game.Online this.preferMessagePack = preferMessagePack; apiState.BindTo(api.State); - apiState.BindValueChanged(state => - { - switch (state.NewValue) - { - case APIState.Failing: - case APIState.Offline: - Task.Run(() => disconnect(true)); - break; + apiState.BindValueChanged(state => connectIfPossible(), true); + } - case APIState.Online: - Task.Run(connect); - break; - } - }, true); + public void Reconnect() + { + Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network); + Task.Run(connectIfPossible); + } + + private void connectIfPossible() + { + switch (apiState.Value) + { + case APIState.Failing: + case APIState.Offline: + Task.Run(() => disconnect(true)); + break; + + case APIState.Online: + Task.Run(connect); + break; + } } private async Task connect() diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs index d2ceb1f030..b168e4669f 100644 --- a/osu.Game/Online/IHubClientConnector.cs +++ b/osu.Game/Online/IHubClientConnector.cs @@ -30,5 +30,10 @@ namespace osu.Game.Online /// Invoked whenever a new hub connection is built, to configure it before it's started. /// public Action? ConfigureConnection { get; set; } + + /// + /// Reconnect if already connected. + /// + void Reconnect(); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs index 4729765084..cda313bd0a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClientExtensions.cs @@ -25,12 +25,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(exception != null); - string message = exception is HubException - // HubExceptions arrive with additional message context added, but we want to display the human readable message: - // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once." - // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now. - ? exception.Message.Substring(exception.Message.IndexOf(':') + 1).Trim() - : exception.Message; + string message = exception.GetHubExceptionMessage() ?? exception.Message; Logger.Log(message, level: LogLevel.Important); onError?.Invoke(exception); @@ -40,5 +35,16 @@ namespace osu.Game.Online.Multiplayer onSuccess?.Invoke(); } }); + + public static string? GetHubExceptionMessage(this Exception exception) + { + if (exception is HubException hubException) + // HubExceptions arrive with additional message context added, but we want to display the human readable message: + // "An unexpected error occurred invoking 'AddPlaylistItem' on the server.InvalidStateException: Can't enqueue more than 3 items at once." + // We generally use the message field for a user-parseable error (eventually to be replaced), so drop the first part for now. + return hubException.Message.Substring(exception.Message.IndexOf(':') + 1).Trim(); + + return null; + } } } diff --git a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs index 4dc23d8b85..a3423d4189 100644 --- a/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/OnlineMultiplayerClient.cs @@ -3,10 +3,12 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -71,14 +73,23 @@ namespace osu.Game.Online.Multiplayer } } - protected override Task JoinRoom(long roomId, string? password = null) + protected override async Task JoinRoom(long roomId, string? password = null) { if (!IsConnected.Value) - return Task.FromCanceled(new CancellationToken(true)); + throw new OperationCanceledException(); Debug.Assert(connection != null); - return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty); + try + { + return await connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + connector?.Reconnect(); + throw; + } } protected override Task LeaveRoomInternal() diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs index a1dcfa5fd9..bca4d78359 100644 --- a/osu.Game/Online/Rooms/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Online.Rooms { public enum RoomCategory @@ -8,5 +10,8 @@ namespace osu.Game.Online.Rooms // used for osu-web deserialization so names shouldn't be changed. Normal, Spotlight, + + [Description("Featured Artist")] + FeaturedArtist, } } diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs index 4d6ca0b311..0f77f723db 100644 --- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs +++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs @@ -5,10 +5,12 @@ using System.Diagnostics; using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; namespace osu.Game.Online.Spectator { @@ -47,14 +49,23 @@ namespace osu.Game.Online.Spectator } } - protected override Task BeginPlayingInternal(SpectatorState state) + protected override async Task BeginPlayingInternal(SpectatorState state) { if (!IsConnected.Value) - return Task.CompletedTask; + return; Debug.Assert(connection != null); - return connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state); + try + { + await connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), state); + } + catch (HubException exception) + { + if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) + connector?.Reconnect(); + throw; + } } protected override Task SendFramesInternal(FrameDataBundle bundle) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index 56efb725cd..e0fc7482f6 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -229,6 +230,8 @@ namespace osu.Game.Overlays.BeatmapSet { Details.BeatmapInfo = b.NewValue; externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineID}"; + + onlineStatusPill.Status = b.NewValue?.Status ?? BeatmapOnlineStatus.None; }; } @@ -272,7 +275,6 @@ namespace osu.Game.Overlays.BeatmapSet featuredArtist.Alpha = setInfo.NewValue.TrackId != null ? 1 : 0; onlineStatusPill.FadeIn(500, Easing.OutQuint); - onlineStatusPill.Status = setInfo.NewValue.Status; downloadButtonsContainer.FadeIn(transition_duration); favouriteButton.FadeIn(transition_duration); diff --git a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs index 4f336d85fc..20ee11c7f6 100644 --- a/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs +++ b/osu.Game/Overlays/BeatmapSet/FeaturedArtistBeatmapBadge.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.BeatmapSet private void load(OsuColour colours) { BadgeText = BeatmapsetsStrings.FeaturedArtistBadgeLabel; - BadgeColour = colours.Blue1; + BadgeColour = colours.FeaturedArtistColour; // todo: add linking support to allow redirecting featured artist badge to corresponding track. } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 591e4cf73e..5f24a6549d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -253,7 +253,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores noScoresPlaceholder.Hide(); - if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.BeatmapSet as IBeatmapSetOnlineInfo)?.Status <= BeatmapOnlineStatus.Pending) + if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.Status <= BeatmapOnlineStatus.Pending)) { Scores = null; Hide(); diff --git a/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs b/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs index 3204f79b21..9c5378a967 100644 --- a/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs +++ b/osu.Game/Overlays/BeatmapSet/SpotlightBeatmapBadge.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.BeatmapSet private void load(OsuColour colours) { BadgeText = BeatmapsetsStrings.SpotlightBadgeLabel; - BadgeColour = colours.Pink1; + BadgeColour = colours.SpotlightColour; // todo: add linking support to allow redirecting spotlight badge to https://osu.ppy.sh/wiki/en/Beatmap_Spotlights. } } diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs index d1ceae604c..47a2d234d1 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -4,16 +4,20 @@ #nullable enable using System; +using System.Linq; using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat.Listing; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Chat.ChannelList { @@ -22,12 +26,20 @@ namespace osu.Game.Overlays.Chat.ChannelList public Action? OnRequestSelect; public Action? OnRequestLeave; + public IEnumerable Channels => groupFlow.Children + .OfType() + .SelectMany(channelGroup => channelGroup.ItemFlow) + .Select(item => item.Channel); + public readonly ChannelListing.ChannelListingChannel ChannelListingChannel = new ChannelListing.ChannelListingChannel(); private readonly Dictionary channelMap = new Dictionary(); - private ChannelListItemFlow publicChannelFlow = null!; - private ChannelListItemFlow privateChannelFlow = null!; + private OsuScrollContainer scroll = null!; + private FillFlowContainer groupFlow = null!; + private ChannelGroup announceChannelGroup = null!; + private ChannelGroup publicChannelGroup = null!; + private ChannelGroup privateChannelGroup = null!; private ChannelListItem selector = null!; [BackgroundDependencyLoader] @@ -40,25 +52,22 @@ namespace osu.Game.Overlays.Chat.ChannelList RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6, }, - new OsuScrollContainer + scroll = new OsuScrollContainer { - Padding = new MarginPadding { Vertical = 7 }, RelativeSizeAxes = Axes.Both, ScrollbarAnchor = Anchor.TopRight, ScrollDistance = 35f, - Child = new FillFlowContainer + Child = groupFlow = new FillFlowContainer { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] { - publicChannelFlow = new ChannelListItemFlow("CHANNELS"), - selector = new ChannelListItem(ChannelListingChannel) - { - Margin = new MarginPadding { Bottom = 10 }, - }, - privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"), + announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()), + publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()), + selector = new ChannelListItem(ChannelListingChannel), + privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()), }, }, }, @@ -76,9 +85,11 @@ namespace osu.Game.Overlays.Chat.ChannelList item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); - ChannelListItemFlow flow = getFlowForChannel(channel); + FillFlowContainer flow = getFlowForChannel(channel); channelMap.Add(channel, item); flow.Add(item); + + updateVisibility(); } public void RemoveChannel(Channel channel) @@ -87,10 +98,12 @@ namespace osu.Game.Overlays.Chat.ChannelList return; ChannelListItem item = channelMap[channel]; - ChannelListItemFlow flow = getFlowForChannel(channel); + FillFlowContainer flow = getFlowForChannel(channel); channelMap.Remove(channel); flow.Remove(item); + + updateVisibility(); } public ChannelListItem GetItem(Channel channel) @@ -101,35 +114,60 @@ namespace osu.Game.Overlays.Chat.ChannelList return channelMap[channel]; } - private ChannelListItemFlow getFlowForChannel(Channel channel) + public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel)); + + private FillFlowContainer getFlowForChannel(Channel channel) { switch (channel.Type) { case ChannelType.Public: - return publicChannelFlow; + return publicChannelGroup.ItemFlow; case ChannelType.PM: - return privateChannelFlow; + return privateChannelGroup.ItemFlow; + + case ChannelType.Announce: + return announceChannelGroup.ItemFlow; default: - throw new ArgumentOutOfRangeException(); + return publicChannelGroup.ItemFlow; } } - private class ChannelListItemFlow : FillFlowContainer + private void updateVisibility() { - public ChannelListItemFlow(string label) + if (announceChannelGroup.ItemFlow.Children.Count == 0) + announceChannelGroup.Hide(); + else + announceChannelGroup.Show(); + } + + private class ChannelGroup : FillFlowContainer + { + public readonly FillFlowContainer ItemFlow; + + public ChannelGroup(LocalisableString label) { Direction = FillDirection.Vertical; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Top = 8 }; - Add(new OsuSpriteText + Children = new Drawable[] { - Text = label, - Margin = new MarginPadding { Left = 18, Bottom = 5 }, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - }); + new OsuSpriteText + { + Text = label, + Margin = new MarginPadding { Left = 18, Bottom = 5 }, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }, + ItemFlow = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }; } } } diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 3a8cd1fb91..79f22b51f7 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Resources.Localisation.Web; using osuTK; using osuTK.Graphics; @@ -58,7 +59,7 @@ namespace osu.Game.Overlays.Chat { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = "osu!chat", + Text = ChatStrings.TitleCompact, Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), Margin = new MarginPadding { Bottom = 2f }, }, diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 404d686d91..5100959eeb 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; +using osu.Game.Resources.Localisation.Web; using osuTK; namespace osu.Game.Overlays.Chat @@ -140,16 +141,16 @@ namespace osu.Game.Overlays.Chat switch (newChannel?.Type) { - case ChannelType.Public: - chattingText.Text = $"chatting in {newChannel.Name}"; + case null: + chattingText.Text = string.Empty; break; case ChannelType.PM: - chattingText.Text = $"chatting with {newChannel.Name}"; + chattingText.Text = ChatStrings.TalkingWith(newChannel.Name); break; default: - chattingText.Text = string.Empty; + chattingText.Text = ChatStrings.TalkingIn(newChannel.Name); break; } }, true); diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs index e0f949caba..1ee0e8445f 100644 --- a/osu.Game/Overlays/Chat/ChatTextBox.cs +++ b/osu.Game/Overlays/Chat/ChatTextBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; namespace osu.Game.Overlays.Chat { @@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Chat { bool showSearch = change.NewValue; - PlaceholderText = showSearch ? "type here to search" : "type here"; + PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder; Text = string.Empty; }, true); } diff --git a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs deleted file mode 100644 index 59989ade7b..0000000000 --- a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs +++ /dev/null @@ -1,191 +0,0 @@ -// 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.Collections.Generic; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Chat; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Overlays.Chat.Selection -{ - public class ChannelListItem : OsuClickableContainer, IFilterable - { - private const float width_padding = 5; - private const float channel_width = 150; - private const float text_size = 15; - private const float transition_duration = 100; - - public readonly Channel Channel; - - private readonly Bindable joinedBind = new Bindable(); - private readonly OsuSpriteText name; - private readonly OsuSpriteText topic; - private readonly SpriteIcon joinedCheckmark; - - private Color4 joinedColour; - private Color4 topicColour; - private Color4 hoverColour; - - public IEnumerable FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty }; - - public bool MatchingFilter - { - set => this.FadeTo(value ? 1f : 0f, 100); - } - - public bool FilteringActive { get; set; } - - public Action OnRequestJoin; - public Action OnRequestLeave; - - public ChannelListItem(Channel channel) - { - Channel = channel; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Action = () => { (channel.Joined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); }; - - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new Container - { - Children = new[] - { - joinedCheckmark = new SpriteIcon - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Icon = FontAwesome.Solid.CheckCircle, - Size = new Vector2(text_size), - Shadow = false, - Margin = new MarginPadding { Right = 10f }, - }, - }, - }, - new Container - { - Width = channel_width, - AutoSizeAxes = Axes.Y, - Children = new[] - { - name = new OsuSpriteText - { - Text = channel.ToString(), - Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold), - Shadow = false, - }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.X, - Width = 0.7f, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Left = width_padding }, - Children = new[] - { - topic = new OsuSpriteText - { - Text = channel.Topic, - Font = OsuFont.GetFont(size: text_size, weight: FontWeight.SemiBold), - Shadow = false, - }, - }, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = width_padding }, - Spacing = new Vector2(3f, 0f), - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Solid.User, - Size = new Vector2(text_size - 2), - Shadow = false, - }, - new OsuSpriteText - { - Text = @"0", - Font = OsuFont.GetFont(size: text_size, weight: FontWeight.SemiBold), - Shadow = false, - }, - }, - }, - }, - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - topicColour = colours.Gray9; - joinedColour = colours.Blue; - hoverColour = colours.Yellow; - - joinedBind.ValueChanged += joined => updateColour(joined.NewValue); - joinedBind.BindTo(Channel.Joined); - - joinedBind.TriggerChange(); - FinishTransforms(true); - } - - protected override bool OnHover(HoverEvent e) - { - if (!Channel.Joined.Value) - name.FadeColour(hoverColour, 50, Easing.OutQuint); - - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - if (!Channel.Joined.Value) - name.FadeColour(Color4.White, transition_duration); - } - - private void updateColour(bool joined) - { - if (joined) - { - name.FadeColour(Color4.White, transition_duration); - joinedCheckmark.FadeTo(1f, transition_duration); - topic.FadeTo(0.8f, transition_duration); - topic.FadeColour(Color4.White, transition_duration); - this.FadeColour(joinedColour, transition_duration); - } - else - { - joinedCheckmark.FadeTo(0f, transition_duration); - topic.FadeTo(1f, transition_duration); - topic.FadeColour(topicColour, transition_duration); - this.FadeColour(Color4.White, transition_duration); - } - } - } -} diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs deleted file mode 100644 index 070332180c..0000000000 --- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs +++ /dev/null @@ -1,58 +0,0 @@ -// 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.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Online.Chat; -using osuTK; - -namespace osu.Game.Overlays.Chat.Selection -{ - public class ChannelSection : Container, IHasFilterableChildren - { - public readonly FillFlowContainer ChannelFlow; - - public IEnumerable FilterableChildren => ChannelFlow.Children; - public IEnumerable FilterTerms => Array.Empty(); - - public bool MatchingFilter - { - set => this.FadeTo(value ? 1f : 0f, 100); - } - - public bool FilteringActive { get; set; } - - public IEnumerable Channels - { - set => ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c)); - } - - public ChannelSection() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), - Text = "All Channels".ToUpperInvariant() - }, - ChannelFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 25 }, - Spacing = new Vector2(0f, 5f), - }, - }; - } - } -} diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs deleted file mode 100644 index 9b0354e264..0000000000 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ /dev/null @@ -1,194 +0,0 @@ -// 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.Collections.Generic; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Chat; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Overlays.Chat.Selection -{ - public class ChannelSelectionOverlay : WaveOverlayContainer - { - public new const float WIDTH_PADDING = 170; - - private const float transition_duration = 500; - - private readonly Box bg; - private readonly Triangles triangles; - private readonly Box headerBg; - private readonly SearchTextBox search; - private readonly SearchContainer sectionsFlow; - - protected override bool DimMainContent => false; - - public Action OnRequestJoin; - public Action OnRequestLeave; - - public ChannelSelectionOverlay() - { - RelativeSizeAxes = Axes.X; - - Waves.FirstWaveColour = Color4Extensions.FromHex("353535"); - Waves.SecondWaveColour = Color4Extensions.FromHex("434343"); - Waves.ThirdWaveColour = Color4Extensions.FromHex("515151"); - Waves.FourthWaveColour = Color4Extensions.FromHex("595959"); - - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] - { - bg = new Box - { - RelativeSizeAxes = Axes.Both, - }, - triangles = new Triangles - { - RelativeSizeAxes = Axes.Both, - TriangleScale = 5, - }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 85, Right = WIDTH_PADDING }, - Children = new[] - { - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - sectionsFlow = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, - Spacing = new Vector2(0f, 20f), - Padding = new MarginPadding { Vertical = 20, Left = WIDTH_PADDING }, - }, - }, - }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - headerBg = new Box - { - RelativeSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 10f), - Padding = new MarginPadding { Top = 10f, Bottom = 10f, Left = WIDTH_PADDING, Right = WIDTH_PADDING }, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = @"Chat Channels", - Font = OsuFont.GetFont(size: 20), - Shadow = false, - }, - search = new HeaderSearchTextBox { RelativeSizeAxes = Axes.X }, - }, - }, - }, - }, - }; - - search.Current.ValueChanged += term => sectionsFlow.SearchTerm = term.NewValue; - } - - public void UpdateAvailableChannels(IEnumerable channels) - { - Scheduler.Add(() => - { - sectionsFlow.ChildrenEnumerable = new[] - { - new ChannelSection { Channels = channels, }, - }; - - foreach (ChannelSection s in sectionsFlow.Children) - { - foreach (ChannelListItem c in s.ChannelFlow.Children) - { - c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); }; - c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); }; - } - } - }); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - bg.Colour = colours.Gray3; - triangles.ColourDark = colours.Gray3; - triangles.ColourLight = Color4Extensions.FromHex(@"353535"); - - headerBg.Colour = colours.Gray2.Opacity(0.75f); - } - - protected override void OnFocus(FocusEvent e) - { - search.TakeFocus(); - base.OnFocus(e); - } - - protected override void PopIn() - { - if (Alpha == 0) this.MoveToY(DrawHeight); - - this.FadeIn(transition_duration, Easing.OutQuint); - this.MoveToY(0, transition_duration, Easing.OutQuint); - - search.HoldFocus = true; - base.PopIn(); - } - - protected override void PopOut() - { - this.FadeOut(transition_duration, Easing.InSine); - this.MoveToY(DrawHeight, transition_duration, Easing.InSine); - - search.HoldFocus = false; - base.PopOut(); - } - - private class HeaderSearchTextBox : BasicSearchTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundFocused = Color4.Black.Opacity(0.2f); - BackgroundUnfocused = Color4.Black.Opacity(0.2f); - } - } - } -} diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs deleted file mode 100644 index e3ede04edd..0000000000 --- a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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.Game.Graphics; -using osu.Game.Online.Chat; - -namespace osu.Game.Overlays.Chat.Tabs -{ - public class ChannelSelectorTabItem : ChannelTabItem - { - public override bool IsRemovable => false; - - public override bool IsSwitchable => false; - - protected override bool IsBoldWhenActive => false; - - public ChannelSelectorTabItem() - : base(new ChannelSelectorTabChannel()) - { - Depth = float.MaxValue; - Width = 45; - - Icon.Alpha = 0; - - Text.Font = Text.Font.With(size: 45); - Text.Truncate = false; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colour) - { - BackgroundInactive = colour.Gray2; - BackgroundActive = colour.Gray3; - } - - public class ChannelSelectorTabChannel : Channel - { - public ChannelSelectorTabChannel() - { - Name = "+"; - Type = ChannelType.System; - } - } - } -} diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs deleted file mode 100644 index c0de093425..0000000000 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ /dev/null @@ -1,114 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Chat; -using osuTK; -using System; -using System.Linq; -using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; - -namespace osu.Game.Overlays.Chat.Tabs -{ - public class ChannelTabControl : OsuTabControl - { - public const float SHEAR_WIDTH = 10; - - public Action OnRequestLeave; - - public readonly Bindable ChannelSelectorActive = new Bindable(); - - private readonly ChannelSelectorTabItem selectorTab; - - public ChannelTabControl() - { - Padding = new MarginPadding { Left = 50 }; - - TabContainer.Spacing = new Vector2(-SHEAR_WIDTH, 0); - TabContainer.Masking = false; - - AddTabItem(selectorTab = new ChannelSelectorTabItem()); - - ChannelSelectorActive.BindTo(selectorTab.Active); - } - - protected override void AddTabItem(TabItem item, bool addToDropdown = true) - { - if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue) - // performTabSort might've made selectorTab's position wonky, fix it - TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); - - ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value); - - base.AddTabItem(item, addToDropdown); - } - - protected override TabItem CreateTabItem(Channel value) - { - switch (value.Type) - { - default: - return new ChannelTabItem(value); - - case ChannelType.PM: - return new PrivateChannelTabItem(value); - } - } - - /// - /// Adds a channel to the ChannelTabControl. - /// The first channel added will automaticly selected. - /// - /// The channel that is going to be added. - public void AddChannel(Channel channel) - { - if (!Items.Contains(channel)) - AddItem(channel); - - Current.Value ??= channel; - } - - /// - /// Removes a channel from the ChannelTabControl. - /// If the selected channel is the one that is being removed, the next available channel will be selected. - /// - /// The channel that is going to be removed. - public void RemoveChannel(Channel channel) - { - RemoveItem(channel); - - if (SelectedTab == null) - SelectChannelSelectorTab(); - } - - public void SelectChannelSelectorTab() => SelectTab(selectorTab); - - protected override void SelectTab(TabItem tab) - { - if (tab is ChannelSelectorTabItem) - { - tab.Active.Value = true; - return; - } - - base.SelectTab(tab); - selectorTab.Active.Value = false; - } - - protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer - { - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.Both, - Depth = -1, - Masking = true - }; - - private class ChannelTabFillFlowContainer : TabFillFlowContainer - { - protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); - } - } -} diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs deleted file mode 100644 index 9d2cd8a21d..0000000000 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs +++ /dev/null @@ -1,238 +0,0 @@ -// 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.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Chat; -using osuTK; -using osuTK.Graphics; -using osuTK.Input; - -namespace osu.Game.Overlays.Chat.Tabs -{ - public class ChannelTabItem : TabItem - { - protected Color4 BackgroundInactive; - private Color4 backgroundHover; - protected Color4 BackgroundActive; - - public override bool IsRemovable => !Pinned; - - protected readonly SpriteText Text; - protected readonly ClickableContainer CloseButton; - private readonly Box box; - private readonly Box highlightBox; - protected readonly SpriteIcon Icon; - - public Action OnRequestClose; - private readonly Container content; - - protected override Container Content => content; - - private Sample sampleTabSwitched; - - public ChannelTabItem(Channel value) - : base(value) - { - Width = 150; - - RelativeSizeAxes = Axes.Y; - - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; - - Shear = new Vector2(ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0); - - Masking = true; - - InternalChildren = new Drawable[] - { - box = new Box - { - EdgeSmoothness = new Vector2(1, 0), - RelativeSizeAxes = Axes.Both, - }, - highlightBox = new Box - { - Width = 5, - Alpha = 0, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - EdgeSmoothness = new Vector2(1, 0), - RelativeSizeAxes = Axes.Y, - }, - content = new Container - { - Shear = new Vector2(-ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0), - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - Icon = new SpriteIcon - { - Icon = DisplayIcon, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.Black, - X = -10, - Alpha = 0.2f, - Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT), - }, - Text = new OsuSpriteText - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Text = value.ToString(), - Font = OsuFont.GetFont(size: 18), - Padding = new MarginPadding(5) - { - Left = LeftTextPadding, - Right = RightTextPadding, - }, - RelativeSizeAxes = Axes.X, - Truncate = true, - }, - CloseButton = new TabCloseButton - { - Alpha = 0, - Margin = new MarginPadding { Right = 20 }, - Origin = Anchor.CentreRight, - Anchor = Anchor.CentreRight, - Action = delegate - { - if (IsRemovable) OnRequestClose?.Invoke(this); - }, - }, - }, - }, - new HoverSounds() - }; - } - - protected virtual float LeftTextPadding => 5; - - protected virtual float RightTextPadding => IsRemovable ? 40 : 5; - - protected virtual IconUsage DisplayIcon => FontAwesome.Solid.Hashtag; - - protected virtual bool ShowCloseOnHover => true; - - protected virtual bool IsBoldWhenActive => true; - - protected override bool OnHover(HoverEvent e) - { - if (IsRemovable && ShowCloseOnHover) - CloseButton.FadeIn(200, Easing.OutQuint); - - if (!Active.Value) - box.FadeColour(backgroundHover, TRANSITION_LENGTH, Easing.OutQuint); - return true; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - CloseButton.FadeOut(200, Easing.OutQuint); - updateState(); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - switch (e.Button) - { - case MouseButton.Middle: - CloseButton.TriggerClick(); - break; - } - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) - { - BackgroundActive = colours.ChatBlue; - BackgroundInactive = colours.Gray4; - backgroundHover = colours.Gray7; - sampleTabSwitched = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select"); - - highlightBox.Colour = colours.Yellow; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - updateState(); - FinishTransforms(true); - } - - private void updateState() - { - if (Active.Value) - FadeActive(); - else - FadeInactive(); - } - - protected const float TRANSITION_LENGTH = 400; - - private readonly EdgeEffectParameters activateEdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 15, - Colour = Color4.Black.Opacity(0.4f), - }; - - private readonly EdgeEffectParameters deactivateEdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Radius = 10, - Colour = Color4.Black.Opacity(0.2f), - }; - - protected virtual void FadeActive() - { - this.ResizeHeightTo(1.1f, TRANSITION_LENGTH, Easing.OutQuint); - - TweenEdgeEffectTo(activateEdgeEffect, TRANSITION_LENGTH); - - box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint); - highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); - - if (IsBoldWhenActive) Text.Font = Text.Font.With(weight: FontWeight.Bold); - } - - protected virtual void FadeInactive() - { - this.ResizeHeightTo(1, TRANSITION_LENGTH, Easing.OutQuint); - - TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH); - - box.FadeColour(IsHovered ? backgroundHover : BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint); - highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); - - Text.Font = Text.Font.With(weight: FontWeight.Medium); - } - - protected override void OnActivated() - { - if (IsLoaded) - sampleTabSwitched?.Play(); - - updateState(); - } - - protected override void OnDeactivated() => updateState(); - } -} diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs deleted file mode 100644 index d01aec630e..0000000000 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Online.Chat; -using osu.Game.Users.Drawables; -using osuTK; - -namespace osu.Game.Overlays.Chat.Tabs -{ - public class PrivateChannelTabItem : ChannelTabItem - { - protected override IconUsage DisplayIcon => FontAwesome.Solid.At; - - public PrivateChannelTabItem(Channel value) - : base(value) - { - if (value.Type != ChannelType.PM) - throw new ArgumentException("Argument value needs to have the targettype user!"); - - DrawableAvatar avatar; - - AddRange(new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Margin = new MarginPadding - { - Horizontal = 3 - }, - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Children = new Drawable[] - { - new CircularContainer - { - Scale = new Vector2(0.95f), - Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Masking = true, - Child = new DelayedLoadWrapper(avatar = new DrawableAvatar(value.Users.First()) - { - RelativeSizeAxes = Axes.Both - }) - { - RelativeSizeAxes = Axes.Both, - } - }, - } - }, - }); - - avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); - } - - protected override float LeftTextPadding => base.LeftTextPadding + ChatOverlay.TAB_AREA_HEIGHT; - - protected override bool ShowCloseOnHover => false; - - protected override void FadeActive() - { - base.FadeActive(); - - this.ResizeWidthTo(200, TRANSITION_LENGTH, Easing.OutQuint); - CloseButton.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); - } - - protected override void FadeInactive() - { - base.FadeInactive(); - - this.ResizeWidthTo(ChatOverlay.TAB_AREA_HEIGHT + 10, TRANSITION_LENGTH, Easing.OutQuint); - CloseButton.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - var user = Value.Users.First(); - - BackgroundActive = user.Colour != null ? Color4Extensions.FromHex(user.Colour) : colours.BlueDark; - BackgroundInactive = BackgroundActive.Darken(0.5f); - } - } -} diff --git a/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs b/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs deleted file mode 100644 index 178afda5ac..0000000000 --- a/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs +++ /dev/null @@ -1,55 +0,0 @@ -// 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.Graphics; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Overlays.Chat.Tabs -{ - public class TabCloseButton : OsuClickableContainer - { - private readonly SpriteIcon icon; - - public TabCloseButton() - { - Size = new Vector2(20); - - Child = icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.75f), - Icon = FontAwesome.Solid.TimesCircle, - RelativeSizeAxes = Axes.Both, - }; - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - icon.ScaleTo(0.5f, 1000, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - icon.ScaleTo(0.75f, 1000, Easing.OutElastic); - base.OnMouseUp(e); - } - - protected override bool OnHover(HoverEvent e) - { - icon.FadeColour(Color4.Red, 200, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - icon.FadeColour(Color4.White, 200, Easing.OutQuint); - base.OnHoverLost(e); - } - } -} diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 034670cf37..02769b5d68 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -1,35 +1,29 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; -using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Chat; -using osu.Game.Overlays.Chat; -using osu.Game.Overlays.Chat.Selection; -using osu.Game.Overlays.Chat.Tabs; -using osuTK.Input; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; -using osu.Game.Online; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.ChannelList; +using osu.Game.Overlays.Chat.Listing; namespace osu.Game.Overlays { @@ -39,271 +33,147 @@ namespace osu.Game.Overlays public LocalisableString Title => ChatStrings.HeaderTitle; public LocalisableString Description => ChatStrings.HeaderDescription; - private const float text_box_height = 60; - private const float channel_selection_min_height = 0.3f; + private ChatOverlayTopBar topBar = null!; + private ChannelList channelList = null!; + private LoadingLayer loading = null!; + private ChannelListing channelListing = null!; + private ChatTextBar textBar = null!; + private Container currentChannelContainer = null!; - [Resolved] - private ChannelManager channelManager { get; set; } + private readonly Dictionary loadedChannels = new Dictionary(); - private Container currentChannelContainer; + protected IEnumerable DrawableChannels => loadedChannels.Values; - private readonly List loadedChannels = new List(); - - private LoadingSpinner loading; - - private FocusedTextBox textBox; - - private const int transition_length = 500; + private readonly BindableFloat chatHeight = new BindableFloat(); + private bool isDraggingTopBar; + private float dragStartChatHeight; public const float DEFAULT_HEIGHT = 0.4f; - public const float TAB_AREA_HEIGHT = 50; + private const int transition_length = 500; + private const float top_bar_height = 40; + private const float side_bar_width = 190; + private const float chat_bar_height = 60; - protected ChannelTabControl ChannelTabControl; + [Resolved] + private OsuConfigManager config { get; set; } = null!; - protected virtual ChannelTabControl CreateChannelTabControl() => new ChannelTabControl(); + [Resolved] + private ChannelManager channelManager { get; set; } = null!; - private Container chatContainer; - private TabsArea tabsArea; - private Box chatBackground; - private Box tabBackground; + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - public Bindable ChatHeight { get; set; } - - private Container channelSelectionContainer; - protected ChannelSelectionOverlay ChannelSelectionOverlay; + [Cached] + private readonly Bindable currentChannel = new Bindable(); private readonly IBindableList availableChannels = new BindableList(); private readonly IBindableList joinedChannels = new BindableList(); - private readonly Bindable currentChannel = new Bindable(); - - public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) - || (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos)); public ChatOverlay() { + Height = DEFAULT_HEIGHT; + + Masking = true; + + const float corner_radius = 7f; + + CornerRadius = corner_radius; + + // Hack to hide the bottom edge corner radius off-screen. + Margin = new MarginPadding { Bottom = -corner_radius }; + Padding = new MarginPadding { Bottom = corner_radius }; + RelativeSizeAxes = Axes.Both; - RelativePositionAxes = Axes.Both; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, OsuColour colours, TextureStore textures) + private void load() { - const float padding = 5; + // Required for the pop in/out animation + RelativePositionAxes = Axes.Both; Children = new Drawable[] { - channelSelectionContainer = new Container + topBar = new ChatOverlayTopBar + { + RelativeSizeAxes = Axes.X, + Height = top_bar_height, + }, + channelList = new ChannelList + { + RelativeSizeAxes = Axes.Y, + Width = side_bar_width, + Padding = new MarginPadding { Top = top_bar_height }, + }, + new Container { RelativeSizeAxes = Axes.Both, - Height = 1f - DEFAULT_HEIGHT, - Masking = true, - Children = new[] + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { - ChannelSelectionOverlay = new ChannelSelectionOverlay + Top = top_bar_height, + Left = side_bar_width, + Bottom = chat_bar_height, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + loading = new LoadingLayer(true), + channelListing = new ChannelListing { RelativeSizeAxes = Axes.Both, }, }, }, - chatContainer = new Container + textBar = new ChatTextBar { - Name = @"chat container", - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Height = DEFAULT_HEIGHT, - Children = new[] - { - new Container - { - Name = @"chat area", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = TAB_AREA_HEIGHT }, - Children = new Drawable[] - { - chatBackground = new Box - { - RelativeSizeAxes = Axes.Both, - }, - new OnlineViewContainer("Sign in to chat") - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - currentChannelContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Bottom = text_box_height - }, - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = text_box_height, - Padding = new MarginPadding - { - Top = padding * 2, - Bottom = padding * 2, - Left = ChatLine.LEFT_PADDING + padding * 2, - Right = padding * 2, - }, - Children = new Drawable[] - { - textBox = new FocusedTextBox - { - RelativeSizeAxes = Axes.Both, - Height = 1, - PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder, - ReleaseFocusOnCommit = false, - HoldFocus = true, - } - } - }, - loading = new LoadingSpinner(), - }, - } - } - }, - tabsArea = new TabsArea - { - Children = new Drawable[] - { - tabBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - new Sprite - { - Texture = textures.Get(IconTexture), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(OverlayTitle.ICON_SIZE), - Margin = new MarginPadding { Left = 10 }, - }, - ChannelTabControl = CreateChannelTabControl().With(d => - { - d.Anchor = Anchor.BottomLeft; - d.Origin = Anchor.BottomLeft; - d.RelativeSizeAxes = Axes.Both; - d.OnRequestLeave = channelManager.LeaveChannel; - d.IsSwitchable = true; - }), - } - }, - }, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Padding = new MarginPadding { Left = side_bar_width }, }, }; - - availableChannels.BindTo(channelManager.AvailableChannels); - joinedChannels.BindTo(channelManager.JoinedChannels); - currentChannel.BindTo(channelManager.CurrentChannel); - - textBox.OnCommit += postMessage; - - ChannelTabControl.Current.ValueChanged += current => currentChannel.Value = current.NewValue; - ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; - ChannelSelectionOverlay.State.ValueChanged += state => - { - // Propagate the visibility state to ChannelSelectorActive - ChannelTabControl.ChannelSelectorActive.Value = state.NewValue == Visibility.Visible; - - if (state.NewValue == Visibility.Visible) - { - textBox.HoldFocus = false; - if (1f - ChatHeight.Value < channel_selection_min_height) - this.TransformBindableTo(ChatHeight, 1f - channel_selection_min_height, 800, Easing.OutQuint); - } - else - textBox.HoldFocus = true; - }; - - ChannelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel); - ChannelSelectionOverlay.OnRequestLeave = channelManager.LeaveChannel; - - ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); - ChatHeight.BindValueChanged(height => - { - chatContainer.Height = height.NewValue; - channelSelectionContainer.Height = 1f - height.NewValue; - tabBackground.FadeTo(height.NewValue == 1f ? 1f : 0.8f, 200); - }, true); - - chatBackground.Colour = colours.ChatBlue; - - loading.Show(); - - // This is a relatively expensive (and blocking) operation. - // Scheduling it ensures that it won't be performed unless the user decides to open chat. - // TODO: Refactor OsuFocusedOverlayContainer / OverlayContainer to support delayed content loading. - Schedule(() => - { - // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. - joinedChannels.BindCollectionChanged(joinedChannelsChanged, true); - availableChannels.BindCollectionChanged(availableChannelsChanged, true); - currentChannel.BindValueChanged(currentChannelChanged, true); - }); } - private void currentChannelChanged(ValueChangedEvent e) + protected override void LoadComplete() { - if (e.NewValue == null) + base.LoadComplete(); + + config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight); + + chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); + + currentChannel.BindTo(channelManager.CurrentChannel); + joinedChannels.BindTo(channelManager.JoinedChannels); + availableChannels.BindTo(channelManager.AvailableChannels); + + Schedule(() => { - textBox.Current.Disabled = true; - currentChannelContainer.Clear(false); - ChannelSelectionOverlay.Show(); - return; - } + currentChannel.BindValueChanged(currentChannelChanged, true); + joinedChannels.BindCollectionChanged(joinedChannelsChanged, true); + availableChannels.BindCollectionChanged(availableChannelsChanged, true); + }); - if (e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel) - return; + channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel; + channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel); - textBox.Current.Disabled = e.NewValue.ReadOnly; + channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel); + channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel); - if (ChannelTabControl.Current.Value != e.NewValue) - Scheduler.Add(() => ChannelTabControl.Current.Value = e.NewValue); - - var loaded = loadedChannels.Find(d => d.Channel == e.NewValue); - - if (loaded == null) - { - currentChannelContainer.FadeOut(500, Easing.OutQuint); - loading.Show(); - - loaded = new DrawableChannel(e.NewValue); - loadedChannels.Add(loaded); - LoadComponentAsync(loaded, l => - { - if (currentChannel.Value != e.NewValue) - return; - - // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means). - if (!loadedChannels.Contains(loaded)) - return; - - loading.Hide(); - - currentChannelContainer.Clear(false); - currentChannelContainer.Add(loaded); - currentChannelContainer.FadeIn(500, Easing.OutQuint); - }); - } - else - { - currentChannelContainer.Clear(false); - currentChannelContainer.Add(loaded); - } - - // mark channel as read when channel switched - if (e.NewValue.Messages.Any()) - channelManager.MarkChannelAsRead(e.NewValue); + textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms; + textBar.OnChatMessageCommitted += handleChatMessage; } /// @@ -320,7 +190,7 @@ namespace osu.Game.Overlays if (!channel.Joined.Value) channel = channelManager.JoinChannel(channel); - currentChannel.Value = channel; + channelManager.CurrentChannel.Value = channel; } channel.HighlightedMessage.Value = message; @@ -328,159 +198,172 @@ namespace osu.Game.Overlays Show(); } - private float startDragChatHeight; - private bool isDragging; - - protected override bool OnDragStart(DragStartEvent e) - { - isDragging = tabsArea.IsHovered; - - if (!isDragging) - return base.OnDragStart(e); - - startDragChatHeight = ChatHeight.Value; - return true; - } - - protected override void OnDrag(DragEvent e) - { - if (isDragging) - { - float targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; - - // If the channel selection screen is shown, mind its minimum height - if (ChannelSelectionOverlay.State.Value == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height) - targetChatHeight = 1f - channel_selection_min_height; - - ChatHeight.Value = targetChatHeight; - } - } - - protected override void OnDragEnd(DragEndEvent e) - { - isDragging = false; - base.OnDragEnd(e); - } - - private void selectTab(int index) - { - var channel = ChannelTabControl.Items - .Where(tab => !(tab is ChannelSelectorTabItem.ChannelSelectorTabChannel)) - .ElementAtOrDefault(index); - if (channel != null) - ChannelTabControl.Current.Value = channel; - } - - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.AltPressed) - { - switch (e.Key) - { - case Key.Number1: - case Key.Number2: - case Key.Number3: - case Key.Number4: - case Key.Number5: - case Key.Number6: - case Key.Number7: - case Key.Number8: - case Key.Number9: - selectTab((int)e.Key - (int)Key.Number1); - return true; - - case Key.Number0: - selectTab(9); - return true; - } - } - - return base.OnKeyDown(e); - } - public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) { case PlatformAction.TabNew: - ChannelTabControl.SelectChannelSelectorTab(); + currentChannel.Value = channelList.ChannelListingChannel; + return true; + + case PlatformAction.DocumentClose: + channelManager.LeaveChannel(currentChannel.Value); return true; case PlatformAction.TabRestore: channelManager.JoinLastClosedChannel(); return true; - case PlatformAction.DocumentClose: - channelManager.LeaveChannel(currentChannel.Value); + case PlatformAction.DocumentPrevious: + cycleChannel(-1); return true; - } - return false; + case PlatformAction.DocumentNext: + cycleChannel(1); + return true; + + default: + return false; + } } public void OnReleased(KeyBindingReleaseEvent e) { } - public override bool AcceptsFocus => true; - - protected override void OnFocus(FocusEvent e) + protected override bool OnDragStart(DragStartEvent e) { - // this is necessary as textbox is masked away and therefore can't get focus :( - textBox.TakeFocus(); - base.OnFocus(e); + isDraggingTopBar = topBar.IsHovered; + + if (!isDraggingTopBar) + return base.OnDragStart(e); + + dragStartChatHeight = chatHeight.Value; + return true; + } + + protected override void OnDrag(DragEvent e) + { + if (!isDraggingTopBar) + return; + + float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; + chatHeight.Value = targetChatHeight; + } + + protected override void OnDragEnd(DragEndEvent e) + { + isDraggingTopBar = false; + base.OnDragEnd(e); } protected override void PopIn() { + base.PopIn(); + this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); - - textBox.HoldFocus = true; - - base.PopIn(); } protected override void PopOut() { + base.PopOut(); + this.MoveToY(Height, transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine); - ChannelSelectionOverlay.Hide(); - - textBox.HoldFocus = false; - base.PopOut(); + textBar.TextBoxKillFocus(); } + protected override void OnFocus(FocusEvent e) + { + textBar.TextBoxTakeFocus(); + base.OnFocus(e); + } + + private void currentChannelChanged(ValueChangedEvent channel) + { + Channel? newChannel = channel.NewValue; + + // null channel denotes that we should be showing the listing. + if (newChannel == null) + { + currentChannel.Value = channelList.ChannelListingChannel; + return; + } + + if (newChannel is ChannelListing.ChannelListingChannel) + { + currentChannelContainer.Clear(false); + channelListing.Show(); + textBar.ShowSearch.Value = true; + } + else + { + channelListing.Hide(); + textBar.ShowSearch.Value = false; + + if (loadedChannels.ContainsKey(newChannel)) + { + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loadedChannels[newChannel]); + } + else + { + loading.Show(); + + // Ensure the drawable channel is stored before async load to prevent double loading + ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel); + loadedChannels.Add(newChannel, drawableChannel); + + LoadComponentAsync(drawableChannel, loadedDrawable => + { + // Ensure the current channel hasn't changed by the time the load completes + if (currentChannel.Value != loadedDrawable.Channel) + return; + + // Ensure the cached reference hasn't been removed from leaving the channel + if (!loadedChannels.ContainsKey(loadedDrawable.Channel)) + return; + + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loadedDrawable); + loading.Hide(); + }); + } + } + + // Mark channel as read when channel switched + if (newChannel.Messages.Any()) + channelManager.MarkChannelAsRead(newChannel); + } + + protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel); + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) { case NotifyCollectionChangedAction.Add: - foreach (Channel channel in args.NewItems.Cast()) - { - if (channel.Type != ChannelType.Multiplayer) - ChannelTabControl.AddChannel(channel); - } + IEnumerable newChannels = args.NewItems.OfType().Where(isChatChannel); + + foreach (var channel in newChannels) + channelList.AddChannel(channel); break; case NotifyCollectionChangedAction.Remove: - foreach (Channel channel in args.OldItems.Cast()) + IEnumerable leftChannels = args.OldItems.OfType().Where(isChatChannel); + + foreach (var channel in leftChannels) { - if (!ChannelTabControl.Items.Contains(channel)) - continue; + channelList.RemoveChannel(channel); - ChannelTabControl.RemoveChannel(channel); - - var loaded = loadedChannels.Find(c => c.Channel == channel); - - if (loaded != null) + if (loadedChannels.ContainsKey(channel)) { - // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared - // to ensure that the previous channel doesn't get updated after it's disposed - loadedChannels.Remove(loaded); - currentChannelContainer.Remove(loaded); + ChatOverlayDrawableChannel loaded = loadedChannels[channel]; + loadedChannels.Remove(channel); + // DrawableChannel removed from cache must be manually disposed loaded.Dispose(); } } @@ -490,35 +373,47 @@ namespace osu.Game.Overlays } private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) - { - ChannelSelectionOverlay.UpdateAvailableChannels(availableChannels); - } + => channelListing.UpdateAvailableChannels(channelManager.AvailableChannels); - private void postMessage(TextBox textBox, bool newText) + private void handleChatMessage(string message) { - string text = textBox.Text.Trim(); - - if (string.IsNullOrWhiteSpace(text)) + if (string.IsNullOrWhiteSpace(message)) return; - if (text[0] == '/') - channelManager.PostCommand(text.Substring(1)); + if (message[0] == '/') + channelManager.PostCommand(message.Substring(1)); else - channelManager.PostMessage(text); - - textBox.Text = string.Empty; + channelManager.PostMessage(message); } - private class TabsArea : Container + private void cycleChannel(int direction) { - // IsHovered is used - public override bool HandlePositionalInput => true; + List overlayChannels = channelList.Channels.ToList(); - public TabsArea() + if (overlayChannels.Count < 2) + return; + + int currentIndex = overlayChannels.IndexOf(currentChannel.Value); + + currentChannel.Value = overlayChannels[(currentIndex + direction + overlayChannels.Count) % overlayChannels.Count]; + + channelList.ScrollChannelIntoView(currentChannel.Value); + } + + /// + /// Whether a channel should be displayed in this overlay, based on its type. + /// + private static bool isChatChannel(Channel channel) + { + switch (channel.Type) { - Name = @"tabs area"; - RelativeSizeAxes = Axes.X; - Height = TAB_AREA_HEIGHT; + case ChannelType.Multiplayer: + case ChannelType.Spectator: + case ChannelType.Temporary: + return false; + + default: + return true; } } } diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs deleted file mode 100644 index ef479ea21b..0000000000 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable enable - -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Diagnostics; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Game.Configuration; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Localisation; -using osu.Game.Online.Chat; -using osu.Game.Overlays.Chat; -using osu.Game.Overlays.Chat.ChannelList; -using osu.Game.Overlays.Chat.Listing; - -namespace osu.Game.Overlays -{ - public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent - { - public string IconTexture => "Icons/Hexacons/messaging"; - public LocalisableString Title => ChatStrings.HeaderTitle; - public LocalisableString Description => ChatStrings.HeaderDescription; - - private ChatOverlayTopBar topBar = null!; - private ChannelList channelList = null!; - private LoadingLayer loading = null!; - private ChannelListing channelListing = null!; - private ChatTextBar textBar = null!; - private Container currentChannelContainer = null!; - - private readonly Dictionary loadedChannels = new Dictionary(); - - protected IEnumerable DrawableChannels => loadedChannels.Values; - - private readonly BindableFloat chatHeight = new BindableFloat(); - private bool isDraggingTopBar; - private float dragStartChatHeight; - - private const int transition_length = 500; - private const float default_chat_height = 0.4f; - private const float top_bar_height = 40; - private const float side_bar_width = 190; - private const float chat_bar_height = 60; - - [Resolved] - private OsuConfigManager config { get; set; } = null!; - - [Resolved] - private ChannelManager channelManager { get; set; } = null!; - - [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - - [Cached] - private readonly Bindable currentChannel = new Bindable(); - - private readonly IBindableList availableChannels = new BindableList(); - private readonly IBindableList joinedChannels = new BindableList(); - - public ChatOverlayV2() - { - Height = default_chat_height; - - Masking = true; - - const float corner_radius = 7f; - - CornerRadius = corner_radius; - - // Hack to hide the bottom edge corner radius off-screen. - Margin = new MarginPadding { Bottom = -corner_radius }; - Padding = new MarginPadding { Bottom = corner_radius }; - - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - } - - [BackgroundDependencyLoader] - private void load() - { - // Required for the pop in/out animation - RelativePositionAxes = Axes.Both; - - Children = new Drawable[] - { - topBar = new ChatOverlayTopBar - { - RelativeSizeAxes = Axes.X, - Height = top_bar_height, - }, - channelList = new ChannelList - { - RelativeSizeAxes = Axes.Y, - Width = side_bar_width, - Padding = new MarginPadding { Top = top_bar_height }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Padding = new MarginPadding - { - Top = top_bar_height, - Left = side_bar_width, - Bottom = chat_bar_height, - }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4, - }, - currentChannelContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - loading = new LoadingLayer(true), - channelListing = new ChannelListing - { - RelativeSizeAxes = Axes.Both, - }, - }, - }, - textBar = new ChatTextBar - { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Padding = new MarginPadding { Left = side_bar_width }, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight); - - chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); - - currentChannel.BindTo(channelManager.CurrentChannel); - currentChannel.BindValueChanged(currentChannelChanged, true); - - joinedChannels.BindTo(channelManager.JoinedChannels); - joinedChannels.BindCollectionChanged(joinedChannelsChanged, true); - - availableChannels.BindTo(channelManager.AvailableChannels); - availableChannels.BindCollectionChanged(availableChannelsChanged, true); - - channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel; - channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel); - - channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel); - channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel); - - textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms; - textBar.OnChatMessageCommitted += handleChatMessage; - } - - /// - /// Highlights a certain message in the specified channel. - /// - /// The message to highlight. - /// The channel containing the message. - public void HighlightMessage(Message message, Channel channel) - { - Debug.Assert(channel.Id == message.ChannelId); - - if (currentChannel.Value?.Id != channel.Id) - { - if (!channel.Joined.Value) - channel = channelManager.JoinChannel(channel); - - channelManager.CurrentChannel.Value = channel; - } - - channel.HighlightedMessage.Value = message; - - Show(); - } - - protected override bool OnDragStart(DragStartEvent e) - { - isDraggingTopBar = topBar.IsHovered; - - if (!isDraggingTopBar) - return base.OnDragStart(e); - - dragStartChatHeight = chatHeight.Value; - return true; - } - - protected override void OnDrag(DragEvent e) - { - if (!isDraggingTopBar) - return; - - float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; - chatHeight.Value = targetChatHeight; - } - - protected override void OnDragEnd(DragEndEvent e) - { - isDraggingTopBar = false; - base.OnDragEnd(e); - } - - protected override void PopIn() - { - base.PopIn(); - - this.MoveToY(0, transition_length, Easing.OutQuint); - this.FadeIn(transition_length, Easing.OutQuint); - } - - protected override void PopOut() - { - base.PopOut(); - - this.MoveToY(Height, transition_length, Easing.InSine); - this.FadeOut(transition_length, Easing.InSine); - - textBar.TextBoxKillFocus(); - } - - protected override void OnFocus(FocusEvent e) - { - textBar.TextBoxTakeFocus(); - base.OnFocus(e); - } - - private void currentChannelChanged(ValueChangedEvent channel) - { - Channel? newChannel = channel.NewValue; - - // null channel denotes that we should be showing the listing. - if (newChannel == null) - { - currentChannel.Value = channelList.ChannelListingChannel; - return; - } - - if (newChannel is ChannelListing.ChannelListingChannel) - { - currentChannelContainer.Clear(false); - channelListing.Show(); - textBar.ShowSearch.Value = true; - } - else - { - channelListing.Hide(); - textBar.ShowSearch.Value = false; - - if (loadedChannels.ContainsKey(newChannel)) - { - currentChannelContainer.Clear(false); - currentChannelContainer.Add(loadedChannels[newChannel]); - } - else - { - loading.Show(); - - // Ensure the drawable channel is stored before async load to prevent double loading - ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel); - loadedChannels.Add(newChannel, drawableChannel); - - LoadComponentAsync(drawableChannel, loadedDrawable => - { - // Ensure the current channel hasn't changed by the time the load completes - if (currentChannel.Value != loadedDrawable.Channel) - return; - - // Ensure the cached reference hasn't been removed from leaving the channel - if (!loadedChannels.ContainsKey(loadedDrawable.Channel)) - return; - - currentChannelContainer.Clear(false); - currentChannelContainer.Add(loadedDrawable); - loading.Hide(); - }); - } - } - } - - protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel); - - private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) - { - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - IEnumerable newChannels = filterChannels(args.NewItems); - - foreach (var channel in newChannels) - channelList.AddChannel(channel); - - break; - - case NotifyCollectionChangedAction.Remove: - IEnumerable leftChannels = filterChannels(args.OldItems); - - foreach (var channel in leftChannels) - { - channelList.RemoveChannel(channel); - - if (loadedChannels.ContainsKey(channel)) - { - ChatOverlayDrawableChannel loaded = loadedChannels[channel]; - loadedChannels.Remove(channel); - // DrawableChannel removed from cache must be manually disposed - loaded.Dispose(); - } - } - - break; - } - } - - private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) - => channelListing.UpdateAvailableChannels(channelManager.AvailableChannels); - - private IEnumerable filterChannels(IList channels) - => channels.Cast().Where(c => c.Type == ChannelType.Public || c.Type == ChannelType.PM); - - private void handleChatMessage(string message) - { - if (string.IsNullOrWhiteSpace(message)) - return; - - if (message[0] == '/') - channelManager.PostCommand(message.Substring(1)); - else - channelManager.PostMessage(message); - } - } -} diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index a9312e9a3a..23f67a06cb 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -9,11 +10,16 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Screens; 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.Spectator; +using osu.Game.Resources.Localisation.Web; using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; @@ -24,26 +30,62 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { + private const float search_textbox_height = 40; + private const float padding = 10; + private readonly IBindableList playingUsers = new BindableList(); - private FillFlowContainer userFlow; + private SearchContainer userFlow; + private BasicSearchTextBox searchTextBox; [Resolved] private SpectatorClient spectatorClient { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = userFlow = new FillFlowContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(10), - Spacing = new Vector2(10), + new Box + { + RelativeSizeAxes = Axes.X, + Height = padding * 2 + search_textbox_height, + Colour = colourProvider.Background4, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Padding = new MarginPadding(padding), + Child = searchTextBox = new BasicSearchTextBox + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Height = search_textbox_height, + ReleaseFocusOnCommit = false, + HoldFocus = true, + PlaceholderText = HomeStrings.SearchPlaceholder, + }, + }, + userFlow = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Padding = new MarginPadding + { + Top = padding * 3 + search_textbox_height, + Bottom = padding, + Right = padding, + Left = padding, + }, + }, }; + + searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue; } [Resolved] @@ -57,6 +99,13 @@ namespace osu.Game.Overlays.Dashboard playingUsers.BindCollectionChanged(onPlayingUsersChanged, true); } + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + + searchTextBox.TakeFocus(); + } + private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => { switch (e.Action) @@ -102,17 +151,34 @@ namespace osu.Game.Overlays.Dashboard panel.Origin = Anchor.TopCentre; }); - private class PlayingUserPanel : CompositeDrawable + public class PlayingUserPanel : CompositeDrawable, IFilterable { public readonly APIUser User; + public IEnumerable FilterTerms { get; } + [Resolved(canBeNull: true)] private IPerformFromScreenRunner performer { get; set; } + public bool FilteringActive { set; get; } + + public bool MatchingFilter + { + set + { + if (value) + Show(); + else + Hide(); + } + } + public PlayingUserPanel(APIUser user) { User = user; + FilterTerms = new LocalisableString[] { User.Username }; + AutoSizeAxes = Axes.Both; } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 83ad8faf1c..79d972bdcc 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -16,6 +16,8 @@ namespace osu.Game.Overlays protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader(); + public override bool AcceptsFocus => false; + protected override void CreateDisplayToLoad(DashboardOverlayTabs tab) { switch (tab) diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs index 17e04c0c99..ddcee7c040 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs @@ -154,12 +154,15 @@ namespace osu.Game.Overlays.FirstRunSetup var downloadTracker = tutorialDownloader.DownloadTrackers.First(); + downloadTracker.State.BindValueChanged(state => + { + if (state.NewValue == DownloadState.LocallyAvailable) + downloadTutorialButton.Complete(); + }, true); + downloadTracker.Progress.BindValueChanged(progress => { downloadTutorialButton.SetProgress(progress.NewValue, false); - - if (progress.NewValue == 1) - downloadTutorialButton.Complete(); }, true); } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 602ace6dea..d79ba593f7 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Localisation; using osu.Framework.Platform; +using osu.Framework.Platform.Windows; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -34,10 +35,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private Bindable sizeFullscreen; private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) }); + private readonly IBindable fullscreenCapability = new Bindable(FullscreenCapability.Capable); [Resolved] private OsuGameBase game { get; set; } + [Resolved] + private GameHost host { get; set; } + private SettingsDropdown resolutionDropdown; private SettingsDropdown displayDropdown; private SettingsDropdown windowModeDropdown; @@ -65,6 +70,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModes.BindTo(host.Window.SupportedWindowModes); } + if (host.Window is WindowsWindow windowsWindow) + fullscreenCapability.BindTo(windowsWindow.FullscreenCapability); + Children = new Drawable[] { windowModeDropdown = new SettingsDropdown @@ -139,6 +147,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } }, }; + + fullscreenCapability.BindValueChanged(_ => Schedule(updateScreenModeWarning), true); } protected override void LoadComplete() @@ -150,8 +160,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown.Current.BindValueChanged(mode => { updateDisplayModeDropdowns(); - - windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default; + updateScreenModeWarning(); }, true); windowModes.BindCollectionChanged((sender, args) => @@ -213,6 +222,38 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } } + private void updateScreenModeWarning() + { + if (windowModeDropdown.Current.Value != WindowMode.Fullscreen) + { + windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true); + return; + } + + if (host.Window is WindowsWindow) + { + switch (fullscreenCapability.Value) + { + case FullscreenCapability.Unknown: + windowModeDropdown.SetNoticeText(LayoutSettingsStrings.CheckingForFullscreenCapabilities, true); + break; + + case FullscreenCapability.Capable: + windowModeDropdown.SetNoticeText(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen); + break; + + case FullscreenCapability.Incapable: + windowModeDropdown.SetNoticeText(LayoutSettingsStrings.UnableToRunExclusiveFullscreen, true); + break; + } + } + else + { + // We can only detect exclusive fullscreen status on windows currently. + windowModeDropdown.ClearNoticeText(); + } + } + private void bindPreviewEvent(Bindable bindable) { bindable.ValueChanged += _ => diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 653f30a018..8c3e45cd62 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -48,7 +48,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics frameLimiterDropdown.Current.BindValueChanged(limit => { - frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? GraphicsSettingsStrings.UnlimitedFramesNote : default; + switch (limit.NewValue) + { + case FrameSync.Unlimited: + frameLimiterDropdown.SetNoticeText(GraphicsSettingsStrings.UnlimitedFramesNote, true); + break; + + default: + frameLimiterDropdown.ClearNoticeText(); + break; + } }, true); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 4235dc0a05..1511d53b6b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -117,9 +117,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { if (highPrecision.NewValue) - highPrecisionMouse.WarningText = MouseSettingsStrings.HighPrecisionPlatformWarning; + highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true); else - highPrecisionMouse.WarningText = null; + highPrecisionMouse.ClearNoticeText(); } }, true); } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 802d442ced..5d31c38ae7 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -11,6 +11,7 @@ using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; using osu.Game.Localisation; @@ -95,11 +96,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input Origin = Anchor.TopCentre, Text = TabletSettingsStrings.NoTabletDetected, }, - new SettingsNoticeText(colours) + new LinkFlowContainer(cp => cp.Colour = colours.Yellow) { TextAnchor = Anchor.TopCentre, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, }.With(t => { if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a34776ddf0..a87e65b735 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -127,9 +127,12 @@ namespace osu.Game.Overlays.Settings.Sections dropdownItems.Add(skin.ToLive(realm)); dropdownItems.Insert(protectedCount, random_skin_info); - skinDropdown.Items = dropdownItems; + Schedule(() => + { + skinDropdown.Items = dropdownItems; - updateSelectedSkinFromConfig(); + updateSelectedSkinFromConfig(); + }); } private void updateSelectedSkinFromConfig() diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 284e9cb2de..fceffa09c5 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface user.BindValueChanged(u => { - backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? UserInterfaceStrings.NotSupporterNote : default; + if (u.NewValue?.IsSupporter != true) + backgroundSourceDropdown.SetNoticeText(UserInterfaceStrings.NotSupporterNote, true); + else + backgroundSourceDropdown.ClearNoticeText(); }, true); } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index ee9daa1c0d..ea076b77ac 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings private SpriteText labelText; - private OsuTextFlowContainer warningText; + private OsuTextFlowContainer noticeText; public bool ShowsDefaultIndicator = true; private readonly Container defaultValueIndicatorContainer; @@ -70,27 +70,32 @@ namespace osu.Game.Overlays.Settings } /// - /// Text to be displayed at the bottom of this . - /// Generally used to recommend the user change their setting as the current one is considered sub-optimal. + /// Clear any warning text. /// - public LocalisableString? WarningText + public void ClearNoticeText() { - set + noticeText?.Expire(); + noticeText = null; + } + + /// + /// Set the text to be displayed at the bottom of this . + /// Generally used to provide feedback to a user about a sub-optimal setting. + /// + /// The text to display. + /// Whether the text is in a warning state. Will decide how this is visually represented. + public void SetNoticeText(LocalisableString text, bool isWarning = false) + { + ClearNoticeText(); + + // construct lazily for cases where the label is not needed (may be provided by the Control). + FlowContent.Add(noticeText = new LinkFlowContainer(cp => cp.Colour = isWarning ? colours.Yellow : colours.Green) { - bool hasValue = value != default; - - if (warningText == null) - { - if (!hasValue) - return; - - // construct lazily for cases where the label is not needed (may be provided by the Control). - FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } }); - } - - warningText.Alpha = hasValue ? 1 : 0; - warningText.Text = value ?? default; - } + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 5 }, + Text = text, + }); } public virtual Bindable Current diff --git a/osu.Game/Overlays/Settings/SettingsNoticeText.cs b/osu.Game/Overlays/Settings/SettingsNoticeText.cs deleted file mode 100644 index 76ecf7edd4..0000000000 --- a/osu.Game/Overlays/Settings/SettingsNoticeText.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Graphics; -using osu.Game.Graphics; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Overlays.Settings -{ - public class SettingsNoticeText : LinkFlowContainer - { - public SettingsNoticeText(OsuColour colours) - : base(s => s.Colour = colours.Yellow) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - } - } -} diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 46b8b35da2..929c362bd8 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -329,7 +329,7 @@ namespace osu.Game.Overlays.Volume if (isPrecise) { - scrollAccumulation += delta * adjust_step * 0.1; + scrollAccumulation += delta * adjust_step; while (Precision.AlmostBigger(Math.Abs(scrollAccumulation), precision)) { diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 7019dad803..aaee15eae8 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Edit public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler where TObject : HitObject { + private const float adjust_step = 0.1f; + public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) { MinValue = 0.1, @@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Edit Child = distanceSpacingSlider = new ExpandableSlider> { Current = { BindTarget = DistanceSpacingMultiplier }, - KeyboardStep = 0.1f, + KeyboardStep = adjust_step, } } }); @@ -93,7 +95,7 @@ namespace osu.Game.Rulesets.Edit { case GlobalAction.EditorIncreaseDistanceSpacing: case GlobalAction.EditorDecreaseDistanceSpacing: - return adjustDistanceSpacing(e.Action, 0.1f); + return adjustDistanceSpacing(e.Action, adjust_step); } return false; @@ -109,7 +111,7 @@ namespace osu.Game.Rulesets.Edit { case GlobalAction.EditorIncreaseDistanceSpacing: case GlobalAction.EditorDecreaseDistanceSpacing: - return adjustDistanceSpacing(e.Action, e.ScrollAmount * (e.IsPrecise ? 0.01f : 0.1f)); + return adjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step); } return false; diff --git a/osu.Game/Screens/Edit/BottomBar.cs b/osu.Game/Screens/Edit/BottomBar.cs new file mode 100644 index 0000000000..62caaced89 --- /dev/null +++ b/osu.Game/Screens/Edit/BottomBar.cs @@ -0,0 +1,81 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Overlays; +using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Edit.Components.Timelines.Summary; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit +{ + internal class BottomBar : CompositeDrawable + { + public TestGameplayButton TestGameplayButton { get; private set; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, Editor editor) + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + RelativeSizeAxes = Axes.X; + + Height = 60; + + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.2f), + Type = EdgeEffectType.Shadow, + Radius = 10f, + }; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 170), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 220), + new Dimension(GridSizeMode.Absolute, 120), + }, + Content = new[] + { + new Drawable[] + { + new TimeInfoContainer { RelativeSizeAxes = Axes.Both }, + new SummaryTimeline { RelativeSizeAxes = Axes.Both }, + new PlaybackControl { RelativeSizeAxes = Axes.Both }, + TestGameplayButton = new TestGameplayButton + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10 }, + Size = new Vector2(1), + Action = editor.TestGameplay, + } + }, + } + }, + } + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index 08091fc3f7..3c63da3a4a 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -8,20 +8,19 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Components { public class BottomBarContainer : Container { - private const float corner_radius = 5; private const float contents_padding = 15; protected readonly IBindable Beatmap = new Bindable(); protected readonly IBindable Track = new Bindable(); - private readonly Drawable background; + protected readonly Drawable Background; private readonly Container content; protected override Container Content => content; @@ -29,11 +28,14 @@ namespace osu.Game.Screens.Edit.Components public BottomBarContainer() { Masking = true; - CornerRadius = corner_radius; InternalChildren = new[] { - background = new Box { RelativeSizeAxes = Axes.Both }, + Background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Transparent, + }, content = new Container { RelativeSizeAxes = Axes.Both, @@ -43,12 +45,10 @@ namespace osu.Game.Screens.Edit.Components } [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours, EditorClock clock) + private void load(IBindable beatmap, EditorClock clock) { Beatmap.BindTo(beatmap); Track.BindTo(clock.Track); - - background.Colour = colours.Gray1; } } } diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index 440071bc4c..20b8bba6da 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -110,11 +110,31 @@ namespace osu.Game.Screens.Edit.Components.Menus case EditorMenuItemSpacer spacer: return new DrawableSpacer(spacer); + case StatefulMenuItem stateful: + return new EditorStatefulMenuItem(stateful); + default: return new EditorMenuItem(item); } } + private class EditorStatefulMenuItem : DrawableStatefulMenuItem + { + public EditorStatefulMenuItem(StatefulMenuItem item) + : base(item) + { + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + BackgroundColour = colourProvider.Background2; + BackgroundColourHover = colourProvider.Background1; + + Foreground.Padding = new MarginPadding { Vertical = 2 }; + } + } + private class EditorMenuItem : DrawableOsuMenuItem { public EditorMenuItem(MenuItem item) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index bdc6e238c8..d1a999c2d1 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK.Input; namespace osu.Game.Screens.Edit.Components @@ -155,10 +156,10 @@ namespace osu.Game.Screens.Edit.Components private Color4 normalColour; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { - text.Colour = normalColour = colours.YellowDarker; - textBold.Colour = hoveredColour = colours.Yellow; + text.Colour = normalColour = colourProvider.Light3; + textBold.Colour = hoveredColour = colourProvider.Content1; } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 0a8c339559..bd5377e578 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -6,28 +6,43 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Game.Extensions; using osu.Game.Graphics; +using osu.Game.Overlays; +using osuTK; namespace osu.Game.Screens.Edit.Components { public class TimeInfoContainer : BottomBarContainer { - private readonly OsuSpriteText trackTimer; + private OsuSpriteText trackTimer; + private OsuSpriteText bpm; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } [Resolved] private EditorClock editorClock { get; set; } - public TimeInfoContainer() + [BackgroundDependencyLoader] + private void load(OsuColour colours, OverlayColourProvider colourProvider) { + Background.Colour = colourProvider.Background5; + Children = new Drawable[] { trackTimer = new OsuSpriteText { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - // intentionally fudged centre to avoid movement of the number portion when - // going negative. - X = -35, - Font = OsuFont.GetFont(size: 25, fixedWidth: true), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = new Vector2(-2, 0), + Font = OsuFont.Torus.With(size: 36, fixedWidth: true, weight: FontWeight.Light), + Y = -10, + }, + bpm = new OsuSpriteText + { + Colour = colours.Orange1, + Anchor = Anchor.CentreLeft, + Font = OsuFont.Torus.With(size: 18, weight: FontWeight.SemiBold), + Position = new Vector2(2, 5), } }; } @@ -36,6 +51,7 @@ namespace osu.Game.Screens.Edit.Components { base.Update(); trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); + bpm.Text = @$"{editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM:0} BPM"; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index e90ae411de..1706c47c96 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; +using osu.Game.Overlays; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; +using osuTK; namespace osu.Game.Screens.Edit.Components.Timelines.Summary { @@ -17,8 +17,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary public class SummaryTimeline : BottomBarContainer { [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { + Background.Colour = colourProvider.Background6; + Children = new Drawable[] { new MarkerPart { RelativeSizeAxes = Axes.Both }, @@ -41,7 +43,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary { Name = "centre line", RelativeSizeAxes = Axes.Both, - Colour = colours.Gray5, + Colour = colourProvider.Background2, Children = new Drawable[] { new Circle diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs index 0d7a4ad057..99cdc014aa 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/TestGameplayButton.cs @@ -28,6 +28,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary BackgroundColour = colours.Orange1; SpriteText.Colour = colourProvider.Background6; + Content.CornerRadius = 0; + Text = "Test!"; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 6812bbb72d..89e9fb2404 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -16,6 +16,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -273,7 +274,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (base.OnMouseDown(e)) beginUserDrag(); - return true; + // handling right button as well breaks context menus inside the timeline, only handle left button for now. + return e.Button == MouseButton.Left; } protected override void OnMouseUp(MouseUpEvent e) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index fda8416ecd..9904d91653 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Vector2 size = Vector2.One; - if (indexInBar != 1) + if (indexInBar != 0) size = BindableBeatDivisor.GetSize(divisor); var line = getNextUsableLine(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bdf204c1b6..1414644a54 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; @@ -26,7 +25,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -37,9 +35,7 @@ using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; -using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; -using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; @@ -48,7 +44,6 @@ using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.Play; using osu.Game.Users; -using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -119,13 +114,13 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; + private BottomBar bottomBar; + [CanBeNull] // Should be non-null once it can support custom rulesets. private EditorChangeHandler changeHandler; private DependencyContainer dependencies; - private TestGameplayButton testGameplayButton; - private bool isNewBeatmap; protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); @@ -148,7 +143,7 @@ namespace osu.Game.Screens.Edit } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuConfigManager config) + private void load(OsuConfigManager config) { var loadableBeatmap = Beatmap.Value; @@ -226,7 +221,7 @@ namespace osu.Game.Screens.Edit AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { new Container { @@ -287,67 +282,7 @@ namespace osu.Game.Screens.Edit }, }, }, - new Container - { - Name = "Bottom bar", - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = 60, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray2 - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = 5, Horizontal = 10 }, - Child = new GridContainer - { - RelativeSizeAxes = Axes.Both, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 220), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 220), - new Dimension(GridSizeMode.Absolute, 120), - }, - Content = new[] - { - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 10 }, - Child = new TimeInfoContainer { RelativeSizeAxes = Axes.Both }, - }, - new SummaryTimeline - { - RelativeSizeAxes = Axes.Both, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10 }, - Child = new PlaybackControl { RelativeSizeAxes = Axes.Both }, - }, - testGameplayButton = new TestGameplayButton - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 10 }, - Size = new Vector2(1), - Action = testGameplay - } - }, - } - }, - } - } - }, + bottomBar = new BottomBar(), } }); @@ -392,6 +327,24 @@ namespace osu.Game.Screens.Edit Clipboard.Content.Value = state.ClipboardContent; }); + public void TestGameplay() + { + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveBeforeGameplayTestDialog(() => + { + Save(); + pushEditorPlayer(); + })); + } + else + { + pushEditorPlayer(); + } + + void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); + } + /// /// Saves the currently edited beatmap. /// @@ -540,7 +493,7 @@ namespace osu.Game.Screens.Edit if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection) scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation)); - scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1); + scrollAccumulation += scrollComponent; // because we are doing snapped seeking, we need to add up precise scrolls until they accumulate to an arbitrary cut-off. while (Math.Abs(scrollAccumulation) >= precision) @@ -589,7 +542,7 @@ namespace osu.Game.Screens.Edit return true; case GlobalAction.EditorTestGameplay: - testGameplayButton.TriggerClick(); + bottomBar.TestGameplayButton.TriggerClick(); return true; default: @@ -935,24 +888,6 @@ namespace osu.Game.Screens.Edit loader?.CancelPendingDifficultySwitch(); } - private void testGameplay() - { - if (HasUnsavedChanges) - { - dialogOverlay.Push(new SaveBeforeGameplayTestDialog(() => - { - Save(); - pushEditorPlayer(); - })); - } - else - { - pushEditorPlayer(); - } - - void pushEditorPlayer() => this.Push(new EditorPlayerLoader(this)); - } - public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); diff --git a/osu.Game/Screens/Edit/EditorTable.cs b/osu.Game/Screens/Edit/EditorTable.cs index a67a060134..26819dcfe7 100644 --- a/osu.Game/Screens/Edit/EditorTable.cs +++ b/osu.Game/Screens/Edit/EditorTable.cs @@ -99,6 +99,15 @@ namespace osu.Game.Screens.Edit colourSelected = colours.Colour3; } + protected override void LoadComplete() + { + base.LoadComplete(); + + // Reduce flicker of rows when offset is being changed rapidly. + // Probably need to reconsider this. + FinishTransforms(true); + } + private bool selected; public bool Selected diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index b95aabc1c4..e0fc5f1aff 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Setup Add(new Box { - Colour = colourProvider.Background2, + Colour = colourProvider.Background3, RelativeSizeAxes = Axes.Both, }); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 0c12eff503..77d875b67f 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -61,6 +61,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(group => { + // TODO: This should scroll the selected row into view. foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue; }, true); } diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index c8944d0357..c9f73411f1 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -31,18 +31,33 @@ namespace osu.Game.Screens.Edit.Timing }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + kiai.Current.BindValueChanged(_ => saveChanges()); + omitBarLine.Current.BindValueChanged(_ => saveChanges()); + scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges()); + + void saveChanges() + { + if (!isRebinding) ChangeHandler?.SaveState(); + } + } + + private bool isRebinding; + protected override void OnControlPointChanged(ValueChangedEvent point) { if (point.NewValue != null) { + isRebinding = true; + kiai.Current = point.NewValue.KiaiModeBindable; - kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; - omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable; - scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + + isRebinding = false; } } diff --git a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs index 4dd7a75d4a..4143c5ea55 100644 --- a/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/MetronomeDisplay.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.LocalisationExtensions; @@ -10,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Containers; @@ -28,12 +31,18 @@ namespace osu.Game.Screens.Edit.Timing private Drawable weight; private Drawable stick; + private IAdjustableClock metronomeClock; + + private Sample clunk; + [Resolved] private OverlayColourProvider overlayColourProvider { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { + clunk = audio.Samples.Get(@"Multiplayer/countdown-tick"); + const float taper = 25; const float swing_vertical_offset = -23; const float lower_cover_height = 32; @@ -192,6 +201,8 @@ namespace osu.Game.Screens.Edit.Timing Y = -3, }, }; + + Clock = new FramedClock(metronomeClock = new StopwatchClock(true)); } private double beatLength; @@ -216,6 +227,8 @@ namespace osu.Game.Screens.Edit.Timing if (BeatSyncSource.ControlPoints == null || BeatSyncSource.Clock == null) return; + metronomeClock.Rate = IsBeatSyncedWithTrack ? BeatSyncSource.Clock.Rate : 1; + timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime); if (beatLength != timingPoint.BeatLength) @@ -262,8 +275,21 @@ namespace osu.Game.Screens.Edit.Timing if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging) { - using (stick.BeginDelayedSequence(beatLength / 2)) + using (BeginDelayedSequence(beatLength / 2)) + { stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint); + + Schedule(() => + { + var channel = clunk?.GetChannel(); + + if (channel != null) + { + channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f); + channel.Play(); + } + }); + } } } } diff --git a/osu.Game/Screens/Edit/Timing/RowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttribute.cs index 74d43628e1..46bb62c9e0 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttribute.cs @@ -19,6 +19,8 @@ namespace osu.Game.Screens.Edit.Timing private readonly string label; + protected Drawable Background { get; private set; } + protected FillFlowContainer Content { get; private set; } public RowAttribute(ControlPoint point, string label) @@ -41,11 +43,11 @@ namespace osu.Game.Screens.Edit.Timing Masking = true; CornerRadius = 3; - InternalChildren = new Drawable[] + InternalChildren = new[] { - new Box + Background = new Box { - Colour = overlayColours.Background4, + Colour = overlayColours.Background5, RelativeSizeAxes = Axes.Both, }, Content = new FillFlowContainer diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs index f8ec4aef25..8a07088545 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/TimingRowAttribute.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Timing.RowAttributes { @@ -24,10 +25,12 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { Content.Add(text = new AttributeText(Point)); + Background.Colour = colourProvider.Background4; + timeSignature.BindValueChanged(_ => updateText()); beatLength.BindValueChanged(_ => updateText(), true); } diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs index d0ab4d1f98..990f8d2ce0 100644 --- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs +++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private EditorClock editorClock { get; set; } + [Resolved] + private EditorBeatmap beatmap { get; set; } + [Resolved] private Bindable selectedGroup { get; set; } @@ -45,6 +49,7 @@ namespace osu.Game.Screens.Edit.Timing { new Dimension(GridSizeMode.Absolute, 200), new Dimension(GridSizeMode.Absolute, 60), + new Dimension(GridSizeMode.Absolute, 60), }, Content = new[] { @@ -77,7 +82,36 @@ namespace osu.Game.Screens.Edit.Timing }, } } - } + }, + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new TimingAdjustButton(1) + { + Text = "Offset", + RelativeSizeAxes = Axes.X, + Width = 0.48f, + Height = 50, + Action = adjustOffset, + }, + new TimingAdjustButton(0.1) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = "BPM", + RelativeSizeAxes = Axes.X, + Width = 0.48f, + Height = 50, + Action = adjustBpm, + } + } + }, }, new Drawable[] { @@ -113,6 +147,35 @@ namespace osu.Game.Screens.Edit.Timing }; } + private void adjustOffset(double adjust) + { + // VERY TEMPORARY + var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray(); + + beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value); + + double newOffset = selectedGroup.Value.Time + adjust; + + foreach (var cp in currentGroupItems) + beatmap.ControlPointInfo.Add(newOffset, cp); + + // the control point might not necessarily exist yet, if currentGroupItems was empty. + selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true); + + if (!editorClock.IsRunning) + editorClock.Seek(newOffset); + } + + private void adjustBpm(double adjust) + { + var timing = selectedGroup.Value.ControlPoints.OfType().FirstOrDefault(); + + if (timing == null) + return; + + timing.BeatLength = 60000 / (timing.BPM + adjust); + } + private void tap() { editorClock.Seek(selectedGroup.Value.Time); diff --git a/osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs b/osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs new file mode 100644 index 0000000000..9fc7e56a3d --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs @@ -0,0 +1,254 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Framework.Threading; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Timing +{ + /// + /// A button with variable constant output based on hold position and length. + /// + public class TimingAdjustButton : CompositeDrawable + { + public Action Action; + + private readonly double adjustAmount; + private ScheduledDelegate adjustDelegate; + + private const int max_multiplier = 10; + + private const int adjust_levels = 4; + + private const double initial_delay = 300; + + private const double minimum_delay = 80; + + public Container Content { get; set; } + + private double adjustDelay = initial_delay; + + private readonly Box background; + + private readonly OsuSpriteText text; + + private Sample sample; + + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + public TimingAdjustButton(double adjustAmount) + { + this.adjustAmount = adjustAmount; + + CornerRadius = 5; + Masking = true; + + AddInternal(Content = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Default.With(weight: FontWeight.SemiBold), + Padding = new MarginPadding(5), + Depth = float.MinValue + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sample = audio.Samples.Get(@"UI/notch-tick"); + + background.Colour = colourProvider.Background3; + + for (int i = 1; i <= adjust_levels; i++) + { + Content.Add(new IncrementBox(i, adjustAmount)); + Content.Add(new IncrementBox(-i, adjustAmount)); + } + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + beginRepeat(); + return true; + } + + protected override void OnMouseUp(MouseUpEvent e) + { + adjustDelegate?.Cancel(); + base.OnMouseUp(e); + } + + private void beginRepeat() + { + adjustDelegate?.Cancel(); + + adjustDelay = initial_delay; + adjustNext(); + + void adjustNext() + { + var hoveredBox = Content.OfType().FirstOrDefault(d => d.IsHovered); + + if (hoveredBox != null) + { + Action(adjustAmount * hoveredBox.Multiplier); + + adjustDelay = Math.Max(minimum_delay, adjustDelay * 0.9f); + + hoveredBox.Flash(); + + var channel = sample?.GetChannel(); + + if (channel != null) + { + double repeatModifier = 0.05f * (Math.Abs(adjustDelay - initial_delay) / minimum_delay); + double multiplierModifier = (hoveredBox.Multiplier / max_multiplier) * 0.2f; + + channel.Frequency.Value = 1 + multiplierModifier + repeatModifier; + channel.Play(); + } + } + else + { + adjustDelay = initial_delay; + } + + adjustDelegate = Scheduler.AddDelayed(adjustNext, adjustDelay); + } + } + + private class IncrementBox : CompositeDrawable + { + public readonly float Multiplier; + + private readonly Box box; + private readonly OsuSpriteText text; + + public IncrementBox(int index, double amount) + { + Multiplier = Math.Sign(index) * convertMultiplier(index); + + float ratio = (float)index / adjust_levels; + + RelativeSizeAxes = Axes.Both; + + Width = 0.5f * Math.Abs(ratio); + + Anchor direction = index < 0 ? Anchor.x2 : Anchor.x0; + + Origin |= direction; + + Depth = Math.Abs(index); + + Anchor = Anchor.TopCentre; + + InternalChildren = new Drawable[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive + }, + text = new OsuSpriteText + { + Anchor = direction, + Origin = direction, + Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold), + Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}", + Padding = new MarginPadding(5), + Alpha = 0, + } + }; + } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + box.Colour = colourProvider.Background1; + box.Alpha = 0.1f; + } + + private float convertMultiplier(int m) + { + switch (Math.Abs(m)) + { + default: return 1; + + case 2: return 2; + + case 3: return 5; + + case 4: + return max_multiplier; + } + } + + protected override bool OnHover(HoverEvent e) + { + box.Colour = colourProvider.Colour0; + + box.FadeTo(0.2f, 100, Easing.OutQuint); + text.FadeIn(100, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + box.Colour = colourProvider.Background1; + + box.FadeTo(0.1f, 500, Easing.OutQuint); + text.FadeOut(100, Easing.OutQuint); + base.OnHoverLost(e); + } + + public void Flash() + { + box + .FadeTo(0.4f, 20, Easing.OutQuint) + .Then() + .FadeTo(0.2f, 400, Easing.OutQuint); + + text + .MoveToY(-5, 20, Easing.OutQuint) + .Then() + .MoveToY(0, 400, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index f71a8d7d22..40e6e8082a 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Timing public class TimingScreen : EditorScreenWithTimeline { [Cached] - private Bindable selectedGroup = new Bindable(); + public readonly Bindable SelectedGroup = new Bindable(); public TimingScreen() : base(EditorScreenMode.Timing) @@ -132,6 +132,40 @@ namespace osu.Game.Screens.Edit.Timing }, true); } + protected override void Update() + { + base.Update(); + + trackActivePoint(); + } + + /// + /// Given the user has selected a control point group, we want to track any group which is + /// active at the current point in time which matches the type the user has selected. + /// + /// So if the user is currently looking at a timing point and seeks into the future, a + /// future timing point would be automatically selected if it is now the new "current" point. + /// + private void trackActivePoint() + { + // For simplicity only match on the first type of the active control point. + var selectedPointType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); + + if (selectedPointType != null) + { + // We don't have an efficient way of looking up groups currently, only individual point types. + // To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo. + + // Find the next group which has the same type as the selected one. + var found = Beatmap.ControlPointInfo.Groups + .Where(g => g.ControlPoints.Any(cp => cp.GetType() == selectedPointType)) + .LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate); + + if (found != null) + selectedGroup.Value = found; + } + } + private void delete() { if (selectedGroup.Value == null) @@ -144,7 +178,27 @@ namespace osu.Game.Screens.Edit.Timing private void addNew() { - selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true); + bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any(); + + var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true); + + if (isFirstControlPoint) + group.Add(new TimingControlPoint()); + else + { + // Try and create matching types from the currently selected control point. + var selected = selectedGroup.Value; + + if (selected != null) + { + foreach (var controlPoint in selected.ControlPoints) + { + group.Add(controlPoint.DeepClone()); + } + } + } + + selectedGroup.Value = group; } } } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index a5abd96d72..1a97058d73 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -28,15 +28,31 @@ namespace osu.Game.Screens.Edit.Timing }); } + protected override void LoadComplete() + { + base.LoadComplete(); + + bpmTextEntry.Current.BindValueChanged(_ => saveChanges()); + timeSignature.Current.BindValueChanged(_ => saveChanges()); + + void saveChanges() + { + if (!isRebinding) ChangeHandler?.SaveState(); + } + } + + private bool isRebinding; + protected override void OnControlPointChanged(ValueChangedEvent point) { if (point.NewValue != null) { - bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; - bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + isRebinding = true; + bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; timeSignature.Current = point.NewValue.TimeSignatureBindable; - timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + + isRebinding = false; } } diff --git a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs index c80d3c4261..0745187e43 100644 --- a/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs +++ b/osu.Game/Screens/Edit/Timing/WaveformComparisonDisplay.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; @@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Timing { private const int total_waveforms = 8; + private const float corner_radius = LabelledDrawable.CORNER_RADIUS; + private readonly BindableNumber beatLength = new BindableDouble(); [Resolved] @@ -42,18 +45,22 @@ namespace osu.Game.Screens.Edit.Timing private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT; - private int lastDisplayedBeatIndex; + private double displayedTime; private double selectedGroupStartTime; private double selectedGroupEndTime; private readonly IBindableList controlPointGroups = new BindableList(); + private readonly BindableBool displayLocked = new BindableBool(); + + private LockedOverlay lockedOverlay = null!; + public WaveformComparisonDisplay() { RelativeSizeAxes = Axes.Both; - CornerRadius = LabelledDrawable.CORNER_RADIUS; + CornerRadius = corner_radius; Masking = true; } @@ -63,7 +70,7 @@ namespace osu.Game.Screens.Edit.Timing for (int i = 0; i < total_waveforms; i++) { - AddInternal(new WaveformRow + AddInternal(new WaveformRow(i == total_waveforms / 2) { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, @@ -81,72 +88,112 @@ namespace osu.Game.Screens.Edit.Timing Width = 3, }); + AddInternal(lockedOverlay = new LockedOverlay()); + selectedGroup.BindValueChanged(_ => updateTimingGroup(), true); controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup()); - beatLength.BindValueChanged(_ => showFrom(lastDisplayedBeatIndex), true); + beatLength.BindValueChanged(_ => regenerateDisplay(true), true); + + displayLocked.BindValueChanged(locked => + { + if (locked.NewValue) + lockedOverlay.Show(); + else + lockedOverlay.Hide(); + }, true); } private void updateTimingGroup() { beatLength.UnbindBindings(); - selectedGroupStartTime = 0; - selectedGroupEndTime = beatmap.Value.Track.Length; - var tcp = selectedGroup.Value?.ControlPoints.OfType().FirstOrDefault(); if (tcp == null) { timingPoint = new TimingControlPoint(); + // During movement of a control point's offset, this clause can be hit momentarily, + // as moving a control point is implemented by removing it and inserting it at the new time. + // We don't want to reset the `selectedGroupStartTime` here as we rely on having the + // last value to update the waveform display below. + selectedGroupEndTime = beatmap.Value.Track.Length; return; } timingPoint = tcp; beatLength.BindTo(timingPoint.BeatLengthBindable); - selectedGroupStartTime = selectedGroup.Value?.Time ?? 0; + double? newStartTime = selectedGroup.Value?.Time; + double? offsetChange = newStartTime - selectedGroupStartTime; var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints .SkipWhile(g => g != tcp) .Skip(1) .FirstOrDefault(); - if (nextGroup != null) - selectedGroupEndTime = nextGroup.Time; + selectedGroupStartTime = newStartTime ?? 0; + selectedGroupEndTime = nextGroup?.Time ?? beatmap.Value.Track.Length; + + if (newStartTime.HasValue && offsetChange.HasValue) + { + // The offset of the selected point may have changed. + // This handles the case the user has locked the view and expects the display to update with this change. + showFromTime(displayedTime + offsetChange.Value, true); + } } protected override bool OnHover(HoverEvent e) => true; protected override bool OnMouseMove(MouseMoveEvent e) { - float trackLength = (float)beatmap.Value.Track.Length; - int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength); + if (!displayLocked.Value) + { + float trackLength = (float)beatmap.Value.Track.Length; + int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength); - Scheduler.AddOnce(showFrom, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable)); + Scheduler.AddOnce(showFromBeat, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable)); + } return base.OnMouseMove(e); } + protected override bool OnClick(ClickEvent e) + { + displayLocked.Toggle(); + return true; + } + protected override void Update() { base.Update(); - if (!IsHovered) + if (!IsHovered && !displayLocked.Value) { int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength); - showFrom(currentBeat); + showFromBeat(currentBeat); } } - private void showFrom(int beatIndex) + private void showFromBeat(int beatIndex) => + showFromTime(selectedGroupStartTime + beatIndex * timingPoint.BeatLength, false); + + private void showFromTime(double time, bool animated) { - if (lastDisplayedBeatIndex == beatIndex) + if (displayedTime == time) return; + displayedTime = time; + regenerateDisplay(animated); + } + + private void regenerateDisplay(bool animated) + { + double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength; + // Chosen as a pretty usable number across all BPMs. // Optimally we'd want this to scale with the BPM in question, but performing // scaling of the display is both expensive in resampling, and decreases usability @@ -156,38 +203,115 @@ namespace osu.Game.Screens.Edit.Timing float trackLength = (float)beatmap.Value.Track.Length; float scale = trackLength / visible_width; + const int start_offset = total_waveforms / 2; + // Start displaying from before the current beat - beatIndex -= total_waveforms / 2; + index -= start_offset; foreach (var row in InternalChildren.OfType()) { // offset to the required beat index. - double time = selectedGroupStartTime + beatIndex * timingPoint.BeatLength; + double time = selectedGroupStartTime + index * timingPoint.BeatLength; float offset = (float)(time - visible_width / 2) / trackLength * scale; row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1; - row.WaveformOffset = -offset; + row.WaveformOffsetTo(-offset, animated); row.WaveformScale = new Vector2(scale, 1); - row.BeatIndex = beatIndex++; + row.BeatIndex = (int)Math.Floor(index); + + index++; + } + } + + internal class LockedOverlay : CompositeDrawable + { + private OsuSpriteText text = null!; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.Both; + Masking = true; + CornerRadius = corner_radius; + BorderColour = colours.Red; + BorderThickness = 3; + Alpha = 0; + + InternalChildren = new Drawable[] + { + new Box + { + AlwaysPresent = true, + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colours.Red, + RelativeSizeAxes = Axes.Both, + }, + text = new OsuSpriteText + { + Colour = colours.GrayF, + Text = "Locked", + Margin = new MarginPadding(5), + Shadow = false, + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + } + } + }, + }; } - lastDisplayedBeatIndex = beatIndex; + public override void Show() + { + this.FadeIn(100, Easing.OutQuint); + + text + .FadeIn().Then().Delay(600) + .FadeOut().Then().Delay(600) + .Loop(); + } + + public override void Hide() + { + this.FadeOut(100, Easing.OutQuint); + } } internal class WaveformRow : CompositeDrawable { + private readonly bool isMainRow; private OsuSpriteText beatIndexText = null!; private WaveformGraph waveformGraph = null!; [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public WaveformRow(bool isMainRow) + { + this.isMainRow = isMainRow; + } + [BackgroundDependencyLoader] private void load(IBindable beatmap) { InternalChildren = new Drawable[] { + new Box + { + Colour = colourProvider.Background3, + Alpha = isMainRow ? 1 : 0, + RelativeSizeAxes = Axes.Both, + }, waveformGraph = new WaveformGraph { RelativeSizeAxes = Axes.Both, @@ -212,7 +336,15 @@ namespace osu.Game.Screens.Edit.Timing public int BeatIndex { set => beatIndexText.Text = value.ToString(); } public Vector2 WaveformScale { set => waveformGraph.Scale = value; } - public float WaveformOffset { set => waveformGraph.X = value; } + + public void WaveformOffsetTo(float value, bool animated) => + this.TransformTo(nameof(waveformOffset), value, animated ? 300 : 0, Easing.OutQuint); + + private float waveformOffset + { + get => waveformGraph.X; + set => waveformGraph.X = value; + } } } } diff --git a/osu.Game/Screens/Edit/Verify/IssueList.cs b/osu.Game/Screens/Edit/Verify/IssueList.cs index 415acc0e22..84b2609a61 100644 --- a/osu.Game/Screens/Edit/Verify/IssueList.cs +++ b/osu.Game/Screens/Edit/Verify/IssueList.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Verify { new Box { - Colour = colours.Background2, + Colour = colours.Background3, RelativeSizeAxes = Axes.Both, }, new OsuScrollContainer diff --git a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs index 9dc5a53907..56e16bb746 100644 --- a/osu.Game/Screens/Edit/Verify/VerifyScreen.cs +++ b/osu.Game/Screens/Edit/Verify/VerifyScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Verify ColumnDimensions = new[] { new Dimension(), - new Dimension(GridSizeMode.Absolute, 200), + new Dimension(GridSizeMode.Absolute, 250), }, Content = new[] { diff --git a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs index 760de354dc..a7ea32ee7c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs @@ -30,8 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { status.BindValueChanged(s => { - this.FadeColour(category.Value == RoomCategory.Spotlight ? colours.Pink : s.NewValue.GetAppropriateColour(colours) - , transitionDuration); + this.FadeColour(colours.ForRoomCategory(category.Value) ?? s.NewValue.GetAppropriateColour(colours), transitionDuration); }, true); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 8e3aa77e7b..772232f6b4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -237,7 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components roomCategory.BindTo(Room.Category); roomCategory.BindValueChanged(c => { - if (c.NewValue == RoomCategory.Spotlight) + if (c.NewValue > RoomCategory.Normal) specialCategoryPill.Show(); else specialCategoryPill.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs index 6cdbeb2af4..539af2ebaf 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomSpecialCategoryPill.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; @@ -13,6 +14,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public class RoomSpecialCategoryPill : OnlinePlayComposite { private SpriteText text; + private PillContainer pill; + + [Resolved] + private OsuColour colours { get; set; } public RoomSpecialCategoryPill() { @@ -20,9 +25,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - InternalChild = new PillContainer + InternalChild = pill = new PillContainer { Background = { @@ -43,7 +48,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { base.LoadComplete(); - Category.BindValueChanged(c => text.Text = c.NewValue.ToString(), true); + Category.BindValueChanged(c => + { + text.Text = c.NewValue.GetLocalisableDescription(); + + var backgroundColour = colours.ForRoomCategory(Category.Value); + if (backgroundColour != null) + pill.Background.Colour = backgroundColour.Value; + }, true); } } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index d61fbea387..0fd9290880 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { foreach (var room in roomFlow) { - roomFlow.SetLayoutPosition(room, room.Room.Category.Value == RoomCategory.Spotlight + roomFlow.SetLayoutPosition(room, room.Room.Category.Value > RoomCategory.Normal // Always show spotlight playlists at the top of the listing. ? float.MinValue : -(room.Room.RoomID.Value ?? 0)); diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index ea7de917e2..b92c197f5a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -38,13 +38,13 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components var req = new GetRoomLeaderboardRequest(roomId.Value ?? 0); - req.Success += r => + req.Success += r => Schedule(() => { if (cancellationToken.IsCancellationRequested) return; SetScores(r.Leaderboard, r.UserScore); - }; + }); return req; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 5dab845999..3bf8a09cb9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -149,9 +149,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override void StartGameplay() { + // We can enter this screen one of two ways: + // 1. Via the automatic natural progression of PlayerLoader into Player. + // We'll arrive here in a Loaded state, and we need to let the server know that we're ready to start. + // 2. Via the server forcefully starting gameplay because players have been hanging out in PlayerLoader for too long. + // We'll arrive here in a Playing state, and we should neither show the loading spinner nor tell the server that we're ready to start (gameplay has already started). + // + // The base call is blocked here because in both cases gameplay is started only when the server says so via onGameplayStarted(). + if (client.LocalUser?.State == MultiplayerUserState.Loaded) { - // block base call, but let the server know we are ready to start. loadingDisplay.Show(); client.ChangeState(MultiplayerUserState.ReadyForGameplay); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs index 7f01bd64ab..e9bf5339a9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayerLoader.cs @@ -26,8 +26,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override bool ReadyForGameplay => - base.ReadyForGameplay - // The server is forcefully starting gameplay. + ( + // The user is ready to enter gameplay. + base.ReadyForGameplay + // And the server has received the message that we're loaded. + && multiplayerClient.LocalUser?.State == MultiplayerUserState.Loaded + ) + // Or the server is forcefully starting gameplay. || multiplayerClient.LocalUser?.State == MultiplayerUserState.Playing; protected override void OnPlayerLoaded() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index dced9b8691..b36e162e3d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -49,6 +50,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists case PlaylistsCategory.Spotlight: criteria.Category = @"spotlight"; break; + + case PlaylistsCategory.FeaturedArtist: + criteria.Category = @"featured_artist"; + break; } return criteria; @@ -73,7 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { Any, Normal, - Spotlight + Spotlight, + + [Description("Featured Artist")] + FeaturedArtist, } } } diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 019a9f9730..bdc98e53f9 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -80,16 +81,13 @@ namespace osu.Game.Screens.Play.HUD difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token) .ContinueWith(task => Schedule(() => { - if (task.Exception != null) - return; - timedAttributes = task.GetResultSafely(); IsValid = true; if (lastJudgement != null) onJudgementChanged(lastJudgement); - })); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 3b4547cb49..ff670e1232 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.Length = 75000; BeatmapInfo.OnlineInfo = new APIBeatmap(); BeatmapInfo.OnlineID = Interlocked.Increment(ref onlineBeatmapID); + BeatmapInfo.Status = BeatmapOnlineStatus.Ranked; Debug.Assert(BeatmapInfo.BeatmapSet != null); diff --git a/osu.Game/Tests/Gameplay/TestGameplayState.cs b/osu.Game/Tests/Gameplay/TestGameplayState.cs new file mode 100644 index 0000000000..0d00f52d15 --- /dev/null +++ b/osu.Game/Tests/Gameplay/TestGameplayState.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System.Collections.Generic; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Gameplay +{ + /// + /// Static class providing a convenience method to retrieve a correctly-initialised instance in testing scenarios. + /// + public static class TestGameplayState + { + /// + /// Creates a correctly-initialised instance for use in testing. + /// + public static GameplayState Create(Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null) + { + var beatmap = new TestBeatmap(ruleset.RulesetInfo); + var workingBeatmap = new TestWorkingBeatmap(beatmap); + var playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + + return new GameplayState(playableBeatmap, ruleset, mods, score); + } + } +} diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 542f06f86b..15e4fc4d8f 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Input.Events; using osu.Game.Beatmaps; @@ -20,8 +21,12 @@ namespace osu.Game.Tests.Visual private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine); protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); + + [Cached] protected new readonly EditorClock Clock; + private readonly Bindable frequencyAdjustment = new BindableDouble(1); + protected virtual bool ScrollUsingMouseWheel => true; protected EditorClockTestScene() @@ -42,14 +47,21 @@ namespace osu.Game.Tests.Visual protected override void LoadComplete() { base.LoadComplete(); + Beatmap.BindValueChanged(beatmapChanged, true); + + AddSliderStep("editor clock rate", 0.0, 2.0, 1.0, v => frequencyAdjustment.Value = v); } private void beatmapChanged(ValueChangedEvent e) { + e.OldValue?.Track.RemoveAdjustment(AdjustableProperty.Frequency, frequencyAdjustment); + Clock.Beatmap = e.NewValue.Beatmap; Clock.ChangeSource(e.NewValue.Track); Clock.ProcessFrame(); + + e.NewValue.Track.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment); } protected override void Update() diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index b6a347a896..df3974664e 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -67,7 +68,25 @@ namespace osu.Game.Tests.Visual.OnlinePlay // To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead. var beatmapManager = dependencies.Get(); - ((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); + ((DummyAPIAccess)API).HandleRequest = request => + { + TaskCompletionSource tcs = new TaskCompletionSource(); + + // Because some of the handlers use realm, we need to ensure the game is still alive when firing. + // If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late. + Scheduler.Add(() => + { + bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager); + tcs.SetResult(result); + }, false); + +#pragma warning disable RS0030 + // We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to + // the task being a TaskCompletionSource. + // Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument). + return tcs.Task.Result; +#pragma warning restore RS0030 + }; }); /// diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 137bf7e0aa..ecc7fc4774 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -48,9 +48,8 @@ namespace osu.Game.Utils options.AutoSessionTracking = true; options.IsEnvironmentUser = false; - // The reported release needs to match release tags on github in order for sentry - // to automatically associate and track against releases. - options.Release = game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty); + // The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml + options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}"; }); Logger.NewEntry += processLogEntry; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 32a0adb859..eb47d0468f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,14 +29,15 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/osu.iOS.props b/osu.iOS.props index 112b5b4615..ccecad6f82 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,8 +61,8 @@ - - + + @@ -84,7 +84,7 @@ - +