mirror of
https://github.com/ppy/osu.git
synced 2025-01-15 07:22:55 +08:00
Merge branch 'master' into exit-multiplayer-is-dangerous
This commit is contained in:
commit
0d8e42b941
@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||||
});
|
});
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||||
|
|
||||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||||
|
@ -97,12 +97,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
|||||||
set => InternalChild = value;
|
set => InternalChild = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
throw new System.NotImplementedException();
|
throw new System.NotImplementedException();
|
||||||
}
|
}
|
||||||
|
@ -56,9 +56,9 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
var result = base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||||
|
|
||||||
switch (ScrollingInfo.Direction.Value)
|
switch (ScrollingInfo.Direction.Value)
|
||||||
{
|
{
|
||||||
@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
|
var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||||
if (result.Time is double time)
|
if (result.Time is double time)
|
||||||
beatSnapGrid.SelectionTimeRange = (time, time);
|
beatSnapGrid.SelectionTimeRange = (time, time);
|
||||||
else
|
else
|
||||||
|
@ -182,10 +182,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
private class SnapProvider : IDistanceSnapProvider
|
private class SnapProvider : IDistanceSnapProvider
|
||||||
{
|
{
|
||||||
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||||
new SnapResult(screenSpacePosition, null);
|
new SnapResult(screenSpacePosition, null);
|
||||||
|
|
||||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||||
|
|
||||||
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||||
|
|
||||||
@ -195,9 +195,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
|
|
||||||
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
|
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
|
||||||
|
|
||||||
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
|
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
|
||||||
|
|
||||||
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
|
public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
double difficulty = 0;
|
double difficulty = 0;
|
||||||
double weight = 1;
|
double weight = 1;
|
||||||
|
|
||||||
List<double> strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList();
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// These sections will not contribute to the difficulty.
|
||||||
|
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
|
||||||
|
|
||||||
|
List<double> strains = peaks.OrderByDescending(d => d).ToList();
|
||||||
|
|
||||||
// We are reducing the highest strains first to account for extreme difficulty spikes
|
// We are reducing the highest strains first to account for extreme difficulty spikes
|
||||||
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
|
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)
|
||||||
|
@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
{
|
{
|
||||||
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account
|
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account
|
||||||
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||||
var result = snapProvider?.SnapScreenSpacePositionToValidTime(newHeadPosition);
|
var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||||
|
|
||||||
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;
|
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;
|
||||||
|
|
||||||
|
@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
|
|
||||||
private void updateSlider()
|
private void updateSlider()
|
||||||
{
|
{
|
||||||
HitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||||
|
|
||||||
bodyPiece.UpdateFrom(HitObject);
|
bodyPiece.UpdateFrom(HitObject);
|
||||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||||
|
@ -59,11 +59,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
LayerBelowRuleset.AddRange(new Drawable[]
|
LayerBelowRuleset.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
new PlayfieldBorder
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
|
||||||
},
|
|
||||||
distanceSnapGridContainer = new Container
|
distanceSnapGridContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
@ -128,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||||
return snapResult;
|
return snapResult;
|
||||||
@ -136,9 +131,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return new SnapResult(screenSpacePosition, null);
|
return new SnapResult(screenSpacePosition, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition);
|
var positionSnap = FindSnappedPosition(screenSpacePosition);
|
||||||
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
if (positionSnap.ScreenSpacePosition != screenSpacePosition)
|
||||||
return positionSnap;
|
return positionSnap;
|
||||||
|
|
||||||
@ -154,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
|
return base.FindSnappedPositionAndTime(screenSpacePosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override string Acronym => @"AL";
|
public override string Acronym => @"AL";
|
||||||
public override string Description => @"Don't use the same key twice in a row!";
|
public override string Description => @"Don't use the same key twice in a row!";
|
||||||
public override double ScoreMultiplier => 1.0;
|
public override double ScoreMultiplier => 1.0;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
|
||||||
public override ModType Type => ModType.Conversion;
|
public override ModType Type => ModType.Conversion;
|
||||||
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
|
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModAutoplay : ModAutoplay
|
public class OsuModAutoplay : ModAutoplay
|
||||||
{
|
{
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
|
||||||
|
|
||||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModCinema : ModCinema<OsuHitObject>
|
public class OsuModCinema : ModCinema<OsuHitObject>
|
||||||
{
|
{
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
|
||||||
|
|
||||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
||||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
||||||
{
|
{
|
||||||
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How early before a hitobject's start time to trigger a hit.
|
/// How early before a hitobject's start time to trigger a hit.
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -23,9 +22,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public override string Name => @"Strict Tracking";
|
public override string Name => @"Strict Tracking";
|
||||||
public override string Acronym => @"ST";
|
public override string Acronym => @"ST";
|
||||||
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
|
|
||||||
public override ModType Type => ModType.DifficultyIncrease;
|
public override ModType Type => ModType.DifficultyIncrease;
|
||||||
public override string Description => @"Follow circles just got serious...";
|
public override string Description => @"Once you start a slider, follow precisely or get a miss.";
|
||||||
public override double ScoreMultiplier => 1.0;
|
public override double ScoreMultiplier => 1.0;
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
|
||||||
|
|
||||||
|
@ -141,7 +141,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||||
double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
|
double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
|
||||||
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
|
|
||||||
|
double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
|
||||||
|
|
||||||
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// These sections will not contribute to the difficulty.
|
||||||
|
if (peak > 0)
|
||||||
|
peaks.Add(peak);
|
||||||
}
|
}
|
||||||
|
|
||||||
double difficulty = 0;
|
double difficulty = 0;
|
||||||
|
@ -213,10 +213,10 @@ namespace osu.Game.Tests.Editing
|
|||||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
|
||||||
|
|
||||||
private void assertSnappedDuration(float distance, double expectedDuration)
|
private void assertSnappedDuration(float distance, double expectedDuration)
|
||||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration);
|
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(new HitObject(), distance) == expectedDuration);
|
||||||
|
|
||||||
private void assertSnappedDistance(float distance, float expectedDistance)
|
private void assertSnappedDistance(float distance, float expectedDistance)
|
||||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance);
|
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(new HitObject(), distance) == expectedDistance);
|
||||||
|
|
||||||
private class TestHitObjectComposer : OsuHitObjectComposer
|
private class TestHitObjectComposer : OsuHitObjectComposer
|
||||||
{
|
{
|
||||||
|
@ -162,10 +162,10 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
private class SnapProvider : IDistanceSnapProvider
|
private class SnapProvider : IDistanceSnapProvider
|
||||||
{
|
{
|
||||||
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||||
new SnapResult(screenSpacePosition, null);
|
new SnapResult(screenSpacePosition, null);
|
||||||
|
|
||||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
|
||||||
|
|
||||||
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
|
||||||
|
|
||||||
@ -175,9 +175,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
|
public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
|
||||||
|
|
||||||
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
|
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
|
||||||
|
|
||||||
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
|
public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu.Edit;
|
|||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||||
|
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -70,6 +71,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value)
|
Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value)
|
||||||
{
|
{
|
||||||
Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset())
|
Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset())
|
||||||
|
{
|
||||||
|
// force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio.
|
||||||
|
FillMode = FillMode.Fit,
|
||||||
|
FillAspectRatio = 4 / 3f
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -87,6 +93,65 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
|
AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType<ComposeBlueprintContainer>().First().CurrentTool is HitCircleCompositionTool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacementFailsWhenClickingButton()
|
||||||
|
{
|
||||||
|
AddStep("clear all control points and hitobjects", () =>
|
||||||
|
{
|
||||||
|
editorBeatmap.ControlPointInfo.Clear();
|
||||||
|
editorBeatmap.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||||
|
|
||||||
|
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||||
|
|
||||||
|
AddStep("move mouse to overlapping toggle button", () =>
|
||||||
|
{
|
||||||
|
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
|
||||||
|
var button = hitObjectComposer
|
||||||
|
.ChildrenOfType<ExpandingToolboxContainer>().First()
|
||||||
|
.ChildrenOfType<DrawableTernaryButton>().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
InputManager.MoveMouseTo(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
|
||||||
|
|
||||||
|
AddStep("attempt place circle", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacementWithinToolboxScrollArea()
|
||||||
|
{
|
||||||
|
AddStep("clear all control points and hitobjects", () =>
|
||||||
|
{
|
||||||
|
editorBeatmap.ControlPointInfo.Clear();
|
||||||
|
editorBeatmap.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||||
|
|
||||||
|
AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
|
||||||
|
|
||||||
|
AddStep("move mouse to scroll area", () =>
|
||||||
|
{
|
||||||
|
// Specifically wanting to test the area of overlap between the left expanding toolbox container
|
||||||
|
// and the playfield/composer.
|
||||||
|
var scrollArea = hitObjectComposer.ChildrenOfType<ExpandingToolboxContainer>().First().ScreenSpaceDrawQuad;
|
||||||
|
var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad;
|
||||||
|
InputManager.MoveMouseTo(new Vector2(scrollArea.TopLeft.X + 1, playfield.Centre.Y));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
|
||||||
|
|
||||||
|
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
|
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDistanceSpacingHotkeys()
|
public void TestDistanceSpacingHotkeys()
|
||||||
{
|
{
|
||||||
|
@ -56,6 +56,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
||||||
|
|
||||||
|
AddStep("press down", () => InputManager.Key(Key.Down));
|
||||||
|
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -73,6 +76,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
||||||
|
|
||||||
|
AddStep("press down", () => InputManager.Key(Key.Down));
|
||||||
|
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -91,6 +97,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
||||||
|
|
||||||
|
AddStep("press down", () => InputManager.Key(Key.Down));
|
||||||
|
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -147,6 +156,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
|
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestKeyboardSelection()
|
||||||
|
{
|
||||||
|
createPlaylist(p => p.AllowSelection = true);
|
||||||
|
|
||||||
|
AddStep("press down", () => InputManager.Key(Key.Down));
|
||||||
|
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
|
||||||
|
|
||||||
|
AddStep("press down", () => InputManager.Key(Key.Down));
|
||||||
|
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
|
||||||
|
|
||||||
|
AddStep("press up", () => InputManager.Key(Key.Up));
|
||||||
|
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
|
||||||
|
|
||||||
|
AddUntilStep("navigate to last item via keyboard", () =>
|
||||||
|
{
|
||||||
|
InputManager.Key(Key.Down);
|
||||||
|
return playlist.SelectedItem.Value == playlist.Items.Last();
|
||||||
|
});
|
||||||
|
AddAssert("last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Last());
|
||||||
|
AddUntilStep("last item is scrolled into view", () =>
|
||||||
|
{
|
||||||
|
var drawableItem = playlist.ItemMap[playlist.Items.Last()];
|
||||||
|
return playlist.ScreenSpaceDrawQuad.Contains(drawableItem.ScreenSpaceDrawQuad.TopLeft)
|
||||||
|
&& playlist.ScreenSpaceDrawQuad.Contains(drawableItem.ScreenSpaceDrawQuad.BottomRight);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("press down", () => InputManager.Key(Key.Down));
|
||||||
|
AddAssert("last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Last());
|
||||||
|
|
||||||
|
AddStep("press up", () => InputManager.Key(Key.Up));
|
||||||
|
AddAssert("second last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Reverse().ElementAt(1));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDownloadButtonHiddenWhenBeatmapExists()
|
public void TestDownloadButtonHiddenWhenBeatmapExists()
|
||||||
{
|
{
|
||||||
|
422
osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
Normal file
422
osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs
Normal file
@ -0,0 +1,422 @@
|
|||||||
|
// 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 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 ChatOverlayV2 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 ChatOverlayV2 { 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 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", () => listingVisibility == Visibility.Visible);
|
||||||
|
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
||||||
|
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||||
|
AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden);
|
||||||
|
AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden);
|
||||||
|
AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1);
|
||||||
|
AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSearchInListing()
|
||||||
|
{
|
||||||
|
AddStep("Show overlay", () => chatOverlay.Show());
|
||||||
|
AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
[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));
|
||||||
|
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
|
||||||
|
AddAssert("Channel 2 is visible", () => 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));
|
||||||
|
AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2);
|
||||||
|
AddAssert("Channel 2 is visible", () => 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TextBoxRetainsFocus()
|
||||||
|
{
|
||||||
|
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 selector", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListSelector>().Single()));
|
||||||
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
|
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
|
||||||
|
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
||||||
|
AddStep("Click drawable channel", () => clickDrawable(chatOverlay.ChildrenOfType<DrawableChannel>().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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Visibility listingVisibility =>
|
||||||
|
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value;
|
||||||
|
|
||||||
|
private Visibility loadingVisibility =>
|
||||||
|
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value;
|
||||||
|
|
||||||
|
private DrawableChannel currentDrawableChannel =>
|
||||||
|
chatOverlay.ChildrenOfType<Container<DrawableChannel>>().Single().Child;
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Dialog;
|
using osu.Game.Overlays.Dialog;
|
||||||
|
|
||||||
@ -15,15 +17,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
private DialogOverlay overlay;
|
private DialogOverlay overlay;
|
||||||
|
|
||||||
[SetUpSteps]
|
|
||||||
public void SetUpSteps()
|
|
||||||
{
|
|
||||||
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBasic()
|
public void TestBasic()
|
||||||
{
|
{
|
||||||
|
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||||
|
|
||||||
TestPopupDialog firstDialog = null;
|
TestPopupDialog firstDialog = null;
|
||||||
TestPopupDialog secondDialog = null;
|
TestPopupDialog secondDialog = null;
|
||||||
|
|
||||||
@ -37,12 +35,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
new PopupDialogOkButton
|
new PopupDialogOkButton
|
||||||
{
|
{
|
||||||
Text = @"I never want to see this again.",
|
Text = @"I never want to see this again.",
|
||||||
Action = () => System.Console.WriteLine(@"OK"),
|
Action = () => Console.WriteLine(@"OK"),
|
||||||
},
|
},
|
||||||
new PopupDialogCancelButton
|
new PopupDialogCancelButton
|
||||||
{
|
{
|
||||||
Text = @"Firetruck, I still want quick ranks!",
|
Text = @"Firetruck, I still want quick ranks!",
|
||||||
Action = () => System.Console.WriteLine(@"Cancel"),
|
Action = () => Console.WriteLine(@"Cancel"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@ -87,9 +85,49 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
|
AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPushBeforeLoad()
|
||||||
|
{
|
||||||
|
PopupDialog dialog = null;
|
||||||
|
|
||||||
|
AddStep("create dialog overlay", () => overlay = new SlowLoadingDialogOverlay());
|
||||||
|
|
||||||
|
AddStep("start loading overlay", () => LoadComponentAsync(overlay, Add));
|
||||||
|
|
||||||
|
AddStep("push dialog before loaded", () =>
|
||||||
|
{
|
||||||
|
overlay.Push(dialog = new TestPopupDialog
|
||||||
|
{
|
||||||
|
Buttons = new PopupDialogButton[]
|
||||||
|
{
|
||||||
|
new PopupDialogOkButton { Text = @"OK" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("complete load", () => ((SlowLoadingDialogOverlay)overlay).LoadEvent.Set());
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => overlay.IsLoaded);
|
||||||
|
|
||||||
|
AddAssert("dialog displayed", () => overlay.CurrentDialog == dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SlowLoadingDialogOverlay : DialogOverlay
|
||||||
|
{
|
||||||
|
public ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
LoadEvent.Wait(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDismissBeforePush()
|
public void TestDismissBeforePush()
|
||||||
{
|
{
|
||||||
|
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||||
|
|
||||||
TestPopupDialog testDialog = null;
|
TestPopupDialog testDialog = null;
|
||||||
AddStep("dismissed dialog push", () =>
|
AddStep("dismissed dialog push", () =>
|
||||||
{
|
{
|
||||||
@ -106,6 +144,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDismissBeforePushViaButtonPress()
|
public void TestDismissBeforePushViaButtonPress()
|
||||||
{
|
{
|
||||||
|
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
|
||||||
|
|
||||||
TestPopupDialog testDialog = null;
|
TestPopupDialog testDialog = null;
|
||||||
AddStep("dismissed dialog push", () =>
|
AddStep("dismissed dialog push", () =>
|
||||||
{
|
{
|
||||||
|
@ -2,17 +2,21 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
@ -64,6 +68,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
||||||
});
|
});
|
||||||
assertCustomisationToggleState(disabled: false, active: false);
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
|
AddAssert("setting items created", () => modSelectScreen.ChildrenOfType<ISettingsItem>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -78,6 +83,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value);
|
||||||
});
|
});
|
||||||
assertCustomisationToggleState(disabled: false, active: false);
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
|
AddAssert("setting items created", () => modSelectScreen.ChildrenOfType<ISettingsItem>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -98,17 +104,25 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
|
AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
|
||||||
AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
|
AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime));
|
||||||
|
AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
|
||||||
|
|
||||||
AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
|
AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick());
|
||||||
AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
|
AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore));
|
||||||
|
AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value);
|
||||||
|
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
|
||||||
|
|
||||||
AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
|
AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick());
|
||||||
AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
|
AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
|
||||||
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
|
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock)));
|
||||||
|
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
|
||||||
|
AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value);
|
||||||
|
|
||||||
AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
|
AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick());
|
||||||
AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
|
AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore))
|
||||||
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
|
&& SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror)));
|
||||||
|
AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value);
|
||||||
|
AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value);
|
||||||
|
AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -150,11 +164,19 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
|
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
|
||||||
assertCustomisationToggleState(disabled: false, active: true);
|
assertCustomisationToggleState(disabled: false, active: true);
|
||||||
|
|
||||||
AddStep("dismiss mod customisation", () =>
|
AddStep("dismiss mod customisation via mouse", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType<ShearedToggleButton>().Single());
|
InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType<ShearedToggleButton>().Single());
|
||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
|
|
||||||
|
AddStep("reset mods", () => SelectedMods.SetDefault());
|
||||||
|
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
|
||||||
|
assertCustomisationToggleState(disabled: false, active: true);
|
||||||
|
|
||||||
|
AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape));
|
||||||
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
|
|
||||||
AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
|
AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
|
||||||
assertCustomisationToggleState(disabled: false, active: false);
|
assertCustomisationToggleState(disabled: false, active: false);
|
||||||
@ -169,6 +191,206 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
|
assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure that two mod overlays are not cross polluting via central settings instances.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestSettingsNotCrossPolluting()
|
||||||
|
{
|
||||||
|
Bindable<IReadOnlyList<Mod>> selectedMods2 = null;
|
||||||
|
ModSelectScreen modSelectScreen2 = null;
|
||||||
|
|
||||||
|
createScreen();
|
||||||
|
AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() });
|
||||||
|
|
||||||
|
AddStep("set setting", () => modSelectScreen.ChildrenOfType<SettingsSlider<float>>().First().Current.Value = 8);
|
||||||
|
|
||||||
|
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
|
||||||
|
|
||||||
|
AddStep("create second bindable", () => selectedMods2 = new Bindable<IReadOnlyList<Mod>>(new Mod[] { new OsuModDifficultyAdjust() }));
|
||||||
|
|
||||||
|
AddStep("create second overlay", () =>
|
||||||
|
{
|
||||||
|
Add(modSelectScreen2 = new UserModSelectScreen().With(d =>
|
||||||
|
{
|
||||||
|
d.Origin = Anchor.TopCentre;
|
||||||
|
d.Anchor = Anchor.TopCentre;
|
||||||
|
d.SelectedMods.BindTarget = selectedMods2;
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("show", () => modSelectScreen2.Show());
|
||||||
|
|
||||||
|
AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);
|
||||||
|
AddAssert("ensure second is default", () => selectedMods2.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSettingsResetOnDeselection()
|
||||||
|
{
|
||||||
|
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
|
||||||
|
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
|
||||||
|
|
||||||
|
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
|
||||||
|
|
||||||
|
AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
|
||||||
|
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
|
||||||
|
|
||||||
|
AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick());
|
||||||
|
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAnimationFlushOnClose()
|
||||||
|
{
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddStep("Select all fun mods", () =>
|
||||||
|
{
|
||||||
|
modSelectScreen.ChildrenOfType<ModColumn>()
|
||||||
|
.Single(c => c.ModType == ModType.DifficultyIncrease)
|
||||||
|
.SelectAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5);
|
||||||
|
|
||||||
|
AddStep("trigger deselect and close overlay", () =>
|
||||||
|
{
|
||||||
|
modSelectScreen.ChildrenOfType<ModColumn>()
|
||||||
|
.Single(c => c.ModType == ModType.DifficultyIncrease)
|
||||||
|
.DeselectAll();
|
||||||
|
|
||||||
|
modSelectScreen.Hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRulesetChanges()
|
||||||
|
{
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
|
||||||
|
|
||||||
|
AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; });
|
||||||
|
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
|
||||||
|
|
||||||
|
changeRuleset(3);
|
||||||
|
|
||||||
|
AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
|
||||||
|
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExternallySetCustomizedMod()
|
||||||
|
{
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
|
||||||
|
|
||||||
|
AddAssert("ensure button is selected and customized accordingly", () =>
|
||||||
|
{
|
||||||
|
var button = getPanelForMod(SelectedMods.Value.Single().GetType());
|
||||||
|
return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSettingsAreRetainedOnReload()
|
||||||
|
{
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
|
||||||
|
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
|
||||||
|
|
||||||
|
createScreen();
|
||||||
|
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestExternallySetModIsReplacedByOverlayInstance()
|
||||||
|
{
|
||||||
|
Mod external = new OsuModDoubleTime();
|
||||||
|
Mod overlayButtonMod = null;
|
||||||
|
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
|
||||||
|
|
||||||
|
AddAssert("ensure button is selected", () =>
|
||||||
|
{
|
||||||
|
var button = getPanelForMod(SelectedMods.Value.Single().GetType());
|
||||||
|
overlayButtonMod = button.Mod;
|
||||||
|
return button.Active.Value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
|
||||||
|
AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
|
||||||
|
|
||||||
|
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
|
||||||
|
AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod)));
|
||||||
|
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeIsValidChangesButtonVisibility()
|
||||||
|
{
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddAssert("double time visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
|
||||||
|
|
||||||
|
AddStep("make double time invalid", () => modSelectScreen.IsValidMod = m => !(m is OsuModDoubleTime));
|
||||||
|
AddUntilStep("double time not visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value));
|
||||||
|
AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
|
||||||
|
|
||||||
|
AddStep("make double time valid again", () => modSelectScreen.IsValidMod = m => true);
|
||||||
|
AddUntilStep("double time visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value));
|
||||||
|
AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType<ModPanel>().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeIsValidPreservesSelection()
|
||||||
|
{
|
||||||
|
createScreen();
|
||||||
|
changeRuleset(0);
|
||||||
|
|
||||||
|
AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() });
|
||||||
|
AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||||
|
|
||||||
|
AddStep("make NF invalid", () => modSelectScreen.IsValidMod = m => !(m is ModNoFail));
|
||||||
|
AddAssert("DT + HD still selected", () => modSelectScreen.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUnimplementedModIsUnselectable()
|
||||||
|
{
|
||||||
|
var testRuleset = new TestUnimplementedModOsuRuleset();
|
||||||
|
|
||||||
|
createScreen();
|
||||||
|
|
||||||
|
AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo);
|
||||||
|
waitForColumnLoad();
|
||||||
|
|
||||||
|
AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value);
|
||||||
|
}
|
||||||
|
|
||||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
||||||
() => modSelectScreen.ChildrenOfType<ModColumn>().Any() && modSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
|
() => modSelectScreen.ChildrenOfType<ModColumn>().Any() && modSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
|
||||||
|
|
||||||
@ -188,5 +410,26 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
private ModPanel getPanelForMod(Type modType)
|
private ModPanel getPanelForMod(Type modType)
|
||||||
=> modSelectScreen.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
|
=> modSelectScreen.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
|
||||||
|
|
||||||
|
private class TestUnimplementedMod : Mod
|
||||||
|
{
|
||||||
|
public override string Name => "Unimplemented mod";
|
||||||
|
public override string Acronym => "UM";
|
||||||
|
public override string Description => "A mod that is not implemented.";
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
public override ModType Type => ModType.Conversion;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestUnimplementedModOsuRuleset : OsuRuleset
|
||||||
|
{
|
||||||
|
public override string ShortName => "unimplemented";
|
||||||
|
|
||||||
|
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||||
|
{
|
||||||
|
if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() });
|
||||||
|
|
||||||
|
return base.GetModsFor(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.UserInterface
|
||||||
|
{
|
||||||
|
public class TestSceneShearedSearchTextBox : OsuTestScene
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestAllColourSchemes()
|
||||||
|
{
|
||||||
|
foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast<OverlayColourScheme>())
|
||||||
|
AddStep($"set {scheme} scheme", () => Child = createContent(scheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Drawable createContent(OverlayColourScheme colourScheme)
|
||||||
|
{
|
||||||
|
var colourProvider = new OverlayColourProvider(colourScheme);
|
||||||
|
|
||||||
|
return new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies = new (Type, object)[]
|
||||||
|
{
|
||||||
|
(typeof(OverlayColourProvider), colourProvider)
|
||||||
|
},
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ShearedSearchTextBox
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.5f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,23 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -109,6 +111,16 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public bool SamplesMatchPlaybackRate { get; set; } = true;
|
public bool SamplesMatchPlaybackRate { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ratio of distance travelled per time unit.
|
||||||
|
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
|
||||||
|
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
|
||||||
|
///
|
||||||
|
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
|
||||||
|
/// </remarks>
|
||||||
public double DistanceSpacing { get; set; } = 1.0;
|
public double DistanceSpacing { get; set; } = 1.0;
|
||||||
|
|
||||||
public int BeatDivisor { get; set; }
|
public int BeatDivisor { get; set; }
|
||||||
|
@ -153,7 +153,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
cacheDownloadRequest.PerformAsync();
|
Task.Run(() => cacheDownloadRequest.PerformAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)
|
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)
|
||||||
|
26
osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs
Normal file
26
osu.Game/Graphics/UserInterface/BasicSearchTextBox.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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 osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterface
|
||||||
|
{
|
||||||
|
public class BasicSearchTextBox : SearchTextBox
|
||||||
|
{
|
||||||
|
public BasicSearchTextBox()
|
||||||
|
{
|
||||||
|
Add(new SpriteIcon
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.Search,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Margin = new MarginPadding { Right = 10 },
|
||||||
|
Size = new Vector2(20),
|
||||||
|
});
|
||||||
|
|
||||||
|
TextFlow.Padding = new MarginPadding { Right = 35 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,9 @@
|
|||||||
// 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.
|
// 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;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osuTK;
|
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
@ -18,16 +15,6 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
public SearchTextBox()
|
public SearchTextBox()
|
||||||
{
|
{
|
||||||
Height = 35;
|
Height = 35;
|
||||||
Add(new SpriteIcon
|
|
||||||
{
|
|
||||||
Icon = FontAwesome.Solid.Search,
|
|
||||||
Origin = Anchor.CentreRight,
|
|
||||||
Anchor = Anchor.CentreRight,
|
|
||||||
Margin = new MarginPadding { Right = 10 },
|
|
||||||
Size = new Vector2(20),
|
|
||||||
});
|
|
||||||
|
|
||||||
TextFlow.Padding = new MarginPadding { Right = 35 };
|
|
||||||
PlaceholderText = HomeStrings.SearchPlaceholder;
|
PlaceholderText = HomeStrings.SearchPlaceholder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A <see cref="SearchTextBox"/> which does not handle left/right arrow keys for seeking.
|
/// A <see cref="SearchTextBox"/> which does not handle left/right arrow keys for seeking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SeekLimitedSearchTextBox : SearchTextBox
|
public class SeekLimitedSearchTextBox : BasicSearchTextBox
|
||||||
{
|
{
|
||||||
public override bool HandleLeftRightArrows => false;
|
public override bool HandleLeftRightArrows => false;
|
||||||
}
|
}
|
||||||
|
137
osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs
Normal file
137
osu.Game/Graphics/UserInterface/ShearedSearchTextBox.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// 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.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Mods;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterface
|
||||||
|
{
|
||||||
|
public class ShearedSearchTextBox : CompositeDrawable, IHasCurrentValue<string>
|
||||||
|
{
|
||||||
|
private const float corner_radius = 7;
|
||||||
|
|
||||||
|
private readonly Box background;
|
||||||
|
private readonly SearchTextBox textBox;
|
||||||
|
|
||||||
|
public Bindable<string> Current
|
||||||
|
{
|
||||||
|
get => textBox.Current;
|
||||||
|
set => textBox.Current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HoldFocus
|
||||||
|
{
|
||||||
|
get => textBox.HoldFocus;
|
||||||
|
set => textBox.HoldFocus = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeFocus() => textBox.TakeFocus();
|
||||||
|
|
||||||
|
public void KillFocus() => textBox.KillFocus();
|
||||||
|
|
||||||
|
public ShearedSearchTextBox()
|
||||||
|
{
|
||||||
|
Height = 42;
|
||||||
|
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);
|
||||||
|
Masking = true;
|
||||||
|
CornerRadius = corner_radius;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
},
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
textBox = new InnerSearchTextBox
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Size = Vector2.One
|
||||||
|
},
|
||||||
|
new SpriteIcon
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.Search,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Size = new Vector2(16),
|
||||||
|
Shear = -Shear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.Absolute, 50),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colourProvider)
|
||||||
|
{
|
||||||
|
background.Colour = colourProvider.Background3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool HandleNonPositionalInput => textBox.HandleNonPositionalInput;
|
||||||
|
|
||||||
|
private class InnerSearchTextBox : SearchTextBox
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colourProvider)
|
||||||
|
{
|
||||||
|
BackgroundFocused = colourProvider.Background4;
|
||||||
|
BackgroundUnfocused = colourProvider.Background4;
|
||||||
|
|
||||||
|
Placeholder.Font = OsuFont.GetFont(size: CalculatedTextSize, weight: FontWeight.SemiBold);
|
||||||
|
PlaceholderText = CommonStrings.InputSearch;
|
||||||
|
|
||||||
|
CornerRadius = corner_radius;
|
||||||
|
TextContainer.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SpriteText CreatePlaceholder() => new SearchPlaceholder();
|
||||||
|
|
||||||
|
internal class SearchPlaceholder : SpriteText
|
||||||
|
{
|
||||||
|
public override void Show()
|
||||||
|
{
|
||||||
|
this
|
||||||
|
.MoveToY(0, 250, Easing.OutQuint)
|
||||||
|
.FadeIn(250, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Hide()
|
||||||
|
{
|
||||||
|
this
|
||||||
|
.MoveToY(3, 250, Easing.OutQuint)
|
||||||
|
.FadeOut(250, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -59,6 +59,9 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
|
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
|
||||||
new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
|
new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
|
||||||
|
|
||||||
|
new KeyBinding(InputKey.Left, GlobalAction.SelectPreviousGroup),
|
||||||
|
new KeyBinding(InputKey.Right, GlobalAction.SelectNextGroup),
|
||||||
|
|
||||||
new KeyBinding(InputKey.Space, GlobalAction.Select),
|
new KeyBinding(InputKey.Space, GlobalAction.Select),
|
||||||
new KeyBinding(InputKey.Enter, GlobalAction.Select),
|
new KeyBinding(InputKey.Enter, GlobalAction.Select),
|
||||||
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
|
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
|
||||||
@ -79,6 +82,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
|
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
|
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
|
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
|
||||||
|
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
|
||||||
|
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
|
||||||
};
|
};
|
||||||
|
|
||||||
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
||||||
@ -103,7 +108,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
|
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
|
||||||
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
|
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
|
||||||
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
|
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
|
||||||
new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions)
|
new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions),
|
||||||
};
|
};
|
||||||
|
|
||||||
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
|
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
|
||||||
@ -301,5 +306,17 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorFlipVertically))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorFlipVertically))]
|
||||||
EditorFlipVertically,
|
EditorFlipVertically,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorIncreaseDistanceSpacing))]
|
||||||
|
EditorIncreaseDistanceSpacing,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDecreaseDistanceSpacing))]
|
||||||
|
EditorDecreaseDistanceSpacing,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectPreviousGroup))]
|
||||||
|
SelectPreviousGroup,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectNextGroup))]
|
||||||
|
SelectNextGroup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,16 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString SelectNext => new TranslatableString(getKey(@"select_next"), @"Next selection");
|
public static LocalisableString SelectNext => new TranslatableString(getKey(@"select_next"), @"Next selection");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Select previous group"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString SelectPreviousGroup => new TranslatableString(getKey(@"select_previous_group"), @"Select previous group");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Select next group"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString SelectNextGroup => new TranslatableString(getKey(@"select_next_group"), @"Select next group");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Home"
|
/// "Home"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -239,6 +249,16 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString EditorFlipVertically => new TranslatableString(getKey(@"editor_flip_vertically"), @"Flip selection vertically");
|
public static LocalisableString EditorFlipVertically => new TranslatableString(getKey(@"editor_flip_vertically"), @"Flip selection vertically");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Increase distance spacing"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorIncreaseDistanceSpacing => new TranslatableString(getKey(@"editor_increase_distance_spacing"), @"Increase distance spacing");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Decrease distance spacing"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorDecreaseDistanceSpacing => new TranslatableString(getKey(@"editor_decrease_distance_spacing"), @"Decrease distance spacing");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Toggle skin editor"
|
/// "Toggle skin editor"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Overlays.Chat;
|
using osu.Game.Overlays.Chat;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Online.Chat
|
namespace osu.Game.Online.Chat
|
||||||
{
|
{
|
||||||
@ -119,6 +120,20 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
public class ChatTextBox : FocusedTextBox
|
public class ChatTextBox : FocusedTextBox
|
||||||
{
|
{
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
// Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other
|
||||||
|
// elements on the same screen.
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Up:
|
||||||
|
case Key.Down:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
@ -163,7 +163,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
|
|
||||||
public void TakeFocus() => textBox.TakeFocus();
|
public void TakeFocus() => textBox.TakeFocus();
|
||||||
|
|
||||||
private class BeatmapSearchTextBox : SearchTextBox
|
private class BeatmapSearchTextBox : BasicSearchTextBox
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Any time the text box receives key events (even while masked).
|
/// Any time the text box receives key events (even while masked).
|
||||||
|
@ -25,14 +25,14 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
public event Action<Channel>? OnRequestSelect;
|
public event Action<Channel>? OnRequestSelect;
|
||||||
public event Action<Channel>? OnRequestLeave;
|
public event Action<Channel>? OnRequestLeave;
|
||||||
|
|
||||||
|
public readonly Channel Channel;
|
||||||
|
|
||||||
public readonly BindableInt Mentions = new BindableInt();
|
public readonly BindableInt Mentions = new BindableInt();
|
||||||
|
|
||||||
public readonly BindableBool Unread = new BindableBool();
|
public readonly BindableBool Unread = new BindableBool();
|
||||||
|
|
||||||
public readonly BindableBool SelectorActive = new BindableBool();
|
public readonly BindableBool SelectorActive = new BindableBool();
|
||||||
|
|
||||||
private readonly Channel channel;
|
|
||||||
|
|
||||||
private Box hoverBox = null!;
|
private Box hoverBox = null!;
|
||||||
private Box selectBox = null!;
|
private Box selectBox = null!;
|
||||||
private OsuSpriteText text = null!;
|
private OsuSpriteText text = null!;
|
||||||
@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
|
|
||||||
public ChannelListItem(Channel channel)
|
public ChannelListItem(Channel channel)
|
||||||
{
|
{
|
||||||
this.channel = channel;
|
Channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = channel.Name,
|
Text = Channel.Name,
|
||||||
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
|
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
|
||||||
Colour = colourProvider.Light3,
|
Colour = colourProvider.Light3,
|
||||||
Margin = new MarginPadding { Bottom = 2 },
|
Margin = new MarginPadding { Bottom = 2 },
|
||||||
@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Margin = new MarginPadding { Right = 3 },
|
Margin = new MarginPadding { Right = 3 },
|
||||||
Action = () => OnRequestLeave?.Invoke(channel),
|
Action = () => OnRequestLeave?.Invoke(Channel),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -119,20 +119,16 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Action = () => OnRequestSelect?.Invoke(channel);
|
Action = () => OnRequestSelect?.Invoke(Channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
selectedChannel.BindValueChanged(_ => updateSelectState(), true);
|
selectedChannel.BindValueChanged(_ => updateState(), true);
|
||||||
SelectorActive.BindValueChanged(_ => updateSelectState(), true);
|
SelectorActive.BindValueChanged(_ => updateState(), true);
|
||||||
|
Unread.BindValueChanged(_ => updateState(), true);
|
||||||
Unread.BindValueChanged(change =>
|
|
||||||
{
|
|
||||||
text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint);
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
@ -151,10 +147,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
|
|
||||||
private Drawable createIcon()
|
private Drawable createIcon()
|
||||||
{
|
{
|
||||||
if (channel.Type != ChannelType.PM)
|
if (Channel.Type != ChannelType.PM)
|
||||||
return Drawable.Empty();
|
return Drawable.Empty();
|
||||||
|
|
||||||
return new UpdateableAvatar(channel.Users.First(), isInteractive: false)
|
return new UpdateableAvatar(Channel.Users.First(), isInteractive: false)
|
||||||
{
|
{
|
||||||
Size = new Vector2(20),
|
Size = new Vector2(20),
|
||||||
Margin = new MarginPadding { Right = 5 },
|
Margin = new MarginPadding { Right = 5 },
|
||||||
@ -165,12 +161,19 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSelectState()
|
private void updateState()
|
||||||
{
|
{
|
||||||
if (selectedChannel.Value == channel && !SelectorActive.Value)
|
bool selected = selectedChannel.Value == Channel && !SelectorActive.Value;
|
||||||
|
|
||||||
|
if (selected)
|
||||||
selectBox.FadeIn(300, Easing.OutQuint);
|
selectBox.FadeIn(300, Easing.OutQuint);
|
||||||
else
|
else
|
||||||
selectBox.FadeOut(200, Easing.OutQuint);
|
selectBox.FadeOut(200, Easing.OutQuint);
|
||||||
|
|
||||||
|
if (Unread.Value || selected)
|
||||||
|
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
|
||||||
|
else
|
||||||
|
text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
|
|
||||||
private Box hoverBox = null!;
|
private Box hoverBox = null!;
|
||||||
private Box selectBox = null!;
|
private Box selectBox = null!;
|
||||||
|
private OsuSpriteText text = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OverlayColourProvider colourProvider)
|
private void load(OverlayColourProvider colourProvider)
|
||||||
@ -46,11 +50,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Padding = new MarginPadding { Left = 18, Right = 10 },
|
Padding = new MarginPadding { Left = 18, Right = 10 },
|
||||||
Child = new OsuSpriteText
|
Child = text = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = "Add More Channels",
|
Text = "Add more channels",
|
||||||
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
|
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
|
||||||
Colour = colourProvider.Light3,
|
Colour = colourProvider.Light3,
|
||||||
Margin = new MarginPadding { Bottom = 2 },
|
Margin = new MarginPadding { Bottom = 2 },
|
||||||
@ -68,9 +72,15 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
SelectorActive.BindValueChanged(selector =>
|
SelectorActive.BindValueChanged(selector =>
|
||||||
{
|
{
|
||||||
if (selector.NewValue)
|
if (selector.NewValue)
|
||||||
|
{
|
||||||
|
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
|
||||||
selectBox.FadeIn(300, Easing.OutQuint);
|
selectBox.FadeIn(300, Easing.OutQuint);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint);
|
||||||
selectBox.FadeOut(200, Easing.OutQuint);
|
selectBox.FadeOut(200, Easing.OutQuint);
|
||||||
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
Action = () => SelectorActive.Value = true;
|
Action = () => SelectorActive.Value = true;
|
||||||
|
83
osu.Game/Overlays/Chat/ChatOverlayTopBar.cs
Normal file
83
osu.Game/Overlays/Chat/ChatOverlayTopBar.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
// 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 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.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.Chat
|
||||||
|
{
|
||||||
|
public class ChatOverlayTopBar : Container
|
||||||
|
{
|
||||||
|
private Box background = null!;
|
||||||
|
|
||||||
|
private Color4 backgroundColour;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colourProvider, TextureStore textures)
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = backgroundColour = colourProvider.Background3,
|
||||||
|
},
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.Absolute, 50),
|
||||||
|
new Dimension(),
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Texture = textures.Get("Icons/Hexacons/messaging"),
|
||||||
|
Size = new Vector2(18),
|
||||||
|
},
|
||||||
|
// Placeholder text
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Text = "osu!chat",
|
||||||
|
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
|
||||||
|
Margin = new MarginPadding { Bottom = 2f },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint);
|
||||||
|
return base.OnHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
background.FadeColour(backgroundColour, 300, Easing.OutQuint);
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,8 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -25,14 +26,19 @@ namespace osu.Game.Overlays.Chat
|
|||||||
|
|
||||||
public event Action<string>? OnSearchTermsChanged;
|
public event Action<string>? OnSearchTermsChanged;
|
||||||
|
|
||||||
|
public void TextBoxTakeFocus() => chatTextBox.TakeFocus();
|
||||||
|
|
||||||
|
public void TextBoxKillFocus() => chatTextBox.KillFocus();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Bindable<Channel> currentChannel { get; set; } = null!;
|
private Bindable<Channel> currentChannel { get; set; } = null!;
|
||||||
|
|
||||||
private OsuTextFlowContainer chattingTextContainer = null!;
|
private Container chattingTextContainer = null!;
|
||||||
|
private OsuSpriteText chattingText = null!;
|
||||||
private Container searchIconContainer = null!;
|
private Container searchIconContainer = null!;
|
||||||
private ChatTextBox chatTextBox = null!;
|
private ChatTextBox chatTextBox = null!;
|
||||||
|
|
||||||
private const float chatting_text_width = 180;
|
private const float chatting_text_width = 240;
|
||||||
private const float search_icon_width = 40;
|
private const float search_icon_width = 40;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -61,16 +67,20 @@ namespace osu.Game.Overlays.Chat
|
|||||||
{
|
{
|
||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
chattingTextContainer = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 20))
|
chattingTextContainer = new Container
|
||||||
{
|
{
|
||||||
Masking = true,
|
|
||||||
Width = chatting_text_width,
|
|
||||||
Padding = new MarginPadding { Left = 10 },
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
TextAnchor = Anchor.CentreRight,
|
Width = chatting_text_width,
|
||||||
Anchor = Anchor.CentreLeft,
|
Masking = true,
|
||||||
Origin = Anchor.CentreLeft,
|
Padding = new MarginPadding { Right = 5 },
|
||||||
Colour = colourProvider.Background1,
|
Child = chattingText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Font = OsuFont.Torus.With(size: 20),
|
||||||
|
Colour = colourProvider.Background1,
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Truncate = true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
searchIconContainer = new Container
|
searchIconContainer = new Container
|
||||||
{
|
{
|
||||||
@ -131,15 +141,15 @@ namespace osu.Game.Overlays.Chat
|
|||||||
switch (newChannel?.Type)
|
switch (newChannel?.Type)
|
||||||
{
|
{
|
||||||
case ChannelType.Public:
|
case ChannelType.Public:
|
||||||
chattingTextContainer.Text = $"chatting in {newChannel.Name}";
|
chattingText.Text = $"chatting in {newChannel.Name}";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ChannelType.PM:
|
case ChannelType.PM:
|
||||||
chattingTextContainer.Text = $"chatting with {newChannel.Name}";
|
chattingText.Text = $"chatting with {newChannel.Name}";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
chattingTextContainer.Text = string.Empty;
|
chattingText.Text = string.Empty;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
@ -25,11 +25,11 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
public event Action<Channel>? OnRequestJoin;
|
public event Action<Channel>? OnRequestJoin;
|
||||||
public event Action<Channel>? OnRequestLeave;
|
public event Action<Channel>? OnRequestLeave;
|
||||||
|
|
||||||
public bool FilteringActive { get; set; }
|
public readonly Channel Channel;
|
||||||
public IEnumerable<string> FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty };
|
|
||||||
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
|
|
||||||
|
|
||||||
private readonly Channel channel;
|
public bool FilteringActive { get; set; }
|
||||||
|
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty };
|
||||||
|
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
|
||||||
|
|
||||||
private Box hoverBox = null!;
|
private Box hoverBox = null!;
|
||||||
private SpriteIcon checkbox = null!;
|
private SpriteIcon checkbox = null!;
|
||||||
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
|
|
||||||
public ChannelListingItem(Channel channel)
|
public ChannelListingItem(Channel channel)
|
||||||
{
|
{
|
||||||
this.channel = channel;
|
Channel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = channel.Name,
|
Text = Channel.Name,
|
||||||
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
|
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
|
||||||
Margin = new MarginPadding { Bottom = 2 },
|
Margin = new MarginPadding { Bottom = 2 },
|
||||||
},
|
},
|
||||||
@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = channel.Topic,
|
Text = Channel.Topic,
|
||||||
Font = OsuFont.Torus.With(size: text_size),
|
Font = OsuFont.Torus.With(size: text_size),
|
||||||
Margin = new MarginPadding { Bottom = 2 },
|
Margin = new MarginPadding { Bottom = 2 },
|
||||||
},
|
},
|
||||||
@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
channelJoined = channel.Joined.GetBoundCopy();
|
channelJoined = Channel.Joined.GetBoundCopy();
|
||||||
channelJoined.BindValueChanged(change =>
|
channelJoined.BindValueChanged(change =>
|
||||||
{
|
{
|
||||||
const double duration = 500;
|
const double duration = 500;
|
||||||
@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel);
|
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
@ -181,7 +181,7 @@ namespace osu.Game.Overlays.Chat.Selection
|
|||||||
base.PopOut();
|
base.PopOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class HeaderSearchTextBox : SearchTextBox
|
private class HeaderSearchTextBox : BasicSearchTextBox
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
|
314
osu.Game/Overlays/ChatOverlayV2.cs
Normal file
314
osu.Game/Overlays/ChatOverlayV2.cs
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
// 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<DrawableChannel> currentChannelContainer = null!;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private readonly BindableBool selectorActive = new BindableBool();
|
||||||
|
|
||||||
|
[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>();
|
||||||
|
|
||||||
|
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 },
|
||||||
|
SelectorActive = { BindTarget = selectorActive },
|
||||||
|
},
|
||||||
|
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<DrawableChannel>
|
||||||
|
{
|
||||||
|
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 },
|
||||||
|
ShowSearch = { BindTarget = selectorActive },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
loading.Show();
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
|
||||||
|
|
||||||
|
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
|
||||||
|
|
||||||
|
currentChannel.BindTo(channelManager.CurrentChannel);
|
||||||
|
channelManager.CurrentChannel.BindValueChanged(currentChannelChanged, true);
|
||||||
|
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
|
||||||
|
channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true);
|
||||||
|
|
||||||
|
channelList.OnRequestSelect += channel =>
|
||||||
|
{
|
||||||
|
// Manually selecting a channel should dismiss the selector
|
||||||
|
selectorActive.Value = false;
|
||||||
|
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;
|
||||||
|
|
||||||
|
selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectorActive.Value = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
loading.Show();
|
||||||
|
|
||||||
|
// Channel is null when leaving the currently selected channel
|
||||||
|
if (newChannel == null)
|
||||||
|
{
|
||||||
|
// Find another channel to switch to
|
||||||
|
newChannel = channelManager.JoinedChannels.FirstOrDefault(c => c != channel.OldValue);
|
||||||
|
|
||||||
|
if (newChannel == null)
|
||||||
|
selectorActive.Value = true;
|
||||||
|
else
|
||||||
|
currentChannel.Value = newChannel;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadComponentAsync(new DrawableChannel(newChannel), loaded =>
|
||||||
|
{
|
||||||
|
currentChannelContainer.Clear();
|
||||||
|
currentChannelContainer.Add(loaded);
|
||||||
|
loading.Hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
|
{
|
||||||
|
switch (args.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
IEnumerable<Channel> joinedChannels = filterChannels(args.NewItems);
|
||||||
|
foreach (var channel in joinedChannels)
|
||||||
|
channelList.AddChannel(channel);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
IEnumerable<Channel> leftChannels = filterChannels(args.OldItems);
|
||||||
|
foreach (var channel in leftChannels)
|
||||||
|
channelList.RemoveChannel(channel);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -49,18 +49,24 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
|
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
|
||||||
|
|
||||||
// if any existing dialog is being displayed, dismiss it before showing a new one.
|
var lastDialog = CurrentDialog;
|
||||||
CurrentDialog?.Hide();
|
|
||||||
|
|
||||||
|
// Immediately update the externally accessible property as this may be used for checks even before
|
||||||
|
// a DialogOverlay instance has finished loading.
|
||||||
CurrentDialog = dialog;
|
CurrentDialog = dialog;
|
||||||
CurrentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
|
|
||||||
|
|
||||||
dialogContainer.Add(CurrentDialog);
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
// if any existing dialog is being displayed, dismiss it before showing a new one.
|
||||||
|
lastDialog?.Hide();
|
||||||
|
dialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
|
||||||
|
dialogContainer.Add(dialog);
|
||||||
|
|
||||||
Show();
|
Show();
|
||||||
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsPresent => dialogContainer.Children.Count > 0;
|
public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
|
||||||
|
|
||||||
protected override bool BlockNonPositionalInput => true;
|
protected override bool BlockNonPositionalInput => true;
|
||||||
|
|
||||||
@ -81,23 +87,16 @@ namespace osu.Game.Overlays
|
|||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
base.PopIn();
|
base.PopIn();
|
||||||
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
|
|
||||||
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
|
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
{
|
{
|
||||||
base.PopOut();
|
base.PopOut();
|
||||||
|
|
||||||
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
|
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
|
||||||
|
|
||||||
if (CurrentDialog?.State.Value == Visibility.Visible)
|
if (CurrentDialog?.State.Value == Visibility.Visible)
|
||||||
{
|
|
||||||
CurrentDialog.Hide();
|
CurrentDialog.Hide();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -10,6 +12,7 @@ using Humanizer;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -25,8 +28,6 @@ using osuTK;
|
|||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Mods
|
namespace osu.Game.Overlays.Mods
|
||||||
{
|
{
|
||||||
public class ModColumn : CompositeDrawable
|
public class ModColumn : CompositeDrawable
|
||||||
@ -52,9 +53,22 @@ namespace osu.Game.Overlays.Mods
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
|
||||||
public Bindable<bool> Active = new BindableBool(true);
|
public Bindable<bool> Active = new BindableBool(true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of mods marked as selected in this column.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Note that the mod instances returned by this property are owned solely by this column
|
||||||
|
/// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances).
|
||||||
|
/// </remarks>
|
||||||
|
public IReadOnlyList<Mod> SelectedMods { get; private set; } = Array.Empty<Mod>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a mod panel has been selected interactively by the user.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? SelectionChangedByUser;
|
||||||
|
|
||||||
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
|
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value;
|
||||||
|
|
||||||
protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod);
|
protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod);
|
||||||
@ -63,6 +77,15 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All mods that are available for the current ruleset in this particular column.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Note that the mod instances in this list are owned solely by this column
|
||||||
|
/// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances).
|
||||||
|
/// </remarks>
|
||||||
|
private IReadOnlyList<Mod> localAvailableMods = Array.Empty<Mod>();
|
||||||
|
|
||||||
private readonly TextFlowContainer headerText;
|
private readonly TextFlowContainer headerText;
|
||||||
private readonly Box headerBackground;
|
private readonly Box headerBackground;
|
||||||
private readonly Container contentContainer;
|
private readonly Container contentContainer;
|
||||||
@ -226,6 +249,9 @@ namespace osu.Game.Overlays.Mods
|
|||||||
private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours)
|
private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours)
|
||||||
{
|
{
|
||||||
availableMods.BindTo(game.AvailableMods);
|
availableMods.BindTo(game.AvailableMods);
|
||||||
|
// this `BindValueChanged` callback is intentionally here, to ensure that local available mods are constructed as early as possible.
|
||||||
|
// this is needed to make sure no external changes to mods are dropped while mod panels are asynchronously loading.
|
||||||
|
availableMods.BindValueChanged(_ => updateLocalAvailableMods(), true);
|
||||||
|
|
||||||
headerBackground.Colour = accentColour = colours.ForModType(ModType);
|
headerBackground.Colour = accentColour = colours.ForModType(ModType);
|
||||||
|
|
||||||
@ -239,31 +265,26 @@ namespace osu.Game.Overlays.Mods
|
|||||||
contentBackground.Colour = colourProvider.Background4;
|
contentBackground.Colour = colourProvider.Background4;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
private void updateLocalAvailableMods()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty<Mod>())
|
||||||
availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods));
|
.Select(m => m.DeepClone())
|
||||||
SelectedMods.BindValueChanged(_ =>
|
.ToList();
|
||||||
{
|
|
||||||
// if a load is in progress, don't try to update the selection - the load flow will do so.
|
if (newMods.SequenceEqual(localAvailableMods))
|
||||||
if (latestLoadTask == null)
|
return;
|
||||||
updateActiveState();
|
|
||||||
});
|
localAvailableMods = newMods;
|
||||||
updateMods();
|
Scheduler.AddOnce(loadPanels);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource? cancellationTokenSource;
|
private CancellationTokenSource? cancellationTokenSource;
|
||||||
|
|
||||||
private void updateMods()
|
private void loadPanels()
|
||||||
{
|
{
|
||||||
var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty<Mod>()).ToList();
|
|
||||||
|
|
||||||
if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod)))
|
|
||||||
return;
|
|
||||||
|
|
||||||
cancellationTokenSource?.Cancel();
|
cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
var panels = newMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)));
|
var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)));
|
||||||
|
|
||||||
Task? loadTask;
|
Task? loadTask;
|
||||||
|
|
||||||
@ -277,13 +298,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
foreach (var panel in panelFlow)
|
foreach (var panel in panelFlow)
|
||||||
{
|
{
|
||||||
panel.Active.BindValueChanged(_ =>
|
panel.Active.BindValueChanged(_ => panelStateChanged(panel));
|
||||||
{
|
|
||||||
updateToggleAllState();
|
|
||||||
SelectedMods.Value = panel.Active.Value
|
|
||||||
? SelectedMods.Value.Append(panel.Mod).ToArray()
|
|
||||||
: SelectedMods.Value.Except(new[] { panel.Mod }).ToArray();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
|
}, (cancellationTokenSource = new CancellationTokenSource()).Token);
|
||||||
loadTask.ContinueWith(_ =>
|
loadTask.ContinueWith(_ =>
|
||||||
@ -296,7 +311,62 @@ namespace osu.Game.Overlays.Mods
|
|||||||
private void updateActiveState()
|
private void updateActiveState()
|
||||||
{
|
{
|
||||||
foreach (var panel in panelFlow)
|
foreach (var panel in panelFlow)
|
||||||
panel.Active.Value = SelectedMods.Value.Contains(panel.Mod, EqualityComparer<Mod>.Default);
|
panel.Active.Value = SelectedMods.Contains(panel.Mod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This flag helps to determine the source of changes to <see cref="SelectedMods"/>.
|
||||||
|
/// If the value is false, then <see cref="SelectedMods"/> are changing due to a user selection on the UI.
|
||||||
|
/// If the value is true, then <see cref="SelectedMods"/> are changing due to an external <see cref="SetSelection"/> call.
|
||||||
|
/// </summary>
|
||||||
|
private bool externalSelectionUpdateInProgress;
|
||||||
|
|
||||||
|
private void panelStateChanged(ModPanel panel)
|
||||||
|
{
|
||||||
|
updateToggleAllState();
|
||||||
|
|
||||||
|
var newSelectedMods = panel.Active.Value
|
||||||
|
? SelectedMods.Append(panel.Mod)
|
||||||
|
: SelectedMods.Except(panel.Mod.Yield());
|
||||||
|
|
||||||
|
SelectedMods = newSelectedMods.ToArray();
|
||||||
|
if (!externalSelectionUpdateInProgress)
|
||||||
|
SelectionChangedByUser?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts the set of selected mods in this column to match the passed in <paramref name="mods"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state.
|
||||||
|
/// <see cref="ModSelectScreen"/> uses this to substitute any external mod references in <see cref="ModSelectScreen.SelectedMods"/>
|
||||||
|
/// to references that are owned by this column.
|
||||||
|
/// </remarks>
|
||||||
|
internal void SetSelection(IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
externalSelectionUpdateInProgress = true;
|
||||||
|
|
||||||
|
var newSelection = new List<Mod>();
|
||||||
|
|
||||||
|
foreach (var mod in localAvailableMods)
|
||||||
|
{
|
||||||
|
var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == mod.GetType());
|
||||||
|
|
||||||
|
if (matchingSelectedMod != null)
|
||||||
|
{
|
||||||
|
mod.CopyFrom(matchingSelectedMod);
|
||||||
|
newSelection.Add(mod);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mod.ResetSettingsToDefaults();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedMods = newSelection;
|
||||||
|
updateActiveState();
|
||||||
|
|
||||||
|
externalSelectionUpdateInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Bulk select / deselect
|
#region Bulk select / deselect
|
||||||
@ -364,6 +434,15 @@ namespace osu.Game.Overlays.Mods
|
|||||||
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
|
pendingSelectionOperations.Enqueue(() => button.Active.Value = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
|
||||||
|
/// </summary>
|
||||||
|
public void FlushPendingSelections()
|
||||||
|
{
|
||||||
|
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
|
||||||
|
dequeuedAction();
|
||||||
|
}
|
||||||
|
|
||||||
private class ToggleAllCheckbox : OsuCheckbox
|
private class ToggleAllCheckbox : OsuCheckbox
|
||||||
{
|
{
|
||||||
private Color4 accentColour;
|
private Color4 accentColour;
|
||||||
|
@ -250,9 +250,9 @@ namespace osu.Game.Overlays.Mods
|
|||||||
protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod);
|
protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Play out all remaining animations immediately to leave mods in a good (final) state.
|
/// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void FlushAnimation()
|
public void FlushPendingSelections()
|
||||||
{
|
{
|
||||||
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
|
while (pendingSelectionOperations.TryDequeue(out var dequeuedAction))
|
||||||
dequeuedAction();
|
dequeuedAction();
|
||||||
|
@ -369,7 +369,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
foreach (var section in ModSectionsContainer)
|
foreach (var section in ModSectionsContainer)
|
||||||
{
|
{
|
||||||
section.FlushAnimation();
|
section.FlushPendingSelections();
|
||||||
}
|
}
|
||||||
|
|
||||||
FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
|
FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine);
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -179,7 +180,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
foreach (var column in columnFlow.Columns)
|
foreach (var column in columnFlow.Columns)
|
||||||
{
|
{
|
||||||
column.SelectedMods.BindValueChanged(updateBindableFromSelection);
|
column.SelectionChangedByUser += updateBindableFromSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
|
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
|
||||||
@ -203,7 +204,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
private void updateAvailableMods()
|
private void updateAvailableMods()
|
||||||
{
|
{
|
||||||
foreach (var column in columnFlow.Columns)
|
foreach (var column in columnFlow.Columns)
|
||||||
column.Filter = isValidMod;
|
column.Filter = m => m.HasImplementation && isValidMod.Invoke(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCustomisation(ValueChangedEvent<IReadOnlyList<Mod>> valueChangedEvent)
|
private void updateCustomisation(ValueChangedEvent<IReadOnlyList<Mod>> valueChangedEvent)
|
||||||
@ -217,7 +218,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
foreach (var mod in SelectedMods.Value)
|
foreach (var mod in SelectedMods.Value)
|
||||||
{
|
{
|
||||||
anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
|
anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
|
||||||
anyModWithRequiredCustomisationAdded |= !valueChangedEvent.OldValue.Contains(mod) && mod.RequiresConfiguration;
|
anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (anyCustomisableMod)
|
if (anyCustomisableMod)
|
||||||
@ -250,33 +251,26 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
private void updateSelectionFromBindable()
|
private void updateSelectionFromBindable()
|
||||||
{
|
{
|
||||||
// note that selectionBindableSyncInProgress is purposefully not checked here.
|
// `SelectedMods` may contain mod references that come from external sources.
|
||||||
// this is because in the case of mod selection in solo gameplay, a user selection of a mod can actually lead to deselection of other incompatible mods.
|
// to ensure isolation, first pull in the potentially-external change into the mod columns...
|
||||||
// to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods,
|
|
||||||
// and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods.
|
|
||||||
// selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call.
|
|
||||||
foreach (var column in columnFlow.Columns)
|
foreach (var column in columnFlow.Columns)
|
||||||
column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray();
|
column.SetSelection(SelectedMods.Value);
|
||||||
|
|
||||||
|
// and then, when done, replace the potentially-external mod references in `SelectedMods` with ones we own.
|
||||||
|
updateBindableFromSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool selectionBindableSyncInProgress;
|
private void updateBindableFromSelection()
|
||||||
|
|
||||||
private void updateBindableFromSelection(ValueChangedEvent<IReadOnlyList<Mod>> modSelectionChange)
|
|
||||||
{
|
{
|
||||||
if (selectionBindableSyncInProgress)
|
var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray();
|
||||||
|
|
||||||
|
if (candidateSelection.SequenceEqual(SelectedMods.Value))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
selectionBindableSyncInProgress = true;
|
SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection);
|
||||||
|
|
||||||
SelectedMods.Value = ComputeNewModsFromSelection(
|
|
||||||
modSelectionChange.NewValue.Except(modSelectionChange.OldValue),
|
|
||||||
modSelectionChange.OldValue.Except(modSelectionChange.NewValue));
|
|
||||||
|
|
||||||
selectionBindableSyncInProgress = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IEnumerable<Mod> addedMods, IEnumerable<Mod> removedMods)
|
protected virtual IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) => newSelection;
|
||||||
=> columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray();
|
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
@ -313,13 +307,26 @@ namespace osu.Game.Overlays.Mods
|
|||||||
{
|
{
|
||||||
const float distance = 700;
|
const float distance = 700;
|
||||||
|
|
||||||
columnFlow[i].Column
|
var column = columnFlow[i].Column;
|
||||||
.TopLevelContent
|
|
||||||
.MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint)
|
column.FlushPendingSelections();
|
||||||
.FadeOut(fade_out_duration, Easing.OutQuint);
|
column.TopLevelContent
|
||||||
|
.MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint)
|
||||||
|
.FadeOut(fade_out_duration, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == GlobalAction.Back && customisationVisible.Value)
|
||||||
|
{
|
||||||
|
customisationVisible.Value = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnPressed(e);
|
||||||
|
}
|
||||||
|
|
||||||
internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
|
internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
|
||||||
{
|
{
|
||||||
public ColumnScrollContainer()
|
public ColumnScrollContainer()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
{
|
{
|
||||||
public class ModSettingsArea : CompositeDrawable
|
public class ModSettingsArea : CompositeDrawable
|
||||||
{
|
{
|
||||||
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>();
|
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||||
|
|
||||||
public const float HEIGHT = 250;
|
public const float HEIGHT = 250;
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
SelectedMods.BindValueChanged(_ => updateMods());
|
SelectedMods.BindValueChanged(_ => updateMods(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateMods()
|
private void updateMods()
|
||||||
|
@ -14,9 +14,12 @@ namespace osu.Game.Overlays.Mods
|
|||||||
{
|
{
|
||||||
protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys);
|
protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys);
|
||||||
|
|
||||||
protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IEnumerable<Mod> addedMods, IEnumerable<Mod> removedMods)
|
protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection)
|
||||||
{
|
{
|
||||||
IEnumerable<Mod> modsAfterRemoval = SelectedMods.Value.Except(removedMods).ToList();
|
var addedMods = newSelection.Except(oldSelection);
|
||||||
|
var removedMods = oldSelection.Except(newSelection);
|
||||||
|
|
||||||
|
IEnumerable<Mod> modsAfterRemoval = newSelection.Except(removedMods).ToList();
|
||||||
|
|
||||||
// the preference is that all new mods should override potential incompatible old mods.
|
// the preference is that all new mods should override potential incompatible old mods.
|
||||||
// in general that's a bit difficult to compute if more than one mod is added at a time,
|
// in general that's a bit difficult to compute if more than one mod is added at a time,
|
||||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Music
|
|||||||
Collection = collectionDropdown.Current.Value?.Collection
|
Collection = collectionDropdown.Current.Value?.Collection
|
||||||
};
|
};
|
||||||
|
|
||||||
public class FilterTextBox : SearchTextBox
|
public class FilterTextBox : BasicSearchTextBox
|
||||||
{
|
{
|
||||||
protected override bool AllowCommit => true;
|
protected override bool AllowCommit => true;
|
||||||
|
|
||||||
|
@ -372,12 +372,12 @@ namespace osu.Game.Overlays.Volume
|
|||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.SelectPrevious:
|
case GlobalAction.SelectPreviousGroup:
|
||||||
State = SelectionState.Selected;
|
State = SelectionState.Selected;
|
||||||
adjust(1, false);
|
adjust(1, false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.SelectNext:
|
case GlobalAction.SelectNextGroup:
|
||||||
State = SelectionState.Selected;
|
State = SelectionState.Selected;
|
||||||
adjust(-1, false);
|
adjust(-1, false);
|
||||||
return true;
|
return true;
|
||||||
|
@ -100,9 +100,13 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
double difficulty = 0;
|
double difficulty = 0;
|
||||||
double weight = 1;
|
double weight = 1;
|
||||||
|
|
||||||
|
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||||
|
// These sections will not contribute to the difficulty.
|
||||||
|
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
|
||||||
|
|
||||||
// Difficulty is the weighted sum of the highest strains from every section.
|
// Difficulty is the weighted sum of the highest strains from every section.
|
||||||
// We're sorting from highest to lowest strain.
|
// We're sorting from highest to lowest strain.
|
||||||
foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
|
foreach (double strain in peaks.OrderByDescending(d => d))
|
||||||
{
|
{
|
||||||
difficulty += strain * weight;
|
difficulty += strain * weight;
|
||||||
weight *= DecayWeight;
|
weight *= DecayWeight;
|
||||||
|
@ -3,13 +3,19 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.OSD;
|
||||||
using osu.Game.Overlays.Settings.Sections;
|
using osu.Game.Overlays.Settings.Sections;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Edit
|
namespace osu.Game.Rulesets.Edit
|
||||||
{
|
{
|
||||||
@ -17,8 +23,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// Represents a <see cref="HitObjectComposer{TObject}"/> for rulesets with the concept of distances between objects.
|
/// Represents a <see cref="HitObjectComposer{TObject}"/> for rulesets with the concept of distances between objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TObject">The base type of supported objects.</typeparam>
|
/// <typeparam name="TObject">The base type of supported objects.</typeparam>
|
||||||
[Cached(typeof(IDistanceSnapProvider))]
|
public abstract class DistancedHitObjectComposer<TObject> : HitObjectComposer<TObject>, IDistanceSnapProvider, IScrollBindingHandler<GlobalAction>
|
||||||
public abstract class DistancedHitObjectComposer<TObject> : HitObjectComposer<TObject>, IDistanceSnapProvider
|
|
||||||
where TObject : HitObject
|
where TObject : HitObject
|
||||||
{
|
{
|
||||||
protected Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
|
protected Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
|
||||||
@ -33,7 +38,9 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
|
protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
|
||||||
|
|
||||||
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider;
|
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider;
|
||||||
private bool distanceSpacingScrollActive;
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private OnScreenDisplay onScreenDisplay { get; set; }
|
||||||
|
|
||||||
protected DistancedHitObjectComposer(Ruleset ruleset)
|
protected DistancedHitObjectComposer(Ruleset ruleset)
|
||||||
: base(ruleset)
|
: base(ruleset)
|
||||||
@ -43,8 +50,9 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer
|
AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
|
||||||
{
|
{
|
||||||
|
Padding = new MarginPadding { Right = 10 },
|
||||||
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
@ -66,45 +74,58 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
if (!DistanceSpacingMultiplier.Disabled)
|
if (!DistanceSpacingMultiplier.Disabled)
|
||||||
{
|
{
|
||||||
DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
|
DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
|
||||||
DistanceSpacingMultiplier.BindValueChanged(v =>
|
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
|
||||||
{
|
{
|
||||||
distanceSpacingSlider.ContractedLabelText = $"D. S. ({v.NewValue:0.##x})";
|
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
|
||||||
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({v.NewValue:0.##x})";
|
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
|
||||||
EditorBeatmap.BeatmapInfo.DistanceSpacing = v.NewValue;
|
|
||||||
|
if (multiplier.NewValue != multiplier.OldValue)
|
||||||
|
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
|
||||||
|
|
||||||
|
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (!DistanceSpacingMultiplier.Disabled && e.ControlPressed && e.AltPressed && !e.Repeat)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
RightSideToolboxContainer.Expanded.Value = true;
|
case GlobalAction.EditorIncreaseDistanceSpacing:
|
||||||
distanceSpacingScrollActive = true;
|
case GlobalAction.EditorDecreaseDistanceSpacing:
|
||||||
return true;
|
return adjustDistanceSpacing(e.Action, 0.1f);
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.OnKeyDown(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnKeyUp(KeyUpEvent e)
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (!DistanceSpacingMultiplier.Disabled && distanceSpacingScrollActive && (!e.AltPressed || !e.ControlPressed))
|
|
||||||
{
|
|
||||||
RightSideToolboxContainer.Expanded.Value = false;
|
|
||||||
distanceSpacingScrollActive = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnScroll(ScrollEvent e)
|
public bool OnScroll(KeyBindingScrollEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (distanceSpacingScrollActive)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
DistanceSpacingMultiplier.Value += e.ScrollDelta.Y * (e.IsPrecise ? 0.01f : 0.1f);
|
case GlobalAction.EditorIncreaseDistanceSpacing:
|
||||||
return true;
|
case GlobalAction.EditorDecreaseDistanceSpacing:
|
||||||
|
return adjustDistanceSpacing(e.Action, e.ScrollAmount * (e.IsPrecise ? 0.01f : 0.1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.OnScroll(e);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool adjustDistanceSpacing(GlobalAction action, float amount)
|
||||||
|
{
|
||||||
|
if (DistanceSpacingMultiplier.Disabled)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (action == GlobalAction.EditorIncreaseDistanceSpacing)
|
||||||
|
DistanceSpacingMultiplier.Value += amount;
|
||||||
|
else if (action == GlobalAction.EditorDecreaseDistanceSpacing)
|
||||||
|
DistanceSpacingMultiplier.Value -= amount;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
|
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
|
||||||
@ -124,10 +145,10 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
|
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
|
public virtual double FindSnappedDuration(HitObject referenceObject, float distance)
|
||||||
=> BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
|
=> BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
|
||||||
|
|
||||||
public virtual float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
|
public virtual float FindSnappedDistance(HitObject referenceObject, float distance)
|
||||||
{
|
{
|
||||||
double startTime = referenceObject.StartTime;
|
double startTime = referenceObject.StartTime;
|
||||||
|
|
||||||
@ -145,18 +166,25 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
return DurationToDistance(referenceObject, snappedEndTime - startTime);
|
return DurationToDistance(referenceObject, snappedEndTime - startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class ExpandingToolboxContainer : ExpandingContainer
|
private class DistanceSpacingToast : Toast
|
||||||
{
|
{
|
||||||
protected override double HoverExpansionDelay => 250;
|
private readonly ValueChangedEvent<double> change;
|
||||||
|
|
||||||
public ExpandingToolboxContainer()
|
public DistanceSpacingToast(LocalisableString value, ValueChangedEvent<double> change)
|
||||||
: base(130, 250)
|
: base(getAction(change).GetLocalisableDescription(), value, string.Empty)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Y;
|
this.change = change;
|
||||||
Padding = new MarginPadding { Left = 10 };
|
|
||||||
|
|
||||||
FillFlow.Spacing = new Vector2(10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuConfigManager config)
|
||||||
|
{
|
||||||
|
ShortcutText.Text = config.LookupKeyBindings(getAction(change)).ToUpper();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GlobalAction getAction(ValueChangedEvent<double> change) => change.NewValue - change.OldValue > 0
|
||||||
|
? GlobalAction.EditorIncreaseDistanceSpacing
|
||||||
|
: GlobalAction.EditorDecreaseDistanceSpacing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
Normal file
34
osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// 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.Graphics;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Edit
|
||||||
|
{
|
||||||
|
public class ExpandingToolboxContainer : ExpandingContainer
|
||||||
|
{
|
||||||
|
protected override double HoverExpansionDelay => 250;
|
||||||
|
|
||||||
|
public ExpandingToolboxContainer(float contractedWidth, float expandedWidth)
|
||||||
|
: base(contractedWidth, expandedWidth)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y;
|
||||||
|
|
||||||
|
FillFlow.Spacing = new Vector2(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos);
|
||||||
|
|
||||||
|
private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.Children.Any(d => d.ScreenSpaceDrawQuad.Contains(screenSpacePos));
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e) => true;
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,6 @@ using osu.Framework.Input;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Rulesets.Configuration;
|
using osu.Game.Rulesets.Configuration;
|
||||||
using osu.Game.Rulesets.Edit.Tools;
|
using osu.Game.Rulesets.Edit.Tools;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// Responsible for providing snapping and generally gluing components together.
|
/// Responsible for providing snapping and generally gluing components together.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TObject">The base type of supported objects.</typeparam>
|
/// <typeparam name="TObject">The base type of supported objects.</typeparam>
|
||||||
[Cached(Type = typeof(IPlacementHandler))]
|
|
||||||
public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler
|
public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler
|
||||||
where TObject : HitObject
|
where TObject : HitObject
|
||||||
{
|
{
|
||||||
@ -115,8 +113,9 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
.WithChild(BlueprintContainer = CreateBlueprintContainer())
|
.WithChild(BlueprintContainer = CreateBlueprintContainer())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new LeftToolboxFlow
|
new ExpandingToolboxContainer(80, 200)
|
||||||
{
|
{
|
||||||
|
Padding = new MarginPadding { Left = 10 },
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new EditorToolboxGroup("toolbox (1-9)")
|
new EditorToolboxGroup("toolbox (1-9)")
|
||||||
@ -362,7 +361,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
|
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
|
||||||
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
|
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
|
||||||
|
|
||||||
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
|
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||||
{
|
{
|
||||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||||
double? targetTime = null;
|
double? targetTime = null;
|
||||||
@ -382,26 +381,13 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private class LeftToolboxFlow : ExpandingButtonContainer
|
|
||||||
{
|
|
||||||
public LeftToolboxFlow()
|
|
||||||
: base(80, 200)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Y;
|
|
||||||
Padding = new MarginPadding { Right = 10 };
|
|
||||||
|
|
||||||
FillFlow.Spacing = new Vector2(10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A non-generic definition of a HitObject composer class.
|
/// A non-generic definition of a HitObject composer class.
|
||||||
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
|
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Cached(typeof(HitObjectComposer))]
|
[Cached]
|
||||||
[Cached(typeof(IPositionSnapProvider))]
|
|
||||||
public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
|
public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
|
||||||
{
|
{
|
||||||
protected HitObjectComposer()
|
protected HitObjectComposer()
|
||||||
@ -428,9 +414,9 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
|
|
||||||
#region IPositionSnapProvider
|
#region IPositionSnapProvider
|
||||||
|
|
||||||
public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
|
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
|
||||||
|
|
||||||
public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||||
new SnapResult(screenSpacePosition, null);
|
new SnapResult(screenSpacePosition, null);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Edit
|
namespace osu.Game.Rulesets.Edit
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A snap provider which given a reference hit object and proposed distance from it, offers a more correct duration or distance value.
|
||||||
|
/// </summary>
|
||||||
|
[Cached]
|
||||||
public interface IDistanceSnapProvider : IPositionSnapProvider
|
public interface IDistanceSnapProvider : IPositionSnapProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The spacing multiplier applied to beat snap distances.
|
/// A multiplier which changes the ratio of distance travelled per time unit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="BeatmapInfo.DistanceSpacing"/>
|
/// <seealso cref="BeatmapInfo.DistanceSpacing"/>
|
||||||
IBindable<double> DistanceSpacingMultiplier { get; }
|
IBindable<double> DistanceSpacingMultiplier { get; }
|
||||||
@ -23,7 +28,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
float GetBeatSnapDistanceAt(HitObject referenceObject);
|
float GetBeatSnapDistanceAt(HitObject referenceObject);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a duration to a distance.
|
/// Converts a duration to a distance without applying any snapping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
||||||
/// <param name="duration">The duration to convert.</param>
|
/// <param name="duration">The duration to convert.</param>
|
||||||
@ -31,7 +36,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
float DurationToDistance(HitObject referenceObject, double duration);
|
float DurationToDistance(HitObject referenceObject, double duration);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a distance to a duration.
|
/// Converts a distance to a duration without applying any snapping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
||||||
/// <param name="distance">The distance to convert.</param>
|
/// <param name="distance">The distance to convert.</param>
|
||||||
@ -39,20 +44,22 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
double DistanceToDuration(HitObject referenceObject, float distance);
|
double DistanceToDuration(HitObject referenceObject, float distance);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a distance to a snapped duration.
|
/// Given a distance from the provided hit object, find the valid snapped duration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
||||||
/// <param name="distance">The distance to convert.</param>
|
/// <param name="distance">The distance to convert.</param>
|
||||||
/// <returns>A value that represents <paramref name="distance"/> as a duration snapped to the closest beat of the timing point.</returns>
|
/// <returns>A value that represents <paramref name="distance"/> as a duration snapped to the closest beat of the timing point.</returns>
|
||||||
double GetSnappedDurationFromDistance(HitObject referenceObject, float distance);
|
double FindSnappedDuration(HitObject referenceObject, float distance);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts an unsnapped distance to a snapped distance.
|
/// Given a distance from the provided hit object, find the valid snapped distance.
|
||||||
/// The returned distance will always be floored (as to never exceed the provided <paramref name="distance"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
||||||
/// <param name="distance">The distance to convert.</param>
|
/// <param name="distance">The distance to convert.</param>
|
||||||
/// <returns>A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.</returns>
|
/// <returns>
|
||||||
float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance);
|
/// A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.
|
||||||
|
/// The distance will always be less than or equal to the provided <paramref name="distance"/>.
|
||||||
|
/// </returns>
|
||||||
|
float FindSnappedDistance(HitObject referenceObject, float distance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,33 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Edit
|
namespace osu.Game.Rulesets.Edit
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap.
|
||||||
|
/// Provided values are inferred in an isolated context, without consideration of other nearby hit objects.
|
||||||
|
/// </summary>
|
||||||
|
[Cached]
|
||||||
public interface IPositionSnapProvider
|
public interface IPositionSnapProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a position, find a valid time and position snap.
|
/// Given a position, find a valid time and position snap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This call should be equivalent to running <see cref="SnapScreenSpacePositionToValidPosition"/> with any additional logic that can be performed without the time immutability restriction.
|
/// This call should be equivalent to running <see cref="FindSnappedPosition"/> with any additional logic that can be performed without the time immutability restriction.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
||||||
/// <returns>The time and position post-snapping.</returns>
|
/// <returns>The time and position post-snapping.</returns>
|
||||||
SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
|
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a position, find a value position snap, restricting time to its input value.
|
/// Given a position, find a valid position snap, without changing the time value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
||||||
/// <returns>The position post-snapping. Time will always be null.</returns>
|
/// <returns>The position post-snapping. Time will always be null.</returns>
|
||||||
SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition);
|
SnapResult FindSnappedPosition(Vector2 screenSpacePosition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +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.Containers;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Edit
|
|
||||||
{
|
|
||||||
public class ScrollingToolboxGroup : EditorToolboxGroup
|
|
||||||
{
|
|
||||||
protected readonly OsuScrollContainer Scroll;
|
|
||||||
|
|
||||||
protected readonly FillFlowContainer FillFlow;
|
|
||||||
|
|
||||||
protected override Container<Drawable> Content { get; }
|
|
||||||
|
|
||||||
public ScrollingToolboxGroup(string title, float scrollAreaHeight)
|
|
||||||
: base(title)
|
|
||||||
{
|
|
||||||
base.Content.Add(Scroll = new OsuScrollContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Height = scrollAreaHeight,
|
|
||||||
Child = Content = FillFlow = new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
|
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
|
||||||
where THitObject : HitObject, IHasPath
|
where THitObject : HitObject, IHasPath
|
||||||
{
|
{
|
||||||
hitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance;
|
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
|
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
|
||||||
var testPosition = originalPosition + distanceTravelled;
|
var testPosition = originalPosition + distanceTravelled;
|
||||||
|
|
||||||
var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition);
|
var positionalResult = snapProvider.FindSnappedPosition(testPosition);
|
||||||
|
|
||||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||||
|
|
||||||
@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
||||||
|
|
||||||
// Retrieve a snapped position.
|
// Retrieve a snapped position.
|
||||||
var result = snapProvider?.SnapScreenSpacePositionToValidTime(movePosition);
|
var result = snapProvider?.FindSnappedPositionAndTime(movePosition);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
|
@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
|
Vector2 normalisedDirection = direction * new Vector2(1f / distance);
|
||||||
Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius;
|
Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius;
|
||||||
|
|
||||||
return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length));
|
return (snappedPosition, StartTime + SnapProvider.FindSnappedDuration(ReferenceObject, (snappedPosition - StartPosition).Length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
private void updatePlacementPosition()
|
private void updatePlacementPosition()
|
||||||
{
|
{
|
||||||
var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
|
var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||||
|
|
||||||
// if no time was found from positional snapping, we should still quantize to the beat.
|
// if no time was found from positional snapping, we should still quantize to the beat.
|
||||||
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
|
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Layout;
|
using osu.Framework.Layout;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose.Components
|
namespace osu.Game.Screens.Edit.Compose.Components
|
||||||
@ -72,33 +75,47 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
int index = 0;
|
int index = 0;
|
||||||
float currentPosition = startPosition;
|
float currentPosition = startPosition;
|
||||||
|
|
||||||
while ((endPosition - currentPosition) * Math.Sign(step) > 0)
|
// Make lines the same width independent of display resolution.
|
||||||
|
float lineWidth = DrawWidth / ScreenSpaceDrawQuad.Width;
|
||||||
|
|
||||||
|
List<Box> generatedLines = new List<Box>();
|
||||||
|
|
||||||
|
while (Precision.AlmostBigger((endPosition - currentPosition) * Math.Sign(step), 0))
|
||||||
{
|
{
|
||||||
var gridLine = new Box
|
var gridLine = new Box
|
||||||
{
|
{
|
||||||
Colour = Colour4.White,
|
Colour = Colour4.White,
|
||||||
Alpha = index == 0 ? 0.3f : 0.1f,
|
Alpha = 0.1f,
|
||||||
EdgeSmoothness = new Vector2(0.2f)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (direction == Direction.Horizontal)
|
if (direction == Direction.Horizontal)
|
||||||
{
|
{
|
||||||
|
gridLine.Origin = Anchor.CentreLeft;
|
||||||
gridLine.RelativeSizeAxes = Axes.X;
|
gridLine.RelativeSizeAxes = Axes.X;
|
||||||
gridLine.Height = 1;
|
gridLine.Height = lineWidth;
|
||||||
gridLine.Y = currentPosition;
|
gridLine.Y = currentPosition;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
gridLine.Origin = Anchor.TopCentre;
|
||||||
gridLine.RelativeSizeAxes = Axes.Y;
|
gridLine.RelativeSizeAxes = Axes.Y;
|
||||||
gridLine.Width = 1;
|
gridLine.Width = lineWidth;
|
||||||
gridLine.X = currentPosition;
|
gridLine.X = currentPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
AddInternal(gridLine);
|
generatedLines.Add(gridLine);
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
currentPosition = startPosition + index * step;
|
currentPosition = startPosition + index * step;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (generatedLines.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
generatedLines.First().Alpha = 0.3f;
|
||||||
|
generatedLines.Last().Alpha = 0.3f;
|
||||||
|
|
||||||
|
AddRangeInternal(generatedLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 GetSnappedPosition(Vector2 original)
|
public Vector2 GetSnappedPosition(Vector2 original)
|
||||||
|
@ -19,7 +19,6 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||||
{
|
{
|
||||||
[Cached(typeof(IPositionSnapProvider))]
|
|
||||||
[Cached]
|
[Cached]
|
||||||
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
|
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
|
||||||
{
|
{
|
||||||
@ -307,10 +306,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double VisibleRange => track.Length / Zoom;
|
public double VisibleRange => track.Length / Zoom;
|
||||||
|
|
||||||
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
|
||||||
new SnapResult(screenSpacePosition, null);
|
new SnapResult(screenSpacePosition, null);
|
||||||
|
|
||||||
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) =>
|
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) =>
|
||||||
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
|
||||||
|
|
||||||
private double getTimeFromPosition(Vector2 localPosition) =>
|
private double getTimeFromPosition(Vector2 localPosition) =>
|
||||||
|
@ -382,7 +382,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
{
|
{
|
||||||
OnDragHandled?.Invoke(e);
|
OnDragHandled?.Invoke(e);
|
||||||
|
|
||||||
if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time)
|
if (timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time)
|
||||||
{
|
{
|
||||||
switch (hitObject)
|
switch (hitObject)
|
||||||
{
|
{
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose
|
namespace osu.Game.Screens.Edit.Compose
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public interface IPlacementHandler
|
public interface IPlacementHandler
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -6,7 +6,10 @@ using System.Linq;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -15,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A scrollable list which displays the <see cref="PlaylistItem"/>s in a <see cref="Room"/>.
|
/// A scrollable list which displays the <see cref="PlaylistItem"/>s in a <see cref="Room"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DrawableRoomPlaylist : OsuRearrangeableListContainer<PlaylistItem>
|
public class DrawableRoomPlaylist : OsuRearrangeableListContainer<PlaylistItem>, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The currently-selected item. Selection is visually represented with a border.
|
/// The currently-selected item. Selection is visually represented with a border.
|
||||||
@ -169,5 +172,78 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
});
|
});
|
||||||
|
|
||||||
protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item);
|
protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item);
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// schedules added as the properties may change value while the drawable items haven't been created yet.
|
||||||
|
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(scrollToSelection));
|
||||||
|
Items.BindCollectionChanged((_, __) => Scheduler.AddOnce(scrollToSelection), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scrollToSelection()
|
||||||
|
{
|
||||||
|
// SelectedItem and ItemMap/drawable items are managed separately,
|
||||||
|
// so if the item can't be unmapped to a drawable, don't try to scroll to it.
|
||||||
|
// best effort is made to not drop any updates, by subscribing to both sources.
|
||||||
|
if (SelectedItem.Value == null || !ItemMap.TryGetValue(SelectedItem.Value, out var drawableItem))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// ScrollIntoView does not handle non-loaded items appropriately, delay scroll until the item finishes loading.
|
||||||
|
// see: https://github.com/ppy/osu-framework/issues/5158
|
||||||
|
if (!drawableItem.IsLoaded)
|
||||||
|
drawableItem.OnLoadComplete += _ => ScrollContainer.ScrollIntoView(drawableItem);
|
||||||
|
else
|
||||||
|
ScrollContainer.ScrollIntoView(drawableItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Key selection logic (shared with BeatmapCarousel and RoomsContainer)
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case GlobalAction.SelectNext:
|
||||||
|
selectNext(1);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case GlobalAction.SelectPrevious:
|
||||||
|
selectNext(-1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectNext(int direction)
|
||||||
|
{
|
||||||
|
if (!AllowSelection)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var visibleItems = ListContainer.AsEnumerable().Where(r => r.IsPresent);
|
||||||
|
|
||||||
|
PlaylistItem item;
|
||||||
|
|
||||||
|
if (SelectedItem.Value == null)
|
||||||
|
item = visibleItems.FirstOrDefault()?.Model;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (direction < 0)
|
||||||
|
visibleItems = visibleItems.Reverse();
|
||||||
|
|
||||||
|
item = visibleItems.SkipWhile(r => r.Model != SelectedItem.Value).Skip(1).FirstOrDefault()?.Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we already have a valid selection only change selection if we still have a room to switch to.
|
||||||
|
if (item != null)
|
||||||
|
SelectedItem.Value = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
public new Func<Mod, bool> IsValidMod
|
public new Func<Mod, bool> IsValidMod
|
||||||
{
|
{
|
||||||
get => base.IsValidMod;
|
get => base.IsValidMod;
|
||||||
set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value.Invoke(m);
|
set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FreeModSelectScreen()
|
public FreeModSelectScreen()
|
||||||
|
@ -139,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
|||||||
return base.OnClick(e);
|
return base.OnClick(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Key selection logic (shared with BeatmapCarousel)
|
#region Key selection logic (shared with BeatmapCarousel and DrawableRoomPlaylist)
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
|
@ -129,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = Header.HEIGHT,
|
Height = Header.HEIGHT,
|
||||||
Child = searchTextBox = new SearchTextBox
|
Child = searchTextBox = new BasicSearchTextBox
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
|
@ -604,34 +604,20 @@ namespace osu.Game.Screens.Select
|
|||||||
public void ScrollToSelected(bool immediate = false) =>
|
public void ScrollToSelected(bool immediate = false) =>
|
||||||
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
|
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
|
||||||
|
|
||||||
#region Key / button selection logic
|
#region Button selection logic
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
|
||||||
{
|
|
||||||
switch (e.Key)
|
|
||||||
{
|
|
||||||
case Key.Left:
|
|
||||||
SelectNext(-1);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case Key.Right:
|
|
||||||
SelectNext();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case GlobalAction.SelectNext:
|
case GlobalAction.SelectNext:
|
||||||
SelectNext(1, false);
|
case GlobalAction.SelectNextGroup:
|
||||||
|
SelectNext(1, e.Action == GlobalAction.SelectNextGroup);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.SelectPrevious:
|
case GlobalAction.SelectPrevious:
|
||||||
SelectNext(-1, false);
|
case GlobalAction.SelectPreviousGroup:
|
||||||
|
SelectNext(-1, e.Action == GlobalAction.SelectPreviousGroup);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ using osu.Game.Screens.Edit.Compose;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.Visual
|
namespace osu.Game.Tests.Visual
|
||||||
{
|
{
|
||||||
[Cached(Type = typeof(IPlacementHandler))]
|
|
||||||
public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
|
public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
|
||||||
{
|
{
|
||||||
protected readonly Container HitObjectContainer;
|
protected readonly Container HitObjectContainer;
|
||||||
|
Loading…
Reference in New Issue
Block a user