1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-15 02:08:21 +08:00

Merge branch 'master' into lazer-speedkeys

This commit is contained in:
Bartłomiej Dach 2024-05-24 13:19:26 +02:00
commit c800bb5339
No known key found for this signature in database
112 changed files with 1746 additions and 884 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.329.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.523.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

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

View File

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

View File

@ -5,13 +5,9 @@ using System;
using Android.App; using Android.App;
using Microsoft.Maui.Devices; using Microsoft.Maui.Devices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Android.Input;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input.Handlers;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
@ -88,24 +84,6 @@ namespace osu.Android
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo(); 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 private class AndroidBatteryInfo : BatteryInfo
{ {
public override double? ChargeLevel => Battery.ChargeLevel; public override double? ChargeLevel => Battery.ChargeLevel;

View File

@ -22,7 +22,6 @@ using osu.Game.IPC;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Performance; using osu.Game.Performance;
using osu.Game.Utils; using osu.Game.Utils;
using SDL2;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -161,7 +160,7 @@ namespace osu.Desktop
host.Window.Title = Name; 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) protected override void Dispose(bool isDisposing)
{ {
@ -169,23 +168,5 @@ namespace osu.Desktop
osuSchemeLinkIPCChannel?.Dispose(); osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.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;
}
} }
} }

View File

@ -13,7 +13,7 @@ using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Tournament; using osu.Game.Tournament;
using SDL2; using SDL;
using Squirrel; using Squirrel;
namespace osu.Desktop namespace osu.Desktop
@ -51,18 +51,21 @@ namespace osu.Desktop
// While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher. // While .NET 8 only supports Windows 10 and above, running on Windows 7/8.1 may still work. We are limited by realm currently, as they choose to only support 8.1 and higher.
// See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/ // See https://www.mongodb.com/docs/realm/sdk/dotnet/compatibility/
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2)) if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
unsafe
{ {
// If users running in compatibility mode becomes more of a common thing, we may want to provide better guidance or even consider // 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. // disabling it ourselves.
// We could also better detect compatibility mode if required: // 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 // 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, SDL3.SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!", "Your operating system is too old to run osu!"u8,
"This version of osu! requires at least Windows 8.1 to run.\n" "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" + "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!", IntPtr.Zero); + "If you are running a newer version of windows, please check you don't have \"Compatibility mode\" turned on for osu!"u8, null);
return; return;
} }
}
setupSquirrel(); 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) if (!host.IsPrimaryInstance)
{ {

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

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

View File

@ -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. // 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> /// <summary>
/// Handles correction of invalid path types. /// 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, _ => var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
{ {
changeHandler?.BeginChange();
foreach (var p in Pieces.Where(p => p.IsSelected.Value)) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type); updatePathType(p, type);
EnsureValidPathTypes(); EnsureValidPathTypes();
changeHandler?.EndChange();
}); });
if (countOfState == totalCount) if (countOfState == totalCount)

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public SliderBodyPiece() public SliderBodyPiece()
{ {
InternalChild = body = new ManualSliderBody AutoSizeAxes = Axes.Both;
{
AccentColour = Color4.Transparent
};
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur. // 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. // Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
AlwaysPresent = true; AlwaysPresent = true;
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.SetVertices(vertices); body.SetVertices(vertices);
} }
Size = body.Size;
OriginPosition = body.PathOffset; OriginPosition = body.PathOffset;
} }

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuSelectionHandler : EditorSelectionHandler 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() protected override void OnSelectionChanged()
{ {
base.OnSelectionChanged(); base.OnSelectionChanged();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanFlipY = quad.Height > 0;
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); 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) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return didFlip; 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(); public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
private void scaleSlider(Slider slider, Vector2 scale) public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
{
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);
}
private void moveSelectionInBounds() private void moveSelectionInBounds()
{ {
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += delta; 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> /// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled. /// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary> /// </summary>

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

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Put your faith in the approach circles..."; public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// </summary> /// </summary>
public partial class ManualSliderBody : SliderBody public partial class ManualSliderBody : SliderBody
{ {
public new void SetVertices(IReadOnlyList<Vector2> vertices) public ManualSliderBody()
{ {
base.SetVertices(vertices); AutoSizeAxes = Axes.Both;
Size = Path.Size; }
}
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
} }
} }

View File

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
// For osu! gameplay, everything is always on screen. // 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) // 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 SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; } public FollowPointRenderer FollowPoints { get; }

View File

@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true); 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;
}
} }
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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;
}
}
}

View File

@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModClassic(), new TaikoModClassic(),
new TaikoModSwap(), new TaikoModSwap(),
new TaikoModSingleTap(), new TaikoModSingleTap(),
new TaikoModConstantSpeed(),
}; };
case ModType.Automation: case ModType.Automation:

View File

@ -82,7 +82,12 @@ namespace osu.Game.Rulesets.Taiko.UI
TimeRange.Value = ComputeTimeRange(); 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() protected override void UpdateAfterChildren()
{ {

View File

@ -7,7 +7,6 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -345,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public void Add(Drawable proxy) => AddInternal(proxy); public void Add(Drawable proxy) => AddInternal(proxy);
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) public override bool UpdateSubTreeMasking()
{ {
// DrawableHitObject disables masking. // DrawableHitObject disables masking.
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this. // Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.

View File

@ -1188,5 +1188,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153)); 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));
}
}
} }
} }

View File

@ -0,0 +1,5 @@
[General]
Mode: 3
[Difficulty]
CircleSize:14

View File

@ -0,0 +1,10 @@
[General]
Mode: 0
[Difficulty]
HPDrainRate:25
CircleSize:25
OverallDifficulty:25
ApproachRate:30
SliderMultiplier:30
SliderTickRate:30

View File

@ -10,9 +10,11 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(SelectionRotationHandler))] [Cached(typeof(SelectionRotationHandler))]
private TestSelectionRotationHandler rotationHandler; private TestSelectionRotationHandler rotationHandler;
[Cached(typeof(SelectionScaleHandler))]
private TestSelectionScaleHandler scaleHandler;
public TestSceneComposeSelectBox() public TestSceneComposeSelectBox()
{ {
rotationHandler = new TestSelectionRotationHandler(() => selectionArea); rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
} }
[SetUp] [SetUp]
@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
CanScaleX = true,
CanScaleY = true,
CanScaleDiagonally = true,
CanFlipX = true, CanFlipX = true,
CanFlipY = true, CanFlipY = true,
OnScale = handleScale
} }
} }
}; };
@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseButton(MouseButton.Left); 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 partial class TestSelectionRotationHandler : SelectionRotationHandler
{ {
private readonly Func<Container> getTargetContainer; 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] [Test]
public void TestRotationHandleShownOnHover() public void TestRotationHandleShownOnHover()
{ {

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Humanizer; using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing
textBox.Current.Value = bank; textBox.Current.Value = bank;
// force a commit via keyboard. // 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. // 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); InputManager.Key(Key.Enter);
}); });

View File

@ -6,6 +6,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple); createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(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"); AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); 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))); 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); createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(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"); AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); 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("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4"); AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
} }

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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,
}));
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -623,7 +624,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press tab", () => InputManager.Key(Key.Tab)); AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); 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)); AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false); 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"); AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero); AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.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("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.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"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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("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"); AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.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("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.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"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.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("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection => editorInfo.Selected.ValueChanged += selection =>
{ {
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one. // 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). // 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. // Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best.

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tournament.Screens.Ladder
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; 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) protected override void OnDrag(DragEvent e)
{ {

View File

@ -27,8 +27,17 @@ namespace osu.Game.Beatmaps.Drawables
set => base.Masking = value; 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; this.coverType = coverType;
InternalChild = new Box 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) 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) protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{ {

View File

@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap); base.ParseStreamInto(stream, beatmap);
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
flushPendingPoints(); flushPendingPoints();
// Objects may be out of order *only* if a user has manually edited an .osu file. // 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> /// <summary>
/// Processes the beatmap such that a new combo is started the first hitobject following each break. /// Processes the beatmap such that a new combo is started the first hitobject following each break.
/// </summary> /// </summary>
private void postProcessBreaks(Beatmap beatmap) private static void postProcessBreaks(Beatmap beatmap)
{ {
int currentBreak = 0; int currentBreak = 0;
bool forceNewCombo = false; bool forceNewCombo = false;
@ -161,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats
/// This method's intention is to restore those legacy defaults. /// 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 /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary> /// </summary>
private void applyLegacyDefaults(BeatmapInfo beatmapInfo) private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
{ {
beatmapInfo.WidescreenStoryboard = false; beatmapInfo.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false; beatmapInfo.SamplesMatchPlaybackRate = false;
@ -402,11 +424,11 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"SliderMultiplier": case @"SliderMultiplier":
difficulty.SliderMultiplier = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.4, 3.6); difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
break; break;
case @"SliderTickRate": case @"SliderTickRate":
difficulty.SliderTickRate = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.5, 8); difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
break; break;
} }
} }

View File

@ -202,7 +202,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
AddInternal(addOrRemoveButton = new IconButton AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
@ -271,6 +271,11 @@ namespace osu.Game.Collections
} }
protected override Drawable CreateContent() => (Content)base.CreateContent(); protected override Drawable CreateContent() => (Content)base.CreateContent();
private partial class NoFocusChangeIconButton : IconButton
{
public override bool ChangeFocusOnClick => false;
}
} }
} }
} }

View File

@ -137,7 +137,7 @@ namespace osu.Game.Collections
this.ScaleTo(0.9f, exit_duration); this.ScaleTo(0.9f, exit_duration);
// Ensure that textboxes commit // Ensure that textboxes commit
GetContainingInputManager()?.TriggerFocusContention(this); GetContainingFocusManager()?.TriggerFocusContention(this);
} }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using JetBrains.Annotations;
using Realms; using Realms;
using Realms.Schema; using Realms.Schema;
@ -15,8 +16,12 @@ namespace osu.Game.Database
{ {
private IList<T> emptySet => Array.Empty<T>(); private IList<T> emptySet => Array.Empty<T>();
[MustDisposeResource]
public IEnumerator<T> GetEnumerator() => emptySet.GetEnumerator(); public IEnumerator<T> GetEnumerator() => emptySet.GetEnumerator();
[MustDisposeResource]
IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator();
public int Count => emptySet.Count; public int Count => emptySet.Count;
public T this[int index] => emptySet[index]; public T this[int index] => emptySet[index];
public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item); public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item);

View File

@ -120,6 +120,7 @@ namespace osu.Game.Graphics
public static IconUsage Cross => get(OsuIconMapping.Cross); public static IconUsage Cross => get(OsuIconMapping.Cross);
public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle); public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle);
public static IconUsage Crown => get(OsuIconMapping.Crown); public static IconUsage Crown => get(OsuIconMapping.Crown);
public static IconUsage DailyChallenge => get(OsuIconMapping.DailyChallenge);
public static IconUsage Debug => get(OsuIconMapping.Debug); public static IconUsage Debug => get(OsuIconMapping.Debug);
public static IconUsage Delete => get(OsuIconMapping.Delete); public static IconUsage Delete => get(OsuIconMapping.Delete);
public static IconUsage Details => get(OsuIconMapping.Details); public static IconUsage Details => get(OsuIconMapping.Details);
@ -218,6 +219,9 @@ namespace osu.Game.Graphics
[Description(@"crown")] [Description(@"crown")]
Crown, Crown,
[Description(@"daily-challenge")]
DailyChallenge,
[Description(@"debug")] [Description(@"debug")]
Debug, Debug,

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Numerics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface 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. /// An <see cref="IExpandable"/> implementation for the UI slider bar control.
/// </summary> /// </summary>
public partial class ExpandableSlider<T, TSlider> : CompositeDrawable, IExpandable, IHasCurrentValue<T> 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() where TSlider : RoundedSliderBar<T>, new()
{ {
private readonly OsuSpriteText label; private readonly OsuSpriteText label;
@ -129,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface
/// An <see cref="IExpandable"/> implementation for the UI slider bar control. /// An <see cref="IExpandable"/> implementation for the UI slider bar control.
/// </summary> /// </summary>
public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>> public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
} }
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface
if (!allowImmediateFocus) if (!allowImmediateFocus)
return; return;
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this)); Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(this));
} }
public new void KillFocus() => base.KillFocus(); public new void KillFocus() => base.KillFocus();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Numerics;
using System.Globalization; using System.Globalization;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -15,7 +16,7 @@ using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public abstract partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip 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; public bool PlaySamplesOnAdjust { get; set; } = true;
@ -85,11 +86,11 @@ namespace osu.Game.Graphics.UserInterface
private LocalisableString getTooltipText(T value) private LocalisableString getTooltipText(T value)
{ {
if (CurrentNumber.IsInteger) 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()) // Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision); int significantDigits = FormatUtils.FindPrecision(decimalPrecision);

View File

@ -8,6 +8,8 @@ using System.Linq;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -143,13 +145,6 @@ namespace osu.Game.Graphics.UserInterface
FadeUnhovered(); FadeUnhovered();
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (accentColour == default)
AccentColour = colours.Blue;
}
public OsuTabItem(T value) public OsuTabItem(T value)
: base(value) : base(value)
{ {
@ -196,10 +191,21 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = 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() protected override void OnActivated()
{ {
Text.Font = Text.Font.With(weight: FontWeight.Bold); 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); Text.Font = Text.Font.With(weight: FontWeight.Medium);
FadeUnhovered(); FadeUnhovered();
} }
protected override void OnActivatedByUser() => selectSample.Play();
} }
} }
} }

View File

@ -7,6 +7,8 @@ using System;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -53,6 +55,8 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private Sample selectSample = null!;
public PageTabItem(T value) public PageTabItem(T value)
: base(value) : base(value)
{ {
@ -78,12 +82,18 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
Anchor = 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); 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 virtual LocalisableString CreateText() => (Value as Enum)?.GetLocalisableDescription() ?? Value.ToString();
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
@ -112,6 +122,8 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnActivated() => slideActive(); protected override void OnActivated() => slideActive();
protected override void OnDeactivated() => slideInactive(); protected override void OnDeactivated() => slideInactive();
protected override void OnActivatedByUser() => selectSample.Play();
} }
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osuTK; using System.Numerics;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -11,11 +11,12 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public partial class RoundedSliderBar<T> : OsuSliderBar<T> 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 Nub Nub;
protected readonly Box LeftBox; protected readonly Box LeftBox;

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osuTK; using System.Numerics;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -12,11 +12,12 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
using static osu.Game.Graphics.UserInterface.ShearedNub; using static osu.Game.Graphics.UserInterface.ShearedNub;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public partial class ShearedSliderBar<T> : OsuSliderBar<T> 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 ShearedNub Nub;
protected readonly Box LeftBox; protected readonly Box LeftBox;

View File

@ -1,14 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Numerics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class LabelledSliderBar<TNumber> : LabelledComponent<SettingsSlider<TNumber>, TNumber> 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() public LabelledSliderBar()
: base(true) : base(true)

View File

@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
base.OnFocus(e); base.OnFocus(e);
GetContainingInputManager().ChangeFocus(Component); GetContainingFocusManager().ChangeFocus(Component);
} }
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Numerics;
using System.Globalization; using System.Globalization;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,12 +10,12 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T> public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
/// <summary> /// <summary>
/// A custom step value for each key press which actuates a change on this control. /// 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); Current.BindValueChanged(updateTextBoxFromSlider, true);
} }
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox); public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
private bool updatingFromTextBox; private bool updatingFromTextBox;
@ -138,7 +138,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
if (updatingFromTextBox) return; 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)}"); textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -54,6 +54,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit"); 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}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

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

View File

@ -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"/>. /// Delivers an update of the <see cref="UserPresence"/> of the user with the supplied <paramref name="userId"/>.
/// </summary> /// </summary>
Task UserPresenceUpdated(int userId, UserPresence? status); 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);
} }
} }

View File

@ -7,7 +7,12 @@ using osu.Game.Users;
namespace osu.Game.Online.Metadata namespace osu.Game.Online.Metadata
{ {
/// <summary> /// <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> /// </summary>
public interface IMetadataServer public interface IMetadataServer
{ {

View File

@ -59,6 +59,15 @@ namespace osu.Game.Online.Metadata
#endregion #endregion
#region Daily Challenge
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
/// <inheritdoc/>
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
#endregion
#region Disconnection handling #region Disconnection handling
public event Action? Disconnecting; public event Action? Disconnecting;

View File

@ -26,6 +26,9 @@ namespace osu.Game.Online.Metadata
public override IBindableDictionary<int, UserPresence> UserStates => userStates; public override IBindableDictionary<int, UserPresence> UserStates => userStates;
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>(); 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 readonly string endpoint;
private IHubClientConnector? connector; private IHubClientConnector? connector;
@ -58,6 +61,7 @@ namespace osu.Game.Online.Metadata
// https://github.com/dotnet/aspnetcore/issues/15198 // https://github.com/dotnet/aspnetcore/issues/15198
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); 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); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
}; };
@ -101,6 +105,7 @@ namespace osu.Game.Online.Metadata
{ {
isWatchingUserPresence.Value = false; isWatchingUserPresence.Value = false;
userStates.Clear(); userStates.Clear();
dailyChallengeInfo.Value = null;
}); });
return; 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() public override async Task DisconnectRequested()
{ {
await base.DisconnectRequested().ConfigureAwait(false); await base.DisconnectRequested().ConfigureAwait(false);

View File

@ -13,5 +13,8 @@ namespace osu.Game.Online.Rooms
[Description("Featured Artist")] [Description("Featured Artist")]
FeaturedArtist, FeaturedArtist,
[Description("Daily Challenge")]
DailyChallenge,
} }
} }

View File

@ -75,6 +75,12 @@ namespace osu.Game
{ {
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" }; 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 OSU_PROTOCOL = "osu://";
public const string CLIENT_STREAM_NAME = @"lazer"; public const string CLIENT_STREAM_NAME = @"lazer";
@ -241,11 +247,7 @@ namespace osu.Game
public OsuGameBase() public OsuGameBase()
{ {
Name = @"osu!"; Name = GAME_NAME;
#if DEBUG
Name += " (development)";
#endif
allowableExceptions = UnhandledExceptionsBeforeCrash; allowableExceptions = UnhandledExceptionsBeforeCrash;
} }

View File

@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation
if (nextTextBox != null) if (nextTextBox != null)
{ {
Schedule(() => GetContainingInputManager().ChangeFocus(nextTextBox)); Schedule(() => GetContainingFocusManager().ChangeFocus(nextTextBox));
return true; return true;
} }

View File

@ -5,6 +5,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -47,13 +49,15 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } private OverlayColourProvider colourProvider { get; set; }
private Sample selectSample = null!;
public TabItem(BeatmapCardSize value) public TabItem(BeatmapCardSize value)
: base(value) : base(value)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Masking = true; Masking = true;
@ -79,8 +83,10 @@ namespace osu.Game.Overlays.BeatmapListing
Icon = getIconForCardSize(Value) Icon = getIconForCardSize(Value)
} }
}, },
new HoverClickSounds(HoverSampleSet.TabSelect) new HoverSounds(HoverSampleSet.TabSelect)
}; };
selectSample = audio.Samples.Get(@"UI/tabselect-select");
} }
private static IconUsage getIconForCardSize(BeatmapCardSize cardSize) private static IconUsage getIconForCardSize(BeatmapCardSize cardSize)
@ -111,6 +117,8 @@ namespace osu.Game.Overlays.BeatmapListing
updateState(); updateState();
} }
protected override void OnActivatedByUser() => selectSample.Play();
protected override void OnDeactivated() protected override void OnDeactivated()
{ {
if (IsLoaded) if (IsLoaded)

View File

@ -128,7 +128,11 @@ namespace osu.Game.Overlays.BeatmapListing
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
base.OnClick(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(); Active.Toggle();
SelectSample.Play();
return true; return true;
} }
} }

View File

@ -5,6 +5,8 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -24,13 +26,15 @@ namespace osu.Game.Overlays.BeatmapListing
private OsuSpriteText text; private OsuSpriteText text;
protected Sample SelectSample { get; private set; } = null!;
public FilterTabItem(T value) public FilterTabItem(T value)
: base(value) : base(value)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
@ -40,10 +44,12 @@ namespace osu.Game.Overlays.BeatmapListing
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
Text = LabelFor(Value) Text = LabelFor(Value)
}, },
new HoverClickSounds(HoverSampleSet.TabSelect) new HoverSounds(HoverSampleSet.TabSelect)
}); });
Enabled.Value = true; Enabled.Value = true;
SelectSample = audio.Samples.Get(@"UI/tabselect-select");
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -71,6 +77,8 @@ namespace osu.Game.Overlays.BeatmapListing
protected override void OnDeactivated() => UpdateState(); protected override void OnDeactivated() => UpdateState();
protected override void OnActivatedByUser() => SelectSample.Play();
/// <summary> /// <summary>
/// Returns the label text to be used for the supplied <paramref name="value"/>. /// Returns the label text to be used for the supplied <paramref name="value"/>.
/// </summary> /// </summary>

View File

@ -21,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet
// propagate value to tab items first to enable only available rulesets. // propagate value to tab items first to enable only available rulesets.
beatmapSet.Value = value; beatmapSet.Value = value;
SelectTab(TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)); Current.Value = TabContainer.TabItems.FirstOrDefault(t => t.Enabled.Value)?.Value;
} }
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete(); base.LoadComplete();
if (!TextBox.ReadOnly) if (!TextBox.ReadOnly)
GetContainingInputManager().ChangeFocus(TextBox); GetContainingFocusManager().ChangeFocus(TextBox);
} }
protected override void OnCommit(string text) protected override void OnCommit(string text)

View File

@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); Schedule(() => { GetContainingFocusManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); });
} }
} }
} }

View File

@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Login
} }
if (form != null) if (form != null)
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(form));
}); });
private void updateDropdownCurrent(UserStatus? status) private void updateDropdownCurrent(UserStatus? status)
@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
if (form != null) GetContainingInputManager().ChangeFocus(form); if (form != null) GetContainingFocusManager().ChangeFocus(form);
base.OnFocus(e); base.OnFocus(e);
} }
} }

View File

@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); }); Schedule(() => { GetContainingFocusManager().ChangeFocus(codeTextBox); });
} }
} }
} }

View File

@ -78,7 +78,7 @@ namespace osu.Game.Overlays
this.FadeIn(transition_time, Easing.OutQuint); this.FadeIn(transition_time, Easing.OutQuint);
FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(panel));
} }
protected override void PopOut() protected override void PopOut()

View File

@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
nameTextBox.Current.BindValueChanged(s => nameTextBox.Current.BindValueChanged(s =>
{ {

View File

@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)

View File

@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods
RequestScroll?.Invoke(this); 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. // 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; return true;
} }

View File

@ -11,6 +11,8 @@ using osuTK;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -65,6 +67,8 @@ namespace osu.Game.Overlays
private readonly SpriteIcon icon; private readonly SpriteIcon icon;
private Sample selectSample = null!;
public PanelDisplayTabItem(OverlayPanelDisplayStyle value) public PanelDisplayTabItem(OverlayPanelDisplayStyle value)
: base(value) : base(value)
{ {
@ -78,14 +82,22 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit 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 OnActivated() => updateState();
protected override void OnDeactivated() => updateState(); protected override void OnDeactivated() => updateState();
protected override void OnActivatedByUser() => selectSample.Play();
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
updateState(); updateState();

View File

@ -10,6 +10,8 @@ using osu.Game.Rulesets;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK; using osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -39,6 +41,8 @@ namespace osu.Game.Overlays
public LocalisableString TooltipText => Value.Name; public LocalisableString TooltipText => Value.Name;
private Sample selectSample = null!;
public OverlayRulesetTabItem(RulesetInfo value) public OverlayRulesetTabItem(RulesetInfo value)
: base(value) : base(value)
{ {
@ -59,12 +63,18 @@ namespace osu.Game.Overlays
Icon = value.CreateInstance().CreateIcon(), Icon = value.CreateInstance().CreateIcon(),
}, },
}, },
new HoverClickSounds() new HoverSounds(HoverSampleSet.TabSelect)
}); });
Enabled.Value = true; Enabled.Value = true;
} }
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
selectSample = audio.Samples.Get(@"UI/tabselect-select");
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -90,6 +100,8 @@ namespace osu.Game.Overlays
protected override void OnDeactivated() => updateState(); protected override void OnDeactivated() => updateState();
protected override void OnActivatedByUser() => selectSample.Play();
private void updateState() private void updateState()
{ {
AccentColour = Enabled.Value ? getActiveColour() : colourProvider.Foreground1; AccentColour = Enabled.Value ? getActiveColour() : colourProvider.Foreground1;

View File

@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK.Graphics; using osuTK.Graphics;
@ -49,8 +51,10 @@ namespace osu.Game.Overlays
Margin = new MarginPadding(PADDING); Margin = new MarginPadding(PADDING);
} }
private Sample selectSample;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours) private void load(OverlayColourProvider colourProvider, OsuColour colours, AudioManager audio)
{ {
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
@ -87,9 +91,11 @@ namespace osu.Game.Overlays
CollapsedSize = 2, CollapsedSize = 2,
Expanded = true Expanded = true
}, },
new HoverClickSounds() new HoverSounds(HoverSampleSet.TabSelect)
}); });
selectSample = audio.Samples.Get(@"UI/tabselect-select");
SelectedItem.BindValueChanged(_ => updateState(), true); SelectedItem.BindValueChanged(_ => updateState(), true);
} }
@ -105,6 +111,8 @@ namespace osu.Game.Overlays
protected override void OnDeactivated() => updateState(); protected override void OnDeactivated() => updateState();
protected override void OnActivatedByUser() => selectSample.Play();
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
updateState(); updateState();

View File

@ -4,6 +4,8 @@
#nullable disable #nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -80,6 +82,8 @@ namespace osu.Game.Overlays
} }
} }
private Sample selectSample = null!;
public OverlayTabItem(T value) public OverlayTabItem(T value)
: base(value) : base(value)
{ {
@ -101,10 +105,16 @@ namespace osu.Game.Overlays
ExpandedSize = 5f, ExpandedSize = 5f,
CollapsedSize = 0 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) protected override bool OnHover(HoverEvent e)
{ {
base.OnHover(e); base.OnHover(e);
@ -136,6 +146,8 @@ namespace osu.Game.Overlays
Text.Font = Text.Font.With(weight: FontWeight.Medium); Text.Font = Text.Font.With(weight: FontWeight.Medium);
} }
protected override void OnActivatedByUser() => selectSample.Play();
private void updateState() private void updateState()
{ {
if (Active.Value) if (Active.Value)

View File

@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
if (HasFocus) if (HasFocus)
GetContainingInputManager().ChangeFocus(null); GetContainingFocusManager().ChangeFocus(null);
cancelAndClearButtons.FadeOut(300, Easing.OutQuint); cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;

View File

@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault();
if (next != null) if (next != null)
GetContainingInputManager().ChangeFocus(next); GetContainingFocusManager().ChangeFocus(next);
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Numerics;
using System.Globalization; using System.Globalization;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; 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. /// A slider intended to show a "size" multiplier number, where 1x is 1.0.
/// </summary> /// </summary>
public partial class SizeSlider<T> : RoundedSliderBar<T> 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); public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x", NumberFormatInfo.CurrentInfo);
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Numerics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings
/// Mostly provided for convenience of use with <see cref="SettingSourceAttribute"/>. /// Mostly provided for convenience of use with <see cref="SettingSourceAttribute"/>.
/// </summary> /// </summary>
public partial class SettingsPercentageSlider<TValue> : SettingsSlider<TValue> 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); protected override Drawable CreateControl() => ((RoundedSliderBar<TValue>)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true);
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Numerics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -9,12 +9,12 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public partial class SettingsSlider<T> : SettingsSlider<T, RoundedSliderBar<T>> 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> 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() where TSlider : RoundedSliderBar<TValue>, new()
{ {
protected override Drawable CreateControl() => new TSlider protected override Drawable CreateControl() => new TSlider

View File

@ -201,7 +201,7 @@ namespace osu.Game.Overlays
searchTextBox.HoldFocus = false; searchTextBox.HoldFocus = false;
if (searchTextBox.HasFocus) if (searchTextBox.HasFocus)
GetContainingInputManager().ChangeFocus(null); GetContainingFocusManager().ChangeFocus(null);
} }
public override bool AcceptsFocus => true; public override bool AcceptsFocus => true;

View File

@ -8,10 +8,8 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -34,148 +32,16 @@ namespace osu.Game.Overlays.SkinEditor
UpdatePosition = updateDrawablePosition UpdatePosition = updateDrawablePosition
}; };
private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); public override SelectionScaleHandler CreateScaleHandler()
public override bool HandleScale(Vector2 scale, Anchor anchor)
{ {
Axes adjustAxis; var scaleHandler = new SkinSelectionScaleHandler
switch (anchor)
{ {
// for corners, adjust scale. UpdatePosition = updateDrawablePosition
case Anchor.TopLeft: };
case Anchor.TopRight:
case Anchor.BottomLeft:
case Anchor.BottomRight:
adjustAxis = Axes.Both;
break;
// for edges, adjust size. scaleHandler.PerformFlipFromScaleHandles += a => SelectionBox.PerformFlipFromScaleHandles(a);
// 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;
adjustAxis = Axes.Y; return scaleHandler;
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;
} }
public override bool HandleFlip(Direction direction, bool flipOverOrigin) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
@ -226,9 +92,6 @@ namespace osu.Game.Overlays.SkinEditor
{ {
base.OnSelectionChanged(); base.OnSelectionChanged();
SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X);
SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y);
SelectionBox.CanScaleDiagonally = true;
SelectionBox.CanFlipX = true; SelectionBox.CanFlipX = true;
SelectionBox.CanFlipY = true; SelectionBox.CanFlipY = true;
SelectionBox.CanReverse = false; SelectionBox.CanReverse = false;
@ -460,16 +323,5 @@ namespace osu.Game.Overlays.SkinEditor
drawable.Origin = localOrigin; drawable.Origin = localOrigin;
drawable.Position += offset; 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;
}
} }
} }

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

View File

@ -3,11 +3,8 @@
#nullable disable #nullable disable
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -24,8 +21,6 @@ namespace osu.Game.Overlays.Toolbar
{ {
protected Drawable ModeButtonLine { get; private set; } protected Drawable ModeButtonLine { get; private set; }
private readonly Dictionary<string, Sample> selectionSamples = new Dictionary<string, Sample>();
public ToolbarRulesetSelector() public ToolbarRulesetSelector()
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
@ -33,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load()
{ {
AddRangeInternal(new[] 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() protected override void LoadComplete()
@ -88,10 +80,6 @@ namespace osu.Game.Overlays.Toolbar
if (SelectedTab != null) if (SelectedTab != null)
{ {
ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 500, Easing.OutElasticQuarter); ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 500, Easing.OutElasticQuarter);
if (hasInitialPosition)
selectionSamples[SelectedTab.Value.ShortName]?.Play();
hasInitialPosition = true; hasInitialPosition = true;
} }
} }
@ -121,7 +109,7 @@ namespace osu.Game.Overlays.Toolbar
RulesetInfo found = Rulesets.AvailableRulesets.ElementAtOrDefault(requested); RulesetInfo found = Rulesets.AvailableRulesets.ElementAtOrDefault(requested);
if (found != null) if (found != null)
Current.Value = found; SelectItem(found);
return true; return true;
} }

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Toolbar
{ {
private readonly RulesetButton ruleset; private readonly RulesetButton ruleset;
private Sample? selectSample;
public ToolbarRulesetTabButton(RulesetInfo value) public ToolbarRulesetTabButton(RulesetInfo value)
: base(value) : base(value)
{ {
@ -34,10 +38,18 @@ namespace osu.Game.Overlays.Toolbar
ruleset.SetIcon(rInstance.CreateIcon()); 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 OnActivated() => ruleset.Active = true;
protected override void OnDeactivated() => ruleset.Active = false; protected override void OnDeactivated() => ruleset.Active = false;
protected override void OnActivatedByUser() => selectSample?.Play();
private partial class RulesetButton : ToolbarButton private partial class RulesetButton : ToolbarButton
{ {
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(); protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();

View File

@ -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 public float MaxValue
{ {

View File

@ -15,7 +15,6 @@ using osu.Framework.Extensions.ListExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -632,7 +631,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
#endregion #endregion
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; public override bool UpdateSubTreeMasking() => false;
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {

View File

@ -129,6 +129,13 @@ namespace osu.Game.Rulesets.Scoring
OnIterationFail?.Invoke($"FAILED drop {testDrop}: recovery too low ({recovery} < {hpRecoveryAvailable})"); 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) if (!fail)
{ {
OnIterationSuccess?.Invoke($"PASSED drop {testDrop}"); OnIterationSuccess?.Invoke($"PASSED drop {testDrop}");

View File

@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.UI
break; break;
base.UpdateSubTree(); base.UpdateSubTree();
UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); UpdateSubTreeMasking();
} while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds); } while (state == PlaybackState.RequiresCatchUp && stopwatch.ElapsedMilliseconds < max_catchup_milliseconds);
return true; return true;

View File

@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.LoadComplete(); base.LoadComplete();
GetContainingInputManager().ChangeFocus(this); GetContainingFocusManager().ChangeFocus(this);
SelectAll(); SelectAll();
} }
} }

View File

@ -27,7 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved] [Resolved]
private SelectionRotationHandler? rotationHandler { get; set; } 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<Direction, bool, bool>? OnFlip;
public Func<bool>? OnReverse; public Func<bool>? OnReverse;
@ -57,60 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly IBindable<bool> canRotate = new BindableBool(); private readonly IBindable<bool> canRotate = new BindableBool();
private bool canScaleX; private readonly IBindable<bool> canScaleX = new BindableBool();
/// <summary> private readonly IBindable<bool> canScaleY = new BindableBool();
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
/// </summary>
public bool CanScaleX
{
get => canScaleX;
set
{
if (canScaleX == value) return;
canScaleX = value; private readonly IBindable<bool> canScaleDiagonally = new BindableBool();
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 bool canFlipX; private bool canFlipX;
@ -176,7 +129,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (rotationHandler != null) if (rotationHandler != null)
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin); 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) protected override bool OnKeyDown(KeyDownEvent e)
@ -265,9 +228,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
}; };
if (CanScaleX) addXScaleComponents(); if (canScaleX.Value) addXScaleComponents();
if (CanScaleDiagonally) addFullScaleComponents(); if (canScaleDiagonally.Value) addFullScaleComponents();
if (CanScaleY) addYScaleComponents(); if (canScaleY.Value) addYScaleComponents();
if (CanFlipX) addXFlipComponents(); if (CanFlipX) addXFlipComponents();
if (CanFlipY) addYFlipComponents(); if (CanFlipY) addYFlipComponents();
if (canRotate.Value) addRotationComponents(); if (canRotate.Value) addRotationComponents();
@ -353,7 +316,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
var handle = new SelectionBoxScaleHandle var handle = new SelectionBoxScaleHandle
{ {
Anchor = anchor, Anchor = anchor,
HandleScale = (delta, a) => OnScale?.Invoke(delta, a)
}; };
handle.OperationStarted += operationStarted; handle.OperationStarted += operationStarted;

View File

@ -1,19 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle
{ {
public Action<Vector2, Anchor> HandleScale { get; set; } [Resolved]
private SelectionScaleHandler? scaleHandler { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -21,10 +23,108 @@ namespace osu.Game.Screens.Edit.Compose.Components
Size = new Vector2(10); 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) protected override void OnDrag(DragEvent e)
{ {
HandleScale?.Invoke(e.Delta, Anchor);
base.OnDrag(e); 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);
}
} }

View File

@ -57,6 +57,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public SelectionRotationHandler RotationHandler { get; private set; } public SelectionRotationHandler RotationHandler { get; private set; }
public SelectionScaleHandler ScaleHandler { get; private set; }
protected SelectionHandler() protected SelectionHandler()
{ {
selectedBlueprints = new List<SelectionBlueprint<T>>(); selectedBlueprints = new List<SelectionBlueprint<T>>();
@ -69,6 +71,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(RotationHandler = CreateRotationHandler()); dependencies.CacheAs(RotationHandler = CreateRotationHandler());
dependencies.CacheAs(ScaleHandler = CreateScaleHandler());
return dependencies; return dependencies;
} }
@ -78,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
RotationHandler, RotationHandler,
ScaleHandler,
SelectionBox = CreateSelectionBox(), SelectionBox = CreateSelectionBox(),
}); });
@ -93,7 +97,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
OperationStarted = OnOperationBegan, OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded, OperationEnded = OnOperationEnded,
OnScale = HandleScale,
OnFlip = HandleFlip, OnFlip = HandleFlip,
OnReverse = HandleReverse, OnReverse = HandleReverse,
}; };
@ -157,6 +160,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether any items could be scaled.</returns> /// <returns>Whether any items could be scaled.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; 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> /// <summary>
/// Handles the selected items being flipped. /// Handles the selected items being flipped.
/// </summary> /// </summary>

View File

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

View File

@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(sliderVelocitySlider)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(sliderVelocitySlider));
} }
} }
} }

View File

@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.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; private static string? getCommonBank(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null;

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -20,7 +19,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public partial class TimelineTickDisplay : TimelinePart<PointVisualisation> 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. // 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] [Resolved]
private EditorBeatmap beatmap { get; set; } = null!; private EditorBeatmap beatmap { get; set; } = null!;

View File

@ -372,7 +372,7 @@ namespace osu.Game.Screens.Edit
} }
} }
}, },
new EditorScreenSwitcherControl screenSwitcher = new EditorScreenSwitcherControl
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
@ -662,23 +662,23 @@ namespace osu.Game.Screens.Edit
return true; return true;
case GlobalAction.EditorComposeMode: case GlobalAction.EditorComposeMode:
Mode.Value = EditorScreenMode.Compose; screenSwitcher.SelectItem(EditorScreenMode.Compose);
return true; return true;
case GlobalAction.EditorDesignMode: case GlobalAction.EditorDesignMode:
Mode.Value = EditorScreenMode.Design; screenSwitcher.SelectItem(EditorScreenMode.Design);
return true; return true;
case GlobalAction.EditorTimingMode: case GlobalAction.EditorTimingMode:
Mode.Value = EditorScreenMode.Timing; screenSwitcher.SelectItem(EditorScreenMode.Timing);
return true; return true;
case GlobalAction.EditorSetupMode: case GlobalAction.EditorSetupMode:
Mode.Value = EditorScreenMode.SongSetup; screenSwitcher.SelectItem(EditorScreenMode.SongSetup);
return true; return true;
case GlobalAction.EditorVerifyMode: case GlobalAction.EditorVerifyMode:
Mode.Value = EditorScreenMode.Verify; screenSwitcher.SelectItem(EditorScreenMode.Verify);
return true; return true;
case GlobalAction.EditorTestGameplay: case GlobalAction.EditorTestGameplay:
@ -959,6 +959,8 @@ namespace osu.Game.Screens.Edit
[CanBeNull] [CanBeNull]
private ScheduledDelegate playbackDisabledDebounce; private ScheduledDelegate playbackDisabledDebounce;
private EditorScreenSwitcherControl screenSwitcher;
private void updateSampleDisabledState() private void updateSampleDisabledState()
{ {
bool shouldDisableSamples = clock.SeekingOrStopped.Value bool shouldDisableSamples = clock.SeekingOrStopped.Value

View File

@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup
OnFocused?.Invoke(); OnFocused?.Invoke();
base.OnFocus(e); base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this); GetContainingFocusManager().TriggerFocusContention(this);
} }
} }
} }

View File

@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup
base.LoadComplete(); base.LoadComplete();
if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) if (string.IsNullOrEmpty(ArtistTextBox.Current.Value))
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(ArtistTextBox));
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System.Numerics;
using System.Globalization; using System.Globalization;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -12,7 +12,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using Vector2 = osuTK.Vector2;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
@ -22,7 +22,7 @@ namespace osu.Game.Screens.Edit.Timing
/// by providing an "indeterminate state". /// by providing an "indeterminate state".
/// </summary> /// </summary>
public partial class IndeterminateSliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T?> public partial class IndeterminateSliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T?>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible where T : struct, INumber<T>, IMinMaxValue<T>
{ {
/// <summary> /// <summary>
/// A custom step value for each key press which actuates a change on this control. /// 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) protected override void OnFocus(FocusEvent e)
{ {
base.OnFocus(e); base.OnFocus(e);
GetContainingInputManager().ChangeFocus(textBox); GetContainingFocusManager().ChangeFocus(textBox);
} }
private void updateState() private void updateState()
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Timing
slider.Current.Value = nonNullValue; 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. // 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.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
textBox.PlaceholderText = string.Empty; textBox.PlaceholderText = string.Empty;
} }

View File

@ -24,6 +24,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -46,6 +47,7 @@ namespace osu.Game.Screens.Menu
public Action? OnSettings; public Action? OnSettings;
public Action? OnMultiplayer; public Action? OnMultiplayer;
public Action? OnPlaylists; public Action? OnPlaylists;
public Action<Room>? OnDailyChallenge;
private readonly IBindable<bool> isIdle = new BindableBool(); private readonly IBindable<bool> isIdle = new BindableBool();
@ -102,10 +104,13 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] 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), new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), 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)
{ {
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, VisibleStateMin = ButtonSystemState.Play,
VisibleStateMax = ButtonSystemState.Edit, VisibleStateMax = ButtonSystemState.Edit,
}, },
@ -127,21 +132,31 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) 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.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), 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)); 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); 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(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), 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)); {
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); 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.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, 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)); 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) 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(buttonsPlay);
buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsEdit);
@ -164,7 +179,7 @@ namespace osu.Game.Screens.Menu
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
} }
private void onMultiplayer() private void onMultiplayer(MainMenuButton _)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {
@ -175,7 +190,7 @@ namespace osu.Game.Screens.Menu
OnMultiplayer?.Invoke(); OnMultiplayer?.Invoke();
} }
private void onPlaylists() private void onPlaylists(MainMenuButton _)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {
@ -186,6 +201,20 @@ namespace osu.Game.Screens.Menu
OnPlaylists?.Invoke(); 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) private void updateIdleState(bool isIdle)
{ {
if (!ReturnToTopOnIdle) if (!ReturnToTopOnIdle)

View 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