1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-20 02:12:57 +08:00

Merge branch 'ppy:master' into colour-rework

This commit is contained in:
vunyunt 2022-06-02 18:35:27 +08:00 committed by GitHub
commit 54dc22b7f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 2346 additions and 3202 deletions

View File

@ -23,4 +23,4 @@ jobs:
SENTRY_URL: https://sentry.ppy.sh/
with:
environment: production
version: ${{ github.ref }}
version: osu@${{ github.ref_name }}

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.513.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.525.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.530.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -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<Mod>());
gameplayState = TestGameplayState.Create(ruleset);
AddStep("change background colour", () =>
{

View File

@ -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

View File

@ -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<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().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<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().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);
}
/// <summary>
/// Rotate a slider about its start position by the specified angle.
/// </summary>
/// <param name="slider">The slider to be rotated.</param>
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
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<SliderTick>().ForEach(rotateNestedObject);
slider.NestedHitObjects.OfType<SliderRepeat>().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);
}
/// <summary>
/// Rotate a vector by the specified angle.
/// </summary>
/// <param name="vector">The vector to be rotated.</param>
/// <param name="rotation">The angle, measured in radians, to rotate the vector by.</param>
/// <returns>The rotated vector.</returns>
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)
);
}
}
}

View File

@ -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;
}
@ -123,17 +132,24 @@ namespace osu.Game.Rulesets.Osu.Utils
float previousAbsoluteAngle = 0f;
if (previous != null)
{
if (previous.HitObject is Slider s)
{
previousAbsoluteAngle = getSliderRotation(s);
}
else
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
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);
}
/// <summary>
@ -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
);
}
/// <summary>
/// Estimate the centre of mass of a slider relative to its start position.
/// </summary>
/// <param name="slider">The slider to process.</param>
/// <returns>The centre of mass of the slider.</returns>
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;
}
/// <summary>
/// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
/// </summary>
/// <param name="slider">The slider to process.</param>
/// <returns>The angle in radians.</returns>
private static float getSliderRotation(Slider slider)
{
var endPositionVector = slider.Path.PositionAt(1);
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
}
public class ObjectPositionInfo
{
/// <summary>
@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
/// </remarks>
public float DistanceFromPrevious { get; set; }
/// <summary>
/// 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.
/// </summary>
public float Rotation { get; set; }
/// <summary>
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
/// </summary>

View File

@ -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

View File

@ -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()

View File

@ -77,6 +77,12 @@ namespace osu.Game.Tests.Visual.Editing
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
}
[Test]
public void TestNoop()
{
AddStep("do nothing", () => { });
}
[Test]
public void TestTapThenReset()
{

View File

@ -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<TimelineHitObjectBlueprint>().Single();
InputManager.MoveMouseTo(blueprint);
InputManager.Click(MouseButton.Left);
});
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("context menu open", () => this.ChildrenOfType<OsuContextMenu>().SingleOrDefault()?.State == MenuState.Open);
}
[Test]
public void TestDisallowZeroDurationObjects()
{

View File

@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<EffectRowAttribute>().Any());
}
[Test]
public void TestTrackingCurrentTimeWhileRunning()
{
AddStep("Select first effect point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().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<EffectRowAttribute>().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;

View File

@ -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,7 +39,10 @@ namespace osu.Game.Tests.Visual.Editing
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
AddRange(new Drawable[]
Add(new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
EditorBeatmap,
Composer,
@ -58,6 +62,7 @@ namespace osu.Game.Tests.Visual.Editing
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
});
}

View File

@ -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<ScoreProcessor>()),
(typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get<HealthProcessor>()),
(typeof(GameplayState), actualComponentsContainer.Dependencies.Get<GameplayState>()),
(typeof(GameplayClock), actualComponentsContainer.Dependencies.Get<GameplayClock>())
},
};
Add(expectedComponentsAdjustmentContainer);

View File

@ -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());

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Mod>());
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
[SetUpSteps]
public void SetUpSteps()

View File

@ -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());

View File

@ -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());

View File

@ -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.

View File

@ -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<Mod>()))
(typeof(GameplayState), TestGameplayState.Create(new OsuRuleset()))
},
Children = new Drawable[]
{

View File

@ -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<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4);
}
[Test]

View File

@ -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<BeatmapSetOnlineStatusPill>().Single().Status == BeatmapOnlineStatus.Loved);
AddAssert("scores container is visible", () => overlay.ChildrenOfType<ScoresContainer>().Single().Alpha == 1);
AddStep("go to second beatmap", () => overlay.ChildrenOfType<BeatmapPicker.DifficultySelectorButton>().ElementAt(1).TriggerClick());
AddAssert("status is graveyard", () => overlay.ChildrenOfType<BeatmapSetOnlineStatusPill>().Single().Status == BeatmapOnlineStatus.Graveyard);
AddAssert("scores container is hidden", () => overlay.ChildrenOfType<ScoresContainer>().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,
});
}

View File

@ -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,
};
}
}
}

View File

@ -1,129 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Channel> 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]);
}
}
}

View File

@ -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<IDialogOverlay>(dialogOverlay);
}
[SetUp]

File diff suppressed because it is too large Load Diff

View File

@ -1,500 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Channel>)?.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<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
.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<ChannelListItemCloseButton>().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<ChannelListItemCloseButton>().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<ChannelListingItem>()
.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<ChannelListing>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().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<ChannelListing>().Single().State.Value == Visibility.Visible;
private bool loadingIsVisible =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value == Visibility.Visible;
private bool channelIsVisible =>
!listingIsVisible && !loadingIsVisible;
private DrawableChannel currentDrawableChannel =>
chatOverlay.ChildrenOfType<DrawableChannel>().Single();
private ChannelListItem getChannelListItem(Channel channel) =>
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel == channel);
private ChatTextBox chatOverlayTextBox =>
chatOverlay.ChildrenOfType<ChatTextBox>().Single();
private ChatOverlayTopBar chatOverlayTopBar =>
chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single();
private ChannelListItem channelSelectorButton =>
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel is ChannelListing.ChannelListingChannel);
private void clickDrawable(Drawable d)
{
InputManager.MoveMouseTo(d);
InputManager.Click(MouseButton.Left);
}
private List<Message> 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> { 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<SlowLoadingDrawableChannel>().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);
}
}
}
}

View File

@ -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
{

View File

@ -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);

View File

@ -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<HitObject, IEnumerable<HitObject>> 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

View File

@ -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<SettingsNoticeText>().Any());
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
AddStep("set warning text", () => numberBox.WarningText = "this is a warning!");
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
AddAssert("warning text created", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
AddStep("unset warning text", () => numberBox.WarningText = default);
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0);
AddStep("unset warning text", () => numberBox.ClearNoticeText());
AddAssert("warning text hidden", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!"));
}
}
}

View File

@ -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);
}
}

View File

@ -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}");
}
}
}

View File

@ -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<TLookup, List<TaskCompletionSource<TValue>>>();
@ -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();

View File

@ -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
}
}
/// <summary>
/// Retrieves the main accent colour for a <see cref="RoomCategory"/>.
/// </summary>
public Color4? ForRoomCategory(RoomCategory roomCategory)
{
switch (roomCategory)
{
case RoomCategory.Spotlight:
return SpotlightColour;
case RoomCategory.FeaturedArtist:
return FeaturedArtistColour;
default:
return null;
}
}
/// <summary>
/// Returns a foreground text colour that is supposed to contrast well with
/// the supplied <paramref name="backgroundColour"/>.
@ -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;
}
}

View File

@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<byte>((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<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
private class MemoryOwnerMemoryStream : Stream
{
private readonly IMemoryOwner<byte> owner;
private readonly Stream stream;
public MemoryOwnerMemoryStream(IMemoryOwner<byte> 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;
}
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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";
/// <summary>
/// "Checking for fullscreen capabilities..."
/// </summary>
public static LocalisableString CheckingForFullscreenCapabilities => new TranslatableString(getKey(@"checking_for_fullscreen_capabilities"), @"Checking for fullscreen capabilities...");
/// <summary>
/// "osu! is running exclusive fullscreen, guaranteeing low latency!"
/// </summary>
public static LocalisableString OsuIsRunningExclusiveFullscreen => new TranslatableString(getKey(@"osu_is_running_exclusive_fullscreen"), @"osu! is running exclusive fullscreen, guaranteeing low latency!");
/// <summary>
/// "Unable to run exclusive fullscreen. You&#39;ll still experience some input latency."
/// </summary>
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}";
}
}

View File

@ -62,13 +62,14 @@ namespace osu.Game.Online.API
}
public virtual void Queue(APIRequest request)
{
Schedule(() =>
{
if (HandleRequest?.Invoke(request) != true)
{
// 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);
request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request."));
}
});
}
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);

View File

@ -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<Channel> 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);

View File

@ -20,6 +20,8 @@ namespace osu.Game.Online
{
public class HubClientConnector : IHubClientConnector
{
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
/// <summary>
/// Invoked whenever a new hub connection is built, to configure it before it's started.
/// </summary>
@ -64,9 +66,18 @@ namespace osu.Game.Online
this.preferMessagePack = preferMessagePack;
apiState.BindTo(api.State);
apiState.BindValueChanged(state =>
apiState.BindValueChanged(state => connectIfPossible(), true);
}
public void Reconnect()
{
switch (state.NewValue)
Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
Task.Run(connectIfPossible);
}
private void connectIfPossible()
{
switch (apiState.Value)
{
case APIState.Failing:
case APIState.Offline:
@ -77,7 +88,6 @@ namespace osu.Game.Online
Task.Run(connect);
break;
}
}, true);
}
private async Task connect()

View File

@ -30,5 +30,10 @@ namespace osu.Game.Online
/// Invoked whenever a new hub connection is built, to configure it before it's started.
/// </summary>
public Action<HubConnection>? ConfigureConnection { get; set; }
/// <summary>
/// Reconnect if already connected.
/// </summary>
void Reconnect();
}
}

View File

@ -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;
}
}
}

View File

@ -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<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
protected override async Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
{
if (!IsConnected.Value)
return Task.FromCanceled<MultiplayerRoom>(new CancellationToken(true));
throw new OperationCanceledException();
Debug.Assert(connection != null);
return connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
try
{
return await connection.InvokeAsync<MultiplayerRoom>(nameof(IMultiplayerServer.JoinRoomWithPassword), roomId, password ?? string.Empty);
}
catch (HubException exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
connector?.Reconnect();
throw;
}
}
protected override Task LeaveRoomInternal()

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
}

View File

@ -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)

View File

@ -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);

View File

@ -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.
}
}

View File

@ -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();

View File

@ -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.
}
}

View File

@ -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<Channel>? OnRequestSelect;
public Action<Channel>? OnRequestLeave;
public IEnumerable<Channel> Channels => groupFlow.Children
.OfType<ChannelGroup>()
.SelectMany(channelGroup => channelGroup.ItemFlow)
.Select(item => item.Channel);
public readonly ChannelListing.ChannelListingChannel ChannelListingChannel = new ChannelListing.ChannelListingChannel();
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
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<ChannelListItem> 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<ChannelListItem> 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<ChannelListItem> 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<ChannelListItem> 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[]
{
new OsuSpriteText
{
Text = label,
Margin = new MarginPadding { Left = 18, Bottom = 5 },
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
});
},
ItemFlow = new FillFlowContainer<ChannelListItem>
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
};
}
}
}

View File

@ -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 },
},

View File

@ -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);

View File

@ -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);
}

View File

@ -1,191 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<bool> joinedBind = new Bindable<bool>();
private readonly OsuSpriteText name;
private readonly OsuSpriteText topic;
private readonly SpriteIcon joinedCheckmark;
private Color4 joinedColour;
private Color4 topicColour;
private Color4 hoverColour;
public IEnumerable<LocalisableString> 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<Channel> OnRequestJoin;
public Action<Channel> 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);
}
}
}
}

View File

@ -1,58 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ChannelListItem> ChannelFlow;
public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children;
public IEnumerable<LocalisableString> FilterTerms => Array.Empty<LocalisableString>();
public bool MatchingFilter
{
set => this.FadeTo(value ? 1f : 0f, 100);
}
public bool FilteringActive { get; set; }
public IEnumerable<Channel> 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<ChannelListItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 25 },
Spacing = new Vector2(0f, 5f),
},
};
}
}
}

View File

@ -1,194 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ChannelSection> sectionsFlow;
protected override bool DimMainContent => false;
public Action<Channel> OnRequestJoin;
public Action<Channel> 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<ChannelSection>
{
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<Channel> 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);
}
}
}
}

View File

@ -1,46 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}
}

View File

@ -1,114 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Channel>
{
public const float SHEAR_WIDTH = 10;
public Action<Channel> OnRequestLeave;
public readonly Bindable<bool> ChannelSelectorActive = new Bindable<bool>();
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<Channel> 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<Channel> CreateTabItem(Channel value)
{
switch (value.Type)
{
default:
return new ChannelTabItem(value);
case ChannelType.PM:
return new PrivateChannelTabItem(value);
}
}
/// <summary>
/// Adds a channel to the ChannelTabControl.
/// The first channel added will automaticly selected.
/// </summary>
/// <param name="channel">The channel that is going to be added.</param>
public void AddChannel(Channel channel)
{
if (!Items.Contains(channel))
AddItem(channel);
Current.Value ??= channel;
}
/// <summary>
/// Removes a channel from the ChannelTabControl.
/// If the selected channel is the one that is being removed, the next available channel will be selected.
/// </summary>
/// <param name="channel">The channel that is going to be removed.</param>
public void RemoveChannel(Channel channel)
{
RemoveItem(channel);
if (SelectedTab == null)
SelectChannelSelectorTab();
}
public void SelectChannelSelectorTab() => SelectTab(selectorTab);
protected override void SelectTab(TabItem<Channel> 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);
}
}
}

View File

@ -1,238 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Channel>
{
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<ChannelTabItem> OnRequestClose;
private readonly Container content;
protected override Container<Drawable> 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();
}
}

View File

@ -1,95 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -1,55 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);
}
}
}

View File

@ -1,35 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ChatOverlayDrawableChannel> currentChannelContainer = null!;
[Resolved]
private ChannelManager channelManager { get; set; }
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
private Container<DrawableChannel> currentChannelContainer;
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
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<float> ChatHeight { get; set; }
private Container channelSelectionContainer;
protected ChannelSelectionOverlay ChannelSelectionOverlay;
[Cached]
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
private readonly IBindableList<Channel> availableChannels = new BindableList<Channel>();
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
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.Both,
Height = 1f - DEFAULT_HEIGHT,
Masking = true,
Children = new[]
{
ChannelSelectionOverlay = new ChannelSelectionOverlay
{
RelativeSizeAxes = Axes.Both,
},
},
},
chatContainer = new Container
{
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<DrawableChannel>
{
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,
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 = padding * 2,
Bottom = padding * 2,
Left = ChatLine.LEFT_PADDING + padding * 2,
Right = padding * 2,
Top = top_bar_height,
Left = side_bar_width,
Bottom = chat_bar_height,
},
Children = new Drawable[]
{
textBox = new FocusedTextBox
new Box
{
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder,
ReleaseFocusOnCommit = false,
HoldFocus = true,
}
}
Colour = colourProvider.Background4,
},
loading = new LoadingSpinner(),
},
}
}
},
tabsArea = new TabsArea
{
Children = new Drawable[]
{
tabBackground = new Box
currentChannelContainer = new Container<ChatOverlayDrawableChannel>
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
new Sprite
loading = new LoadingLayer(true),
channelListing = new ChannelListing
{
Texture = textures.Get(IconTexture),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(OverlayTitle.ICON_SIZE),
Margin = new MarginPadding { Left = 10 },
RelativeSizeAxes = Axes.Both,
},
ChannelTabControl = CreateChannelTabControl().With(d =>
},
},
textBar = new ChatTextBar
{
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 },
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
availableChannels.BindTo(channelManager.AvailableChannels);
joinedChannels.BindTo(channelManager.JoinedChannels);
currentChannel.BindTo(channelManager.CurrentChannel);
joinedChannels.BindTo(channelManager.JoinedChannels);
availableChannels.BindTo(channelManager.AvailableChannels);
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<float>(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.
currentChannel.BindValueChanged(currentChannelChanged, true);
joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
availableChannels.BindCollectionChanged(availableChannelsChanged, true);
currentChannel.BindValueChanged(currentChannelChanged, true);
});
}
private void currentChannelChanged(ValueChangedEvent<Channel> e)
{
if (e.NewValue == null)
{
textBox.Current.Disabled = true;
currentChannelContainer.Clear(false);
ChannelSelectionOverlay.Show();
return;
}
channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel;
channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
if (e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel)
return;
channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
textBox.Current.Disabled = e.NewValue.ReadOnly;
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;
}
/// <summary>
@ -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<PlatformAction> 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;
}
case PlatformAction.DocumentNext:
cycleChannel(1);
return true;
default:
return false;
}
}
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> 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)
{
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<Channel>())
{
if (channel.Type != ChannelType.Multiplayer)
ChannelTabControl.AddChannel(channel);
}
IEnumerable<Channel> newChannels = args.NewItems.OfType<Channel>().Where(isChatChannel);
foreach (var channel in newChannels)
channelList.AddChannel(channel);
break;
case NotifyCollectionChangedAction.Remove:
foreach (Channel channel in args.OldItems.Cast<Channel>())
IEnumerable<Channel> leftChannels = args.OldItems.OfType<Channel>().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<Channel> 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);
}
/// <summary>
/// Whether a channel should be displayed in this overlay, based on its type.
/// </summary>
private static bool isChatChannel(Channel channel)
{
Name = @"tabs area";
RelativeSizeAxes = Axes.X;
Height = TAB_AREA_HEIGHT;
switch (channel.Type)
{
case ChannelType.Multiplayer:
case ChannelType.Spectator:
case ChannelType.Temporary:
return false;
default:
return true;
}
}
}

View File

@ -1,350 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ChatOverlayDrawableChannel> currentChannelContainer = null!;
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
protected IEnumerable<DrawableChannel> 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<Channel> currentChannel = new Bindable<Channel>();
private readonly IBindableList<Channel> availableChannels = new BindableList<Channel>();
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
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<ChatOverlayDrawableChannel>
{
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;
}
/// <summary>
/// Highlights a certain message in the specified channel.
/// </summary>
/// <param name="message">The message to highlight.</param>
/// <param name="channel">The channel containing the message.</param>
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)
{
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<Channel> newChannels = filterChannels(args.NewItems);
foreach (var channel in newChannels)
channelList.AddChannel(channel);
break;
case NotifyCollectionChangedAction.Remove:
IEnumerable<Channel> 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<Channel> filterChannels(IList channels)
=> channels.Cast<Channel>().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);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<int> playingUsers = new BindableList<int>();
private FillFlowContainer<PlayingUserPanel> userFlow;
private SearchContainer<PlayingUserPanel> 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<PlayingUserPanel>
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.X,
Height = padding * 2 + search_textbox_height,
Colour = colourProvider.Background4,
},
new Container<BasicSearchTextBox>
{
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<PlayingUserPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(10),
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<LocalisableString> 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;
}

View File

@ -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)

View File

@ -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);
}

View File

@ -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<Size> sizeFullscreen;
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private GameHost host { get; set; }
private SettingsDropdown<Size> resolutionDropdown;
private SettingsDropdown<Display> displayDropdown;
private SettingsDropdown<WindowMode> 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<WindowMode>
@ -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<float> bindable)
{
bindable.ValueChanged += _ =>

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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)

View File

@ -127,9 +127,12 @@ namespace osu.Game.Overlays.Settings.Sections
dropdownItems.Add(skin.ToLive(realm));
dropdownItems.Insert(protectedCount, random_skin_info);
Schedule(() =>
{
skinDropdown.Items = dropdownItems;
updateSelectedSkinFromConfig();
});
}
private void updateSelectedSkinFromConfig()

View File

@ -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);
}
}

View File

@ -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
}
/// <summary>
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
/// Clear any warning text.
/// </summary>
public LocalisableString? WarningText
public void ClearNoticeText()
{
set
{
bool hasValue = value != default;
noticeText?.Expire();
noticeText = null;
}
if (warningText == null)
/// <summary>
/// Set the text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to provide feedback to a user about a sub-optimal setting.
/// </summary>
/// <param name="text">The text to display.</param>
/// <param name="isWarning">Whether the text is in a warning state. Will decide how this is visually represented.</param>
public void SetNoticeText(LocalisableString text, bool isWarning = false)
{
if (!hasValue)
return;
ClearNoticeText();
// 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;
}
FlowContent.Add(noticeText = new LinkFlowContainer(cp => cp.Colour = isWarning ? colours.Yellow : colours.Green)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 5 },
Text = text,
});
}
public virtual Bindable<T> Current

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}

View File

@ -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))
{

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Edit
public abstract class DistancedHitObjectComposer<TObject> : HitObjectComposer<TObject>, IDistanceSnapProvider, IScrollBindingHandler<GlobalAction>
where TObject : HitObject
{
private const float adjust_step = 0.1f;
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
{
MinValue = 0.1,
@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Edit
Child = distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
{
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;

View File

@ -0,0 +1,81 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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,
}
},
}
},
}
};
}
}
}

View File

@ -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<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Drawable background;
protected readonly Drawable Background;
private readonly Container content;
protected override Container<Drawable> 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<WorkingBeatmap> beatmap, OsuColour colours, EditorClock clock)
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
background.Colour = colours.Gray1;
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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";
}
}
}

View File

@ -1,13 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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

View File

@ -28,6 +28,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
BackgroundColour = colours.Orange1;
SpriteText.Colour = colourProvider.Background6;
Content.CornerRadius = 0;
Text = "Test!";
}
}

View File

@ -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)

View File

@ -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();

View File

@ -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));
}
/// <summary>
/// Saves the currently edited beatmap.
/// </summary>
@ -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);

View File

@ -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

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Setup
Add(new Box
{
Colour = colourProvider.Background2,
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
});

View File

@ -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);
}

View File

@ -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<EffectControlPoint> 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;
}
}

View File

@ -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();
}
});
}
}
}
}

View File

@ -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

View File

@ -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);
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<ControlPointGroup> 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<TimingControlPoint>().FirstOrDefault();
if (timing == null)
return;
timing.BeatLength = 60000 / (timing.BPM + adjust);
}
private void tap()
{
editorClock.Seek(selectedGroup.Value.Time);

View File

@ -0,0 +1,254 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// A button with variable constant output based on hold position and length.
/// </summary>
public class TimingAdjustButton : CompositeDrawable
{
public Action<double> 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<IncrementBox>().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);
}
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Timing
public class TimingScreen : EditorScreenWithTimeline
{
[Cached]
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
public readonly Bindable<ControlPointGroup> SelectedGroup = new Bindable<ControlPointGroup>();
public TimingScreen()
: base(EditorScreenMode.Timing)
@ -132,6 +132,40 @@ namespace osu.Game.Screens.Edit.Timing
}, true);
}
protected override void Update()
{
base.Update();
trackActivePoint();
}
/// <summary>
/// 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.
/// </summary>
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;
}
}
}

View File

@ -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<TimingControlPoint> 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;
}
}

View File

@ -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<Drawable>.CORNER_RADIUS;
private readonly BindableNumber<double> 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<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
private readonly BindableBool displayLocked = new BindableBool();
private LockedOverlay lockedOverlay = null!;
public WaveformComparisonDisplay()
{
RelativeSizeAxes = Axes.Both;
CornerRadius = LabelledDrawable<Drawable>.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<TimingControlPoint>().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)
{
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<WaveformRow>())
{
// 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++;
}
}
lastDisplayedBeatIndex = beatIndex;
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),
}
}
},
};
}
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<WorkingBeatmap> 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;
}
}
}
}

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Verify
{
new Box
{
Colour = colours.Background2,
Colour = colours.Background3,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer

View File

@ -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[]
{

View File

@ -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);
}
}

Some files were not shown because too many files have changed in this diff Show More