Merge branch 'master' into consume-bindable-current-factory

This commit is contained in:
Salman Ahmed 2021-07-08 18:13:48 +03:00 committed by GitHub
commit f5166d8dd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1202 additions and 722 deletions

View File

@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away! A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
## Status ## Status
@ -23,7 +23,7 @@ We are accepting bug reports (please report with as much detail as possible and
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer). - Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
- You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management). - You can learn more about our approach to [project management](https://github.com/ppy/osu/wiki/Project-management).
- Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where lazer is currently and the roadmap going forward. - Read peppy's [latest blog post](https://blog.ppy.sh/a-definitive-lazer-faq/) exploring where the project is currently and the roadmap going forward.
## Running osu! ## Running osu!

View File

@ -6,6 +6,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
@ -23,5 +24,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
: base(new THitObject()) : base(new THitObject())
{ {
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
} }
} }

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject> public abstract class CatchSelectionBlueprint<THitObject> : HitObjectSelectionBlueprint<THitObject>
where THitObject : CatchHitObject where THitObject : CatchHitObject
{ {
protected override bool AlwaysShowWhenSelected => true;
public override Vector2 ScreenSpaceSelectionPoint public override Vector2 ScreenSpaceSelectionPoint
{ {
get get

View File

@ -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 JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -18,7 +19,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{ {
Anchor = Anchor.BottomLeft; Anchor = Anchor.BottomLeft;
Origin = Anchor.Centre; Origin = Anchor.Centre;
Size = new Vector2(2 * CatchHitObject.OBJECT_RADIUS);
InternalChild = new BorderPiece(); InternalChild = new BorderPiece();
} }
@ -28,10 +28,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
Colour = osuColour.Yellow; Colour = osuColour.Yellow;
} }
public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject) public void UpdateFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject, [CanBeNull] CatchHitObject parent = null)
{ {
X = hitObject.EffectiveX; X = hitObject.EffectiveX - (parent?.OriginalX ?? 0);
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime); Y = hitObjectContainer.PositionAtTime(hitObject.StartTime, parent?.StartTime ?? hitObjectContainer.Time.Current);
Scale = new Vector2(hitObject.Scale); Scale = new Vector2(hitObject.Scale);
} }
} }

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class NestedOutlineContainer : CompositeDrawable
{
private readonly List<CatchHitObject> nestedHitObjects = new List<CatchHitObject>();
public NestedOutlineContainer()
{
Anchor = Anchor.BottomLeft;
}
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
X = parentHitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(parentHitObject.StartTime);
}
public void UpdateNestedObjectsFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject parentHitObject)
{
nestedHitObjects.Clear();
nestedHitObjects.AddRange(parentHitObject.NestedHitObjects
.OfType<CatchHitObject>()
.Where(h => !(h is TinyDroplet)));
while (nestedHitObjects.Count < InternalChildren.Count)
RemoveInternal(InternalChildren[^1]);
while (InternalChildren.Count < nestedHitObjects.Count)
AddInternal(new FruitOutline());
for (int i = 0; i < nestedHitObjects.Count; i++)
{
var hitObject = nestedHitObjects[i];
var outline = (FruitOutline)InternalChildren[i];
outline.UpdateFrom(hitObjectContainer, hitObject, parentHitObject);
outline.Scale *= hitObject is Droplet ? 0.5f : 1;
}
}
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}

View 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.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
{
public class ScrollingPath : CompositeDrawable
{
private readonly Path drawablePath;
private readonly List<(double Distance, float X)> vertices = new List<(double, float)>();
public ScrollingPath()
{
Anchor = Anchor.BottomLeft;
InternalChildren = new Drawable[]
{
drawablePath = new SmoothPath
{
PathRadius = 2,
Alpha = 0.5f
},
};
}
public void UpdatePositionFrom(ScrollingHitObjectContainer hitObjectContainer, CatchHitObject hitObject)
{
X = hitObject.OriginalX;
Y = hitObjectContainer.PositionAtTime(hitObject.StartTime);
}
public void UpdatePathFrom(ScrollingHitObjectContainer hitObjectContainer, JuiceStream hitObject)
{
double distanceToYFactor = -hitObjectContainer.LengthAtTime(hitObject.StartTime, hitObject.StartTime + 1 / hitObject.Velocity);
computeDistanceXs(hitObject);
drawablePath.Vertices = vertices
.Select(v => new Vector2(v.X, (float)(v.Distance * distanceToYFactor)))
.ToArray();
drawablePath.OriginPosition = drawablePath.PositionInBoundingBox(Vector2.Zero);
}
private void computeDistanceXs(JuiceStream hitObject)
{
vertices.Clear();
var sliderVertices = new List<Vector2>();
hitObject.Path.GetPathToProgress(sliderVertices, 0, 1);
if (sliderVertices.Count == 0)
return;
double distance = 0;
Vector2 lastPosition = Vector2.Zero;
for (int repeat = 0; repeat < hitObject.RepeatCount + 1; repeat++)
{
foreach (var position in sliderVertices)
{
distance += Vector2.Distance(lastPosition, position);
lastPosition = position;
vertices.Add((distance, position.X));
}
sliderVertices.Reverse();
}
}
// Because this has 0x0 size, the contents are otherwise masked away if the start position is outside the screen.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
}

View File

@ -3,7 +3,10 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK; using osuTK;
@ -17,9 +20,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private float minNestedX; private float minNestedX;
private float maxNestedX; private float maxNestedX;
private readonly ScrollingPath scrollingPath;
private readonly NestedOutlineContainer nestedOutlineContainer;
private readonly Cached pathCache = new Cached();
public JuiceStreamSelectionBlueprint(JuiceStream hitObject) public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject) : base(hitObject)
{ {
InternalChildren = new Drawable[]
{
scrollingPath = new ScrollingPath(),
nestedOutlineContainer = new NestedOutlineContainer()
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -29,7 +43,28 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
computeObjectBounds(); computeObjectBounds();
} }
private void onDefaultsApplied(HitObject _) => computeObjectBounds(); protected override void Update()
{
base.Update();
if (!IsSelected) return;
scrollingPath.UpdatePositionFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdatePositionFrom(HitObjectContainer, HitObject);
if (pathCache.IsValid) return;
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
pathCache.Validate();
}
private void onDefaultsApplied(HitObject _)
{
computeObjectBounds();
pathCache.Invalidate();
}
private void computeObjectBounds() private void computeObjectBounds()
{ {

View File

@ -2,6 +2,8 @@
// 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.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -20,6 +22,16 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods); new DrawableCatchEditorRuleset(ruleset, beatmap, mods);

View File

@ -0,0 +1,11 @@
#include "sh_Utils.h"
varying mediump vec2 v_TexCoord;
varying mediump vec4 v_TexRect;
void main(void)
{
float hueValue = v_TexCoord.x / (v_TexRect[2] - v_TexRect[0]);
gl_FragColor = hsv2rgb(vec4(hueValue, 1, 1, 1));
}

View File

@ -0,0 +1,31 @@
#include "sh_Utils.h"
attribute highp vec2 m_Position;
attribute lowp vec4 m_Colour;
attribute mediump vec2 m_TexCoord;
attribute mediump vec4 m_TexRect;
attribute mediump vec2 m_BlendRange;
varying highp vec2 v_MaskingPosition;
varying lowp vec4 v_Colour;
varying mediump vec2 v_TexCoord;
varying mediump vec4 v_TexRect;
varying mediump vec2 v_BlendRange;
uniform highp mat4 g_ProjMatrix;
uniform highp mat3 g_ToMaskingSpace;
void main(void)
{
// Transform from screen space to masking space.
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour;
v_TexCoord = m_TexCoord;
v_TexRect = m_TexRect;
v_BlendRange = m_BlendRange;
gl_Position = gProjMatrix * vec4(m_Position, 1.0, 1.0);
}

View File

@ -13,8 +13,10 @@ 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.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -31,12 +33,14 @@ namespace osu.Game.Tests.Rulesets
DrawableWithDependencies drawable = null; DrawableWithDependencies drawable = null;
TestTextureStore textureStore = null; TestTextureStore textureStore = null;
TestSampleStore sampleStore = null; TestSampleStore sampleStore = null;
TestShaderManager shaderManager = null;
AddStep("add dependencies", () => AddStep("add dependencies", () =>
{ {
Child = drawable = new DrawableWithDependencies(); Child = drawable = new DrawableWithDependencies();
textureStore = drawable.ParentTextureStore; textureStore = drawable.ParentTextureStore;
sampleStore = drawable.ParentSampleStore; sampleStore = drawable.ParentSampleStore;
shaderManager = drawable.ParentShaderManager;
}); });
AddStep("clear children", Clear); AddStep("clear children", Clear);
@ -52,12 +56,14 @@ namespace osu.Game.Tests.Rulesets
AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed); AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed);
AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed); AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed);
AddAssert("parent shader manager not disposed", () => !shaderManager.IsDisposed);
} }
private class DrawableWithDependencies : CompositeDrawable private class DrawableWithDependencies : CompositeDrawable
{ {
public TestTextureStore ParentTextureStore { get; private set; } public TestTextureStore ParentTextureStore { get; private set; }
public TestSampleStore ParentSampleStore { get; private set; } public TestSampleStore ParentSampleStore { get; private set; }
public TestShaderManager ParentShaderManager { get; private set; }
public DrawableWithDependencies() public DrawableWithDependencies()
{ {
@ -70,6 +76,7 @@ namespace osu.Game.Tests.Rulesets
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore()); dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore());
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore()); dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager());
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies); return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
} }
@ -135,5 +142,23 @@ namespace osu.Game.Tests.Rulesets
public int PlaybackConcurrency { get; set; } public int PlaybackConcurrency { get; set; }
} }
private class TestShaderManager : ShaderManager
{
public TestShaderManager()
: base(new ResourceStore<byte[]>())
{
}
public override byte[] LoadRaw(string name) => null;
public bool IsDisposed { get; private set; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
IsDisposed = true;
}
}
} }
} }

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Skins
{
[HeadlessTest]
public class TestSceneSkinProvidingContainer : OsuTestScene
{
/// <summary>
/// Ensures that the first inserted skin after resetting (via source change)
/// is always prioritised over others when providing the same resource.
/// </summary>
[Test]
public void TestPriorityPreservation()
{
TestSkinProvidingContainer provider = null;
TestSkin mostPrioritisedSource = null;
AddStep("setup sources", () =>
{
var sources = new List<TestSkin>();
for (int i = 0; i < 10; i++)
sources.Add(new TestSkin());
mostPrioritisedSource = sources.First();
Child = provider = new TestSkinProvidingContainer(sources);
});
AddAssert("texture provided by expected skin", () =>
{
return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
});
AddStep("trigger source change", () => provider.TriggerSourceChanged());
AddAssert("texture still provided by expected skin", () =>
{
return provider.FindProvider(s => s.GetTexture(TestSkin.TEXTURE_NAME) != null) == mostPrioritisedSource;
});
}
private class TestSkinProvidingContainer : SkinProvidingContainer
{
private readonly IEnumerable<ISkin> sources;
public TestSkinProvidingContainer(IEnumerable<ISkin> sources)
{
this.sources = sources;
}
public new void TriggerSourceChanged() => base.TriggerSourceChanged();
protected override void OnSourceChanged()
{
ResetSources();
sources.ForEach(AddSource);
}
}
private class TestSkin : ISkin
{
public const string TEXTURE_NAME = "virtual-texture";
public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
if (componentName == TEXTURE_NAME)
return Texture.WhitePixel;
return null;
}
public ISample GetSample(ISampleInfo sampleInfo) => throw new System.NotImplementedException();
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new System.NotImplementedException();
}
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Configuration.Tracking; using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -45,6 +46,14 @@ namespace osu.Game.Tests.Testing
Dependencies.Get<ISampleStore>().Get(@"test-sample") != null); Dependencies.Get<ISampleStore>().Get(@"test-sample") != null);
} }
[Test]
public void TestRetrieveShader()
{
AddAssert("ruleset shaders retrieved", () =>
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestVertex.vs") != null &&
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestFragment.fs") != null);
}
[Test] [Test]
public void TestResolveConfigManager() public void TestResolveConfigManager()
{ {

View File

@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay(); showOverlay();
AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value); AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay(); showOverlay();
AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => getButton(0).Selected.Value); AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -111,11 +111,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => failOverlay.Show()); AddStep("Show overlay", () => failOverlay.Show());
AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -127,11 +127,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => failOverlay.Show()); AddStep("Show overlay", () => failOverlay.Show());
AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); AddAssert("Last button selected", () => failOverlay.Buttons.Last().State == SelectionState.Selected);
AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); AddAssert("First button selected", () => failOverlay.Buttons.First().State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First())); AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First()));
AddStep("Hide overlay", () => failOverlay.Hide()); AddStep("Hide overlay", () => failOverlay.Hide());
AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected.Value)); AddAssert("Overlay state is reset", () => failOverlay.Buttons.All(b => b.State == SelectionState.NotSelected));
} }
/// <summary> /// <summary>
@ -162,11 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hide overlay", () => pauseOverlay.Hide()); AddStep("Hide overlay", () => pauseOverlay.Hide());
showOverlay(); showOverlay();
AddAssert("First button not selected", () => !getButton(0).Selected.Value); AddAssert("First button not selected", () => getButton(0).State == SelectionState.NotSelected);
AddStep("Move slightly", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(1))); AddStep("Move slightly", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(1)));
AddAssert("First button selected", () => getButton(0).Selected.Value); AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -179,8 +179,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddAssert("First button not selected", () => !getButton(0).Selected.Value); AddAssert("First button not selected", () => getButton(0).State == SelectionState.NotSelected);
AddAssert("Second button selected", () => getButton(1).Selected.Value); AddAssert("Second button selected", () => getButton(1).State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -196,8 +196,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddStep("Up arrow", () => InputManager.Key(Key.Up));
AddAssert("Second button not selected", () => !getButton(1).Selected.Value); AddAssert("Second button not selected", () => getButton(1).State == SelectionState.NotSelected);
AddAssert("First button selected", () => getButton(0).Selected.Value); AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected);
} }
/// <summary> /// <summary>
@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1)));
AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero)); AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero));
AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Down arrow", () => InputManager.Key(Key.Down));
AddAssert("First button selected", () => getButton(0).Selected.Value); // Initial state condition AddAssert("First button selected", () => getButton(0).State == SelectionState.Selected); // Initial state condition
} }
/// <summary> /// <summary>
@ -282,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
showOverlay(); showOverlay();
AddAssert("No button selected", AddAssert("No button selected",
() => pauseOverlay.Buttons.All(button => !button.Selected.Value)); () => pauseOverlay.Buttons.All(button => button.State == SelectionState.NotSelected));
} }
private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show()); private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show());

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Solo; using osu.Game.Online.Solo;
@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -25,6 +27,15 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = (TestBeatmap)base.CreateBeatmap(ruleset);
beatmap.HitObjects = beatmap.HitObjects.Take(10).ToList();
return beatmap;
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
[Test] [Test]

View File

@ -168,7 +168,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void Disable() public void Disable()
{ {
allow = false; allow = false;
OnSourceChanged(); TriggerSourceChanged();
} }
public SwitchableSkinProvidingContainer(ISkin skin) public SwitchableSkinProvidingContainer(ISkin skin)

View File

@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual.Settings
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Add(new DirectorySelector { RelativeSizeAxes = Axes.Both }); Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both });
} }
} }
} }

View File

@ -12,13 +12,13 @@ namespace osu.Game.Tests.Visual.Settings
[Test] [Test]
public void TestAllFiles() public void TestAllFiles()
{ {
AddStep("create", () => Child = new FileSelector { RelativeSizeAxes = Axes.Both }); AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
} }
[Test] [Test]
public void TestJpgFilesOnly() public void TestJpgFilesOnly()
{ {
AddStep("create", () => Child = new FileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both }); AddStep("create", () => Child = new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both });
} }
} }
} }

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Volume;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneVolumeOverlay : OsuTestScene
{
private VolumeOverlay volume;
protected override void LoadComplete()
{
base.LoadComplete();
AddRange(new Drawable[]
{
volume = new VolumeOverlay(),
new VolumeControlReceptor
{
RelativeSizeAxes = Axes.Both,
ActionRequested = action => volume.Adjust(action),
ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
},
});
AddStep("show controls", () => volume.Show());
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Setup
[Resolved] [Resolved]
private MatchIPCInfo ipc { get; set; } private MatchIPCInfo ipc { get; set; }
private DirectorySelector directorySelector; private OsuDirectorySelector directorySelector;
private DialogOverlay overlay; private DialogOverlay overlay;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
@ -79,7 +79,7 @@ namespace osu.Game.Tournament.Screens.Setup
}, },
new Drawable[] new Drawable[]
{ {
directorySelector = new DirectorySelector(initialPath) directorySelector = new OsuDirectorySelector(initialPath)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
} }

View File

@ -0,0 +1,87 @@
// 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.Diagnostics;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// A FillFlowContainer that provides functionality to cycle selection between children
/// The selection wraps around when overflowing past the first or last child.
/// </summary>
public class SelectionCycleFillFlowContainer<T> : FillFlowContainer<T> where T : Drawable, IStateful<SelectionState>
{
public T Selected => (selectedIndex >= 0 && selectedIndex < Count) ? this[selectedIndex.Value] : null;
private int? selectedIndex;
public void SelectNext()
{
if (!selectedIndex.HasValue || selectedIndex == Count - 1)
setSelected(0);
else
setSelected(selectedIndex.Value + 1);
}
public void SelectPrevious()
{
if (!selectedIndex.HasValue || selectedIndex == 0)
setSelected(Count - 1);
else
setSelected(selectedIndex.Value - 1);
}
public void Deselect() => setSelected(null);
public void Select(T item)
{
var newIndex = IndexOf(item);
if (newIndex < 0)
setSelected(null);
else
setSelected(IndexOf(item));
}
public override void Add(T drawable)
{
base.Add(drawable);
Debug.Assert(drawable != null);
drawable.StateChanged += state => selectionChanged(drawable, state);
}
public override bool Remove(T drawable)
=> throw new NotSupportedException($"Cannot remove drawables from {nameof(SelectionCycleFillFlowContainer<T>)}");
private void setSelected(int? value)
{
if (selectedIndex == value)
return;
// Deselect the previously-selected button
if (selectedIndex.HasValue)
this[selectedIndex.Value].State = SelectionState.NotSelected;
selectedIndex = value;
// Select the newly-selected button
if (selectedIndex.HasValue)
this[selectedIndex.Value].State = SelectionState.Selected;
}
private void selectionChanged(T drawable, SelectionState state)
{
if (state == SelectionState.NotSelected)
Deselect();
else
Select(drawable);
}
}
}

View File

@ -1,25 +1,26 @@
// 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.Bindables; using System;
using osuTK; using osu.Framework;
using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions;
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.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class DialogButton : OsuClickableContainer public class DialogButton : OsuClickableContainer, IStateful<SelectionState>
{ {
private const float idle_width = 0.8f; private const float idle_width = 0.8f;
private const float hover_width = 0.9f; private const float hover_width = 0.9f;
@ -27,7 +28,22 @@ namespace osu.Game.Graphics.UserInterface
private const float hover_duration = 500; private const float hover_duration = 500;
private const float click_duration = 200; private const float click_duration = 200;
public readonly BindableBool Selected = new BindableBool(); public event Action<SelectionState> StateChanged;
private SelectionState state;
public SelectionState State
{
get => state;
set
{
if (state == value)
return;
state = value;
StateChanged?.Invoke(value);
}
}
private readonly Container backgroundContainer; private readonly Container backgroundContainer;
private readonly Container colourContainer; private readonly Container colourContainer;
@ -153,7 +169,7 @@ namespace osu.Game.Graphics.UserInterface
updateGlow(); updateGlow();
Selected.ValueChanged += selectionChanged; StateChanged += selectionChanged;
} }
private Color4 buttonColour; private Color4 buttonColour;
@ -221,7 +237,7 @@ namespace osu.Game.Graphics.UserInterface
.OnComplete(_ => .OnComplete(_ =>
{ {
clickAnimating = false; clickAnimating = false;
Selected.TriggerChange(); StateChanged?.Invoke(State);
}); });
return base.OnClick(e); return base.OnClick(e);
@ -235,7 +251,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnMouseUp(MouseUpEvent e) protected override void OnMouseUp(MouseUpEvent e)
{ {
if (Selected.Value) if (State == SelectionState.Selected)
colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In); colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
base.OnMouseUp(e); base.OnMouseUp(e);
} }
@ -243,7 +259,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
base.OnHover(e); base.OnHover(e);
Selected.Value = true; State = SelectionState.Selected;
return true; return true;
} }
@ -251,15 +267,15 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
base.OnHoverLost(e); base.OnHoverLost(e);
Selected.Value = false; State = SelectionState.NotSelected;
} }
private void selectionChanged(ValueChangedEvent<bool> args) private void selectionChanged(SelectionState newState)
{ {
if (clickAnimating) if (clickAnimating)
return; return;
if (args.NewValue) if (newState == SelectionState.Selected)
{ {
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic); colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);

View File

@ -1,297 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
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.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class DirectorySelector : CompositeDrawable
{
private FillFlowContainer directoryFlow;
[Resolved]
private GameHost host { get; set; }
[Cached]
public readonly Bindable<DirectoryInfo> CurrentPath = new Bindable<DirectoryInfo>();
public DirectorySelector(string initialPath = null)
{
CurrentPath.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
}
[BackgroundDependencyLoader]
private void load()
{
Padding = new MarginPadding(10);
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 50),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
new CurrentDirectoryDisplay
{
RelativeSizeAxes = Axes.Both,
},
},
new Drawable[]
{
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = directoryFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
}
}
}
}
};
CurrentPath.BindValueChanged(updateDisplay, true);
}
private void updateDisplay(ValueChangedEvent<DirectoryInfo> directory)
{
directoryFlow.Clear();
try
{
if (directory.NewValue == null)
{
var drives = DriveInfo.GetDrives();
foreach (var drive in drives)
directoryFlow.Add(new DirectoryPiece(drive.RootDirectory));
}
else
{
directoryFlow.Add(new ParentDirectoryPiece(CurrentPath.Value.Parent));
directoryFlow.AddRange(GetEntriesForPath(CurrentPath.Value));
}
}
catch (Exception)
{
CurrentPath.Value = directory.OldValue;
this.FlashColour(Color4.Red, 300);
}
}
protected virtual IEnumerable<DisplayPiece> GetEntriesForPath(DirectoryInfo path)
{
foreach (var dir in path.GetDirectories().OrderBy(d => d.Name))
{
if ((dir.Attributes & FileAttributes.Hidden) == 0)
yield return new DirectoryPiece(dir);
}
}
private class CurrentDirectoryDisplay : CompositeDrawable
{
[Resolved]
private Bindable<DirectoryInfo> currentDirectory { get; set; }
private FillFlowContainer flow;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(5),
Height = DisplayPiece.HEIGHT,
Direction = FillDirection.Horizontal,
},
};
currentDirectory.BindValueChanged(updateDisplay, true);
}
private void updateDisplay(ValueChangedEvent<DirectoryInfo> dir)
{
flow.Clear();
List<DirectoryPiece> pathPieces = new List<DirectoryPiece>();
DirectoryInfo ptr = dir.NewValue;
while (ptr != null)
{
pathPieces.Insert(0, new CurrentDisplayPiece(ptr));
ptr = ptr.Parent;
}
flow.ChildrenEnumerable = new Drawable[]
{
new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DisplayPiece.HEIGHT), },
new ComputerPiece(),
}.Concat(pathPieces);
}
private class ComputerPiece : CurrentDisplayPiece
{
protected override IconUsage? Icon => null;
public ComputerPiece()
: base(null, "Computer")
{
}
}
private class CurrentDisplayPiece : DirectoryPiece
{
public CurrentDisplayPiece(DirectoryInfo directory, string displayName = null)
: base(directory, displayName)
{
}
[BackgroundDependencyLoader]
private void load()
{
Flow.Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(FONT_SIZE / 2)
});
}
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null;
}
}
private class ParentDirectoryPiece : DirectoryPiece
{
protected override IconUsage? Icon => FontAwesome.Solid.Folder;
public ParentDirectoryPiece(DirectoryInfo directory)
: base(directory, "..")
{
}
}
protected class DirectoryPiece : DisplayPiece
{
protected readonly DirectoryInfo Directory;
[Resolved]
private Bindable<DirectoryInfo> currentDirectory { get; set; }
public DirectoryPiece(DirectoryInfo directory, string displayName = null)
: base(displayName)
{
Directory = directory;
}
protected override bool OnClick(ClickEvent e)
{
currentDirectory.Value = Directory;
return true;
}
protected override string FallbackName => Directory.Name;
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
? FontAwesome.Solid.Database
: FontAwesome.Regular.Folder;
}
protected abstract class DisplayPiece : CompositeDrawable
{
public const float HEIGHT = 20;
protected const float FONT_SIZE = 16;
private readonly string displayName;
protected FillFlowContainer Flow;
protected DisplayPiece(string displayName = null)
{
this.displayName = displayName;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.GreySeafoamDarker,
RelativeSizeAxes = Axes.Both,
},
Flow = new FillFlowContainer
{
AutoSizeAxes = Axes.X,
Height = 20,
Margin = new MarginPadding { Vertical = 2, Horizontal = 5 },
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
}
};
if (Icon.HasValue)
{
Flow.Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = Icon.Value,
Size = new Vector2(FONT_SIZE)
});
}
Flow.Add(new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = displayName ?? FallbackName,
Font = OsuFont.Default.With(size: FONT_SIZE)
});
}
protected abstract string FallbackName { get; }
protected abstract IconUsage? Icon { get; }
}
}
}

View File

@ -1,94 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class FileSelector : DirectorySelector
{
private readonly string[] validFileExtensions;
[Cached]
public readonly Bindable<FileInfo> CurrentFile = new Bindable<FileInfo>();
public FileSelector(string initialPath = null, string[] validFileExtensions = null)
: base(initialPath)
{
this.validFileExtensions = validFileExtensions ?? Array.Empty<string>();
}
protected override IEnumerable<DisplayPiece> GetEntriesForPath(DirectoryInfo path)
{
foreach (var dir in base.GetEntriesForPath(path))
yield return dir;
IEnumerable<FileInfo> files = path.GetFiles();
if (validFileExtensions.Length > 0)
files = files.Where(f => validFileExtensions.Contains(f.Extension));
foreach (var file in files.OrderBy(d => d.Name))
{
if ((file.Attributes & FileAttributes.Hidden) == 0)
yield return new FilePiece(file);
}
}
protected class FilePiece : DisplayPiece
{
private readonly FileInfo file;
[Resolved]
private Bindable<FileInfo> currentFile { get; set; }
public FilePiece(FileInfo file)
{
this.file = file;
}
protected override bool OnClick(ClickEvent e)
{
currentFile.Value = file;
return true;
}
protected override string FallbackName => file.Name;
protected override IconUsage? Icon
{
get
{
switch (file.Extension)
{
case ".ogg":
case ".mp3":
case ".wav":
return FontAwesome.Regular.FileAudio;
case ".jpg":
case ".jpeg":
case ".png":
return FontAwesome.Regular.FileImage;
case ".mp4":
case ".avi":
case ".mov":
case ".flv":
return FontAwesome.Regular.FileVideo;
default:
return FontAwesome.Regular.File;
}
}
}
}
}
}

View File

@ -0,0 +1,38 @@
// 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.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class OsuDirectorySelector : DirectorySelector
{
public const float ITEM_HEIGHT = 20;
public OsuDirectorySelector(string initialPath = null)
: base(initialPath)
{
}
[BackgroundDependencyLoader]
private void load()
{
Padding = new MarginPadding(10);
}
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300);
}
}

View File

@ -0,0 +1,64 @@
// 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.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
internal class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay
{
protected override Drawable CreateCaption() => new OsuSpriteText
{
Text = "Current Directory: ",
Font = OsuFont.Default.With(size: OsuDirectorySelector.ITEM_HEIGHT),
};
protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer();
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
[BackgroundDependencyLoader]
private void load()
{
Height = 50;
}
private class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
{
protected override IconUsage? Icon => null;
public OsuBreadcrumbDisplayComputer()
: base(null, "Computer")
{
}
}
private class OsuBreadcrumbDisplayDirectory : OsuDirectorySelectorDirectory
{
public OsuBreadcrumbDisplayDirectory(DirectoryInfo directory, string displayName = null)
: base(directory, displayName)
{
}
[BackgroundDependencyLoader]
private void load()
{
Flow.Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.ChevronRight,
Size = new Vector2(FONT_SIZE / 2)
});
}
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? base.Icon : null;
}
}
}

View File

@ -0,0 +1,58 @@
// 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.IO;
using osu.Framework.Allocation;
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;
namespace osu.Game.Graphics.UserInterfaceV2
{
internal class OsuDirectorySelectorDirectory : DirectorySelectorDirectory
{
public OsuDirectorySelectorDirectory(DirectoryInfo directory, string displayName = null)
: base(directory, displayName)
{
}
[BackgroundDependencyLoader]
private void load()
{
Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddInternal(new Background
{
Depth = 1
});
}
protected override SpriteText CreateSpriteText() => new OsuSpriteText();
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
? FontAwesome.Solid.Database
: FontAwesome.Regular.Folder;
internal class Background : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
InternalChild = new Box
{
Colour = colours.GreySeafoamDarker,
RelativeSizeAxes = Axes.Both,
};
}
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterfaceV2
{
internal class OsuDirectorySelectorParentDirectory : OsuDirectorySelectorDirectory
{
protected override IconUsage? Icon => FontAwesome.Solid.Folder;
public OsuDirectorySelectorParentDirectory(DirectoryInfo directory)
: base(directory, "..")
{
}
}
}

View File

@ -0,0 +1,90 @@
// 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.IO;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class OsuFileSelector : FileSelector
{
public OsuFileSelector(string initialPath = null, string[] validFileExtensions = null)
: base(initialPath, validFileExtensions)
{
}
[BackgroundDependencyLoader]
private void load()
{
Padding = new MarginPadding(10);
}
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override DirectoryListingFile CreateFileItem(FileInfo file) => new OsuDirectoryListingFile(file);
protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300);
protected class OsuDirectoryListingFile : DirectoryListingFile
{
public OsuDirectoryListingFile(FileInfo file)
: base(file)
{
}
[BackgroundDependencyLoader]
private void load()
{
Flow.AutoSizeAxes = Axes.X;
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
AddInternal(new OsuDirectorySelectorDirectory.Background
{
Depth = 1
});
}
protected override IconUsage? Icon
{
get
{
switch (File.Extension)
{
case @".ogg":
case @".mp3":
case @".wav":
return FontAwesome.Regular.FileAudio;
case @".jpg":
case @".jpeg":
case @".png":
return FontAwesome.Regular.FileImage;
case @".mp4":
case @".avi":
case @".mov":
case @".flv":
return FontAwesome.Regular.FileVideo;
default:
return FontAwesome.Regular.File;
}
}
}
protected override SpriteText CreateSpriteText() => new OsuSpriteText();
}
}
}

View File

@ -103,6 +103,9 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume), new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume), new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.PreviousVolumeMeter),
new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.NextVolumeMeter),
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute), new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev), new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev),
@ -263,5 +266,11 @@ namespace osu.Game.Input.Bindings
[Description("Toggle skin editor")] [Description("Toggle skin editor")]
ToggleSkinEditor, ToggleSkinEditor,
[Description("Previous volume meter")]
PreviousVolumeMeter,
[Description("Next volume meter")]
NextVolumeMeter,
} }
} }

View File

@ -1,4 +1,4 @@
// 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.Allocation;
@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Rankings
public class CountryFilter : CompositeDrawable, IHasCurrentValue<Country> public class CountryFilter : CompositeDrawable, IHasCurrentValue<Country>
{ {
private const int duration = 200; private const int duration = 200;
private const int height = 50; private const int height = 70;
private readonly BindableWithCurrent<Country> current = new BindableWithCurrent<Country>(); private readonly BindableWithCurrent<Country> current = new BindableWithCurrent<Country>();

View File

@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Rankings
InternalChild = content = new CircularContainer InternalChild = content = new CircularContainer
{ {
Height = 25, Height = 30,
AutoSizeDuration = duration, AutoSizeDuration = duration,
AutoSizeEasing = Easing.OutQuint, AutoSizeEasing = Easing.OutQuint,
Masking = true, Masking = true,
@ -58,9 +58,9 @@ namespace osu.Game.Overlays.Rankings
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 10 }, Margin = new MarginPadding { Horizontal = 15 },
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(8, 0), Spacing = new Vector2(15, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new FillFlowContainer
@ -70,14 +70,14 @@ namespace osu.Game.Overlays.Rankings
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(3, 0), Spacing = new Vector2(5, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
flag = new UpdateableFlag flag = new UpdateableFlag
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(22, 15) Size = new Vector2(30, 20)
}, },
countryName = new OsuSpriteText countryName = new OsuSpriteText
{ {
@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Rankings
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Add(icon = new SpriteIcon Add(icon = new SpriteIcon
{ {
Size = new Vector2(8), Size = new Vector2(10),
Icon = FontAwesome.Solid.Times Icon = FontAwesome.Solid.Times
}); });
} }

View File

@ -15,6 +15,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Overlays.Rankings namespace osu.Game.Overlays.Rankings
{ {
@ -46,6 +47,7 @@ namespace osu.Game.Overlays.Rankings
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
background = new Box background = new Box
@ -139,7 +141,7 @@ namespace osu.Game.Overlays.Rankings
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Direction = FillDirection.Vertical; Direction = FillDirection.Vertical;
Margin = new MarginPadding { Vertical = 10 }; Padding = new MarginPadding { Vertical = 15 };
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuSpriteText new OsuSpriteText
@ -150,11 +152,11 @@ namespace osu.Game.Overlays.Rankings
new Container new Container
{ {
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Height = 20, Height = 25,
Child = valueText = new OsuSpriteText Child = valueText = new OsuSpriteText
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light), Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light),
} }
} }
@ -174,11 +176,34 @@ namespace osu.Game.Overlays.Rankings
protected override DropdownMenu CreateMenu() => menu = base.CreateMenu().With(m => m.MaxHeight = 400); protected override DropdownMenu CreateMenu() => menu = base.CreateMenu().With(m => m.MaxHeight = 400);
protected override DropdownHeader CreateHeader() => new SpotlightsDropdownHeader();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
// osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour
AccentColour = colourProvider.Background6.Opacity(0.8f);
menu.BackgroundColour = colourProvider.Background5; menu.BackgroundColour = colourProvider.Background5;
AccentColour = colourProvider.Background6; Padding = new MarginPadding { Vertical = 20 };
}
private class SpotlightsDropdownHeader : OsuDropdownHeader
{
public SpotlightsDropdownHeader()
{
AutoSizeAxes = Axes.Y;
Text.Font = OsuFont.GetFont(size: 15);
Text.Padding = new MarginPadding { Vertical = 1.5f }; // osu-web line-height difference compensation
Foreground.Padding = new MarginPadding { Horizontal = 10, Vertical = 15 };
Margin = Icon.Margin = new MarginPadding(0);
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colourProvider.Background6.Opacity(0.5f);
BackgroundColourHover = colourProvider.Background5;
}
} }
} }
} }

View File

@ -1,15 +1,14 @@
// 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;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using System; using System;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Rankings.Tables namespace osu.Game.Overlays.Rankings.Tables
{ {
@ -62,35 +61,20 @@ namespace osu.Game.Overlays.Rankings.Tables
} }
}; };
private class CountryName : OsuHoverContainer private class CountryName : LinkFlowContainer
{ {
protected override IEnumerable<Drawable> EffectTargets => new[] { text };
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private RankingsOverlay rankings { get; set; } private RankingsOverlay rankings { get; set; }
private readonly OsuSpriteText text;
private readonly Country country;
public CountryName(Country country) public CountryName(Country country)
: base(t => t.Font = OsuFont.GetFont(size: 12))
{ {
this.country = country; AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
TextAnchor = Anchor.CentreLeft;
AutoSizeAxes = Axes.Both; if (!string.IsNullOrEmpty(country.FullName))
Add(text = new OsuSpriteText AddLink(country.FullName, () => rankings?.ShowCountry(country));
{
Font = OsuFont.GetFont(size: 12),
Text = country.FullName ?? string.Empty,
});
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
IdleColour = colourProvider.Light2;
HoverColour = colourProvider.Content2;
Action = () => rankings?.ShowCountry(country);
} }
} }
} }

View File

@ -1,4 +1,4 @@
// 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;
@ -20,7 +20,8 @@ namespace osu.Game.Overlays.Rankings.Tables
{ {
protected const int TEXT_SIZE = 12; protected const int TEXT_SIZE = 12;
private const float horizontal_inset = 20; private const float horizontal_inset = 20;
private const float row_height = 25; private const float row_height = 32;
private const float row_spacing = 3;
private const int items_per_page = 50; private const int items_per_page = 50;
private readonly int page; private readonly int page;
@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Rankings.Tables
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = horizontal_inset }; Padding = new MarginPadding { Horizontal = horizontal_inset };
RowSize = new Dimension(GridSizeMode.Absolute, row_height); RowSize = new Dimension(GridSizeMode.Absolute, row_height + row_spacing);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -47,10 +48,11 @@ namespace osu.Game.Overlays.Rankings.Tables
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = 1f, Depth = 1f,
Margin = new MarginPadding { Top = row_height } Margin = new MarginPadding { Top = row_height + row_spacing },
Spacing = new Vector2(0, row_spacing),
}); });
rankings.ForEach(_ => backgroundFlow.Add(new TableRowBackground())); rankings.ForEach(_ => backgroundFlow.Add(new TableRowBackground { Height = row_height }));
Columns = mainHeaders.Concat(CreateAdditionalHeaders()).ToArray(); Columns = mainHeaders.Concat(CreateAdditionalHeaders()).ToArray();
Content = rankings.Select((s, i) => createContent((page - 1) * items_per_page + i, s)).ToArray().ToRectangular(); Content = rankings.Select((s, i) => createContent((page - 1) * items_per_page + i, s)).ToArray().ToRectangular();
@ -68,13 +70,19 @@ namespace osu.Game.Overlays.Rankings.Tables
protected abstract Drawable[] CreateAdditionalContent(TModel item); protected abstract Drawable[] CreateAdditionalContent(TModel item);
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty, HighlightedColumn()); protected virtual string HighlightedColumn => @"Performance";
protected override Drawable CreateHeader(int index, TableColumn column)
{
var title = column?.Header ?? string.Empty;
return new HeaderText(title, title == HighlightedColumn);
}
protected abstract Country GetCountry(TModel item); protected abstract Country GetCountry(TModel item);
protected abstract Drawable CreateFlagContent(TModel item); protected abstract Drawable CreateFlagContent(TModel item);
private OsuSpriteText createIndexDrawable(int index) => new OsuSpriteText private OsuSpriteText createIndexDrawable(int index) => new RowText
{ {
Text = $"#{index + 1}", Text = $"#{index + 1}",
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.SemiBold) Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.SemiBold)
@ -84,37 +92,36 @@ namespace osu.Game.Overlays.Rankings.Tables
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(7, 0), Spacing = new Vector2(10, 0),
Margin = new MarginPadding { Bottom = row_spacing },
Children = new[] Children = new[]
{ {
new UpdateableFlag(GetCountry(item)) new UpdateableFlag(GetCountry(item))
{ {
Size = new Vector2(20, 13), Size = new Vector2(30, 20),
ShowPlaceholderOnNull = false, ShowPlaceholderOnNull = false,
}, },
CreateFlagContent(item) CreateFlagContent(item)
} }
}; };
protected virtual string HighlightedColumn() => @"Performance"; protected class HeaderText : OsuSpriteText
private class HeaderText : OsuSpriteText
{ {
private readonly string highlighted; private readonly bool isHighlighted;
public HeaderText(string text, string highlighted) public HeaderText(string text, bool isHighlighted)
{ {
this.highlighted = highlighted; this.isHighlighted = isHighlighted;
Text = text; Text = text;
Font = OsuFont.GetFont(size: 12); Font = OsuFont.GetFont(size: 12);
Margin = new MarginPadding { Horizontal = 10 }; Margin = new MarginPadding { Vertical = 5, Horizontal = 10 };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
if (Text != highlighted) if (!isHighlighted)
Colour = colourProvider.Foreground1; Colour = colourProvider.Foreground1;
} }
} }
@ -124,7 +131,7 @@ namespace osu.Game.Overlays.Rankings.Tables
public RowText() public RowText()
{ {
Font = OsuFont.GetFont(size: TEXT_SIZE); Font = OsuFont.GetFont(size: TEXT_SIZE);
Margin = new MarginPadding { Horizontal = 10 }; Margin = new MarginPadding { Horizontal = 10, Bottom = row_spacing };
} }
} }

View File

@ -33,6 +33,6 @@ namespace osu.Game.Overlays.Rankings.Tables
} }
}; };
protected override string HighlightedColumn() => @"Ranked Score"; protected override string HighlightedColumn => @"Ranked Score";
} }
} }

View File

@ -1,4 +1,4 @@
// 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.Allocation;
@ -22,10 +22,10 @@ namespace osu.Game.Overlays.Rankings.Tables
public TableRowBackground() public TableRowBackground()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 25;
CornerRadius = 3; CornerRadius = 4;
Masking = true; Masking = true;
MaskingSmoothness = 0.5f;
InternalChild = background = new Box InternalChild = background = new Box
{ {

View File

@ -19,22 +19,32 @@ namespace osu.Game.Overlays.Rankings.Tables
{ {
} }
protected virtual IEnumerable<string> GradeColumns => new List<string> { "SS", "S", "A" };
protected override TableColumn[] CreateAdditionalHeaders() => new[] protected override TableColumn[] CreateAdditionalHeaders() => new[]
{ {
new TableColumn("Accuracy", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), new TableColumn("Accuracy", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Play Count", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), new TableColumn("Play Count", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)),
}.Concat(CreateUniqueHeaders()).Concat(new[] }.Concat(CreateUniqueHeaders())
.Concat(GradeColumns.Select(grade => new TableColumn(grade, Anchor.Centre, new Dimension(GridSizeMode.AutoSize))))
.ToArray();
protected override Drawable CreateHeader(int index, TableColumn column)
{ {
new TableColumn("SS", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), var title = column?.Header ?? string.Empty;
new TableColumn("S", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), return new UserTableHeaderText(title, HighlightedColumn == title, GradeColumns.Contains(title));
new TableColumn("A", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), }
}).ToArray();
protected sealed override Country GetCountry(UserStatistics item) => item.User.Country; protected sealed override Country GetCountry(UserStatistics item) => item.User.Country;
protected sealed override Drawable CreateFlagContent(UserStatistics item) protected sealed override Drawable CreateFlagContent(UserStatistics item)
{ {
var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true)) { AutoSizeAxes = Axes.Both }; var username = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(size: TEXT_SIZE, italics: true))
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
TextAnchor = Anchor.CentreLeft
};
username.AddUserLink(item.User); username.AddUserLink(item.User);
return username; return username;
} }
@ -53,5 +63,19 @@ namespace osu.Game.Overlays.Rankings.Tables
protected abstract TableColumn[] CreateUniqueHeaders(); protected abstract TableColumn[] CreateUniqueHeaders();
protected abstract Drawable[] CreateUniqueContent(UserStatistics item); protected abstract Drawable[] CreateUniqueContent(UserStatistics item);
private class UserTableHeaderText : HeaderText
{
public UserTableHeaderText(string text, bool isHighlighted, bool isGrade)
: base(text, isHighlighted)
{
Margin = new MarginPadding
{
// Grade columns have extra horizontal padding for readibility
Horizontal = isGrade ? 20 : 10,
Vertical = 5
};
}
}
} }
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
private TriangleButton selectionButton; private TriangleButton selectionButton;
private DirectorySelector directorySelector; private OsuDirectorySelector directorySelector;
/// <summary> /// <summary>
/// Text to display in the header to inform the user of what they are selecting. /// Text to display in the header to inform the user of what they are selecting.
@ -91,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}, },
new Drawable[] new Drawable[]
{ {
directorySelector = new DirectorySelector directorySelector = new OsuDirectorySelector
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
} }

View File

@ -30,6 +30,8 @@ namespace osu.Game.Overlays.Volume
return true; return true;
case GlobalAction.ToggleMute: case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
ActionRequested?.Invoke(action); ActionRequested?.Invoke(action);
return true; return true;
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -19,13 +20,14 @@ using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Overlays.Volume namespace osu.Game.Overlays.Volume
{ {
public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction> public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction>, IStateful<SelectionState>
{ {
private CircularProgress volumeCircle; private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow; private CircularProgress volumeCircleGlow;
@ -38,9 +40,32 @@ namespace osu.Game.Overlays.Volume
private OsuSpriteText text; private OsuSpriteText text;
private BufferedContainer maxGlow; private BufferedContainer maxGlow;
private Container selectedGlowContainer;
private Sample sample; private Sample sample;
private double sampleLastPlaybackTime; private double sampleLastPlaybackTime;
public event Action<SelectionState> StateChanged;
private SelectionState state;
public SelectionState State
{
get => state;
set
{
if (state == value)
return;
state = value;
StateChanged?.Invoke(value);
updateSelectedState();
}
}
private const float transition_length = 500;
public VolumeMeter(string name, float circleSize, Color4 meterColour) public VolumeMeter(string name, float circleSize, Color4 meterColour)
{ {
this.circleSize = circleSize; this.circleSize = circleSize;
@ -75,7 +100,6 @@ namespace osu.Game.Overlays.Volume
{ {
new BufferedContainer new BufferedContainer
{ {
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -147,6 +171,24 @@ namespace osu.Game.Overlays.Volume
}, },
}, },
}, },
selectedGlowContainer = new CircularContainer
{
Masking = true,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
},
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = meterColour.Opacity(0.1f),
Radius = 10,
}
},
maxGlow = (text = new OsuSpriteText maxGlow = (text = new OsuSpriteText
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -171,7 +213,6 @@ namespace osu.Game.Overlays.Volume
{ {
new Box new Box
{ {
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = backgroundColour, Colour = backgroundColour,
}, },
@ -305,17 +346,14 @@ namespace osu.Game.Overlays.Volume
return true; return true;
} }
private const float transition_length = 500; protected override bool OnMouseMove(MouseMoveEvent e)
protected override bool OnHover(HoverEvent e)
{ {
this.ScaleTo(1.04f, transition_length, Easing.OutExpo); State = SelectionState.Selected;
return false; return base.OnMouseMove(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
this.ScaleTo(1f, transition_length, Easing.OutExpo);
} }
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
@ -326,10 +364,12 @@ namespace osu.Game.Overlays.Volume
switch (action) switch (action)
{ {
case GlobalAction.SelectPrevious: case GlobalAction.SelectPrevious:
State = SelectionState.Selected;
adjust(1, false); adjust(1, false);
return true; return true;
case GlobalAction.SelectNext: case GlobalAction.SelectNext:
State = SelectionState.Selected;
adjust(-1, false); adjust(-1, false);
return true; return true;
} }
@ -340,5 +380,21 @@ namespace osu.Game.Overlays.Volume
public void OnReleased(GlobalAction action) public void OnReleased(GlobalAction action)
{ {
} }
private void updateSelectedState()
{
switch (state)
{
case SelectionState.Selected:
this.ScaleTo(1.04f, transition_length, Easing.OutExpo);
selectedGlowContainer.FadeIn(transition_length, Easing.OutExpo);
break;
case SelectionState.NotSelected:
this.ScaleTo(1f, transition_length, Easing.OutExpo);
selectedGlowContainer.FadeOut(transition_length, Easing.OutExpo);
break;
}
}
} }
} }

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays.Volume; using osu.Game.Overlays.Volume;
using osuTK; using osuTK;
@ -32,6 +33,8 @@ namespace osu.Game.Overlays
public Bindable<bool> IsMuted { get; } = new Bindable<bool>(); public Bindable<bool> IsMuted { get; } = new Bindable<bool>();
private SelectionCycleFillFlowContainer<VolumeMeter> volumeMeters;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours) private void load(AudioManager audio, OsuColour colours)
{ {
@ -53,7 +56,7 @@ namespace osu.Game.Overlays
Margin = new MarginPadding(10), Margin = new MarginPadding(10),
Current = { BindTarget = IsMuted } Current = { BindTarget = IsMuted }
}, },
new FillFlowContainer volumeMeters = new SelectionCycleFillFlowContainer<VolumeMeter>
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -61,7 +64,7 @@ namespace osu.Game.Overlays
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Spacing = new Vector2(0, offset), Spacing = new Vector2(0, offset),
Margin = new MarginPadding { Left = offset }, Margin = new MarginPadding { Left = offset },
Children = new Drawable[] Children = new[]
{ {
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker), volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker),
volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker),
@ -87,9 +90,9 @@ namespace osu.Game.Overlays
{ {
base.LoadComplete(); base.LoadComplete();
volumeMeterMaster.Bindable.ValueChanged += _ => Show(); foreach (var volumeMeter in volumeMeters)
volumeMeterEffect.Bindable.ValueChanged += _ => Show(); volumeMeter.Bindable.ValueChanged += _ => Show();
volumeMeterMusic.Bindable.ValueChanged += _ => Show();
muteButton.Current.ValueChanged += _ => Show(); muteButton.Current.ValueChanged += _ => Show();
} }
@ -102,23 +105,27 @@ namespace osu.Game.Overlays
case GlobalAction.DecreaseVolume: case GlobalAction.DecreaseVolume:
if (State.Value == Visibility.Hidden) if (State.Value == Visibility.Hidden)
Show(); Show();
else if (volumeMeterMusic.IsHovered)
volumeMeterMusic.Decrease(amount, isPrecise);
else if (volumeMeterEffect.IsHovered)
volumeMeterEffect.Decrease(amount, isPrecise);
else else
volumeMeterMaster.Decrease(amount, isPrecise); volumeMeters.Selected?.Decrease(amount, isPrecise);
return true; return true;
case GlobalAction.IncreaseVolume: case GlobalAction.IncreaseVolume:
if (State.Value == Visibility.Hidden) if (State.Value == Visibility.Hidden)
Show(); Show();
else if (volumeMeterMusic.IsHovered)
volumeMeterMusic.Increase(amount, isPrecise);
else if (volumeMeterEffect.IsHovered)
volumeMeterEffect.Increase(amount, isPrecise);
else else
volumeMeterMaster.Increase(amount, isPrecise); volumeMeters.Selected?.Increase(amount, isPrecise);
return true;
case GlobalAction.NextVolumeMeter:
if (State.Value == Visibility.Visible)
volumeMeters.SelectNext();
Show();
return true;
case GlobalAction.PreviousVolumeMeter:
if (State.Value == Visibility.Visible)
volumeMeters.SelectPrevious();
Show();
return true; return true;
case GlobalAction.ToggleMute: case GlobalAction.ToggleMute:
@ -134,6 +141,10 @@ namespace osu.Game.Overlays
public override void Show() public override void Show()
{ {
// Focus on the master meter as a default if previously hidden
if (State.Value == Visibility.Hidden)
volumeMeters.Select(volumeMeterMaster);
if (State.Value == Visibility.Visible) if (State.Value == Visibility.Visible)
schedulePopOut(); schedulePopOut();

View File

@ -10,6 +10,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -34,6 +35,11 @@ namespace osu.Game.Rulesets.UI
/// </remarks> /// </remarks>
public ISampleStore SampleStore { get; } public ISampleStore SampleStore { get; }
/// <summary>
/// The shader manager to be used for the ruleset.
/// </summary>
public ShaderManager ShaderManager { get; }
/// <summary> /// <summary>
/// The ruleset config manager. /// The ruleset config manager.
/// </summary> /// </summary>
@ -52,6 +58,9 @@ namespace osu.Game.Rulesets.UI
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples")); SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>())); CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
ShaderManager = new ShaderManager(new NamespacedResourceStore<byte[]>(resources, @"Shaders"));
CacheAs(ShaderManager = new FallbackShaderManager(ShaderManager, parent.Get<ShaderManager>()));
} }
RulesetConfigManager = parent.Get<RulesetConfigCache>().GetConfigFor(ruleset); RulesetConfigManager = parent.Get<RulesetConfigCache>().GetConfigFor(ruleset);
@ -84,6 +93,7 @@ namespace osu.Game.Rulesets.UI
SampleStore?.Dispose(); SampleStore?.Dispose();
TextureStore?.Dispose(); TextureStore?.Dispose();
ShaderManager?.Dispose();
RulesetConfigManager = null; RulesetConfigManager = null;
} }
@ -172,5 +182,26 @@ namespace osu.Game.Rulesets.UI
primary?.Dispose(); primary?.Dispose();
} }
} }
private class FallbackShaderManager : ShaderManager
{
private readonly ShaderManager primary;
private readonly ShaderManager fallback;
public FallbackShaderManager(ShaderManager primary, ShaderManager fallback)
: base(new ResourceStore<byte[]>())
{
this.primary = primary;
this.fallback = fallback;
}
public override byte[] LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
primary?.Dispose();
}
}
} }
} }

View File

@ -56,9 +56,9 @@ namespace osu.Game.Screens.Edit.Setup
public void DisplayFileChooser() public void DisplayFileChooser()
{ {
FileSelector fileSelector; OsuFileSelector fileSelector;
Target.Child = fileSelector = new FileSelector(currentFile.Value?.DirectoryName, handledExtensions) Target.Child = fileSelector = new OsuFileSelector(currentFile.Value?.DirectoryName, handledExtensions)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 400, Height = 400,

View File

@ -23,7 +23,7 @@ namespace osu.Game.Screens.Import
{ {
public override bool HideOverlaysOnEnter => true; public override bool HideOverlaysOnEnter => true;
private FileSelector fileSelector; private OsuFileSelector fileSelector;
private Container contentContainer; private Container contentContainer;
private TextFlowContainer currentFileText; private TextFlowContainer currentFileText;
@ -57,7 +57,7 @@ namespace osu.Game.Screens.Import
Colour = colours.GreySeafoamDark, Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
fileSelector = new FileSelector(validFileExtensions: game.HandledExtensions.ToArray()) fileSelector = new OsuFileSelector(validFileExtensions: game.HandledExtensions.ToArray())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.65f Width = 0.65f

View File

@ -2,23 +2,24 @@
// 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 Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Effects;
using osuTK;
using osuTK.Graphics;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using Humanizer; using osuTK;
using osu.Framework.Graphics.Effects; using osuTK.Graphics;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -46,13 +47,13 @@ namespace osu.Game.Screens.Play
/// <summary> /// <summary>
/// Action that is invoked when <see cref="GlobalAction.Select"/> is triggered. /// Action that is invoked when <see cref="GlobalAction.Select"/> is triggered.
/// </summary> /// </summary>
protected virtual Action SelectAction => () => InternalButtons.Children.FirstOrDefault(f => f.Selected.Value)?.Click(); protected virtual Action SelectAction => () => InternalButtons.Selected?.Click();
public abstract string Header { get; } public abstract string Header { get; }
public abstract string Description { get; } public abstract string Description { get; }
protected ButtonContainer InternalButtons; protected SelectionCycleFillFlowContainer<DialogButton> InternalButtons;
public IReadOnlyList<DialogButton> Buttons => InternalButtons; public IReadOnlyList<DialogButton> Buttons => InternalButtons;
private FillFlowContainer retryCounterContainer; private FillFlowContainer retryCounterContainer;
@ -116,7 +117,7 @@ namespace osu.Game.Screens.Play
} }
} }
}, },
InternalButtons = new ButtonContainer InternalButtons = new SelectionCycleFillFlowContainer<DialogButton>
{ {
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
@ -183,8 +184,6 @@ namespace osu.Game.Screens.Play
} }
}; };
button.Selected.ValueChanged += selected => buttonSelectionChanged(button, selected.NewValue);
InternalButtons.Add(button); InternalButtons.Add(button);
} }
@ -216,14 +215,6 @@ namespace osu.Game.Screens.Play
{ {
} }
private void buttonSelectionChanged(DialogButton button, bool isSelected)
{
if (!isSelected)
InternalButtons.Deselect();
else
InternalButtons.Select(button);
}
private void updateRetryCount() private void updateRetryCount()
{ {
// "You've retried 1,065 times in this session" // "You've retried 1,065 times in this session"
@ -255,46 +246,6 @@ namespace osu.Game.Screens.Play
}; };
} }
protected class ButtonContainer : FillFlowContainer<DialogButton>
{
private int selectedIndex = -1;
private void setSelected(int value)
{
if (selectedIndex == value)
return;
// Deselect the previously-selected button
if (selectedIndex != -1)
this[selectedIndex].Selected.Value = false;
selectedIndex = value;
// Select the newly-selected button
if (selectedIndex != -1)
this[selectedIndex].Selected.Value = true;
}
public void SelectNext()
{
if (selectedIndex == -1 || selectedIndex == Count - 1)
setSelected(0);
else
setSelected(selectedIndex + 1);
}
public void SelectPrevious()
{
if (selectedIndex == -1 || selectedIndex == 0)
setSelected(Count - 1);
else
setSelected(selectedIndex - 1);
}
public void Deselect() => setSelected(-1);
public void Select(DialogButton button) => setSelected(IndexOf(button));
}
private class Button : DialogButton private class Button : DialogButton
{ {
// required to ensure keyboard navigation always starts from an extremity (unless the cursor is moved) // required to ensure keyboard navigation always starts from an extremity (unless the cursor is moved)
@ -302,7 +253,7 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
Selected.Value = true; State = SelectionState.Selected;
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
} }

View File

@ -83,9 +83,9 @@ namespace osu.Game.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
beatmapSkins.BindValueChanged(_ => OnSourceChanged()); beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapColours.BindValueChanged(_ => OnSourceChanged()); beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => OnSourceChanged()); beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
} }
} }
} }

View File

@ -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.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -46,57 +48,51 @@ namespace osu.Game.Skinning
}; };
} }
private ISkinSource parentSource;
private ResourceStoreBackedSkin rulesetResourcesSkin; private ResourceStoreBackedSkin rulesetResourcesSkin;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
parentSource = parent.Get<ISkinSource>();
parentSource.SourceChanged += OnSourceChanged;
if (Ruleset.CreateResourceStore() is IResourceStore<byte[]> resources) if (Ruleset.CreateResourceStore() is IResourceStore<byte[]> resources)
rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get<GameHost>(), parent.Get<AudioManager>()); rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get<GameHost>(), parent.Get<AudioManager>());
// ensure sources are populated and ready for use before childrens' asynchronous load flow.
UpdateSkinSources();
return base.CreateChildDependencies(parent); return base.CreateChildDependencies(parent);
} }
protected override void OnSourceChanged() protected override void OnSourceChanged()
{ {
UpdateSkinSources(); ResetSources();
base.OnSourceChanged();
}
protected virtual void UpdateSkinSources() // Populate a local list first so we can adjust the returned order as we go.
{ var sources = new List<ISkin>();
SkinSources.Clear();
foreach (var skin in parentSource.AllSources) Debug.Assert(ParentSource != null);
foreach (var skin in ParentSource.AllSources)
{ {
switch (skin) switch (skin)
{ {
case LegacySkin legacySkin: case LegacySkin legacySkin:
SkinSources.Add(GetLegacyRulesetTransformedSkin(legacySkin)); sources.Add(GetLegacyRulesetTransformedSkin(legacySkin));
break; break;
default: default:
SkinSources.Add(skin); sources.Add(skin);
break; break;
} }
} }
int lastDefaultSkinIndex = SkinSources.IndexOf(SkinSources.OfType<DefaultSkin>().LastOrDefault()); int lastDefaultSkinIndex = sources.IndexOf(sources.OfType<DefaultSkin>().LastOrDefault());
// Ruleset resources should be given the ability to override game-wide defaults // Ruleset resources should be given the ability to override game-wide defaults
// This is achieved by placing them before the last instance of DefaultSkin. // This is achieved by placing them before the last instance of DefaultSkin.
// Note that DefaultSkin may not be present in some test scenes. // Note that DefaultSkin may not be present in some test scenes.
if (lastDefaultSkinIndex >= 0) if (lastDefaultSkinIndex >= 0)
SkinSources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin); sources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin);
else else
SkinSources.Add(rulesetResourcesSkin); sources.Add(rulesetResourcesSkin);
foreach (var skin in sources)
AddSource(skin);
} }
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
@ -115,9 +111,6 @@ namespace osu.Game.Skinning
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (parentSource != null)
parentSource.SourceChanged -= OnSourceChanged;
rulesetResourcesSkin?.Dispose(); rulesetResourcesSkin?.Dispose();
} }
} }

View File

@ -3,8 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -24,19 +22,8 @@ namespace osu.Game.Skinning
{ {
public event Action SourceChanged; public event Action SourceChanged;
/// <summary>
/// Skins which should be exposed by this container, in order of lookup precedence.
/// </summary>
protected readonly BindableList<ISkin> SkinSources = new BindableList<ISkin>();
/// <summary>
/// A dictionary mapping each <see cref="ISkin"/> from the <see cref="SkinSources"/>
/// to one that performs the "allow lookup" checks before proceeding with a lookup.
/// </summary>
private readonly Dictionary<ISkin, DisableableSkinSource> disableableSkinSources = new Dictionary<ISkin, DisableableSkinSource>();
[CanBeNull] [CanBeNull]
private ISkinSource fallbackSource; protected ISkinSource ParentSource { get; private set; }
/// <summary> /// <summary>
/// Whether falling back to parent <see cref="ISkinSource"/>s is allowed in this container. /// Whether falling back to parent <see cref="ISkinSource"/>s is allowed in this container.
@ -53,6 +40,11 @@ namespace osu.Game.Skinning
protected virtual bool AllowColourLookup => true; protected virtual bool AllowColourLookup => true;
/// <summary>
/// A dictionary mapping each <see cref="ISkin"/> source to a wrapper which handles lookup allowances.
/// </summary>
private readonly List<(ISkin skin, DisableableSkinSource wrapped)> skinSources = new List<(ISkin, DisableableSkinSource)>();
/// <summary> /// <summary>
/// Constructs a new <see cref="SkinProvidingContainer"/> initialised with a single skin source. /// Constructs a new <see cref="SkinProvidingContainer"/> initialised with a single skin source.
/// </summary> /// </summary>
@ -60,87 +52,56 @@ namespace osu.Game.Skinning
: this() : this()
{ {
if (skin != null) if (skin != null)
SkinSources.Add(skin); AddSource(skin);
} }
/// <summary> /// <summary>
/// Constructs a new <see cref="SkinProvidingContainer"/> with no sources. /// Constructs a new <see cref="SkinProvidingContainer"/> with no sources.
/// Implementations can add or change sources through the <see cref="SkinSources"/> list.
/// </summary> /// </summary>
protected SkinProvidingContainer() protected SkinProvidingContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
SkinSources.BindCollectionChanged(((_, args) =>
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var skin in args.NewItems.Cast<ISkin>())
{
disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
if (skin is ISkinSource source)
source.SourceChanged += OnSourceChanged;
} }
break; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (var skin in args.OldItems.Cast<ISkin>())
{ {
disableableSkinSources.Remove(skin); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
if (skin is ISkinSource source) ParentSource = dependencies.Get<ISkinSource>();
source.SourceChanged -= OnSourceChanged; if (ParentSource != null)
} ParentSource.SourceChanged += TriggerSourceChanged;
break; dependencies.CacheAs<ISkinSource>(this);
case NotifyCollectionChangedAction.Replace: TriggerSourceChanged();
foreach (var skin in args.OldItems.Cast<ISkin>())
{
disableableSkinSources.Remove(skin);
if (skin is ISkinSource source) return dependencies;
source.SourceChanged -= OnSourceChanged;
}
foreach (var skin in args.NewItems.Cast<ISkin>())
{
disableableSkinSources.Add(skin, new DisableableSkinSource(skin, this));
if (skin is ISkinSource source)
source.SourceChanged += OnSourceChanged;
}
break;
}
}), true);
} }
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
{ {
foreach (var skin in SkinSources) foreach (var (skin, lookupWrapper) in skinSources)
{ {
if (lookupFunction(disableableSkinSources[skin])) if (lookupFunction(lookupWrapper))
return skin; return skin;
} }
return fallbackSource?.FindProvider(lookupFunction); if (!AllowFallingBackToParent)
return null;
return ParentSource?.FindProvider(lookupFunction);
} }
public IEnumerable<ISkin> AllSources public IEnumerable<ISkin> AllSources
{ {
get get
{ {
foreach (var skin in SkinSources) foreach (var i in skinSources)
yield return skin; yield return i.skin;
if (fallbackSource != null) if (AllowFallingBackToParent && ParentSource != null)
{ {
foreach (var skin in fallbackSource.AllSources) foreach (var skin in ParentSource.AllSources)
yield return skin; yield return skin;
} }
} }
@ -148,68 +109,110 @@ namespace osu.Game.Skinning
public Drawable GetDrawableComponent(ISkinComponent component) public Drawable GetDrawableComponent(ISkinComponent component)
{ {
foreach (var skin in SkinSources) foreach (var (_, lookupWrapper) in skinSources)
{ {
Drawable sourceDrawable; Drawable sourceDrawable;
if ((sourceDrawable = disableableSkinSources[skin]?.GetDrawableComponent(component)) != null) if ((sourceDrawable = lookupWrapper.GetDrawableComponent(component)) != null)
return sourceDrawable; return sourceDrawable;
} }
return fallbackSource?.GetDrawableComponent(component); if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetDrawableComponent(component);
} }
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
foreach (var skin in SkinSources) foreach (var (_, lookupWrapper) in skinSources)
{ {
Texture sourceTexture; Texture sourceTexture;
if ((sourceTexture = disableableSkinSources[skin]?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) if ((sourceTexture = lookupWrapper.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
return sourceTexture; return sourceTexture;
} }
return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT); if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetTexture(componentName, wrapModeS, wrapModeT);
} }
public ISample GetSample(ISampleInfo sampleInfo) public ISample GetSample(ISampleInfo sampleInfo)
{ {
foreach (var skin in SkinSources) foreach (var (_, lookupWrapper) in skinSources)
{ {
ISample sourceSample; ISample sourceSample;
if ((sourceSample = disableableSkinSources[skin]?.GetSample(sampleInfo)) != null) if ((sourceSample = lookupWrapper.GetSample(sampleInfo)) != null)
return sourceSample; return sourceSample;
} }
return fallbackSource?.GetSample(sampleInfo); if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetSample(sampleInfo);
} }
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
foreach (var skin in SkinSources) foreach (var (_, lookupWrapper) in skinSources)
{ {
IBindable<TValue> bindable; IBindable<TValue> bindable;
if ((bindable = disableableSkinSources[skin]?.GetConfig<TLookup, TValue>(lookup)) != null) if ((bindable = lookupWrapper.GetConfig<TLookup, TValue>(lookup)) != null)
return bindable; return bindable;
} }
return fallbackSource?.GetConfig<TLookup, TValue>(lookup); if (!AllowFallingBackToParent)
return null;
return ParentSource?.GetConfig<TLookup, TValue>(lookup);
} }
protected virtual void OnSourceChanged() => SourceChanged?.Invoke(); /// <summary>
/// Add a new skin to this provider. Will be added to the end of the lookup order precedence.
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) /// </summary>
/// <param name="skin">The skin to add.</param>
protected void AddSource(ISkin skin)
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); skinSources.Add((skin, new DisableableSkinSource(skin, this)));
if (AllowFallingBackToParent) if (skin is ISkinSource source)
{ source.SourceChanged += TriggerSourceChanged;
fallbackSource = dependencies.Get<ISkinSource>();
if (fallbackSource != null)
fallbackSource.SourceChanged += OnSourceChanged;
} }
dependencies.CacheAs<ISkinSource>(this); /// <summary>
/// Remove a skin from this provider.
/// </summary>
/// <param name="skin">The skin to remove.</param>
protected void RemoveSource(ISkin skin)
{
if (skinSources.RemoveAll(s => s.skin == skin) == 0)
return;
return dependencies; if (skin is ISkinSource source)
source.SourceChanged -= TriggerSourceChanged;
}
/// <summary>
/// Clears all skin sources.
/// </summary>
protected void ResetSources()
{
foreach (var i in skinSources.ToArray())
RemoveSource(i.skin);
}
/// <summary>
/// Invoked when any source has changed (either <see cref="ParentSource"/> or a source registered via <see cref="AddSource"/>).
/// This is also invoked once initially during <see cref="CreateChildDependencies"/> to ensure sources are ready for children consumption.
/// </summary>
protected virtual void OnSourceChanged() { }
protected void TriggerSourceChanged()
{
// Expose to implementations, giving them a chance to react before notifying external consumers.
OnSourceChanged();
SourceChanged?.Invoke();
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@ -219,11 +222,14 @@ namespace osu.Game.Skinning
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (fallbackSource != null) if (ParentSource != null)
fallbackSource.SourceChanged -= OnSourceChanged; ParentSource.SourceChanged -= TriggerSourceChanged;
foreach (var source in SkinSources.OfType<ISkinSource>()) foreach (var i in skinSources)
source.SourceChanged -= OnSourceChanged; {
if (i.skin is ISkinSource source)
source.SourceChanged -= TriggerSourceChanged;
}
} }
private class DisableableSkinSource : ISkin private class DisableableSkinSource : ISkin