1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 19:27:24 +08:00

Merge pull request #12077 from peppy/tablet-configuration

Add tablet configuration section
This commit is contained in:
Dan Balasescu 2021-03-19 21:49:32 +09:00 committed by GitHub
commit d54e9ab481
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 543 additions and 37 deletions

View File

@ -0,0 +1,73 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Platform;
using osu.Framework.Utils;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK;
namespace osu.Game.Tests.Visual.Settings
{
[TestFixture]
public class TestSceneTabletSettings : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(GameHost host)
{
var tabletHandler = new TestTabletHandler();
AddRange(new Drawable[]
{
new TabletSettings(tabletHandler)
{
RelativeSizeAxes = Axes.None,
Width = SettingsPanel.WIDTH,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
});
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 700)));
AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
}
public class TestTabletHandler : ITabletHandler
{
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();
public Bindable<Vector2> AreaSize { get; } = new Bindable<Vector2>();
public IBindable<TabletInfo> Tablet => tablet;
private readonly Bindable<TabletInfo> tablet = new Bindable<TabletInfo>();
public BindableBool Enabled { get; } = new BindableBool(true);
public void SetTabletSize(Vector2 size)
{
tablet.Value = size != Vector2.Zero ? new TabletInfo($"test tablet T-{RNG.Next(999):000}", size) : null;
AreaSize.Default = new Vector2(size.X, size.Y);
// if it's clear the user has not configured the area, take the full area from the tablet that was just found.
if (AreaSize.Value == Vector2.Zero)
AreaSize.SetDefault();
AreaOffset.Default = new Vector2(size.X / 2, size.Y / 2);
// likewise with the position, use the centre point if it has not been configured.
// it's safe to assume no user would set their centre point to 0,0 for now.
if (AreaOffset.Value == Vector2.Zero)
AreaOffset.SetDefault();
}
}
}
}

View File

@ -1,34 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using osuTK;
namespace osu.Game.IO.Serialization.Converters
{
/// <summary>
/// A type of <see cref="JsonConverter"/> that serializes only the X and Y coordinates of a <see cref="Vector2"/>.
/// </summary>
public class Vector2Converter : JsonConverter<Vector2>
{
public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
return new Vector2((float)obj["x"], (float)obj["y"]);
}
public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
{
writer.WriteStartObject();
writer.WritePropertyName("x");
writer.WriteValue(value.X);
writer.WritePropertyName("y");
writer.WriteValue(value.Y);
writer.WriteEndObject();
}
}
}

View File

@ -1,8 +1,9 @@
// 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 Newtonsoft.Json;
using osu.Game.IO.Serialization.Converters;
using osu.Framework.IO.Serialization;
namespace osu.Game.IO.Serialization
{
@ -28,7 +29,7 @@ namespace osu.Game.IO.Serialization
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace,
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
Converters = new JsonConverter[] { new Vector2Converter() },
Converters = new List<JsonConverter> { new Vector2Converter() },
ContractResolver = new KeyContractResolver()
};
}

View File

@ -0,0 +1,182 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Tablet;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings.Sections.Input
{
public class TabletAreaSelection : CompositeDrawable
{
private readonly ITabletHandler handler;
private Container tabletContainer;
private Container usableAreaContainer;
private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>();
private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>();
private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>();
private OsuSpriteText tabletName;
private Box usableFill;
private OsuSpriteText usableAreaText;
public TabletAreaSelection(ITabletHandler handler)
{
this.handler = handler;
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = tabletContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
CornerRadius = 5,
BorderThickness = 2,
BorderColour = colour.Gray3,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colour.Gray1,
},
usableAreaContainer = new Container
{
Origin = Anchor.Centre,
Children = new Drawable[]
{
usableFill = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.6f,
},
new Box
{
Colour = Color4.White,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 5,
},
new Box
{
Colour = Color4.White,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 5,
},
usableAreaText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.White,
Font = OsuFont.Default.With(size: 12),
Y = 10
}
}
},
tabletName = new OsuSpriteText
{
Padding = new MarginPadding(3),
Font = OsuFont.Default.With(size: 8)
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
areaOffset.BindTo(handler.AreaOffset);
areaOffset.BindValueChanged(val =>
{
usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint)
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
}, true);
areaSize.BindTo(handler.AreaSize);
areaSize.BindValueChanged(val =>
{
usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint)
.OnComplete(_ => checkBounds()); // required as we are using SSDQ.
int x = (int)val.NewValue.X;
int y = (int)val.NewValue.Y;
int commonDivider = greatestCommonDivider(x, y);
usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
}, true);
tablet.BindTo(handler.Tablet);
tablet.BindValueChanged(val =>
{
tabletContainer.Size = val.NewValue?.Size ?? Vector2.Zero;
tabletName.Text = val.NewValue?.Name ?? string.Empty;
checkBounds();
}, true);
// initial animation should be instant.
FinishTransforms(true);
}
private static int greatestCommonDivider(int a, int b)
{
while (b != 0)
{
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
[Resolved]
private OsuColour colour { get; set; }
private void checkBounds()
{
if (tablet.Value == null)
return;
var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad;
bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft + new Vector2(1)) &&
tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1));
usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
}
protected override void Update()
{
base.Update();
if (!(tablet.Value?.Size is Vector2 size))
return;
float fitX = size.X / (DrawWidth - Padding.Left - Padding.Right);
float fitY = size.Y / DrawHeight;
float adjust = MathF.Max(fitX, fitY);
tabletContainer.Scale = new Vector2(1 / adjust);
}
}
}

View File

@ -0,0 +1,276 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Overlays.Settings.Sections.Input
{
public class TabletSettings : SettingsSubsection
{
private readonly ITabletHandler tabletHandler;
private readonly Bindable<Vector2> areaOffset = new Bindable<Vector2>();
private readonly Bindable<Vector2> areaSize = new Bindable<Vector2>();
private readonly IBindable<TabletInfo> tablet = new Bindable<TabletInfo>();
private readonly BindableNumber<float> offsetX = new BindableNumber<float> { MinValue = 0 };
private readonly BindableNumber<float> offsetY = new BindableNumber<float> { MinValue = 0 };
private readonly BindableNumber<float> sizeX = new BindableNumber<float> { MinValue = 10 };
private readonly BindableNumber<float> sizeY = new BindableNumber<float> { MinValue = 10 };
[Resolved]
private GameHost host { get; set; }
/// <summary>
/// Based on ultrawide monitor configurations.
/// </summary>
private const float largest_feasible_aspect_ratio = 21f / 9;
private readonly BindableNumber<float> aspectRatio = new BindableFloat(1)
{
MinValue = 1 / largest_feasible_aspect_ratio,
MaxValue = largest_feasible_aspect_ratio,
Precision = 0.01f,
};
private readonly BindableBool aspectLock = new BindableBool();
private ScheduledDelegate aspectRatioApplication;
private FillFlowContainer mainSettings;
private OsuSpriteText noTabletMessage;
protected override string Header => "Tablet";
public TabletSettings(ITabletHandler tabletHandler)
{
this.tabletHandler = tabletHandler;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = "Enabled",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Current = tabletHandler.Enabled
},
noTabletMessage = new OsuSpriteText
{
Text = "No tablet detected!",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }
},
mainSettings = new FillFlowContainer
{
Alpha = 0,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 8),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new TabletAreaSelection(tabletHandler)
{
RelativeSizeAxes = Axes.X,
Height = 300,
},
new DangerousSettingsButton
{
Text = "Reset to full area",
Action = () =>
{
aspectLock.Value = false;
areaOffset.SetDefault();
areaSize.SetDefault();
},
},
new SettingsButton
{
Text = "Conform to current game aspect ratio",
Action = () =>
{
forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height);
}
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = "Aspect Ratio",
Current = aspectRatio
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = "X Offset",
Current = offsetX
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = "Y Offset",
Current = offsetY
},
new SettingsCheckbox
{
LabelText = "Lock aspect ratio",
Current = aspectLock
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = "Width",
Current = sizeX
},
new SettingsSlider<float>
{
TransferValueOnCommit = true,
LabelText = "Height",
Current = sizeY
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
areaOffset.BindTo(tabletHandler.AreaOffset);
areaOffset.BindValueChanged(val =>
{
offsetX.Value = val.NewValue.X;
offsetY.Value = val.NewValue.Y;
}, true);
offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y));
offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue));
areaSize.BindTo(tabletHandler.AreaSize);
areaSize.BindValueChanged(val =>
{
sizeX.Value = val.NewValue.X;
sizeY.Value = val.NewValue.Y;
}, true);
sizeX.BindValueChanged(val =>
{
areaSize.Value = new Vector2(val.NewValue, areaSize.Value.Y);
aspectRatioApplication?.Cancel();
aspectRatioApplication = Schedule(() => applyAspectRatio(sizeX));
});
sizeY.BindValueChanged(val =>
{
areaSize.Value = new Vector2(areaSize.Value.X, val.NewValue);
aspectRatioApplication?.Cancel();
aspectRatioApplication = Schedule(() => applyAspectRatio(sizeY));
});
updateAspectRatio();
aspectRatio.BindValueChanged(aspect =>
{
aspectRatioApplication?.Cancel();
aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
});
tablet.BindTo(tabletHandler.Tablet);
tablet.BindValueChanged(val =>
{
var tab = val.NewValue;
bool tabletFound = tab != null;
if (!tabletFound)
{
mainSettings.Hide();
noTabletMessage.Show();
return;
}
mainSettings.Show();
noTabletMessage.Hide();
offsetX.MaxValue = tab.Size.X;
offsetX.Default = tab.Size.X / 2;
sizeX.Default = sizeX.MaxValue = tab.Size.X;
offsetY.MaxValue = tab.Size.Y;
offsetY.Default = tab.Size.Y / 2;
sizeY.Default = sizeY.MaxValue = tab.Size.Y;
areaSize.Default = new Vector2(sizeX.Default, sizeY.Default);
}, true);
}
private void applyAspectRatio(BindableNumber<float> sizeChanged)
{
try
{
if (!aspectLock.Value)
{
float proposedAspectRatio = curentAspectRatio;
if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
{
// aspect ratio was in a valid range.
updateAspectRatio();
return;
}
}
// if lock is applied (or the specified values were out of range) aim to adjust the axis the user was not adjusting to conform.
if (sizeChanged == sizeX)
sizeY.Value = (int)(areaSize.Value.X / aspectRatio.Value);
else
sizeX.Value = (int)(areaSize.Value.Y * aspectRatio.Value);
}
finally
{
// cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
// this avoids a potential feedback loop.
aspectRatioApplication?.Cancel();
}
}
private void forceAspectRatio(float aspectRatio)
{
aspectLock.Value = false;
int proposedHeight = (int)(sizeX.Value / aspectRatio);
if (proposedHeight < sizeY.MaxValue)
sizeY.Value = proposedHeight;
else
sizeX.Value = (int)(sizeY.Value * aspectRatio);
updateAspectRatio();
aspectRatioApplication?.Cancel();
aspectLock.Value = true;
}
private void updateAspectRatio() => aspectRatio.Value = curentAspectRatio;
private float curentAspectRatio => sizeX.Value / sizeY.Value;
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Midi;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Platform;
using osu.Game.Overlays.Settings.Sections.Input;
@ -55,6 +56,11 @@ namespace osu.Game.Overlays.Settings.Sections
switch (handler)
{
// ReSharper disable once SuspiciousTypeConversion.Global (net standard fuckery)
case ITabletHandler th:
section = new TabletSettings(th);
break;
case MouseHandler mh:
section = new MouseSettings(mh);
break;

View File

@ -8,10 +8,12 @@ using osu.Game.Graphics.Sprites;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Settings
{
[ExcludeFromDynamicCompile]
public abstract class SettingsSubsection : FillFlowContainer, IHasFilterableChildren
{
protected override Container<Drawable> Content => FlowContent;

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays
private const float sidebar_width = Sidebar.DEFAULT_WIDTH;
protected const float WIDTH = 400;
public const float WIDTH = 400;
protected Container<Drawable> ContentContainer;