mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 15:27:30 +08:00
Merge branch 'master' into lazer-speedkeys
This commit is contained in:
commit
c800bb5339
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.329.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.523.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -1,76 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Android.Input;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
public partial class AndroidJoystickSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => JoystickSettingsStrings.JoystickGamepad;
|
||||
|
||||
private readonly AndroidJoystickHandler joystickHandler;
|
||||
|
||||
private readonly Bindable<bool> enabled = new BindableBool(true);
|
||||
|
||||
private SettingsSlider<float> deadzoneSlider = null!;
|
||||
|
||||
private Bindable<float> handlerDeadzone = null!;
|
||||
|
||||
private Bindable<float> localDeadzone = null!;
|
||||
|
||||
public AndroidJoystickSettings(AndroidJoystickHandler joystickHandler)
|
||||
{
|
||||
this.joystickHandler = joystickHandler;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
// use local bindable to avoid changing enabled state of game host's bindable.
|
||||
handlerDeadzone = joystickHandler.DeadzoneThreshold.GetBoundCopy();
|
||||
localDeadzone = handlerDeadzone.GetUnboundCopy();
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = CommonStrings.Enabled,
|
||||
Current = enabled
|
||||
},
|
||||
deadzoneSlider = new SettingsSlider<float>
|
||||
{
|
||||
LabelText = JoystickSettingsStrings.DeadzoneThreshold,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true,
|
||||
Current = localDeadzone,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
enabled.BindTo(joystickHandler.Enabled);
|
||||
enabled.BindValueChanged(e => deadzoneSlider.Current.Disabled = !e.NewValue, true);
|
||||
|
||||
handlerDeadzone.BindValueChanged(val =>
|
||||
{
|
||||
bool disabled = localDeadzone.Disabled;
|
||||
|
||||
localDeadzone.Disabled = false;
|
||||
localDeadzone.Value = val.NewValue;
|
||||
localDeadzone.Disabled = disabled;
|
||||
}, true);
|
||||
|
||||
localDeadzone.BindValueChanged(val => handlerDeadzone.Value = val.NewValue);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +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 Android.OS;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Android.Input;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Settings.Sections.Input;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
public partial class AndroidMouseSettings : SettingsSubsection
|
||||
{
|
||||
private readonly AndroidMouseHandler mouseHandler;
|
||||
|
||||
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
|
||||
|
||||
private Bindable<double> handlerSensitivity = null!;
|
||||
|
||||
private Bindable<double> localSensitivity = null!;
|
||||
|
||||
private Bindable<bool> relativeMode = null!;
|
||||
|
||||
public AndroidMouseSettings(AndroidMouseHandler mouseHandler)
|
||||
{
|
||||
this.mouseHandler = mouseHandler;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager osuConfig)
|
||||
{
|
||||
// use local bindable to avoid changing enabled state of game host's bindable.
|
||||
handlerSensitivity = mouseHandler.Sensitivity.GetBoundCopy();
|
||||
localSensitivity = handlerSensitivity.GetUnboundCopy();
|
||||
|
||||
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
|
||||
|
||||
// High precision/pointer capture is only available on Android 8.0 and up
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
|
||||
{
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.HighPrecisionMouse,
|
||||
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
|
||||
Current = relativeMode,
|
||||
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", @"captured", @"pointer" },
|
||||
},
|
||||
new MouseSettings.SensitivitySetting
|
||||
{
|
||||
LabelText = MouseSettingsStrings.CursorSensitivity,
|
||||
Current = localSensitivity,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
|
||||
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
relativeMode.BindValueChanged(relative => localSensitivity.Disabled = !relative.NewValue, true);
|
||||
|
||||
handlerSensitivity.BindValueChanged(val =>
|
||||
{
|
||||
bool disabled = localSensitivity.Disabled;
|
||||
|
||||
localSensitivity.Disabled = false;
|
||||
localSensitivity.Value = val.NewValue;
|
||||
localSensitivity.Disabled = disabled;
|
||||
}, true);
|
||||
|
||||
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,13 +5,9 @@ using System;
|
||||
using Android.App;
|
||||
using Microsoft.Maui.Devices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Android.Input;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Input.Handlers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Settings.Sections.Input;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@ -88,24 +84,6 @@ namespace osu.Android
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
|
||||
|
||||
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
|
||||
{
|
||||
switch (handler)
|
||||
{
|
||||
case AndroidMouseHandler mh:
|
||||
return new AndroidMouseSettings(mh);
|
||||
|
||||
case AndroidJoystickHandler jh:
|
||||
return new AndroidJoystickSettings(jh);
|
||||
|
||||
case AndroidTouchHandler th:
|
||||
return new TouchSettings(th);
|
||||
|
||||
default:
|
||||
return base.CreateSettingsSubsectionFor(handler);
|
||||
}
|
||||
}
|
||||
|
||||
private class AndroidBatteryInfo : BatteryInfo
|
||||
{
|
||||
public override double? ChargeLevel => Battery.ChargeLevel;
|
||||
|
@ -22,7 +22,6 @@ using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Performance;
|
||||
using osu.Game.Utils;
|
||||
using SDL2;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
@ -161,7 +160,7 @@ namespace osu.Desktop
|
||||
host.Window.Title = Name;
|
||||
}
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
|
||||
protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
@ -169,23 +168,5 @@ namespace osu.Desktop
|
||||
osuSchemeLinkIPCChannel?.Dispose();
|
||||
archiveImportIPCChannel?.Dispose();
|
||||
}
|
||||
|
||||
private class SDL2BatteryInfo : BatteryInfo
|
||||
{
|
||||
public override double? ChargeLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
SDL.SDL_GetPowerInfo(out _, out int percentage);
|
||||
|
||||
if (percentage == -1)
|
||||
return null;
|
||||
|
||||
return percentage / 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnBattery => SDL.SDL_GetPowerInfo(out _, out _) == SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Tournament;
|
||||
using SDL2;
|
||||
using SDL;
|
||||
using Squirrel;
|
||||
|
||||
namespace osu.Desktop
|
||||
@ -52,16 +52,19 @@ namespace osu.Desktop
|
||||
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
|
||||
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
|
||||
{
|
||||
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
|
||||
// disabling it ourselves.
|
||||
// We could also better detect compatibility mode if required:
|
||||
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
||||
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||
"Your operating system is too old to run osu!",
|
||||
"This version of osu! requires at least Windows 8.1 to run.\n"
|
||||
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"
|
||||
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!", IntPtr.Zero);
|
||||
return;
|
||||
unsafe
|
||||
{
|
||||
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider
|
||||
// disabling it ourselves.
|
||||
// We could also better detect compatibility mode if required:
|
||||
// https://stackoverflow.com/questions/10744651/how-i-can-detect-if-my-application-is-running-under-compatibility-mode#comment58183249_10744730
|
||||
SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||
"Your operating system is too old to run osu!"u8,
|
||||
"This version of osu! requires at least Windows 8.1 to run.\n"u8
|
||||
+ "Please upgrade your operating system or consider using an older version of osu!.\n\n"u8
|
||||
+ "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setupSquirrel();
|
||||
@ -104,7 +107,13 @@ namespace osu.Desktop
|
||||
}
|
||||
}
|
||||
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null }))
|
||||
var hostOptions = new HostOptions
|
||||
{
|
||||
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null,
|
||||
FriendlyGameName = OsuGameBase.GAME_NAME,
|
||||
};
|
||||
|
||||
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, hostOptions))
|
||||
{
|
||||
if (!host.IsPrimaryInstance)
|
||||
{
|
||||
|
25
osu.Desktop/SDL2BatteryInfo.cs
Normal file
25
osu.Desktop/SDL2BatteryInfo.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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.Game.Utils;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
internal class SDL2BatteryInfo : BatteryInfo
|
||||
{
|
||||
public override double? ChargeLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
|
||||
|
||||
if (percentage == -1)
|
||||
return null;
|
||||
|
||||
return percentage / 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||
}
|
||||
}
|
27
osu.Desktop/SDL3BatteryInfo.cs
Normal file
27
osu.Desktop/SDL3BatteryInfo.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// 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.Game.Utils;
|
||||
using SDL;
|
||||
|
||||
namespace osu.Desktop
|
||||
{
|
||||
internal unsafe class SDL3BatteryInfo : BatteryInfo
|
||||
{
|
||||
public override double? ChargeLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
int percentage;
|
||||
SDL3.SDL_GetPowerInfo(null, &percentage);
|
||||
|
||||
if (percentage == -1)
|
||||
return null;
|
||||
|
||||
return percentage / 100.0;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
|
||||
}
|
||||
}
|
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
|
||||
// Generally all the control points are within the visible area all the time.
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => true;
|
||||
public override bool UpdateSubTreeMasking() => true;
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
@ -435,10 +435,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
updatePathType(p, type);
|
||||
|
||||
EnsureValidPathTypes();
|
||||
|
||||
changeHandler?.EndChange();
|
||||
});
|
||||
|
||||
if (countOfState == totalCount)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
public SliderBodyPiece()
|
||||
{
|
||||
InternalChild = body = new ManualSliderBody
|
||||
{
|
||||
AccentColour = Color4.Transparent
|
||||
};
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
|
||||
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
|
||||
AlwaysPresent = true;
|
||||
|
||||
InternalChild = body = new ManualSliderBody
|
||||
{
|
||||
AccentColour = Color4.Transparent
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
body.SetVertices(vertices);
|
||||
}
|
||||
|
||||
Size = body.Size;
|
||||
OriginPosition = body.PathOffset;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||
/// can be maintained throughout the operation.
|
||||
/// </summary>
|
||||
private List<PathType?>? referencePathTypes;
|
||||
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
|
||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
|
||||
SelectionBox.CanFlipX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = quad.Height > 0;
|
||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||
}
|
||||
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
|
||||
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return didFlip;
|
||||
}
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor reference)
|
||||
{
|
||||
adjustScaleFromAnchor(ref scale, reference);
|
||||
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
|
||||
scaleSlider(slider, scale);
|
||||
else
|
||||
scaleHitObjects(hitObjects, reference, scale);
|
||||
|
||||
moveSelectionInBounds();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
||||
|
||||
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
||||
|
||||
Vector2 pathRelativeDeltaScale = new Vector2(
|
||||
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
|
||||
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
|
||||
|
||||
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
{
|
||||
oldControlPoints.Enqueue(point.Position);
|
||||
point.Position *= pathRelativeDeltaScale;
|
||||
}
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
||||
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
return;
|
||||
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
point.Position = oldControlPoints.Dequeue();
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
}
|
||||
|
||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
scale = getClampedScale(hitObjects, reference, scale);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
{
|
||||
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
|
||||
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
|
||||
|
||||
return (xInBounds, yInBounds);
|
||||
}
|
||||
public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
{
|
||||
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
h.Position += delta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hitobjects to be scaled</param>
|
||||
/// <param name="reference">The anchor from which the scale operation is performed</param>
|
||||
/// <param name="scale">The scale to be clamped</param>
|
||||
/// <returns>The clamped scale vector</returns>
|
||||
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
||||
|
||||
//max Size -> playfield bounds
|
||||
if (scaledQuad.TopLeft.X < 0)
|
||||
scale.X += scaledQuad.TopLeft.X;
|
||||
if (scaledQuad.TopLeft.Y < 0)
|
||||
scale.Y += scaledQuad.TopLeft.Y;
|
||||
|
||||
if (scaledQuad.BottomRight.X > DrawWidth)
|
||||
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
|
||||
if (scaledQuad.BottomRight.Y > DrawHeight)
|
||||
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
|
||||
|
||||
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
|
||||
Vector2 scaledSize = selectionQuad.Size + scale;
|
||||
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
|
||||
|
||||
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
|
220
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
220
osu.Game.Rulesets.Osu/Edit/OsuSelectionScaleHandler.cs
Normal file
@ -0,0 +1,220 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||
|
||||
CanScaleX.Value = quad.Width > 0;
|
||||
CanScaleY.Value = quad.Height > 0;
|
||||
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
|
||||
}
|
||||
|
||||
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
|
||||
private Vector2? defaultOrigin;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInScale != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
|
||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
|
||||
// for the time being, allow resizing of slider paths only if the slider is
|
||||
// the only hit object selected. with a group selection, it's likely the user
|
||||
// is not looking to change the duration of the slider but expand the whole pattern.
|
||||
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
|
||||
{
|
||||
var originalInfo = objectsInScale[slider];
|
||||
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
|
||||
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
|
||||
}
|
||||
else
|
||||
{
|
||||
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
|
||||
|
||||
foreach (var (ho, originalState) in objectsInScale)
|
||||
{
|
||||
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
|
||||
}
|
||||
}
|
||||
|
||||
moveSelectionInBounds();
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInScale = null;
|
||||
OriginalSurroundingQuad = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
|
||||
{
|
||||
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
|
||||
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
|
||||
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
|
||||
}
|
||||
|
||||
// Snap the slider's length to the current beat divisor
|
||||
// to calculate the final resulting duration / bounding box before the final checks.
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
|
||||
|
||||
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
|
||||
slider.SnapTo(snapProvider);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
{
|
||||
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
|
||||
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
|
||||
|
||||
return (xInBounds, yInBounds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
|
||||
/// </summary>
|
||||
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
|
||||
/// <param name="origin">The origin from which the scale operation is performed</param>
|
||||
/// <param name="scale">The scale to be clamped</param>
|
||||
/// <returns>The clamped scale vector</returns>
|
||||
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale)
|
||||
{
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
|
||||
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin);
|
||||
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
|
||||
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
|
||||
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
|
||||
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0))
|
||||
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
|
||||
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0))
|
||||
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
|
||||
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0))
|
||||
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
|
||||
|
||||
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
|
||||
}
|
||||
|
||||
private void moveSelectionInBounds()
|
||||
{
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
if (quad.TopLeft.X < 0)
|
||||
delta.X -= quad.TopLeft.X;
|
||||
if (quad.TopLeft.Y < 0)
|
||||
delta.Y -= quad.TopLeft.Y;
|
||||
|
||||
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
|
||||
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
|
||||
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
|
||||
|
||||
foreach (var (h, _) in objectsInScale!)
|
||||
h.Position += delta;
|
||||
}
|
||||
|
||||
private struct OriginalHitObjectState
|
||||
{
|
||||
public Vector2 Position { get; }
|
||||
public Vector2[]? PathControlPointPositions { get; }
|
||||
public PathType?[]? PathControlPointTypes { get; }
|
||||
|
||||
public OriginalHitObjectState(OsuHitObject hitObject)
|
||||
{
|
||||
Position = hitObject.Position;
|
||||
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
|
||||
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override LocalisableString Description => "Put your faith in the approach circles...";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
/// </summary>
|
||||
public partial class ManualSliderBody : SliderBody
|
||||
{
|
||||
public new void SetVertices(IReadOnlyList<Vector2> vertices)
|
||||
public ManualSliderBody()
|
||||
{
|
||||
base.SetVertices(vertices);
|
||||
Size = Path.Size;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
// For osu! gameplay, everything is always on screen.
|
||||
// Skipping masking calculations improves performance in intense beatmaps (ie. https://osu.ppy.sh/beatmapsets/150945#osu/372245)
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
|
||||
public override bool UpdateSubTreeMasking() => false;
|
||||
|
||||
public SmokeContainer Smoke { get; }
|
||||
public FollowPointRenderer FollowPoints { get; }
|
||||
|
@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
|
||||
}
|
||||
|
||||
protected override double ComputeTimeRange()
|
||||
{
|
||||
// Adjust when we're using constant algorithm to not be sluggish.
|
||||
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
|
||||
return base.ComputeTimeRange() / multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs
Normal file
29
osu.Game.Rulesets.Taiko/Mods/TaikoModConstantSpeed.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
|
||||
{
|
||||
public override string Name => "Constant Speed";
|
||||
public override string Acronym => "CS";
|
||||
public override double ScoreMultiplier => 0.9;
|
||||
public override LocalisableString Description => "No more tricky speed changes!";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Equals;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
|
||||
}
|
||||
}
|
||||
}
|
@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new TaikoModClassic(),
|
||||
new TaikoModSwap(),
|
||||
new TaikoModSingleTap(),
|
||||
new TaikoModConstantSpeed(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -82,7 +82,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
TimeRange.Value = ComputeTimeRange();
|
||||
}
|
||||
|
||||
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange();
|
||||
protected virtual double ComputeTimeRange()
|
||||
{
|
||||
// Adjust when we're using constant algorithm to not be sluggish.
|
||||
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
|
||||
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
|
@ -7,7 +7,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -345,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public void Add(Drawable proxy) => AddInternal(proxy);
|
||||
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
|
||||
public override bool UpdateSubTreeMasking()
|
||||
{
|
||||
// DrawableHitObject disables masking.
|
||||
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.
|
||||
|
@ -1188,5 +1188,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapDifficultyIsClamped()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream).Difficulty;
|
||||
Assert.That(decoded.DrainRate, Is.EqualTo(10));
|
||||
Assert.That(decoded.CircleSize, Is.EqualTo(10));
|
||||
Assert.That(decoded.OverallDifficulty, Is.EqualTo(10));
|
||||
Assert.That(decoded.ApproachRate, Is.EqualTo(10));
|
||||
Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6));
|
||||
Assert.That(decoded.SliderTickRate, Is.EqualTo(8));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManiaBeatmapDifficultyCircleSizeClamp()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var decoded = decoder.Decode(stream).Difficulty;
|
||||
Assert.That(decoded.CircleSize, Is.EqualTo(14));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
[General]
|
||||
Mode: 3
|
||||
|
||||
[Difficulty]
|
||||
CircleSize:14
|
10
osu.Game.Tests/Resources/out-of-range-difficulties.osu
Normal file
10
osu.Game.Tests/Resources/out-of-range-difficulties.osu
Normal file
@ -0,0 +1,10 @@
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:25
|
||||
CircleSize:25
|
||||
OverallDifficulty:25
|
||||
ApproachRate:30
|
||||
SliderMultiplier:30
|
||||
SliderTickRate:30
|
@ -10,9 +10,11 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Cached(typeof(SelectionRotationHandler))]
|
||||
private TestSelectionRotationHandler rotationHandler;
|
||||
|
||||
[Cached(typeof(SelectionScaleHandler))]
|
||||
private TestSelectionScaleHandler scaleHandler;
|
||||
|
||||
public TestSceneComposeSelectBox()
|
||||
{
|
||||
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
||||
scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
CanScaleX = true,
|
||||
CanScaleY = true,
|
||||
CanScaleDiagonally = true,
|
||||
CanFlipX = true,
|
||||
CanFlipY = true,
|
||||
|
||||
OnScale = handleScale
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
private bool handleScale(Vector2 amount, Anchor reference)
|
||||
{
|
||||
if ((reference & Anchor.y1) == 0)
|
||||
{
|
||||
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
|
||||
if (directionY < 0)
|
||||
selectionArea.Y += amount.Y;
|
||||
selectionArea.Height += directionY * amount.Y;
|
||||
}
|
||||
|
||||
if ((reference & Anchor.x1) == 0)
|
||||
{
|
||||
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
|
||||
if (directionX < 0)
|
||||
selectionArea.X += amount.X;
|
||||
selectionArea.Width += directionX * amount.X;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
private readonly Func<Container> getTargetContainer;
|
||||
@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
}
|
||||
|
||||
private partial class TestSelectionScaleHandler : SelectionScaleHandler
|
||||
{
|
||||
private readonly Func<Container> getTargetContainer;
|
||||
|
||||
public TestSelectionScaleHandler(Func<Container> getTargetContainer)
|
||||
{
|
||||
this.getTargetContainer = getTargetContainer;
|
||||
|
||||
CanScaleX.Value = true;
|
||||
CanScaleY.Value = true;
|
||||
CanScaleDiagonally.Value = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private Container targetContainer;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (targetContainer != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Vector2 actualOrigin = origin ?? Vector2.Zero;
|
||||
|
||||
targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft);
|
||||
targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale;
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
targetContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRotationHandleShownOnHover()
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
textBox.Current.Value = bank;
|
||||
// force a commit via keyboard.
|
||||
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
|
||||
InputManager.ChangeFocus(textBox);
|
||||
((IFocusManager)InputManager).ChangeFocus(textBox);
|
||||
InputManager.Key(Key.Enter);
|
||||
});
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||
|
||||
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
||||
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
|
||||
|
||||
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
|
||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||
|
||||
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
||||
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
|
||||
}
|
||||
|
||||
@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||
|
||||
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
||||
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
|
||||
|
||||
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
|
||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||
|
||||
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
||||
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
|
||||
}
|
||||
|
@ -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;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK.Input;
|
||||
using Color4 = osuTK.Graphics.Color4;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneMainMenuButton : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[Test]
|
||||
public void TestStandardButton()
|
||||
{
|
||||
AddStep("add button", () => Child = new MainMenuButton(
|
||||
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ButtonSystemState = ButtonSystemState.TopLevel,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDailyChallengeButton()
|
||||
{
|
||||
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
|
||||
|
||||
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetRoomRequest getRoomRequest:
|
||||
if (getRoomRequest.RoomId != 1234)
|
||||
return false;
|
||||
|
||||
var beatmap = CreateAPIBeatmap();
|
||||
beatmap.OnlineID = 1001;
|
||||
getRoomRequest.TriggerSuccess(new Room
|
||||
{
|
||||
RoomID = { Value = 1234 },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem(beatmap)
|
||||
},
|
||||
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ButtonSystemState = ButtonSystemState.TopLevel,
|
||||
});
|
||||
|
||||
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
|
||||
{
|
||||
RoomID = 1234,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@ -623,7 +624,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
|
||||
AddStep("unfocus search text box externally", () => InputManager.ChangeFocus(null));
|
||||
AddStep("unfocus search text box externally", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||
|
||||
AddStep("press tab", () => InputManager.Key(Key.Tab));
|
||||
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
|
||||
|
||||
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
|
||||
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
|
||||
AddStep("change text", () => textBox.Text = "3");
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
|
||||
@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
|
||||
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
||||
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
|
||||
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
|
||||
AddStep("set text to invalid", () => textBox.Text = "garbage");
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
@ -71,12 +72,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
||||
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
|
||||
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
|
||||
AddStep("set text to invalid", () => textBox.Text = "garbage");
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
||||
AddStep("lose focus", () => InputManager.ChangeFocus(null));
|
||||
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
|
||||
|
||||
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
|
||||
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
|
||||
AddStep("change text", () => textBox.Text = "3");
|
||||
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
|
||||
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
|
||||
@ -106,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
||||
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
|
||||
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
|
||||
AddStep("set text to invalid", () => textBox.Text = "garbage");
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
@ -116,12 +117,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
||||
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
|
||||
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
|
||||
AddStep("set text to invalid", () => textBox.Text = "garbage");
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
||||
AddStep("lose focus", () => InputManager.ChangeFocus(null));
|
||||
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
|
||||
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
|
||||
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
|
||||
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
|
||||
editorInfo.Selected.ValueChanged += selection =>
|
||||
{
|
||||
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
|
||||
GetContainingInputManager().TriggerFocusContention(null);
|
||||
GetContainingFocusManager().TriggerFocusContention(null);
|
||||
|
||||
// Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process).
|
||||
// Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best.
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Screens.Ladder
|
||||
|
||||
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
|
||||
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
|
||||
public override bool UpdateSubTreeMasking() => false;
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
|
@ -27,8 +27,17 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
set => base.Masking = value;
|
||||
}
|
||||
|
||||
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
|
||||
protected override double LoadDelay { get; }
|
||||
|
||||
private readonly double timeBeforeUnload;
|
||||
|
||||
protected override double TransformDuration => 400;
|
||||
|
||||
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover, double timeBeforeLoad = 500, double timeBeforeUnload = 1000)
|
||||
{
|
||||
LoadDelay = timeBeforeLoad;
|
||||
this.timeBeforeUnload = timeBeforeUnload;
|
||||
|
||||
this.coverType = coverType;
|
||||
|
||||
InternalChild = new Box
|
||||
@ -38,12 +47,12 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
};
|
||||
}
|
||||
|
||||
protected override double LoadDelay => 500;
|
||||
|
||||
protected override double TransformDuration => 400;
|
||||
|
||||
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
|
||||
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
|
||||
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, timeBeforeUnload)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
|
||||
{
|
||||
|
@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
base.ParseStreamInto(stream, beatmap);
|
||||
|
||||
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
|
||||
|
||||
flushPendingPoints();
|
||||
|
||||
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
||||
@ -102,10 +104,30 @@ namespace osu.Game.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that all <see cref="BeatmapDifficulty"/> settings are within the allowed ranges.
|
||||
/// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614
|
||||
/// </summary>
|
||||
private static void applyDifficultyRestrictions(BeatmapDifficulty difficulty, Beatmap beatmap)
|
||||
{
|
||||
difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10);
|
||||
|
||||
// mania uses "circle size" for key count, thus different allowable range
|
||||
difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3
|
||||
? Math.Clamp(difficulty.CircleSize, 0, 10)
|
||||
: Math.Clamp(difficulty.CircleSize, 1, 18);
|
||||
|
||||
difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10);
|
||||
difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10);
|
||||
|
||||
difficulty.SliderMultiplier = Math.Clamp(difficulty.SliderMultiplier, 0.4, 3.6);
|
||||
difficulty.SliderTickRate = Math.Clamp(difficulty.SliderTickRate, 0.5, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the beatmap such that a new combo is started the first hitobject following each break.
|
||||
/// </summary>
|
||||
private void postProcessBreaks(Beatmap beatmap)
|
||||
private static void postProcessBreaks(Beatmap beatmap)
|
||||
{
|
||||
int currentBreak = 0;
|
||||
bool forceNewCombo = false;
|
||||
@ -161,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
/// This method's intention is to restore those legacy defaults.
|
||||
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
|
||||
/// </summary>
|
||||
private void applyLegacyDefaults(BeatmapInfo beatmapInfo)
|
||||
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
beatmapInfo.WidescreenStoryboard = false;
|
||||
beatmapInfo.SamplesMatchPlaybackRate = false;
|
||||
@ -402,11 +424,11 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"SliderMultiplier":
|
||||
difficulty.SliderMultiplier = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.4, 3.6);
|
||||
difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
|
||||
break;
|
||||
|
||||
case @"SliderTickRate":
|
||||
difficulty.SliderTickRate = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.5, 8);
|
||||
difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -202,7 +202,7 @@ namespace osu.Game.Collections
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(addOrRemoveButton = new IconButton
|
||||
AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
@ -271,6 +271,11 @@ namespace osu.Game.Collections
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
private partial class NoFocusChangeIconButton : IconButton
|
||||
{
|
||||
public override bool ChangeFocusOnClick => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ namespace osu.Game.Collections
|
||||
this.ScaleTo(0.9f, exit_duration);
|
||||
|
||||
// Ensure that textboxes commit
|
||||
GetContainingInputManager()?.TriggerFocusContention(this);
|
||||
GetContainingFocusManager()?.TriggerFocusContention(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using JetBrains.Annotations;
|
||||
using Realms;
|
||||
using Realms.Schema;
|
||||
|
||||
@ -15,8 +16,12 @@ namespace osu.Game.Database
|
||||
{
|
||||
private IList<T> emptySet => Array.Empty<T>();
|
||||
|
||||
[MustDisposeResource]
|
||||
public IEnumerator<T> GetEnumerator() => emptySet.GetEnumerator();
|
||||
|
||||
[MustDisposeResource]
|
||||
IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator();
|
||||
|
||||
public int Count => emptySet.Count;
|
||||
public T this[int index] => emptySet[index];
|
||||
public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item);
|
||||
|
@ -120,6 +120,7 @@ namespace osu.Game.Graphics
|
||||
public static IconUsage Cross => get(OsuIconMapping.Cross);
|
||||
public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle);
|
||||
public static IconUsage Crown => get(OsuIconMapping.Crown);
|
||||
public static IconUsage DailyChallenge => get(OsuIconMapping.DailyChallenge);
|
||||
public static IconUsage Debug => get(OsuIconMapping.Debug);
|
||||
public static IconUsage Delete => get(OsuIconMapping.Delete);
|
||||
public static IconUsage Details => get(OsuIconMapping.Details);
|
||||
@ -218,6 +219,9 @@ namespace osu.Game.Graphics
|
||||
[Description(@"crown")]
|
||||
Crown,
|
||||
|
||||
[Description(@"daily-challenge")]
|
||||
DailyChallenge,
|
||||
|
||||
[Description(@"debug")]
|
||||
Debug,
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@ -18,7 +18,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// An <see cref="IExpandable"/> implementation for the UI slider bar control.
|
||||
/// </summary>
|
||||
public partial class ExpandableSlider<T, TSlider> : CompositeDrawable, IExpandable, IHasCurrentValue<T>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
where TSlider : RoundedSliderBar<T>, new()
|
||||
{
|
||||
private readonly OsuSpriteText label;
|
||||
@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// An <see cref="IExpandable"/> implementation for the UI slider bar control.
|
||||
/// </summary>
|
||||
public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
if (!allowImmediateFocus)
|
||||
return;
|
||||
|
||||
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this));
|
||||
Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(this));
|
||||
}
|
||||
|
||||
public new void KillFocus() => base.KillFocus();
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -15,7 +16,7 @@ using osu.Game.Utils;
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public abstract partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
public bool PlaySamplesOnAdjust { get; set; } = true;
|
||||
|
||||
@ -85,11 +86,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private LocalisableString getTooltipText(T value)
|
||||
{
|
||||
if (CurrentNumber.IsInteger)
|
||||
return value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0");
|
||||
return int.CreateTruncating(value).ToString("N0");
|
||||
|
||||
double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo);
|
||||
double floatValue = double.CreateTruncating(value);
|
||||
|
||||
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
|
||||
decimal decimalPrecision = normalise(decimal.CreateTruncating(CurrentNumber.Precision), max_decimal_digits);
|
||||
|
||||
// Find the number of significant digits (we could have less than 5 after normalize())
|
||||
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
|
||||
|
@ -8,6 +8,8 @@ using System.Linq;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -143,13 +145,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
FadeUnhovered();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
if (accentColour == default)
|
||||
AccentColour = colours.Blue;
|
||||
}
|
||||
|
||||
public OsuTabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
@ -196,10 +191,21 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
}
|
||||
|
||||
private Sample selectSample;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, AudioManager audio)
|
||||
{
|
||||
if (accentColour == default)
|
||||
AccentColour = colours.Blue;
|
||||
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
protected override void OnActivated()
|
||||
{
|
||||
Text.Font = Text.Font.With(weight: FontWeight.Bold);
|
||||
@ -211,6 +217,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Text.Font = Text.Font.With(weight: FontWeight.Medium);
|
||||
FadeUnhovered();
|
||||
}
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ using System;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -53,6 +55,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
private Sample selectSample = null!;
|
||||
|
||||
public PageTabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
@ -78,12 +82,18 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
|
||||
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
protected virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@ -112,6 +122,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override void OnActivated() => slideActive();
|
||||
|
||||
protected override void OnDeactivated() => slideInactive();
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
using System.Numerics;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -11,11 +11,12 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Overlays;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class RoundedSliderBar<T> : OsuSliderBar<T>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
protected readonly Nub Nub;
|
||||
protected readonly Box LeftBox;
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osuTK;
|
||||
using System.Numerics;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -12,11 +12,12 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Overlays;
|
||||
using static osu.Game.Graphics.UserInterface.ShearedNub;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ShearedSliderBar<T> : OsuSliderBar<T>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
protected readonly ShearedNub Nub;
|
||||
protected readonly Box LeftBox;
|
||||
|
@ -1,14 +1,14 @@
|
||||
// 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.Numerics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class LabelledSliderBar<TNumber> : LabelledComponent<SettingsSlider<TNumber>, TNumber>
|
||||
where TNumber : struct, IEquatable<TNumber>, IComparable<TNumber>, IConvertible
|
||||
where TNumber : struct, INumber<TNumber>, IMinMaxValue<TNumber>
|
||||
{
|
||||
public LabelledSliderBar()
|
||||
: base(true)
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
GetContainingInputManager().ChangeFocus(Component);
|
||||
GetContainingFocusManager().ChangeFocus(Component);
|
||||
}
|
||||
|
||||
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -10,12 +10,12 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom step value for each key press which actuates a change on this control.
|
||||
@ -85,7 +85,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
Current.BindValueChanged(updateTextBoxFromSlider, true);
|
||||
}
|
||||
|
||||
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
|
||||
public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
|
||||
|
||||
private bool updatingFromTextBox;
|
||||
|
||||
@ -138,7 +138,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
if (updatingFromTextBox) return;
|
||||
|
||||
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
|
||||
decimal decimalValue = decimal.CreateTruncating(slider.Current.Value);
|
||||
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
@ -54,6 +54,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit");
|
||||
|
||||
/// <summary>
|
||||
/// "daily challenge"
|
||||
/// </summary>
|
||||
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
16
osu.Game/Online/Metadata/DailyChallengeInfo.cs
Normal file
16
osu.Game/Online/Metadata/DailyChallengeInfo.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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 MessagePack;
|
||||
|
||||
namespace osu.Game.Online.Metadata
|
||||
{
|
||||
[MessagePackObject]
|
||||
[Serializable]
|
||||
public struct DailyChallengeInfo
|
||||
{
|
||||
[Key(0)]
|
||||
public long RoomID { get; set; }
|
||||
}
|
||||
}
|
@ -20,5 +20,11 @@ namespace osu.Game.Online.Metadata
|
||||
/// Delivers an update of the <see cref="UserPresence"/> of the user with the supplied <paramref name="userId"/>.
|
||||
/// </summary>
|
||||
Task UserPresenceUpdated(int userId, UserPresence? status);
|
||||
|
||||
/// <summary>
|
||||
/// Delivers an update of the current "daily challenge" status.
|
||||
/// Null value means there is no "daily challenge" currently active.
|
||||
/// </summary>
|
||||
Task DailyChallengeUpdated(DailyChallengeInfo? info);
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,12 @@ using osu.Game.Users;
|
||||
namespace osu.Game.Online.Metadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Metadata server is responsible for keeping the osu! client up-to-date with any changes.
|
||||
/// Metadata server is responsible for keeping the osu! client up-to-date with various real-time happenings, such as:
|
||||
/// <list type="bullet">
|
||||
/// <item>beatmap updates via BSS,</item>
|
||||
/// <item>online user activity/status updates,</item>
|
||||
/// <item>other real-time happenings, such as current "daily challenge" status.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public interface IMetadataServer
|
||||
{
|
||||
|
@ -59,6 +59,15 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
#endregion
|
||||
|
||||
#region Daily Challenge
|
||||
|
||||
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Disconnection handling
|
||||
|
||||
public event Action? Disconnecting;
|
||||
|
@ -26,6 +26,9 @@ namespace osu.Game.Online.Metadata
|
||||
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
|
||||
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
|
||||
|
||||
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
|
||||
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
|
||||
|
||||
private readonly string endpoint;
|
||||
|
||||
private IHubClientConnector? connector;
|
||||
@ -58,6 +61,7 @@ namespace osu.Game.Online.Metadata
|
||||
// https://github.com/dotnet/aspnetcore/issues/15198
|
||||
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
|
||||
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
|
||||
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
|
||||
};
|
||||
|
||||
@ -101,6 +105,7 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
isWatchingUserPresence.Value = false;
|
||||
userStates.Clear();
|
||||
dailyChallengeInfo.Value = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -229,6 +234,12 @@ namespace osu.Game.Online.Metadata
|
||||
}
|
||||
}
|
||||
|
||||
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
|
||||
{
|
||||
Schedule(() => dailyChallengeInfo.Value = info);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task DisconnectRequested()
|
||||
{
|
||||
await base.DisconnectRequested().ConfigureAwait(false);
|
||||
|
@ -13,5 +13,8 @@ namespace osu.Game.Online.Rooms
|
||||
|
||||
[Description("Featured Artist")]
|
||||
FeaturedArtist,
|
||||
|
||||
[Description("Daily Challenge")]
|
||||
DailyChallenge,
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,12 @@ namespace osu.Game
|
||||
{
|
||||
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
|
||||
|
||||
#if DEBUG
|
||||
public const string GAME_NAME = "osu! (development)";
|
||||
#else
|
||||
public const string GAME_NAME = "osu!";
|
||||
#endif
|
||||
|
||||
public const string OSU_PROTOCOL = "osu://";
|
||||
|
||||
public const string CLIENT_STREAM_NAME = @"lazer";
|
||||
@ -241,11 +247,7 @@ namespace osu.Game
|
||||
|
||||
public OsuGameBase()
|
||||
{
|
||||
Name = @"osu!";
|
||||
|
||||
#if DEBUG
|
||||
Name += " (development)";
|
||||
#endif
|
||||
Name = GAME_NAME;
|
||||
|
||||
allowableExceptions = UnhandledExceptionsBeforeCrash;
|
||||
}
|
||||
|
@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
|
||||
if (nextTextBox != null)
|
||||
{
|
||||
Schedule(() => GetContainingInputManager().ChangeFocus(nextTextBox));
|
||||
Schedule(() => GetContainingFocusManager().ChangeFocus(nextTextBox));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -47,13 +49,15 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
private Sample selectSample = null!;
|
||||
|
||||
public TabItem(BeatmapCardSize value)
|
||||
: base(value)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
@ -79,8 +83,10 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Icon = getIconForCardSize(Value)
|
||||
}
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
private static IconUsage getIconForCardSize(BeatmapCardSize cardSize)
|
||||
@ -111,6 +117,8 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
|
||||
protected override void OnDeactivated()
|
||||
{
|
||||
if (IsLoaded)
|
||||
|
@ -128,7 +128,11 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
base.OnClick(e);
|
||||
|
||||
// this tab item implementation is not managed by a TabControl,
|
||||
// therefore we have to manually update Active state and play select sound when this tab item is clicked.
|
||||
Active.Toggle();
|
||||
SelectSample.Play();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -24,13 +26,15 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
private OsuSpriteText text;
|
||||
|
||||
protected Sample SelectSample { get; private set; } = null!;
|
||||
|
||||
public FilterTabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
AddRangeInternal(new Drawable[]
|
||||
@ -40,10 +44,12 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
|
||||
Text = LabelFor(Value)
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
});
|
||||
|
||||
Enabled.Value = true;
|
||||
|
||||
SelectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -71,6 +77,8 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
protected override void OnDeactivated() => UpdateState();
|
||||
|
||||
protected override void OnActivatedByUser() => SelectSample.Play();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the label text to be used for the supplied <paramref name="value"/>.
|
||||
/// </summary>
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
// propagate value to tab items first to enable only available rulesets.
|
||||
beatmapSet.Value = value;
|
||||
|
||||
SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value));
|
||||
Current.Value = TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)?.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments
|
||||
base.LoadComplete();
|
||||
|
||||
if (!TextBox.ReadOnly)
|
||||
GetContainingInputManager().ChangeFocus(TextBox);
|
||||
GetContainingFocusManager().ChangeFocus(TextBox);
|
||||
}
|
||||
|
||||
protected override void OnCommit(string text)
|
||||
|
@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); });
|
||||
Schedule(() => { GetContainingFocusManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Login
|
||||
}
|
||||
|
||||
if (form != null)
|
||||
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(form));
|
||||
});
|
||||
|
||||
private void updateDropdownCurrent(UserStatus? status)
|
||||
@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
if (form != null) GetContainingInputManager().ChangeFocus(form);
|
||||
if (form != null) GetContainingFocusManager().ChangeFocus(form);
|
||||
base.OnFocus(e);
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); });
|
||||
Schedule(() => { GetContainingFocusManager().ChangeFocus(codeTextBox); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Overlays
|
||||
this.FadeIn(transition_time, Easing.OutQuint);
|
||||
FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
|
||||
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(panel));
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
|
||||
|
||||
nameTextBox.Current.BindValueChanged(s =>
|
||||
{
|
||||
|
@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
|
@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods
|
||||
RequestScroll?.Invoke(this);
|
||||
|
||||
// Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action.
|
||||
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null));
|
||||
Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(null));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ using osuTK;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
@ -65,6 +67,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
private Sample selectSample = null!;
|
||||
|
||||
public PanelDisplayTabItem(OverlayPanelDisplayStyle value)
|
||||
: base(value)
|
||||
{
|
||||
@ -78,14 +82,22 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
protected override void OnActivated() => updateState();
|
||||
|
||||
protected override void OnDeactivated() => updateState();
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
|
@ -10,6 +10,8 @@ using osu.Game.Rulesets;
|
||||
using osuTK.Graphics;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -39,6 +41,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
public LocalisableString TooltipText => Value.Name;
|
||||
|
||||
private Sample selectSample = null!;
|
||||
|
||||
public OverlayRulesetTabItem(RulesetInfo value)
|
||||
: base(value)
|
||||
{
|
||||
@ -59,12 +63,18 @@ namespace osu.Game.Overlays
|
||||
Icon = value.CreateInstance().CreateIcon(),
|
||||
},
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
});
|
||||
|
||||
Enabled.Value = true;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -90,6 +100,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void OnDeactivated() => updateState();
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
AccentColour = Enabled.Value ? getActiveColour() : colourProvider.Foreground1;
|
||||
|
@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
@ -49,8 +51,10 @@ namespace osu.Game.Overlays
|
||||
Margin = new MarginPadding(PADDING);
|
||||
}
|
||||
|
||||
private Sample selectSample;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours, AudioManager audio)
|
||||
{
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
@ -87,9 +91,11 @@ namespace osu.Game.Overlays
|
||||
CollapsedSize = 2,
|
||||
Expanded = true
|
||||
},
|
||||
new HoverClickSounds()
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
});
|
||||
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
|
||||
SelectedItem.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
@ -105,6 +111,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override void OnDeactivated() => updateState();
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
|
@ -4,6 +4,8 @@
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -80,6 +82,8 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
|
||||
private Sample selectSample = null!;
|
||||
|
||||
public OverlayTabItem(T value)
|
||||
: base(value)
|
||||
{
|
||||
@ -101,10 +105,16 @@ namespace osu.Game.Overlays
|
||||
ExpandedSize = 5f,
|
||||
CollapsedSize = 0
|
||||
},
|
||||
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||
new HoverSounds(HoverSampleSet.TabSelect)
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
selectSample = audio.Samples.Get(@"UI/tabselect-select");
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
base.OnHover(e);
|
||||
@ -136,6 +146,8 @@ namespace osu.Game.Overlays
|
||||
Text.Font = Text.Font.With(weight: FontWeight.Medium);
|
||||
}
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample.Play();
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Active.Value)
|
||||
|
@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
}
|
||||
|
||||
if (HasFocus)
|
||||
GetContainingInputManager().ChangeFocus(null);
|
||||
GetContainingFocusManager().ChangeFocus(null);
|
||||
|
||||
cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
|
||||
cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;
|
||||
|
@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault();
|
||||
if (next != null)
|
||||
GetContainingInputManager().ChangeFocus(next);
|
||||
GetContainingFocusManager().ChangeFocus(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
/// A slider intended to show a "size" multiplier number, where 1x is 1.0.
|
||||
/// </summary>
|
||||
public partial class SizeSlider<T> : RoundedSliderBar<T>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible, IFormattable
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>, IFormattable
|
||||
{
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x", NumberFormatInfo.CurrentInfo);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings
|
||||
/// Mostly provided for convenience of use with <see cref="SettingSourceAttribute"/>.
|
||||
/// </summary>
|
||||
public partial class SettingsPercentageSlider<TValue> : SettingsSlider<TValue>
|
||||
where TValue : struct, IEquatable<TValue>, IComparable<TValue>, IConvertible
|
||||
where TValue : struct, INumber<TValue>, IMinMaxValue<TValue>
|
||||
{
|
||||
protected override Drawable CreateControl() => ((RoundedSliderBar<TValue>)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -9,12 +9,12 @@ using osu.Game.Graphics.UserInterface;
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public partial class SettingsSlider<T> : SettingsSlider<T, RoundedSliderBar<T>>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
}
|
||||
|
||||
public partial class SettingsSlider<TValue, TSlider> : SettingsItem<TValue>
|
||||
where TValue : struct, IEquatable<TValue>, IComparable<TValue>, IConvertible
|
||||
where TValue : struct, INumber<TValue>, IMinMaxValue<TValue>
|
||||
where TSlider : RoundedSliderBar<TValue>, new()
|
||||
{
|
||||
protected override Drawable CreateControl() => new TSlider
|
||||
|
@ -201,7 +201,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
searchTextBox.HoldFocus = false;
|
||||
if (searchTextBox.HasFocus)
|
||||
GetContainingInputManager().ChangeFocus(null);
|
||||
GetContainingFocusManager().ChangeFocus(null);
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
@ -8,10 +8,8 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -34,148 +32,16 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
UpdatePosition = updateDrawablePosition
|
||||
};
|
||||
|
||||
private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false);
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor anchor)
|
||||
public override SelectionScaleHandler CreateScaleHandler()
|
||||
{
|
||||
Axes adjustAxis;
|
||||
|
||||
switch (anchor)
|
||||
var scaleHandler = new SkinSelectionScaleHandler
|
||||
{
|
||||
// for corners, adjust scale.
|
||||
case Anchor.TopLeft:
|
||||
case Anchor.TopRight:
|
||||
case Anchor.BottomLeft:
|
||||
case Anchor.BottomRight:
|
||||
adjustAxis = Axes.Both;
|
||||
break;
|
||||
UpdatePosition = updateDrawablePosition
|
||||
};
|
||||
|
||||
// for edges, adjust size.
|
||||
// autosize elements can't be easily handled so just disable sizing for now.
|
||||
case Anchor.TopCentre:
|
||||
case Anchor.BottomCentre:
|
||||
if (!allSelectedSupportManualSizing(Axes.Y))
|
||||
return false;
|
||||
scaleHandler.PerformFlipFromScaleHandles += a => SelectionBox.PerformFlipFromScaleHandles(a);
|
||||
|
||||
adjustAxis = Axes.Y;
|
||||
break;
|
||||
|
||||
case Anchor.CentreLeft:
|
||||
case Anchor.CentreRight:
|
||||
if (!allSelectedSupportManualSizing(Axes.X))
|
||||
return false;
|
||||
|
||||
adjustAxis = Axes.X;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null);
|
||||
}
|
||||
|
||||
// convert scale to screen space
|
||||
scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero);
|
||||
|
||||
adjustScaleFromAnchor(ref scale, anchor);
|
||||
|
||||
// the selection quad is always upright, so use an AABB rect to make mutating the values easier.
|
||||
var selectionRect = getSelectionQuad().AABBFloat;
|
||||
|
||||
// If the selection has no area we cannot scale it
|
||||
if (selectionRect.Area == 0)
|
||||
return false;
|
||||
|
||||
// copy to mutate, as we will need to compare to the original later on.
|
||||
var adjustedRect = selectionRect;
|
||||
bool isRotated = false;
|
||||
|
||||
// for now aspect lock scale adjustments that occur at corners..
|
||||
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
|
||||
{
|
||||
// project scale vector along diagonal
|
||||
Vector2 diag = (selectionRect.TopLeft - selectionRect.BottomRight).Normalized();
|
||||
scale = Vector2.Dot(scale, diag) * diag;
|
||||
}
|
||||
// ..or if any of the selection have been rotated.
|
||||
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
|
||||
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)))
|
||||
{
|
||||
isRotated = true;
|
||||
if (anchor.HasFlagFast(Anchor.x1))
|
||||
// if dragging from the horizontal centre, only a vertical component is available.
|
||||
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
|
||||
else
|
||||
// in all other cases (arbitrarily) use the horizontal component for aspect lock.
|
||||
scale.Y = scale.X / selectionRect.Width * selectionRect.Height;
|
||||
}
|
||||
|
||||
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
|
||||
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
|
||||
|
||||
// Maintain the selection's centre position if dragging from the centre anchors and selection is rotated.
|
||||
if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2;
|
||||
if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2;
|
||||
|
||||
adjustedRect.Width += scale.X;
|
||||
adjustedRect.Height += scale.Y;
|
||||
|
||||
if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0)
|
||||
{
|
||||
Axes toFlip = Axes.None;
|
||||
|
||||
if (adjustedRect.Width <= 0) toFlip |= Axes.X;
|
||||
if (adjustedRect.Height <= 0) toFlip |= Axes.Y;
|
||||
|
||||
SelectionBox.PerformFlipFromScaleHandles(toFlip);
|
||||
return true;
|
||||
}
|
||||
|
||||
// scale adjust applied to each individual item should match that of the quad itself.
|
||||
var scaledDelta = new Vector2(
|
||||
adjustedRect.Width / selectionRect.Width,
|
||||
adjustedRect.Height / selectionRect.Height
|
||||
);
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
// each drawable's relative position should be maintained in the scaled quad.
|
||||
var screenPosition = drawableItem.ToScreenSpace(drawableItem.OriginPosition);
|
||||
|
||||
var relativePositionInOriginal =
|
||||
new Vector2(
|
||||
(screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width,
|
||||
(screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height
|
||||
);
|
||||
|
||||
var newPositionInAdjusted = new Vector2(
|
||||
adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X,
|
||||
adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y
|
||||
);
|
||||
|
||||
updateDrawablePosition(drawableItem, newPositionInAdjusted);
|
||||
|
||||
var currentScaledDelta = scaledDelta;
|
||||
if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90))
|
||||
currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X);
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
drawableItem.Width *= currentScaledDelta.X;
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
drawableItem.Height *= currentScaledDelta.Y;
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
drawableItem.Scale *= currentScaledDelta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return scaleHandler;
|
||||
}
|
||||
|
||||
public override bool HandleFlip(Direction direction, bool flipOverOrigin)
|
||||
@ -226,9 +92,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X);
|
||||
SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y);
|
||||
SelectionBox.CanScaleDiagonally = true;
|
||||
SelectionBox.CanFlipX = true;
|
||||
SelectionBox.CanFlipY = true;
|
||||
SelectionBox.CanReverse = false;
|
||||
@ -460,16 +323,5 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
drawable.Origin = localOrigin;
|
||||
drawable.Position += offset;
|
||||
}
|
||||
|
||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((reference & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((reference & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
178
osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs
Normal file
178
osu.Game/Overlays/SkinEditor/SkinSelectionScaleHandler.cs
Normal file
@ -0,0 +1,178 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinSelectionScaleHandler : SelectionScaleHandler
|
||||
{
|
||||
public Action<Drawable, Vector2> UpdatePosition { get; init; } = null!;
|
||||
|
||||
public event Action<Axes>? PerformFlipFromScaleHandles;
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private BindableList<ISerialisableDrawable> selectedItems { get; } = new BindableList<ISerialisableDrawable>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinEditor skinEditor)
|
||||
{
|
||||
selectedItems.BindTo(skinEditor.SelectedComponents);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
CanScaleX.Value = allSelectedSupportManualSizing(Axes.X);
|
||||
CanScaleY.Value = allSelectedSupportManualSizing(Axes.Y);
|
||||
CanScaleDiagonally.Value = true;
|
||||
}
|
||||
|
||||
private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false);
|
||||
|
||||
private Dictionary<Drawable, OriginalDrawableState>? objectsInScale;
|
||||
private Vector2? defaultOrigin;
|
||||
|
||||
private bool isFlippedX;
|
||||
private bool isFlippedY;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInScale != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInScale = selectedItems.Cast<Drawable>().ToDictionary(d => d, d => new OriginalDrawableState(d));
|
||||
OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())));
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
|
||||
isFlippedX = false;
|
||||
isFlippedY = false;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
|
||||
|
||||
var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value);
|
||||
|
||||
if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) ||
|
||||
(adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X)))
|
||||
return;
|
||||
|
||||
// If the selection has no area we cannot scale it
|
||||
if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0)
|
||||
return;
|
||||
|
||||
// for now aspect lock scale adjustments that occur at corners.
|
||||
if (adjustAxis == Axes.Both)
|
||||
{
|
||||
// project scale vector along diagonal
|
||||
scale = new Vector2((scale.X + scale.Y) * 0.5f);
|
||||
}
|
||||
// If any of the selection have been rotated and the adjust axis is not both,
|
||||
// we would require skew logic to achieve a correct image editor-like scale.
|
||||
// For now we just ignore, because it would likely not be the user's expected transform anyway.
|
||||
|
||||
bool flippedX = scale.X < 0;
|
||||
bool flippedY = scale.Y < 0;
|
||||
Axes toFlip = Axes.None;
|
||||
|
||||
if (flippedX != isFlippedX)
|
||||
{
|
||||
isFlippedX = flippedX;
|
||||
toFlip |= Axes.X;
|
||||
}
|
||||
|
||||
if (flippedY != isFlippedY)
|
||||
{
|
||||
isFlippedY = flippedY;
|
||||
toFlip |= Axes.Y;
|
||||
}
|
||||
|
||||
if (toFlip != Axes.None)
|
||||
{
|
||||
PerformFlipFromScaleHandles?.Invoke(toFlip);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (b, originalState) in objectsInScale)
|
||||
{
|
||||
UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition));
|
||||
|
||||
var currentScale = scale;
|
||||
if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90))
|
||||
currentScale = new Vector2(scale.Y, scale.X);
|
||||
|
||||
switch (adjustAxis)
|
||||
{
|
||||
case Axes.X:
|
||||
b.Width = MathF.Abs(originalState.Width * currentScale.X);
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
b.Height = MathF.Abs(originalState.Height * currentScale.Y);
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
b.Scale = originalState.Scale * currentScale;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInScale == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInScale = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private struct OriginalDrawableState
|
||||
{
|
||||
public float Width { get; }
|
||||
public float Height { get; }
|
||||
public Vector2 Scale { get; }
|
||||
public Vector2 ScreenSpaceOriginPosition { get; }
|
||||
|
||||
public OriginalDrawableState(Drawable drawable)
|
||||
{
|
||||
Width = drawable.Width;
|
||||
Height = drawable.Height;
|
||||
Scale = drawable.Scale;
|
||||
ScreenSpaceOriginPosition = drawable.ToScreenSpace(drawable.OriginPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,11 +3,8 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -24,8 +21,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
{
|
||||
protected Drawable ModeButtonLine { get; private set; }
|
||||
|
||||
private readonly Dictionary<string, Sample> selectionSamples = new Dictionary<string, Sample>();
|
||||
|
||||
public ToolbarRulesetSelector()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
@ -33,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
private void load()
|
||||
{
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
@ -59,9 +54,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
foreach (var ruleset in Rulesets.AvailableRulesets)
|
||||
selectionSamples[ruleset.ShortName] = audio.Samples.Get($"UI/ruleset-select-{ruleset.ShortName}");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -88,10 +80,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
if (SelectedTab != null)
|
||||
{
|
||||
ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 500, Easing.OutElasticQuarter);
|
||||
|
||||
if (hasInitialPosition)
|
||||
selectionSamples[SelectedTab.Value.ShortName]?.Play();
|
||||
|
||||
hasInitialPosition = true;
|
||||
}
|
||||
}
|
||||
@ -121,7 +109,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
RulesetInfo found = Rulesets.AvailableRulesets.ElementAtOrDefault(requested);
|
||||
if (found != null)
|
||||
Current.Value = found;
|
||||
SelectItem(found);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar
|
||||
{
|
||||
private readonly RulesetButton ruleset;
|
||||
|
||||
private Sample? selectSample;
|
||||
|
||||
public ToolbarRulesetTabButton(RulesetInfo value)
|
||||
: base(value)
|
||||
{
|
||||
@ -34,10 +38,18 @@ namespace osu.Game.Overlays.Toolbar
|
||||
ruleset.SetIcon(rInstance.CreateIcon());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
selectSample = audio.Samples.Get($@"UI/ruleset-select-{Value.ShortName}");
|
||||
}
|
||||
|
||||
protected override void OnActivated() => ruleset.Active = true;
|
||||
|
||||
protected override void OnDeactivated() => ruleset.Active = false;
|
||||
|
||||
protected override void OnActivatedByUser() => selectSample?.Play();
|
||||
|
||||
private partial class RulesetButton : ToolbarButton
|
||||
{
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
||||
|
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
private float maxValue;
|
||||
private float maxValue = 10; // matches default max value of `CurrentNumber`
|
||||
|
||||
public float MaxValue
|
||||
{
|
||||
|
@ -15,7 +15,6 @@ using osu.Framework.Extensions.ListExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
@ -632,7 +631,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
#endregion
|
||||
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
|
||||
public override bool UpdateSubTreeMasking() => false;
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
|
@ -129,6 +129,13 @@ namespace osu.Game.Rulesets.Scoring
|
||||
OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})");
|
||||
}
|
||||
|
||||
if (!fail && double.IsInfinity(HpMultiplierNormal))
|
||||
{
|
||||
OnIterationSuccess?.Invoke("Drain computation algorithm diverged to infinity. PASSING with zero drop, resetting HP multiplier to 1.");
|
||||
HpMultiplierNormal = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!fail)
|
||||
{
|
||||
OnIterationSuccess?.Invoke($"PASSED drop {testDrop}");
|
||||
|
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.UI
|
||||
break;
|
||||
|
||||
base.UpdateSubTree();
|
||||
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
|
||||
UpdateSubTreeMasking();
|
||||
} while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds);
|
||||
|
||||
return true;
|
||||
|
@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
GetContainingInputManager().ChangeFocus(this);
|
||||
GetContainingFocusManager().ChangeFocus(this);
|
||||
SelectAll();
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved]
|
||||
private SelectionRotationHandler? rotationHandler { get; set; }
|
||||
|
||||
public Func<Vector2, Anchor, bool>? OnScale;
|
||||
[Resolved]
|
||||
private SelectionScaleHandler? scaleHandler { get; set; }
|
||||
|
||||
public Func<Direction, bool, bool>? OnFlip;
|
||||
public Func<bool>? OnReverse;
|
||||
|
||||
@ -57,60 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly IBindable<bool> canRotate = new BindableBool();
|
||||
|
||||
private bool canScaleX;
|
||||
private readonly IBindable<bool> canScaleX = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleX
|
||||
{
|
||||
get => canScaleX;
|
||||
set
|
||||
{
|
||||
if (canScaleX == value) return;
|
||||
private readonly IBindable<bool> canScaleY = new BindableBool();
|
||||
|
||||
canScaleX = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleY;
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleY
|
||||
{
|
||||
get => canScaleY;
|
||||
set
|
||||
{
|
||||
if (canScaleY == value) return;
|
||||
|
||||
canScaleY = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleDiagonally;
|
||||
|
||||
/// <summary>
|
||||
/// Whether diagonal scaling (from a corner) support should be enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There are some cases where we only want to allow proportional resizing, and not allow
|
||||
/// one or both explicit directions of scale.
|
||||
/// </remarks>
|
||||
public bool CanScaleDiagonally
|
||||
{
|
||||
get => canScaleDiagonally;
|
||||
set
|
||||
{
|
||||
if (canScaleDiagonally == value) return;
|
||||
|
||||
canScaleDiagonally = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
private readonly IBindable<bool> canScaleDiagonally = new BindableBool();
|
||||
|
||||
private bool canFlipX;
|
||||
|
||||
@ -176,7 +129,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (rotationHandler != null)
|
||||
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate(), true);
|
||||
if (scaleHandler != null)
|
||||
{
|
||||
canScaleX.BindTo(scaleHandler.CanScaleX);
|
||||
canScaleY.BindTo(scaleHandler.CanScaleY);
|
||||
canScaleDiagonally.BindTo(scaleHandler.CanScaleDiagonally);
|
||||
}
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate());
|
||||
canScaleX.BindValueChanged(_ => recreate());
|
||||
canScaleY.BindValueChanged(_ => recreate());
|
||||
canScaleDiagonally.BindValueChanged(_ => recreate(), true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
@ -265,9 +228,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
};
|
||||
|
||||
if (CanScaleX) addXScaleComponents();
|
||||
if (CanScaleDiagonally) addFullScaleComponents();
|
||||
if (CanScaleY) addYScaleComponents();
|
||||
if (canScaleX.Value) addXScaleComponents();
|
||||
if (canScaleDiagonally.Value) addFullScaleComponents();
|
||||
if (canScaleY.Value) addYScaleComponents();
|
||||
if (CanFlipX) addXFlipComponents();
|
||||
if (CanFlipY) addYFlipComponents();
|
||||
if (canRotate.Value) addRotationComponents();
|
||||
@ -353,7 +316,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
var handle = new SelectionBoxScaleHandle
|
||||
{
|
||||
Anchor = anchor,
|
||||
HandleScale = (delta, a) => OnScale?.Invoke(delta, a)
|
||||
};
|
||||
|
||||
handle.OperationStarted += operationStarted;
|
||||
|
@ -1,19 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle
|
||||
{
|
||||
public Action<Vector2, Anchor> HandleScale { get; set; }
|
||||
[Resolved]
|
||||
private SelectionScaleHandler? scaleHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -21,10 +23,108 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Size = new Vector2(10);
|
||||
}
|
||||
|
||||
private Anchor originalAnchor;
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
return false;
|
||||
|
||||
if (scaleHandler == null) return false;
|
||||
|
||||
originalAnchor = Anchor;
|
||||
|
||||
scaleHandler.Begin();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Vector2 rawScale;
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
HandleScale?.Invoke(e.Delta, Anchor);
|
||||
base.OnDrag(e);
|
||||
|
||||
if (scaleHandler == null) return;
|
||||
|
||||
rawScale = convertDragEventToScaleMultiplier(e);
|
||||
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (IsDragged)
|
||||
{
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (IsDragged)
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
scaleHandler?.Commit();
|
||||
}
|
||||
|
||||
private Vector2 convertDragEventToScaleMultiplier(DragEvent e)
|
||||
{
|
||||
Vector2 scale = e.MousePosition - e.MouseDownPosition;
|
||||
adjustScaleFromAnchor(ref scale);
|
||||
|
||||
var surroundingQuad = scaleHandler!.OriginalSurroundingQuad!.Value;
|
||||
scale.X = Precision.AlmostEquals(surroundingQuad.Width, 0) ? 0 : scale.X / surroundingQuad.Width;
|
||||
scale.Y = Precision.AlmostEquals(surroundingQuad.Height, 0) ? 0 : scale.Y / surroundingQuad.Height;
|
||||
|
||||
return scale + Vector2.One;
|
||||
}
|
||||
|
||||
private void adjustScaleFromAnchor(ref Vector2 scale)
|
||||
{
|
||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||
if ((originalAnchor & Anchor.x1) > 0) scale.X = 0;
|
||||
if ((originalAnchor & Anchor.y1) > 0) scale.Y = 0;
|
||||
|
||||
// reverse the scale direction if dragging from top or left.
|
||||
if ((originalAnchor & Anchor.x0) > 0) scale.X = -scale.X;
|
||||
if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
private void applyScale(bool shouldLockAspectRatio)
|
||||
{
|
||||
var newScale = shouldLockAspectRatio
|
||||
? new Vector2((rawScale.X + rawScale.Y) * 0.5f)
|
||||
: rawScale;
|
||||
|
||||
var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value);
|
||||
scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis());
|
||||
}
|
||||
|
||||
private Axes getAdjustAxis()
|
||||
{
|
||||
switch (originalAnchor)
|
||||
{
|
||||
case Anchor.TopCentre:
|
||||
case Anchor.BottomCentre:
|
||||
return Axes.Y;
|
||||
|
||||
case Anchor.CentreLeft:
|
||||
case Anchor.CentreRight:
|
||||
return Axes.X;
|
||||
|
||||
default:
|
||||
return Axes.Both;
|
||||
}
|
||||
}
|
||||
|
||||
private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1);
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; private set; }
|
||||
|
||||
public SelectionScaleHandler ScaleHandler { get; private set; }
|
||||
|
||||
protected SelectionHandler()
|
||||
{
|
||||
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
||||
@ -69,6 +71,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
dependencies.CacheAs(RotationHandler = CreateRotationHandler());
|
||||
dependencies.CacheAs(ScaleHandler = CreateScaleHandler());
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@ -78,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
RotationHandler,
|
||||
ScaleHandler,
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
});
|
||||
|
||||
@ -93,7 +97,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
OperationStarted = OnOperationBegan,
|
||||
OperationEnded = OnOperationEnded,
|
||||
|
||||
OnScale = HandleScale,
|
||||
OnFlip = HandleFlip,
|
||||
OnReverse = HandleReverse,
|
||||
};
|
||||
@ -157,6 +160,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <returns>Whether any items could be scaled.</returns>
|
||||
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler to use for scale operations.
|
||||
/// </summary>
|
||||
public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected items being flipped.
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,104 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Base handler for editor scale operations.
|
||||
/// </summary>
|
||||
public partial class SelectionScaleHandler : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanScaleX { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanScaleY { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether diagonal scaling (from a corner) support should be enabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There are some cases where we only want to allow proportional resizing, and not allow
|
||||
/// one or both explicit directions of scale.
|
||||
/// </remarks>
|
||||
public Bindable<bool> CanScaleDiagonally { get; private set; } = new BindableBool();
|
||||
|
||||
public Quad? OriginalSurroundingQuad { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Performs a single, instant, atomic scale operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is intended to be used in atomic contexts (such as when pressing a single button).
|
||||
/// For continuous operations, see the <see cref="Begin"/>-<see cref="Update"/>-<see cref="Commit"/> flow.
|
||||
/// </remarks>
|
||||
/// <param name="scale">The scale to apply, as multiplier.</param>
|
||||
/// <param name="origin">
|
||||
/// The origin point to scale from.
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
Begin();
|
||||
Update(scale, origin, adjustAxis);
|
||||
Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a continuous scale operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
|
||||
/// </remarks>
|
||||
public virtual void Begin()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a continuous scale operation.
|
||||
/// Must be preceded by a <see cref="Begin"/> call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
|
||||
/// As such, the values of <paramref name="scale"/> and <paramref name="origin"/> supplied should be relative to the state of the objects being scaled
|
||||
/// when <see cref="Begin"/> was called, rather than instantaneous deltas.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="scale">The Scale to apply, as multiplier.</param>
|
||||
/// <param name="origin">
|
||||
/// The origin point to scale from.
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
|
||||
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a continuous scale operation.
|
||||
/// Must be preceded by a <see cref="Begin"/> call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
|
||||
/// </remarks>
|
||||
public virtual void Commit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(sliderVelocitySlider));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(sliderVelocitySlider));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(volume));
|
||||
}
|
||||
|
||||
private static string? getCommonBank(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null;
|
||||
|
@ -7,7 +7,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
@ -20,7 +19,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
public partial class TimelineTickDisplay : TimelinePart<PointVisualisation>
|
||||
{
|
||||
// With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away.
|
||||
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
|
||||
public override bool UpdateSubTreeMasking() => false;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
@ -372,7 +372,7 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
}
|
||||
},
|
||||
new EditorScreenSwitcherControl
|
||||
screenSwitcher = new EditorScreenSwitcherControl
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
@ -662,23 +662,23 @@ namespace osu.Game.Screens.Edit
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorComposeMode:
|
||||
Mode.Value = EditorScreenMode.Compose;
|
||||
screenSwitcher.SelectItem(EditorScreenMode.Compose);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorDesignMode:
|
||||
Mode.Value = EditorScreenMode.Design;
|
||||
screenSwitcher.SelectItem(EditorScreenMode.Design);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorTimingMode:
|
||||
Mode.Value = EditorScreenMode.Timing;
|
||||
screenSwitcher.SelectItem(EditorScreenMode.Timing);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorSetupMode:
|
||||
Mode.Value = EditorScreenMode.SongSetup;
|
||||
screenSwitcher.SelectItem(EditorScreenMode.SongSetup);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorVerifyMode:
|
||||
Mode.Value = EditorScreenMode.Verify;
|
||||
screenSwitcher.SelectItem(EditorScreenMode.Verify);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorTestGameplay:
|
||||
@ -959,6 +959,8 @@ namespace osu.Game.Screens.Edit
|
||||
[CanBeNull]
|
||||
private ScheduledDelegate playbackDisabledDebounce;
|
||||
|
||||
private EditorScreenSwitcherControl screenSwitcher;
|
||||
|
||||
private void updateSampleDisabledState()
|
||||
{
|
||||
bool shouldDisableSamples = clock.SeekingOrStopped.Value
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
OnFocused?.Invoke();
|
||||
base.OnFocus(e);
|
||||
|
||||
GetContainingInputManager().TriggerFocusContention(this);
|
||||
GetContainingFocusManager().TriggerFocusContention(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
base.LoadComplete();
|
||||
|
||||
if (string.IsNullOrEmpty(ArtistTextBox.Current.Value))
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox));
|
||||
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(ArtistTextBox));
|
||||
|
||||
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
|
||||
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -12,7 +12,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using Vector2 = osuTK.Vector2;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
/// by providing an "indeterminate state".
|
||||
/// </summary>
|
||||
public partial class IndeterminateSliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T?>
|
||||
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
|
||||
where T : struct, INumber<T>, IMinMaxValue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom step value for each key press which actuates a change on this control.
|
||||
@ -126,7 +126,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
GetContainingInputManager().ChangeFocus(textBox);
|
||||
GetContainingFocusManager().ChangeFocus(textBox);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
slider.Current.Value = nonNullValue;
|
||||
|
||||
// use the value from the slider to ensure that any precision/min/max set on it via the initial indeterminate value have been applied correctly.
|
||||
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
|
||||
decimal decimalValue = decimal.CreateTruncating(slider.Current.Value);
|
||||
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
|
||||
textBox.PlaceholderText = string.Empty;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -46,6 +47,7 @@ namespace osu.Game.Screens.Menu
|
||||
public Action? OnSettings;
|
||||
public Action? OnMultiplayer;
|
||||
public Action? OnPlaylists;
|
||||
public Action<Room>? OnDailyChallenge;
|
||||
|
||||
private readonly IBindable<bool> isIdle = new BindableBool();
|
||||
|
||||
@ -102,10 +104,13 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
buttonArea.AddRange(new Drawable[]
|
||||
{
|
||||
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O, Key.S),
|
||||
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
|
||||
-WEDGE_WIDTH)
|
||||
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S)
|
||||
{
|
||||
Padding = new MarginPadding { Right = WEDGE_WIDTH },
|
||||
},
|
||||
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel)
|
||||
{
|
||||
Padding = new MarginPadding { Right = WEDGE_WIDTH },
|
||||
VisibleStateMin = ButtonSystemState.Play,
|
||||
VisibleStateMax = ButtonSystemState.Edit,
|
||||
},
|
||||
@ -127,21 +132,31 @@ namespace osu.Game.Screens.Menu
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
|
||||
{
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P)
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
||||
});
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M));
|
||||
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L));
|
||||
buttonsPlay.Add(new DailyChallengeButton(@"button-default-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
|
||||
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
|
||||
|
||||
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B, Key.E));
|
||||
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S));
|
||||
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E)
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
||||
});
|
||||
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S));
|
||||
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
|
||||
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P, Key.M, Key.L));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
|
||||
{
|
||||
Padding = new MarginPadding { Left = WEDGE_WIDTH },
|
||||
});
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D));
|
||||
|
||||
if (host.CanExit)
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
|
||||
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q));
|
||||
|
||||
buttonArea.AddRange(buttonsPlay);
|
||||
buttonArea.AddRange(buttonsEdit);
|
||||
@ -164,7 +179,7 @@ namespace osu.Game.Screens.Menu
|
||||
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
|
||||
}
|
||||
|
||||
private void onMultiplayer()
|
||||
private void onMultiplayer(MainMenuButton _)
|
||||
{
|
||||
if (api.State.Value != APIState.Online)
|
||||
{
|
||||
@ -175,7 +190,7 @@ namespace osu.Game.Screens.Menu
|
||||
OnMultiplayer?.Invoke();
|
||||
}
|
||||
|
||||
private void onPlaylists()
|
||||
private void onPlaylists(MainMenuButton _)
|
||||
{
|
||||
if (api.State.Value != APIState.Online)
|
||||
{
|
||||
@ -186,6 +201,20 @@ namespace osu.Game.Screens.Menu
|
||||
OnPlaylists?.Invoke();
|
||||
}
|
||||
|
||||
private void onDailyChallenge(MainMenuButton button)
|
||||
{
|
||||
if (api.State.Value != APIState.Online)
|
||||
{
|
||||
loginOverlay?.Show();
|
||||
return;
|
||||
}
|
||||
|
||||
var dailyChallengeButton = (DailyChallengeButton)button;
|
||||
|
||||
if (dailyChallengeButton.Room != null)
|
||||
OnDailyChallenge?.Invoke(dailyChallengeButton.Room);
|
||||
}
|
||||
|
||||
private void updateIdleState(bool isIdle)
|
||||
{
|
||||
if (!ReturnToTopOnIdle)
|
||||
|
221
osu.Game/Screens/Menu/DailyChallengeButton.cs
Normal file
221
osu.Game/Screens/Menu/DailyChallengeButton.cs
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Menu
|
||||
{
|
||||
public partial class DailyChallengeButton : MainMenuButton, IHasCustomTooltip<APIBeatmapSet?>
|
||||
{
|
||||
public Room? Room { get; private set; }
|
||||
|
||||
private readonly OsuSpriteText countdown;
|
||||
private ScheduledDelegate? scheduledCountdownUpdate;
|
||||
|
||||
private UpdateableOnlineBeatmapSetCover cover = null!;
|
||||
private IBindable<DailyChallengeInfo?> info = null!;
|
||||
|
||||
private Box gradientLayer = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
|
||||
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
|
||||
{
|
||||
BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT);
|
||||
|
||||
Content.Add(countdown = new OsuSpriteText
|
||||
{
|
||||
Shadow = true,
|
||||
AllowMultiline = false,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Left = -3,
|
||||
Bottom = 22,
|
||||
},
|
||||
Font = OsuFont.Default.With(size: 12),
|
||||
Alpha = 0,
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateBackground(Colour4 accentColour) => new BufferedContainer
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
cover = new UpdateableOnlineBeatmapSetCover(timeBeforeLoad: 0, timeBeforeUnload: 600_000)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
},
|
||||
gradientLayer = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.2f), accentColour),
|
||||
Blending = BlendingParameters.Additive,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = accentColour.Opacity(0.7f)
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(MetadataClient metadataClient)
|
||||
{
|
||||
info = metadataClient.DailyChallengeInfo.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
info.BindValueChanged(updateDisplay, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (cover.LatestTransformEndTime == Time.Current)
|
||||
{
|
||||
const double duration = 3000;
|
||||
|
||||
float scale = 1 + RNG.NextSingle();
|
||||
|
||||
cover.ScaleTo(scale, duration, Easing.InOutSine)
|
||||
.RotateTo(RNG.NextSingle(-4, 4) * (scale - 1), duration, Easing.InOutSine)
|
||||
.MoveTo(new Vector2(
|
||||
RNG.NextSingle(-0.5f, 0.5f) * (scale - 1),
|
||||
RNG.NextSingle(-0.5f, 0.5f) * (scale - 1)
|
||||
), duration, Easing.InOutSine);
|
||||
|
||||
gradientLayer.FadeIn(duration / 2)
|
||||
.Then()
|
||||
.FadeOut(duration / 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDisplay(ValueChangedEvent<DailyChallengeInfo?> info)
|
||||
{
|
||||
UpdateState();
|
||||
|
||||
scheduledCountdownUpdate?.Cancel();
|
||||
scheduledCountdownUpdate = null;
|
||||
|
||||
if (info.NewValue == null)
|
||||
{
|
||||
Room = null;
|
||||
cover.OnlineInfo = TooltipContent = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID);
|
||||
|
||||
roomRequest.Success += room =>
|
||||
{
|
||||
Room = room;
|
||||
cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet;
|
||||
|
||||
updateCountdown();
|
||||
Scheduler.AddDelayed(updateCountdown, 1000, true);
|
||||
};
|
||||
api.Queue(roomRequest);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCountdown()
|
||||
{
|
||||
if (Room == null)
|
||||
return;
|
||||
|
||||
var remaining = (Room.EndDate.Value - DateTimeOffset.Now) ?? TimeSpan.Zero;
|
||||
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
countdown.FadeOut(250, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (countdown.Alpha == 0)
|
||||
countdown.FadeIn(250, Easing.OutQuint);
|
||||
|
||||
countdown.Text = remaining.ToString(@"hh\:mm\:ss");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateState()
|
||||
{
|
||||
if (info.IsNotNull() && info.Value == null)
|
||||
{
|
||||
ContractStyle = 0;
|
||||
State = ButtonState.Contracted;
|
||||
return;
|
||||
}
|
||||
|
||||
base.UpdateState();
|
||||
}
|
||||
|
||||
public ITooltip<APIBeatmapSet?> GetCustomTooltip() => new DailyChallengeTooltip();
|
||||
|
||||
public APIBeatmapSet? TooltipContent { get; private set; }
|
||||
|
||||
internal partial class DailyChallengeTooltip : CompositeDrawable, ITooltip<APIBeatmapSet?>
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
private APIBeatmapSet? lastContent;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
public void SetContent(APIBeatmapSet? content)
|
||||
{
|
||||
if (content == lastContent)
|
||||
return;
|
||||
|
||||
lastContent = content;
|
||||
|
||||
ClearInternal();
|
||||
if (content != null)
|
||||
AddInternal(new BeatmapCardNano(content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user