mirror of
https://github.com/ppy/osu.git
synced 2024-11-15 16:27:43 +08:00
Merge remote-tracking branch 'upstream/master' into grids-4
This commit is contained in:
commit
f8397cccc7
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -135,5 +135,8 @@ jobs:
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install maui-ios
|
||||
|
||||
- name: Select Xcode 16
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.916.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.927.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -26,7 +26,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="8.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.626" />
|
||||
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
29
osu.Game.Benchmarks/BenchmarkGeometryUtils.cs
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkGeometryUtils : BenchmarkTest
|
||||
{
|
||||
[Params(100, 1000, 2000, 4000, 8000, 10000)]
|
||||
public int N;
|
||||
|
||||
private Vector2[] points = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
points = new Vector2[N];
|
||||
|
||||
for (int i = 0; i < points.Length; ++i)
|
||||
points[i] = new Vector2(RNG.Next(512), RNG.Next(384));
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void MinimumEnclosingCircle() => GeometryUtils.MinimumEnclosingCircle(points);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
@ -172,7 +173,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||
{
|
||||
editablePath.AddVertex(rightMouseDownPosition);
|
||||
});
|
||||
})
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -114,6 +114,26 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
handleToggleViaKey(e);
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
handleToggleViaKey(e);
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
private void handleToggleViaKey(KeyboardEvent key)
|
||||
{
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -92,5 +93,30 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("second object flipped", () => second.StartTime, () => Is.EqualTo(250));
|
||||
AddAssert("third object flipped", () => third.StartTime, () => Is.EqualTo(1250));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOffScreenObjectsRemainSelectedOnColumnChange()
|
||||
{
|
||||
AddStep("create objects", () =>
|
||||
{
|
||||
for (int i = 0; i < 20; ++i)
|
||||
EditorBeatmap.Add(new Note { StartTime = 1000 * i, Column = 0 });
|
||||
});
|
||||
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
AddStep("start drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First());
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("end drag", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last());
|
||||
InputManager.ReleaseButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("all objects in last column", () => EditorBeatmap.HitObjects.All(ho => ((ManiaHitObject)ho).Column == 3));
|
||||
AddAssert("all objects remain selected", () => EditorBeatmap.SelectedHitObjects.SequenceEqual(EditorBeatmap.HitObjects));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,8 +104,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
int minColumn = int.MaxValue;
|
||||
int maxColumn = int.MinValue;
|
||||
|
||||
var selectedObjects = EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>().ToArray();
|
||||
|
||||
// find min/max in an initial pass before actually performing the movement.
|
||||
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
|
||||
foreach (var obj in selectedObjects)
|
||||
{
|
||||
if (obj.Column < minColumn)
|
||||
minColumn = obj.Column;
|
||||
@ -121,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
((ManiaHitObject)h).Column += columnDelta;
|
||||
maniaPlayfield.Add(h);
|
||||
});
|
||||
|
||||
// `HitObjectUsageEventBuffer`'s usage transferal flows and the playfield's `SetKeepAlive()` functionality do not combine well with this operation's usage pattern,
|
||||
// leading to selections being sometimes partially dropped if some of the objects being moved are off screen
|
||||
// (check blame for detailed explanation).
|
||||
// thus, ensure that selection is preserved manually.
|
||||
EditorBeatmap.SelectedHitObjects.Clear();
|
||||
EditorBeatmap.SelectedHitObjects.AddRange(selectedObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (segment.Count == 0)
|
||||
return;
|
||||
|
||||
var first = segment[0];
|
||||
PathControlPoint first = segment[0];
|
||||
|
||||
if (first.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
@ -273,10 +273,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (selectedPieces.Length != 1)
|
||||
return false;
|
||||
|
||||
var selectedPiece = selectedPieces.Single();
|
||||
var selectedPoint = selectedPiece.ControlPoint;
|
||||
PathControlPointPiece<T> selectedPiece = selectedPieces.Single();
|
||||
PathControlPoint selectedPoint = selectedPiece.ControlPoint;
|
||||
|
||||
var validTypes = path_types;
|
||||
PathType?[] validTypes = path_types;
|
||||
|
||||
if (selectedPoint == controlPoints[0])
|
||||
validTypes = validTypes.Where(t => t != null).ToArray();
|
||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (Pieces.All(p => !p.IsSelected.Value))
|
||||
return false;
|
||||
|
||||
var type = path_types[e.Key - Key.Number1];
|
||||
PathType? type = path_types[e.Key - Key.Number1];
|
||||
|
||||
// The first control point can never be inherit type
|
||||
if (Pieces[0].IsSelected.Value && type == null)
|
||||
@ -353,9 +353,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
double originalDistance = hitObject.Path.Distance;
|
||||
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
{
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
List<PathControlPoint> pointsInSegment = hitObject.Path.PointsInSegment(p.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(p.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
@ -375,6 +377,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
EnsureValidPathTypes();
|
||||
|
||||
if (hitObject.Path.Distance < originalDistance)
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
else
|
||||
hitObject.Path.ExpectedDistance.Value = originalDistance;
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
@ -405,14 +412,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public void DragInProgress(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = hitObject.Position;
|
||||
Vector2 oldPosition = hitObject.Position;
|
||||
double oldStartTime = hitObject.StartTime;
|
||||
|
||||
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
||||
{
|
||||
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
||||
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||
|
||||
@ -421,7 +428,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
var controlPoint = hitObject.Path.ControlPoints[i];
|
||||
PathControlPoint controlPoint = hitObject.Path.ControlPoints[i];
|
||||
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
||||
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
||||
// All other selected control points (if any) will move together with the head point
|
||||
@ -432,13 +439,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
|
||||
|
||||
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
|
||||
for (int i = 0; i < controlPoints.Count; ++i)
|
||||
{
|
||||
var controlPoint = controlPoints[i];
|
||||
PathControlPoint controlPoint = controlPoints[i];
|
||||
if (selectedControlPoints.Contains(controlPoint))
|
||||
controlPoint.Position = dragStartPositions[i] + movementDelta;
|
||||
}
|
||||
@ -488,8 +495,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
curveTypeItems = new List<MenuItem>();
|
||||
|
||||
foreach (PathType? type in path_types)
|
||||
for (int i = 0; i < path_types.Length; ++i)
|
||||
{
|
||||
PathType? type = path_types[i];
|
||||
|
||||
// special inherit case
|
||||
if (type == null)
|
||||
{
|
||||
@ -499,7 +508,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
curveTypeItems.Add(new OsuMenuItemSpacer());
|
||||
}
|
||||
|
||||
curveTypeItems.Add(createMenuItemForPathType(type));
|
||||
curveTypeItems.Add(createMenuItemForPathType(type, InputKey.Number1 + i));
|
||||
}
|
||||
|
||||
if (selectedPieces.Any(piece => piece.ControlPoint.Type?.Type == SplineType.Catmull))
|
||||
@ -533,7 +542,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
return menuItems.ToArray();
|
||||
|
||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type) => new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type));
|
||||
CurveTypeMenuItem createMenuItemForPathType(PathType? type, InputKey? key = null)
|
||||
{
|
||||
Hotkey hotkey = default;
|
||||
|
||||
if (key != null)
|
||||
hotkey = new Hotkey(new KeyCombination(InputKey.Alt, key.Value));
|
||||
|
||||
return new CurveTypeMenuItem(type, _ => updatePathTypeOfSelectedPieces(type)) { Hotkey = hotkey };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -401,7 +401,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (state == SliderPlacementState.Drawing)
|
||||
HitObject.Path.ExpectedDistance.Value = (float)HitObject.Path.CalculatedDistance;
|
||||
else
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? (float)HitObject.Path.CalculatedDistance;
|
||||
|
||||
bodyPiece.UpdateFrom(HitObject);
|
||||
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
@ -269,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1, DistanceSnapTarget.Start) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
@ -593,8 +594,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
changeHandler?.BeginChange();
|
||||
addControlPoint(lastRightClickPosition);
|
||||
changeHandler?.EndChange();
|
||||
}),
|
||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
||||
})
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
},
|
||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
|
||||
},
|
||||
};
|
||||
|
||||
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
@ -87,6 +88,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private ExpandableSlider<float> spacingSlider = null!;
|
||||
private ExpandableSlider<float> gridLinesRotationSlider = null!;
|
||||
private RoundedButton gridFromPointsButton = null!;
|
||||
private ExpandableButton useSelectedObjectPositionButton = null!;
|
||||
private EditorRadioButtonCollection gridTypeButtons = null!;
|
||||
|
||||
public event Action? GridFromPointsClicked;
|
||||
@ -133,6 +135,19 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Current = StartPositionY,
|
||||
KeyboardStep = 1,
|
||||
},
|
||||
useSelectedObjectPositionButton = new ExpandableButton
|
||||
{
|
||||
ExpandedLabelText = "Centre on selected object",
|
||||
Action = () =>
|
||||
{
|
||||
if (editorBeatmap.SelectedHitObjects.Count != 1)
|
||||
return;
|
||||
|
||||
StartPosition.Value = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
|
||||
updateEnabledStates();
|
||||
},
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
spacingSlider = new ExpandableSlider<float>
|
||||
{
|
||||
Current = Spacing,
|
||||
@ -225,14 +240,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
|
||||
}, true);
|
||||
|
||||
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||
{
|
||||
gridFromPointsButton.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||
gridFromPointsButton.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||
}, true);
|
||||
|
||||
GridType.BindValueChanged(v =>
|
||||
{
|
||||
GridLinesRotation.Disabled = v.NewValue == PositionSnapGridType.Circle;
|
||||
@ -252,6 +259,24 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
editorBeatmap.BeatmapReprocessed += updateEnabledStates;
|
||||
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates());
|
||||
expandingContainer?.Expanded.BindValueChanged(v =>
|
||||
{
|
||||
gridFromPointsButton.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||
gridFromPointsButton.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
|
||||
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
|
||||
updateEnabledStates();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void updateEnabledStates()
|
||||
{
|
||||
useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true
|
||||
&& editorBeatmap.SelectedHitObjects.Count == 1
|
||||
&& StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
|
||||
}
|
||||
|
||||
private float normalizeRotation(float rotation, float period)
|
||||
|
@ -371,6 +371,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
gridSnapMomentary = shiftPressed;
|
||||
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||
}
|
||||
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
||||
|
@ -47,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
|
||||
@ -61,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedMovableObjects.ToArray();
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
||||
DefaultOrigin = GeometryUtils.MinimumEnclosingCircle(objectsInRotation).Item1;
|
||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (!OperationInProgress.Value)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
||||
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && DefaultOrigin != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
Vector2 actualOrigin = origin ?? DefaultOrigin.Value;
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
@ -103,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
defaultOrigin = null;
|
||||
DefaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
|
@ -84,10 +84,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
|
||||
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => slider.Position + p.Position))
|
||||
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
|
||||
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
|
||||
originalConvexHull = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider2
|
||||
? GeometryUtils.GetConvexHull(slider2.Path.ControlPoints.Select(p => slider2.Position + p.Position))
|
||||
: GeometryUtils.GetConvexHull(objectsInScale.Keys);
|
||||
defaultOrigin = GeometryUtils.MinimumEnclosingCircle(originalConvexHull).Item1;
|
||||
}
|
||||
|
||||
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)
|
||||
|
@ -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 NUnit.Framework;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Argon;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class VolumeAwareHitSampleInfoTest
|
||||
{
|
||||
[Test]
|
||||
public void TestVolumeAwareHitSampleInfoIsNotEqualToItsUnderlyingSample(
|
||||
[Values(HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP)]
|
||||
string sample,
|
||||
[Values(HitSampleInfo.BANK_NORMAL, HitSampleInfo.BANK_SOFT)]
|
||||
string bank,
|
||||
[Values(30, 70, 100)] int volume)
|
||||
{
|
||||
var underlyingSample = new HitSampleInfo(sample, bank, volume: volume);
|
||||
var volumeAwareSample = new VolumeAwareHitSampleInfo(underlyingSample);
|
||||
|
||||
Assert.That(underlyingSample, Is.Not.EqualTo(volumeAwareSample));
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -86,10 +87,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
{
|
||||
if (selection.All(s => s.Item is Hit))
|
||||
yield return new TernaryStateToggleMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
||||
{
|
||||
yield return new TernaryStateToggleMenuItem("Rim")
|
||||
{
|
||||
State = { BindTarget = selectionRimState },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.W), new KeyCombination(InputKey.R)),
|
||||
};
|
||||
}
|
||||
|
||||
if (selection.All(s => s.Item is TaikoHitObject))
|
||||
yield return new TernaryStateToggleMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
||||
{
|
||||
yield return new TernaryStateToggleMenuItem("Strong")
|
||||
{
|
||||
State = { BindTarget = selectionStrongState },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.E)),
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
@ -48,5 +49,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
return originalBank;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(HitSampleInfo? other) => other is VolumeAwareHitSampleInfo && base.Equals(other);
|
||||
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This override attempts to match the <see cref="Equals"/> override above, but in theory it is not strictly necessary.
|
||||
/// Recall that <see cref="GetHashCode"/> <a href="https://learn.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-8.0#notes-to-inheritors">must meet the following requirements</a>:
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// "If two objects compare as equal, the <see cref="GetHashCode"/> method for each object must return the same value.
|
||||
/// However, if two objects do not compare as equal, <see cref="GetHashCode"/> methods for the two objects do not have to return different values."
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Making this override combine the value generated by the base <see cref="GetHashCode"/> implementation with a constant means
|
||||
/// that <see cref="HitSampleInfo"/> and <see cref="VolumeAwareHitSampleInfo"/> instances which have the same values of their members
|
||||
/// will not have equal hash codes, which is slightly more efficient when these objects are used as dictionary keys.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), 1);
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,7 @@ namespace osu.Game.Tests.Editing
|
||||
{
|
||||
SliderVelocityMultiplier = slider_velocity
|
||||
};
|
||||
AddStep("add to beatmap", () => composer.EditorBeatmap.Add(referenceObject));
|
||||
|
||||
assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
|
||||
assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
|
||||
@ -227,26 +228,65 @@ namespace osu.Game.Tests.Editing
|
||||
assertSnappedDistance(400, 400);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnsnappedObject()
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
// simulate object snapped to 1/3rds
|
||||
// this object's end time will be 2000 / 3 = 666.66... ms
|
||||
new PathControlPoint(new Vector2(200 / 3f, 0)),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep("add slider", () => composer.EditorBeatmap.Add(slider));
|
||||
AddStep("set snap to 1/4", () => BeatDivisor.Value = 4);
|
||||
|
||||
// with default beat length of 1000ms and snap at 1/4, the valid snap times are 500ms, 750ms, and 1000ms
|
||||
// with default settings, the snapped distance will be a tenth of the difference of the time delta
|
||||
|
||||
// (500 - 666.66...) / 10 = -16.66... = -100 / 6
|
||||
assertSnappedDistance(0, -100 / 6f, slider);
|
||||
assertSnappedDistance(7, -100 / 6f, slider);
|
||||
|
||||
// (750 - 666.66...) / 10 = 8.33... = 100 / 12
|
||||
assertSnappedDistance(9, 100 / 12f, slider);
|
||||
assertSnappedDistance(33, 100 / 12f, slider);
|
||||
|
||||
// (1000 - 666.66...) / 10 = 33.33... = 100 / 3
|
||||
assertSnappedDistance(34, 100 / 3f, slider);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseCurrentSnap()
|
||||
{
|
||||
ExpandableButton getCurrentSnapButton() => composer.ChildrenOfType<EditorToolboxGroup>().Single(g => g.Name == "snapping")
|
||||
.ChildrenOfType<ExpandableButton>().Single();
|
||||
|
||||
AddStep("add objects to beatmap", () =>
|
||||
{
|
||||
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
|
||||
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
|
||||
});
|
||||
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
|
||||
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
|
||||
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(getCurrentSnapButton()));
|
||||
AddUntilStep("use current snap expanded", () => getCurrentSnapButton().Expanded.Value, () => Is.True);
|
||||
|
||||
AddStep("seek before first object", () => EditorClock.Seek(0));
|
||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
||||
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||
|
||||
AddStep("seek to between objects", () => EditorClock.Seek(1500));
|
||||
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
|
||||
AddUntilStep("use current snap available", () => getCurrentSnapButton().Enabled.Value, () => Is.True);
|
||||
|
||||
AddStep("seek after last object", () => EditorClock.Seek(2500));
|
||||
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
|
||||
AddUntilStep("use current snap not available", () => getCurrentSnapButton().Enabled.Value, () => Is.False);
|
||||
}
|
||||
|
||||
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
|
||||
@ -262,7 +302,7 @@ namespace osu.Game.Tests.Editing
|
||||
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
|
||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance, DistanceSnapTarget.End), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
|
||||
|
||||
private partial class TestHitObjectComposer : OsuHitObjectComposer
|
||||
{
|
||||
|
@ -29,5 +29,23 @@ namespace osu.Game.Tests.Utils
|
||||
|
||||
Assert.That(hull, Is.EquivalentTo(expectedPoints));
|
||||
}
|
||||
|
||||
[TestCase(new int[] { }, 0, 0, 0)]
|
||||
[TestCase(new[] { 0, 0 }, 0, 0, 0)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0 }, 1, 0, 1)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 1, -1, 2, 0, 1, 0 }, 1, 0, 1)]
|
||||
[TestCase(new[] { 0, 0, 1, 1, 2, -1, 2, 0, 1, 0, 4, 10 }, 3, 4.5f, 5.5901699f)]
|
||||
public void TestMinimumEnclosingCircle(int[] values, float x, float y, float r)
|
||||
{
|
||||
var points = new Vector2[values.Length / 2];
|
||||
for (int i = 0; i < values.Length; i += 2)
|
||||
points[i / 2] = new Vector2(values[i], values[i + 1]);
|
||||
|
||||
(var centre, float radius) = GeometryUtils.MinimumEnclosingCircle(points);
|
||||
|
||||
Assert.That(centre.X, Is.EqualTo(x).Within(0.0001));
|
||||
Assert.That(centre.Y, Is.EqualTo(y).Within(0.0001));
|
||||
Assert.That(radius, Is.EqualTo(r).Within(0.0001));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
initialRotation = targetContainer!.Rotation;
|
||||
DefaultOrigin = ToLocalSpace(targetContainer.ToScreenSpace(Vector2.Zero));
|
||||
|
||||
base.Begin();
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
@ -36,6 +37,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private ContextMenuContainer contextMenuContainer
|
||||
=> Editor.ChildrenOfType<ContextMenuContainer>().First();
|
||||
|
||||
private SelectionBoxScaleHandle getScaleHandle(Anchor anchor)
|
||||
=> Editor.ChildrenOfType<SelectionBoxScaleHandle>().First(it => it.Anchor == anchor);
|
||||
|
||||
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||
{
|
||||
AddStep("move mouse to object", () =>
|
||||
@ -215,6 +219,51 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiSelectWithDragBox()
|
||||
{
|
||||
var addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(100) },
|
||||
new HitCircle { StartTime = 300, Position = new Vector2(512, 0) },
|
||||
new HitCircle { StartTime = 400, Position = new Vector2(412, 100) },
|
||||
};
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
|
||||
|
||||
AddStep("start dragging", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopLeft - new Vector2(5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||
|
||||
AddStep("start dragging with control", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
|
||||
|
||||
AddAssert("4 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(4));
|
||||
|
||||
AddStep("start dragging without control", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
AddStep("drag to left corner", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.TopRight + new Vector2(5, -5)));
|
||||
AddStep("end dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNearestSelection()
|
||||
{
|
||||
@ -519,5 +568,137 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShiftModifierMaintainsAspectRatio()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
float aspectRatioBeforeDrag = 0;
|
||||
|
||||
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
aspectRatioBeforeDrag = getAspectRatio();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAltModifierScalesAroundCenter()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||
|
||||
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
centerBeforeDrag = getCenter();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
|
||||
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShiftAndAltModifierKeys()
|
||||
{
|
||||
HitCircle[] addedObjects = null!;
|
||||
|
||||
float aspectRatioBeforeDrag = 0;
|
||||
|
||||
Vector2 centerBeforeDrag = Vector2.Zero;
|
||||
|
||||
float getAspectRatio() => (addedObjects[1].X - addedObjects[0].X) / (addedObjects[1].Y - addedObjects[0].Y);
|
||||
|
||||
Vector2 getCenter() => (addedObjects[0].Position + addedObjects[1].Position) / 2;
|
||||
|
||||
AddStep("add hitobjects", () =>
|
||||
{
|
||||
EditorBeatmap.AddRange(addedObjects = new[]
|
||||
{
|
||||
new HitCircle { StartTime = 100, Position = new Vector2(150, 150) },
|
||||
new HitCircle { StartTime = 200, Position = new Vector2(250, 200) },
|
||||
});
|
||||
|
||||
aspectRatioBeforeDrag = getAspectRatio();
|
||||
centerBeforeDrag = getCenter();
|
||||
});
|
||||
|
||||
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
|
||||
|
||||
AddStep("move mouse to handle", () => InputManager.MoveMouseTo(getScaleHandle(Anchor.BottomRight).ScreenSpaceDrawQuad.Centre));
|
||||
|
||||
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + new Vector2(50, 0)));
|
||||
|
||||
AddStep("aspect ratio does not equal", () => Assert.AreNotEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("aspect ratio does equal", () => Assert.AreEqual(aspectRatioBeforeDrag, getAspectRatio()));
|
||||
|
||||
AddStep("center does not equal", () => Assert.AreNotEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("press alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
|
||||
AddStep("center does equal", () => Assert.AreEqual(centerBeforeDrag, getCenter()));
|
||||
|
||||
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK.Input;
|
||||
@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
beatmapSetHashBefore = Beatmap.Value.BeatmapSetInfo.Hash;
|
||||
});
|
||||
|
||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
||||
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||
|
||||
if (i == 11)
|
||||
{
|
||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
EditorBeatmap.EndChange();
|
||||
});
|
||||
|
||||
AddStep("click File", () => this.ChildrenOfType<DrawableOsuMenuItem>().First().TriggerClick());
|
||||
AddStep("click File", () => this.ChildrenOfType<EditorMenuBar.DrawableEditorBarMenuItem>().First().TriggerClick());
|
||||
|
||||
AddStep("click delete", () => getDeleteMenuItem().TriggerClick());
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||
|
@ -199,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
|
||||
|
||||
public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
|
||||
public float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target) => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,20 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUrlDecodingOfArgs()
|
||||
{
|
||||
setUpEditor(new OsuRuleset().RulesetInfo);
|
||||
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("jump to encoded link", () => Game.HandleLink("osu://edit/00:14:142%20(1)"));
|
||||
|
||||
AddUntilStep("wait for seek", () => editorClock.SeekingOrStopped.Value);
|
||||
|
||||
AddAssert("time is correct", () => editorClock.CurrentTime, () => Is.EqualTo(14_142));
|
||||
AddAssert("selected object is correct", () => editorBeatmap.SelectedHitObjects.Single().StartTime, () => Is.EqualTo(14_142));
|
||||
}
|
||||
|
||||
private void addStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
||||
{
|
||||
AddStep($"{step} {timestamp}", () =>
|
||||
|
@ -19,6 +19,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@ -167,14 +168,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestSpectatingDuringGameplay()
|
||||
{
|
||||
start();
|
||||
sendFrames(300);
|
||||
sendFrames(300, initialResultCount: 100);
|
||||
|
||||
loadSpectatingScreen();
|
||||
waitForPlayerCurrent();
|
||||
|
||||
sendFrames(300);
|
||||
sendFrames(300, initialResultCount: 100);
|
||||
|
||||
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime, () => Is.GreaterThan(30000));
|
||||
AddAssert("check judgement counts are correct", () => player.ChildrenOfType<JudgementCountController>().Single().Counters.Sum(c => c.ResultCount.Value),
|
||||
() => Is.GreaterThanOrEqualTo(100));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -405,9 +408,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private void checkPaused(bool state) =>
|
||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||
|
||||
private void sendFrames(int count = 10, double startTime = 0)
|
||||
private void sendFrames(int count = 10, double startTime = 0, int initialResultCount = 0)
|
||||
{
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime, initialResultCount));
|
||||
}
|
||||
|
||||
private void loadSpectatingScreen()
|
||||
|
@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
|
||||
|
||||
AddStep("disable shuffle", () => Game.MusicController.Shuffle.Value = false);
|
||||
|
||||
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
|
||||
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);
|
||||
|
||||
|
@ -5,6 +5,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@ -19,16 +20,20 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(50),
|
||||
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(20),
|
||||
Width = 0.5f,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(50),
|
||||
ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -66,6 +71,13 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
|
||||
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
|
||||
public Bindable<int?> IntTextBoxBindable { get; } = new Bindable<int?>();
|
||||
|
||||
[SettingSource("Sample colour", "Change the colour", SettingControlType = typeof(SettingsColour))]
|
||||
public BindableColour4 ColourBindable { get; } = new BindableColour4
|
||||
{
|
||||
Default = Colour4.White,
|
||||
Value = Colour4.Red
|
||||
};
|
||||
}
|
||||
|
||||
private enum TestEnum
|
||||
|
@ -175,6 +175,29 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
increaseModSpeed();
|
||||
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
|
||||
|
||||
OsuModDoubleTime dtWithAdjustPitch = new OsuModDoubleTime
|
||||
{
|
||||
SpeedChange = { Value = 1.05 },
|
||||
AdjustPitch = { Value = true },
|
||||
};
|
||||
changeMods(dtWithAdjustPitch);
|
||||
|
||||
decreaseModSpeed();
|
||||
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
|
||||
|
||||
decreaseModSpeed();
|
||||
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
|
||||
AddAssert("half time has adjust pitch active", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
|
||||
|
||||
AddStep("turn off adjust pitch", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value = false);
|
||||
|
||||
increaseModSpeed();
|
||||
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
|
||||
|
||||
increaseModSpeed();
|
||||
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
|
||||
AddAssert("double time has adjust pitch inactive", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.False);
|
||||
|
||||
void increaseModSpeed() => AddStep("increase mod speed", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osuTK;
|
||||
@ -28,9 +29,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
private TestCancellableCommentEditor cancellableCommentEditor = null!;
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
Add(new FillFlowContainer
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create content", () => Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -43,7 +45,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
commentEditor = new TestCommentEditor(),
|
||||
cancellableCommentEditor = new TestCancellableCommentEditor()
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommitViaKeyboard()
|
||||
@ -133,6 +136,34 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
assertLoggedInState();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommentsDisabled()
|
||||
{
|
||||
AddStep("no reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||
{
|
||||
CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes(),
|
||||
});
|
||||
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.False);
|
||||
|
||||
AddStep("specific reason for disable", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||
{
|
||||
CurrentUserAttributes = new CommentableMeta.CommentableCurrentUserAttributes
|
||||
{
|
||||
CanNewCommentReason = "This comment section is disabled. For reasons.",
|
||||
}
|
||||
});
|
||||
AddAssert("textbox disabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.True);
|
||||
|
||||
AddStep("entire commentable meta missing", () => commentEditor.CommentableMeta.Value = null);
|
||||
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.False);
|
||||
|
||||
AddStep("current user attributes missing", () => commentEditor.CommentableMeta.Value = new CommentableMeta
|
||||
{
|
||||
CurrentUserAttributes = null,
|
||||
});
|
||||
AddAssert("textbox enabled", () => commentEditor.ChildrenOfType<TextBox>().Single().ReadOnly, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCancelAction()
|
||||
{
|
||||
@ -167,8 +198,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"Commit" : "You're logged out!";
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? @"This text box is empty" : "Still empty, but now you can't type in it.";
|
||||
protected override LocalisableString GetPlaceholderText() => @"This text box is empty";
|
||||
}
|
||||
|
||||
private partial class TestCancellableCommentEditor : CancellableCommentEditor
|
||||
@ -189,7 +219,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}
|
||||
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) => @"Save";
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) => @"Multiline textboxes soon";
|
||||
protected override LocalisableString GetPlaceholderText() => @"Multiline textboxes soon";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,6 +100,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "Audio file",
|
||||
PlaceholderText = "Select an audio file",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneHotkeyDisplay : ThemeComparisonTestScene
|
||||
{
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new[]
|
||||
{
|
||||
new HotkeyDisplay { Hotkey = new Hotkey(new KeyCombination(InputKey.MouseLeft)) },
|
||||
new HotkeyDisplay { Hotkey = new Hotkey(GlobalAction.EditorDecreaseDistanceSpacing) },
|
||||
new HotkeyDisplay { Hotkey = new Hotkey(PlatformAction.Save) },
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneSettingsColour : OsuManualInputManagerTestScene
|
||||
{
|
||||
private SettingsColour? component;
|
||||
|
||||
[Test]
|
||||
public void TestColour()
|
||||
{
|
||||
createContent();
|
||||
|
||||
AddRepeatStep("set random colour", () => component!.Current.Value = randomColour(), 4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserInteractions()
|
||||
{
|
||||
createContent();
|
||||
|
||||
AddStep("click colour", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(component!);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("colour picker spawned", () => this.ChildrenOfType<OsuColourPicker>().Any());
|
||||
}
|
||||
|
||||
private void createContent()
|
||||
{
|
||||
AddStep("create component", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 500,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
component = new SettingsColour
|
||||
{
|
||||
LabelText = "a sample component",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private Colour4 randomColour() => new Color4(
|
||||
RNG.NextSingle(),
|
||||
RNG.NextSingle(),
|
||||
RNG.NextSingle(),
|
||||
1);
|
||||
}
|
||||
}
|
@ -96,7 +96,7 @@ namespace osu.Game.Audio
|
||||
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
|
||||
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
|
||||
|
||||
public bool Equals(HitSampleInfo? other)
|
||||
public virtual bool Equals(HitSampleInfo? other)
|
||||
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
|
@ -183,7 +183,14 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
#region Beatmap
|
||||
|
||||
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
||||
public virtual bool BeatmapLoaded
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (beatmapFetchLock)
|
||||
return beatmapLoadTask?.IsCompleted ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
public IBeatmap Beatmap
|
||||
{
|
||||
|
@ -186,6 +186,16 @@ namespace osu.Game.Configuration
|
||||
|
||||
break;
|
||||
|
||||
case BindableColour4 bColour:
|
||||
yield return new SettingsColour
|
||||
{
|
||||
LabelText = attr.Label,
|
||||
TooltipText = attr.Description,
|
||||
Current = bColour
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case IBindable bindable:
|
||||
var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
|
||||
var dropdown = (Drawable)Activator.CreateInstance(dropdownType)!;
|
||||
@ -227,6 +237,9 @@ namespace osu.Game.Configuration
|
||||
case Bindable<bool> b:
|
||||
return b.Value;
|
||||
|
||||
case BindableColour4 c:
|
||||
return c.Value.ToHex();
|
||||
|
||||
case IBindable u:
|
||||
return BindableValueAccessor.GetValue(u);
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -20,12 +21,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class DrawableOsuMenuItem : Menu.DrawableMenuItem
|
||||
{
|
||||
public const int MARGIN_HORIZONTAL = 17;
|
||||
public const int MARGIN_HORIZONTAL = 10;
|
||||
public const int MARGIN_VERTICAL = 4;
|
||||
private const int text_size = 17;
|
||||
private const int transition_length = 80;
|
||||
public const int TEXT_SIZE = 17;
|
||||
public const int TRANSITION_LENGTH = 80;
|
||||
|
||||
private TextContainer text;
|
||||
private HotkeyDisplay hotkey;
|
||||
private HoverClickSounds hoverClickSounds;
|
||||
|
||||
public DrawableOsuMenuItem(MenuItem item)
|
||||
@ -39,32 +41,33 @@ namespace osu.Game.Graphics.UserInterface
|
||||
BackgroundColour = Color4.Transparent;
|
||||
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
|
||||
|
||||
AddInternal(hotkey = new HotkeyDisplay
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Margin = new MarginPadding { Right = 10, Top = 1 },
|
||||
});
|
||||
AddInternal(hoverClickSounds = new HoverClickSounds());
|
||||
|
||||
updateTextColour();
|
||||
updateText();
|
||||
|
||||
bool hasSubmenu = Item.Items.Any();
|
||||
|
||||
// Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`).
|
||||
if (hasSubmenu && RelativeSizeAxes == Axes.X)
|
||||
if (showChevron)
|
||||
{
|
||||
AddInternal(new SpriteIcon
|
||||
{
|
||||
Margin = new MarginPadding(6),
|
||||
Margin = new MarginPadding { Horizontal = 10, },
|
||||
Size = new Vector2(8),
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
});
|
||||
|
||||
text.Padding = new MarginPadding
|
||||
{
|
||||
// Add some padding for the chevron above.
|
||||
Right = 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Only add right chevron if direction of menu items is vertical (i.e. width is relative size, see `DrawableMenuItem.SetFlowDirection()`).
|
||||
private bool showChevron => Item.Items.Any() && RelativeSizeAxes == Axes.X;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -73,9 +76,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
FinishTransforms();
|
||||
}
|
||||
|
||||
private void updateTextColour()
|
||||
private void updateText()
|
||||
{
|
||||
switch ((Item as OsuMenuItem)?.Type)
|
||||
var osuMenuItem = Item as OsuMenuItem;
|
||||
|
||||
switch (osuMenuItem?.Type)
|
||||
{
|
||||
default:
|
||||
case MenuItemType.Standard:
|
||||
@ -90,6 +95,20 @@ namespace osu.Game.Graphics.UserInterface
|
||||
text.Colour = Color4Extensions.FromHex(@"ffcc22");
|
||||
break;
|
||||
}
|
||||
|
||||
hotkey.Hotkey = osuMenuItem?.Hotkey ?? default;
|
||||
hotkey.Alpha = EqualityComparer<Hotkey>.Default.Equals(hotkey.Hotkey, default) ? 0 : 1;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// this hack ensures that the menu can auto-size while leaving enough space for the hotkey display.
|
||||
// the gist of it is that while the hotkey display is not in the text / "content" that determines sizing
|
||||
// (because it cannot be, because we want the hotkey display to align to the *right* and not the left),
|
||||
// enough padding to fit the hotkey with _its_ spacing is added as padding of the text to compensate.
|
||||
text.Padding = new MarginPadding { Right = hotkey.Alpha > 0 || showChevron ? hotkey.DrawWidth + 15 : 0 };
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@ -111,13 +130,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (IsHovered && IsActionable)
|
||||
{
|
||||
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
|
||||
text.BoldText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.BoldText.FadeOut(transition_length, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(transition_length, Easing.OutQuint);
|
||||
text.BoldText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,32 +157,53 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public readonly SpriteText NormalText;
|
||||
public readonly SpriteText BoldText;
|
||||
public readonly Container CheckboxContainer;
|
||||
|
||||
public TextContainer()
|
||||
{
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
NormalText = new OsuSpriteText
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(10),
|
||||
Direction = FillDirection.Horizontal,
|
||||
Padding = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL, },
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: text_size),
|
||||
Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL },
|
||||
},
|
||||
BoldText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL, Vertical = MARGIN_VERTICAL },
|
||||
CheckboxContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = MARGIN_HORIZONTAL,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
NormalText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE),
|
||||
},
|
||||
BoldText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -46,12 +46,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
state = menuItem.State.GetBoundCopy();
|
||||
|
||||
Add(stateIcon = new SpriteIcon
|
||||
CheckboxContainer.Add(stateIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(10),
|
||||
Margin = new MarginPadding { Horizontal = MARGIN_HORIZONTAL },
|
||||
AlwaysPresent = true,
|
||||
});
|
||||
}
|
||||
@ -62,14 +61,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
state.BindValueChanged(updateState, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Todo: This is bad. This can maybe be done better with a refactor of DrawableOsuMenuItem.
|
||||
stateIcon.X = BoldText.DrawWidth + 10;
|
||||
}
|
||||
|
||||
private void updateState(ValueChangedEvent<object> state)
|
||||
{
|
||||
var icon = menuItem.GetIconForState(state.NewValue);
|
||||
|
59
osu.Game/Graphics/UserInterface/Hotkey.cs
Normal file
59
osu.Game/Graphics/UserInterface/Hotkey.cs
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public readonly record struct Hotkey
|
||||
{
|
||||
public KeyCombination[]? KeyCombinations { get; init; }
|
||||
public GlobalAction? GlobalAction { get; init; }
|
||||
public PlatformAction? PlatformAction { get; init; }
|
||||
|
||||
public Hotkey(params KeyCombination[] keyCombinations)
|
||||
{
|
||||
KeyCombinations = keyCombinations;
|
||||
}
|
||||
|
||||
public Hotkey(GlobalAction globalAction)
|
||||
{
|
||||
GlobalAction = globalAction;
|
||||
}
|
||||
|
||||
public Hotkey(PlatformAction platformAction)
|
||||
{
|
||||
PlatformAction = platformAction;
|
||||
}
|
||||
|
||||
public IEnumerable<string> ResolveKeyCombination(ReadableKeyCombinationProvider keyCombinationProvider, RealmKeyBindingStore keyBindingStore, GameHost gameHost)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (KeyCombinations != null)
|
||||
{
|
||||
result.AddRange(KeyCombinations.Select(keyCombinationProvider.GetReadableString));
|
||||
}
|
||||
|
||||
if (GlobalAction != null)
|
||||
{
|
||||
result.AddRange(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.Value));
|
||||
}
|
||||
|
||||
if (PlatformAction != null)
|
||||
{
|
||||
var action = PlatformAction.Value;
|
||||
var bindings = gameHost.PlatformKeyBindings.Where(kb => (PlatformAction)kb.Action == action);
|
||||
result.AddRange(bindings.Select(b => keyCombinationProvider.GetReadableString(b.KeyCombination)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
110
osu.Game/Graphics/UserInterface/HotkeyDisplay.cs
Normal file
110
osu.Game/Graphics/UserInterface/HotkeyDisplay.cs
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class HotkeyDisplay : CompositeDrawable
|
||||
{
|
||||
private Hotkey hotkey;
|
||||
|
||||
public Hotkey Hotkey
|
||||
{
|
||||
get => hotkey;
|
||||
set
|
||||
{
|
||||
if (EqualityComparer<Hotkey>.Default.Equals(hotkey, value))
|
||||
return;
|
||||
|
||||
hotkey = value;
|
||||
|
||||
if (IsLoaded)
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
private FillFlowContainer flow = null!;
|
||||
|
||||
[Resolved]
|
||||
private ReadableKeyCombinationProvider readableKeyCombinationProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmKeyBindingStore realmKeyBindingStore { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost gameHost { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = flow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5)
|
||||
};
|
||||
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
flow.Clear();
|
||||
foreach (string h in hotkey.ResolveKeyCombination(readableKeyCombinationProvider, realmKeyBindingStore, gameHost))
|
||||
flow.Add(new HotkeyBox(h));
|
||||
}
|
||||
|
||||
private partial class HotkeyBox : CompositeDrawable
|
||||
{
|
||||
private readonly string hotkey;
|
||||
|
||||
public HotkeyBox(string hotkey)
|
||||
{
|
||||
this.hotkey = hotkey;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
CornerRadius = 3;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider?.Background6 ?? Colour4.Black.Opacity(0.7f),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding { Horizontal = 5, Bottom = 1, },
|
||||
Text = hotkey.ToUpperInvariant(),
|
||||
Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold),
|
||||
Colour = colourProvider?.Light1 ?? colours.GrayA,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -109,7 +109,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Colour = BackgroundColourHover,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 2f,
|
||||
Width = 0.8f,
|
||||
Width = 0.9f,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public readonly MenuItemType Type;
|
||||
|
||||
public Hotkey Hotkey { get; init; }
|
||||
|
||||
public OsuMenuItem(LocalisableString text, MenuItemType type = MenuItemType.Standard)
|
||||
: this(text, type, null)
|
||||
{
|
||||
|
@ -0,0 +1,59 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
|
||||
{
|
||||
internal partial class BackgroundLayer : CompositeDrawable
|
||||
{
|
||||
private Box background = null!;
|
||||
|
||||
private readonly float defaultAlpha;
|
||||
|
||||
public BackgroundLayer(float defaultAlpha = 0f)
|
||||
{
|
||||
Depth = float.MaxValue;
|
||||
|
||||
this.defaultAlpha = defaultAlpha;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new HoverClickSounds(),
|
||||
background = new Box
|
||||
{
|
||||
Alpha = defaultAlpha,
|
||||
Colour = overlayColourProvider.Background3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
background.FadeTo(1, 200, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
background.FadeTo(defaultAlpha, 500, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,11 @@ using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
|
||||
{
|
||||
internal partial class OsuDirectorySelectorHiddenToggle : OsuCheckbox
|
||||
internal partial class HiddenFilesToggleCheckbox : OsuCheckbox
|
||||
{
|
||||
public OsuDirectorySelectorHiddenToggle()
|
||||
public HiddenFilesToggleCheckbox()
|
||||
{
|
||||
RelativeSizeAxes = Axes.None;
|
||||
AutoSizeAxes = Axes.None;
|
@ -13,7 +13,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
|
||||
{
|
||||
internal partial class OsuDirectorySelectorBreadcrumbDisplay : DirectorySelectorBreadcrumbDisplay
|
||||
{
|
||||
@ -80,7 +80,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new Background
|
||||
new BackgroundLayer(0.5f)
|
||||
{
|
||||
Depth = 1
|
||||
},
|
||||
@ -101,24 +101,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
protected override SpriteText CreateSpriteText() => new OsuSpriteText().With(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold));
|
||||
|
||||
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) ? FontAwesome.Solid.Database : null;
|
||||
|
||||
internal partial class Background : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
|
||||
InternalChild = new Box
|
||||
{
|
||||
Colour = overlayColourProvider.Background3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -7,9 +7,8 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
|
||||
{
|
||||
internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory
|
||||
{
|
||||
@ -24,10 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
Flow.AutoSizeAxes = Axes.X;
|
||||
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new HoverClickSounds()
|
||||
});
|
||||
AddInternal(new BackgroundLayer());
|
||||
|
||||
Colour = colours.Orange1;
|
||||
}
|
@ -6,7 +6,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
|
||||
{
|
||||
internal partial class OsuDirectorySelectorParentDirectory : OsuDirectorySelectorDirectory
|
||||
{
|
280
osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs
Normal file
280
osu.Game/Graphics/UserInterfaceV2/FormFileSelector.cs
Normal file
@ -0,0 +1,280 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
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.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class FormFileSelector : CompositeDrawable, IHasCurrentValue<FileInfo?>, ICanAcceptFiles, IHasPopover
|
||||
{
|
||||
public Bindable<FileInfo?> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableWithCurrent<FileInfo?> current = new BindableWithCurrent<FileInfo?>();
|
||||
|
||||
public IEnumerable<string> HandledExtensions => handledExtensions;
|
||||
|
||||
private readonly string[] handledExtensions;
|
||||
|
||||
/// <summary>
|
||||
/// The initial path to use when displaying the <see cref="FileChooserPopover"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Uses a <see langword="null"/> value before the first selection is made
|
||||
/// to ensure that the first selection starts at <see cref="GameHost.InitialFileSelectorPath"/>.
|
||||
/// </remarks>
|
||||
private string? initialChooserPath;
|
||||
|
||||
private readonly Bindable<Visibility> popoverState = new Bindable<Visibility>();
|
||||
|
||||
/// <summary>
|
||||
/// Caption describing this file selector, displayed on top of the controls.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hint text containing an extended description of this file selector, displayed in a tooltip when hovering the caption.
|
||||
/// </summary>
|
||||
public LocalisableString HintText { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Text displayed in the selector when no file is selected.
|
||||
/// </summary>
|
||||
public LocalisableString PlaceholderText { get; init; }
|
||||
|
||||
private Box background = null!;
|
||||
|
||||
private FormFieldCaption caption = null!;
|
||||
private OsuSpriteText placeholderText = null!;
|
||||
private OsuSpriteText filenameText = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
public FormFileSelector(params string[] handledExtensions)
|
||||
{
|
||||
this.handledExtensions = handledExtensions;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 50;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(9),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
caption = new FormFieldCaption
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Caption = Caption,
|
||||
TooltipText = HintText,
|
||||
},
|
||||
placeholderText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Text = PlaceholderText,
|
||||
Colour = colourProvider.Foreground1,
|
||||
},
|
||||
filenameText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Icon = FontAwesome.Solid.FolderOpen,
|
||||
Size = new Vector2(16),
|
||||
Colour = colourProvider.Light1,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
popoverState.BindValueChanged(_ => updateState());
|
||||
current.BindValueChanged(_ =>
|
||||
{
|
||||
updateState();
|
||||
onFileSelected();
|
||||
});
|
||||
current.BindDisabledChanged(_ => updateState(), true);
|
||||
game.RegisterImportHandler(this);
|
||||
}
|
||||
|
||||
private void onFileSelected()
|
||||
{
|
||||
if (Current.Value != null)
|
||||
this.HidePopover();
|
||||
|
||||
initialChooserPath = Current.Value?.DirectoryName;
|
||||
placeholderText.Alpha = Current.Value == null ? 1 : 0;
|
||||
filenameText.Text = Current.Value?.Name ?? string.Empty;
|
||||
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
this.ShowPopover();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
|
||||
filenameText.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
|
||||
|
||||
if (!Current.Disabled)
|
||||
{
|
||||
BorderThickness = IsHovered || popoverState.Value == Visibility.Visible ? 2 : 0;
|
||||
BorderColour = popoverState.Value == Visibility.Visible ? colourProvider.Highlight1 : colourProvider.Light4;
|
||||
|
||||
if (popoverState.Value == Visibility.Visible)
|
||||
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
|
||||
else if (IsHovered)
|
||||
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
|
||||
else
|
||||
background.Colour = colourProvider.Background5;
|
||||
}
|
||||
else
|
||||
{
|
||||
background.Colour = colourProvider.Background4;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (game.IsNotNull())
|
||||
game.UnregisterImportHandler(this);
|
||||
}
|
||||
|
||||
Task ICanAcceptFiles.Import(params string[] paths)
|
||||
{
|
||||
Schedule(() => Current.Value = new FileInfo(paths.First()));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
|
||||
|
||||
public Popover GetPopover()
|
||||
{
|
||||
var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath);
|
||||
popoverState.UnbindBindings();
|
||||
popoverState.BindTo(popover.State);
|
||||
return popover;
|
||||
}
|
||||
|
||||
private partial class FileChooserPopover : OsuPopover
|
||||
{
|
||||
protected override string PopInSampleName => "UI/overlay-big-pop-in";
|
||||
protected override string PopOutSampleName => "UI/overlay-big-pop-out";
|
||||
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
|
||||
: base(false)
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
Size = new Vector2(600, 400),
|
||||
// simplest solution to avoid underlying text to bleed through the bottom border
|
||||
// https://github.com/ppy/osu/pull/30005#issuecomment-2378884430
|
||||
Padding = new MarginPadding { Bottom = 1 },
|
||||
Child = new OsuFileSelector(chooserPath, handledExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CurrentFile = { BindTarget = currentFile }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Add(new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = 2,
|
||||
CornerRadius = 10,
|
||||
BorderColour = colourProvider.Highlight1,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Transparent,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterfaceV2.FileSelection;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
@ -57,7 +58,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuDirectorySelectorHiddenToggle
|
||||
new HiddenFilesToggleCheckbox
|
||||
{
|
||||
Current = { BindTarget = ShowHiddenItems },
|
||||
},
|
||||
|
@ -11,7 +11,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2.FileSelection;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuDirectorySelectorHiddenToggle
|
||||
new HiddenFilesToggleCheckbox
|
||||
{
|
||||
Current = { BindTarget = ShowHiddenItems },
|
||||
},
|
||||
@ -87,10 +87,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
Flow.AutoSizeAxes = Axes.X;
|
||||
Flow.Height = OsuDirectorySelector.ITEM_HEIGHT;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new HoverClickSounds()
|
||||
});
|
||||
AddInternal(new BackgroundLayer());
|
||||
|
||||
Colour = colourProvider.Light3;
|
||||
}
|
||||
|
@ -59,6 +59,26 @@ namespace osu.Game.Localisation.SkinComponents
|
||||
/// </summary>
|
||||
public static LocalisableString ShowLabelDescription => new TranslatableString(getKey(@"show_label_description"), @"Whether the component's label should be shown.");
|
||||
|
||||
/// <summary>
|
||||
/// "Colour"
|
||||
/// </summary>
|
||||
public static LocalisableString Colour => new TranslatableString(getKey(@"colour"), @"Colour");
|
||||
|
||||
/// <summary>
|
||||
/// "The colour of the component."
|
||||
/// </summary>
|
||||
public static LocalisableString ColourDescription => new TranslatableString(getKey(@"colour_description"), @"The colour of the component.");
|
||||
|
||||
/// <summary>
|
||||
/// "Text colour"
|
||||
/// </summary>
|
||||
public static LocalisableString TextColour => new TranslatableString(getKey(@"text_colour"), @"Text colour");
|
||||
|
||||
/// <summary>
|
||||
/// "The colour of the text."
|
||||
/// </summary>
|
||||
public static LocalisableString TextColourDescription => new TranslatableString(getKey(@"text_colour_description"), @"The colour of the text.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -24,5 +24,14 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("current_user_attributes")]
|
||||
public CommentableCurrentUserAttributes? CurrentUserAttributes { get; set; }
|
||||
|
||||
public struct CommentableCurrentUserAttributes
|
||||
{
|
||||
[JsonProperty("can_new_comment_reason")]
|
||||
public string? CanNewCommentReason { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
|
||||
@ -234,7 +235,7 @@ namespace osu.Game.Online.Chat
|
||||
return new LinkDetails(LinkAction.External, url);
|
||||
}
|
||||
|
||||
return new LinkDetails(linkType, args[2]);
|
||||
return new LinkDetails(linkType, HttpUtility.UrlDecode(args[2]));
|
||||
|
||||
case "osump":
|
||||
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
|
||||
|
@ -445,10 +445,11 @@ namespace osu.Game
|
||||
break;
|
||||
|
||||
case LinkAction.SearchBeatmapSet:
|
||||
if (link.Argument is RomanisableString romanisable)
|
||||
SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript));
|
||||
if (link.Argument is LocalisableString localisable)
|
||||
SearchBeatmapSet(Localisation.GetLocalisedString(localisable));
|
||||
else
|
||||
SearchBeatmapSet(argString);
|
||||
|
||||
break;
|
||||
|
||||
case LinkAction.FilterBeatmapSetGenre:
|
||||
|
@ -409,6 +409,7 @@ namespace osu.Game
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
|
||||
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||
dependencies.Cache(KeyBindingStore);
|
||||
|
||||
dependencies.Cache(globalBindings);
|
||||
|
||||
|
@ -242,7 +242,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
title.Clear();
|
||||
artist.Clear();
|
||||
|
||||
title.AddLink(titleText, LinkAction.SearchBeatmapSet, $@"title=""""{titleText}""""");
|
||||
title.AddLink(titleText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"title=""""{titleText}"""""));
|
||||
|
||||
title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
|
||||
title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
|
||||
@ -259,7 +259,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
|
||||
}
|
||||
|
||||
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, $@"artist=""""{artistText}""""");
|
||||
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, LocalisableString.Interpolate($@"artist=""""{artistText}"""""));
|
||||
|
||||
if (setInfo.NewValue.TrackId != null)
|
||||
{
|
||||
|
@ -14,6 +14,8 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -21,6 +23,8 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
public abstract partial class CommentEditor : CompositeDrawable
|
||||
{
|
||||
public Bindable<CommentableMeta?> CommentableMeta { get; set; } = new Bindable<CommentableMeta?>();
|
||||
|
||||
private const int side_padding = 8;
|
||||
|
||||
protected abstract LocalisableString FooterText { get; }
|
||||
@ -53,8 +57,7 @@ namespace osu.Game.Overlays.Comments
|
||||
/// <summary>
|
||||
/// Returns the placeholder text for the comment box.
|
||||
/// </summary>
|
||||
/// <param name="isLoggedIn">Whether the current user is logged in.</param>
|
||||
protected abstract LocalisableString GetPlaceholderText(bool isLoggedIn);
|
||||
protected abstract LocalisableString GetPlaceholderText();
|
||||
|
||||
protected bool ShowLoadingSpinner
|
||||
{
|
||||
@ -65,7 +68,7 @@ namespace osu.Game.Overlays.Comments
|
||||
else
|
||||
loadingSpinner.Hide();
|
||||
|
||||
updateCommitButtonState();
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,25 +170,33 @@ namespace osu.Game.Overlays.Comments
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Current.BindValueChanged(_ => updateCommitButtonState(), true);
|
||||
apiState.BindValueChanged(updateStateForLoggedIn, true);
|
||||
Current.BindValueChanged(_ => updateState());
|
||||
apiState.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
CommentableMeta.BindValueChanged(_ => Scheduler.AddOnce(updateState));
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected abstract void OnCommit(string text);
|
||||
|
||||
private void updateCommitButtonState() =>
|
||||
commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
|
||||
|
||||
private void updateStateForLoggedIn(ValueChangedEvent<APIState> state) => Schedule(() =>
|
||||
private void updateState()
|
||||
{
|
||||
bool isAvailable = state.NewValue > APIState.Offline;
|
||||
bool isOnline = apiState.Value > APIState.Offline;
|
||||
LocalisableString? canNewCommentReason = CommentEditor.canNewCommentReason(CommentableMeta.Value);
|
||||
bool commentsDisabled = canNewCommentReason != null;
|
||||
bool canComment = isOnline && !commentsDisabled;
|
||||
|
||||
TextBox.PlaceholderText = GetPlaceholderText(isAvailable);
|
||||
TextBox.ReadOnly = !isAvailable;
|
||||
if (!isOnline)
|
||||
TextBox.PlaceholderText = AuthorizationStrings.RequireLogin;
|
||||
else if (canNewCommentReason != null)
|
||||
TextBox.PlaceholderText = canNewCommentReason.Value;
|
||||
else
|
||||
TextBox.PlaceholderText = GetPlaceholderText();
|
||||
TextBox.ReadOnly = !canComment;
|
||||
|
||||
if (isAvailable)
|
||||
if (isOnline)
|
||||
{
|
||||
commitButton.Show();
|
||||
commitButton.Enabled.Value = !commentsDisabled && loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
|
||||
logInButton.Hide();
|
||||
}
|
||||
else
|
||||
@ -193,7 +204,25 @@ namespace osu.Game.Overlays.Comments
|
||||
commitButton.Hide();
|
||||
logInButton.Show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// https://github.com/ppy/osu-web/blob/83816dbe24ad2927273cba968f2fcd2694a121a9/resources/js/components/comment-editor.tsx#L54-L60
|
||||
// careful here, logic is VERY finicky.
|
||||
private static LocalisableString? canNewCommentReason(CommentableMeta? meta)
|
||||
{
|
||||
if (meta == null)
|
||||
return null;
|
||||
|
||||
if (meta.CurrentUserAttributes != null)
|
||||
{
|
||||
if (meta.CurrentUserAttributes.Value.CanNewCommentReason is string reason)
|
||||
return reason;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return AuthorizationStrings.CommentStoreDisabled;
|
||||
}
|
||||
|
||||
private partial class EditorTextBox : OsuTextBox
|
||||
{
|
||||
|
@ -20,6 +20,7 @@ using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Users.Drawables;
|
||||
@ -49,6 +50,7 @@ namespace osu.Game.Overlays.Comments
|
||||
private int currentPage;
|
||||
|
||||
private FillFlowContainer pinnedContent;
|
||||
private NewCommentEditor newCommentEditor;
|
||||
private FillFlowContainer content;
|
||||
private DeletedCommentsCounter deletedCommentsCounter;
|
||||
private CommentsShowMoreButton moreButton;
|
||||
@ -114,7 +116,7 @@ namespace osu.Game.Overlays.Comments
|
||||
Padding = new MarginPadding { Left = 60 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Child = new NewCommentEditor
|
||||
Child = newCommentEditor = new NewCommentEditor
|
||||
{
|
||||
OnPost = prependPostedComments
|
||||
}
|
||||
@ -242,6 +244,7 @@ namespace osu.Game.Overlays.Comments
|
||||
protected void OnSuccess(CommentBundle response)
|
||||
{
|
||||
commentCounter.Current.Value = response.Total;
|
||||
newCommentEditor.CommentableMeta.Value = response.CommentableMeta.SingleOrDefault(m => m.Id == id.Value && m.Type == type.Value.ToString().ToSnakeCase().ToLowerInvariant());
|
||||
|
||||
if (!response.Comments.Any())
|
||||
{
|
||||
@ -413,8 +416,7 @@ namespace osu.Game.Overlays.Comments
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommonStrings.ButtonsPost : CommentsStrings.GuestButtonNew;
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommentsStrings.PlaceholderNew : AuthorizationStrings.RequireLogin;
|
||||
protected override LocalisableString GetPlaceholderText() => CommentsStrings.PlaceholderNew;
|
||||
|
||||
protected override void OnCommit(string text)
|
||||
{
|
||||
|
@ -428,7 +428,7 @@ namespace osu.Game.Overlays.Comments
|
||||
if (replyEditorContainer.Count == 0)
|
||||
{
|
||||
replyEditorContainer.Show();
|
||||
replyEditorContainer.Add(new ReplyCommentEditor(Comment)
|
||||
replyEditorContainer.Add(new ReplyCommentEditor(Comment, Meta)
|
||||
{
|
||||
OnPost = comments =>
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Localisation;
|
||||
@ -26,12 +27,12 @@ namespace osu.Game.Overlays.Comments
|
||||
protected override LocalisableString GetButtonText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommonStrings.ButtonsReply : CommentsStrings.GuestButtonReply;
|
||||
|
||||
protected override LocalisableString GetPlaceholderText(bool isLoggedIn) =>
|
||||
isLoggedIn ? CommentsStrings.PlaceholderReply : AuthorizationStrings.RequireLogin;
|
||||
protected override LocalisableString GetPlaceholderText() => CommentsStrings.PlaceholderReply;
|
||||
|
||||
public ReplyCommentEditor(Comment parent)
|
||||
public ReplyCommentEditor(Comment parent, IEnumerable<CommentableMeta> meta)
|
||||
{
|
||||
parentComment = parent;
|
||||
CommentableMeta.Value = meta.SingleOrDefault(m => m.Id == parent.CommentableId && m.Type == parent.CommentableType);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
@ -20,18 +19,17 @@ using static osu.Game.Overlays.Mods.ModCustomisationPanel;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class ModCustomisationHeader : OsuHoverContainer
|
||||
public partial class ModCustomisationHeader : OsuClickableContainer
|
||||
{
|
||||
private Box background = null!;
|
||||
private Box hoverBackground = null!;
|
||||
private Box backgroundFlash = null!;
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
protected override IEnumerable<Drawable> EffectTargets => new[] { background };
|
||||
|
||||
public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>(ModCustomisationPanelState.Collapsed);
|
||||
public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>();
|
||||
|
||||
private readonly ModCustomisationPanel panel;
|
||||
|
||||
@ -53,6 +51,13 @@ namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
hoverBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = OsuColour.Gray(50),
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0,
|
||||
},
|
||||
backgroundFlash = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -84,9 +89,6 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IdleColour = colourProvider.Dark3;
|
||||
HoverColour = colourProvider.Light4;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -109,15 +111,37 @@ namespace osu.Game.Overlays.Mods
|
||||
ExpandedState.BindValueChanged(v =>
|
||||
{
|
||||
icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint);
|
||||
|
||||
switch (v.NewValue)
|
||||
{
|
||||
case ModCustomisationPanelState.Collapsed:
|
||||
background.FadeColour(colourProvider.Dark3, 500, Easing.OutQuint);
|
||||
break;
|
||||
|
||||
case ModCustomisationPanelState.Expanded:
|
||||
case ModCustomisationPanelState.ExpandedByMod:
|
||||
background.FadeColour(colourProvider.Light4, 500, Easing.OutQuint);
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
if (Enabled.Value && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
|
||||
if (!Enabled.Value)
|
||||
return base.OnHover(e);
|
||||
|
||||
if (panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
|
||||
panel.ExpandedState.Value = ModCustomisationPanelState.Expanded;
|
||||
|
||||
hoverBackground.FadeTo(0.4f, 200, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
hoverBackground.FadeOut(200, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -13,8 +14,10 @@ using osu.Framework.Graphics.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio.Effects;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -43,6 +46,8 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
public readonly BindableBool AllowTrackControl = new BindableBool(true);
|
||||
|
||||
public readonly BindableBool Shuffle = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
|
||||
/// Includes direction information for display purposes.
|
||||
@ -66,12 +71,18 @@ namespace osu.Game.Overlays
|
||||
|
||||
private AudioFilter audioDuckFilter = null!;
|
||||
|
||||
private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
|
||||
private readonly List<BeatmapSetInfo> previousRandomSets = new List<BeatmapSetInfo>();
|
||||
private int randomHistoryDirection;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
private void load(AudioManager audio, OsuConfigManager configManager)
|
||||
{
|
||||
AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer));
|
||||
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume);
|
||||
sampleVolume = audio.VolumeSample.GetBoundCopy();
|
||||
|
||||
configManager.BindWith(OsuSetting.RandomSelectAlgorithm, randomSelectAlgorithm);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -238,8 +249,15 @@ namespace osu.Game.Overlays
|
||||
|
||||
queuedDirection = TrackChangeDirection.Prev;
|
||||
|
||||
var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
|
||||
BeatmapSetInfo? playableSet;
|
||||
|
||||
if (Shuffle.Value)
|
||||
playableSet = getNextRandom(-1, allowProtectedTracks);
|
||||
else
|
||||
{
|
||||
playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
|
||||
?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks);
|
||||
}
|
||||
|
||||
if (playableSet != null)
|
||||
{
|
||||
@ -327,8 +345,17 @@ namespace osu.Game.Overlays
|
||||
|
||||
queuedDirection = TrackChangeDirection.Next;
|
||||
|
||||
var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1)
|
||||
BeatmapSetInfo? playableSet;
|
||||
|
||||
if (Shuffle.Value)
|
||||
playableSet = getNextRandom(1, allowProtectedTracks);
|
||||
else
|
||||
{
|
||||
playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo))
|
||||
.Where(i => !i.Protected || allowProtectedTracks)
|
||||
.ElementAtOrDefault(1)
|
||||
?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks);
|
||||
}
|
||||
|
||||
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
|
||||
|
||||
@ -342,6 +369,58 @@ namespace osu.Game.Overlays
|
||||
return false;
|
||||
}
|
||||
|
||||
private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks)
|
||||
{
|
||||
BeatmapSetInfo result;
|
||||
|
||||
var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray();
|
||||
|
||||
if (possibleSets.Length == 0)
|
||||
return null;
|
||||
|
||||
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
|
||||
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
|
||||
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
|
||||
// in both cases, it means that we have a history of previous random selections that we can rewind.
|
||||
if (randomHistoryDirection * direction < 0)
|
||||
{
|
||||
Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count);
|
||||
result = previousRandomSets[^1];
|
||||
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
|
||||
randomHistoryDirection += direction;
|
||||
return result;
|
||||
}
|
||||
|
||||
// if the early-return above didn't cover it, it means that we have no history to fall back on
|
||||
// and need to actually choose something random.
|
||||
switch (randomSelectAlgorithm.Value)
|
||||
{
|
||||
case RandomSelectAlgorithm.Random:
|
||||
result = possibleSets[RNG.Next(possibleSets.Length)];
|
||||
break;
|
||||
|
||||
case RandomSelectAlgorithm.RandomPermutation:
|
||||
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray();
|
||||
|
||||
if (notYetPlayedSets.Length == 0)
|
||||
{
|
||||
notYetPlayedSets = possibleSets;
|
||||
previousRandomSets.Clear();
|
||||
randomHistoryDirection = 0;
|
||||
}
|
||||
|
||||
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)];
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
|
||||
}
|
||||
|
||||
previousRandomSets.Add(result);
|
||||
randomHistoryDirection += direction;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void restartTrack()
|
||||
{
|
||||
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
|
||||
|
@ -47,6 +47,7 @@ namespace osu.Game.Overlays
|
||||
private IconButton prevButton = null!;
|
||||
private IconButton playButton = null!;
|
||||
private IconButton nextButton = null!;
|
||||
private MusicIconButton shuffleButton = null!;
|
||||
private IconButton playlistButton = null!;
|
||||
|
||||
private ScrollingTextContainer title = null!, artist = null!;
|
||||
@ -69,6 +70,7 @@ namespace osu.Game.Overlays
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private Bindable<bool> allowTrackControl = null!;
|
||||
private readonly BindableBool shuffle = new BindableBool(true);
|
||||
|
||||
public NowPlayingOverlay()
|
||||
{
|
||||
@ -164,6 +166,14 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
}
|
||||
},
|
||||
shuffleButton = new MusicIconButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(bottom_black_area_height / 2, 0),
|
||||
Action = shuffle.Toggle,
|
||||
Icon = FontAwesome.Solid.Random,
|
||||
},
|
||||
playlistButton = new MusicIconButton
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
@ -227,6 +237,9 @@ namespace osu.Game.Overlays
|
||||
allowTrackControl = musicController.AllowTrackControl.GetBoundCopy();
|
||||
allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true);
|
||||
|
||||
shuffle.BindTo(musicController.Shuffle);
|
||||
shuffle.BindValueChanged(s => shuffleButton.FadeColour(s.NewValue ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
|
||||
|
||||
musicController.TrackChanged += trackChanged;
|
||||
trackChanged(beatmap.Value);
|
||||
}
|
||||
|
80
osu.Game/Overlays/Settings/SettingsColour.cs
Normal file
80
osu.Game/Overlays/Settings/SettingsColour.cs
Normal file
@ -0,0 +1,80 @@
|
||||
// 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.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public partial class SettingsColour : SettingsItem<Colour4>
|
||||
{
|
||||
protected override Drawable CreateControl() => new ColourControl();
|
||||
|
||||
public partial class ColourControl : OsuClickableContainer, IHasPopover, IHasCurrentValue<Colour4>
|
||||
{
|
||||
private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>(Colour4.White);
|
||||
|
||||
public Bindable<Colour4> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly Box fill;
|
||||
private readonly OsuSpriteText colourHexCode;
|
||||
|
||||
public ColourControl()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 40;
|
||||
CornerRadius = 20;
|
||||
Masking = true;
|
||||
Action = this.ShowPopover;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
fill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
colourHexCode = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Default.With(size: 20)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(_ => updateColour(), true);
|
||||
}
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
fill.Colour = Current.Value;
|
||||
colourHexCode.Text = Current.Value.ToHex();
|
||||
colourHexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new OsuPopover(false)
|
||||
{
|
||||
Child = new OsuColourPicker
|
||||
{
|
||||
Current = { BindTarget = Current }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -33,6 +33,7 @@ using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
@ -118,107 +119,111 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new GridContainer
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
Child = new GridContainer
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
},
|
||||
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
Content = new[]
|
||||
{
|
||||
new Container
|
||||
new Drawable[]
|
||||
{
|
||||
Name = @"Menu container",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MinValue,
|
||||
Height = MENU_HEIGHT,
|
||||
Children = new Drawable[]
|
||||
new Container
|
||||
{
|
||||
new EditorMenuBar
|
||||
Name = @"Menu container",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = float.MinValue,
|
||||
Height = MENU_HEIGHT,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Items = new[]
|
||||
new EditorMenuBar
|
||||
{
|
||||
new MenuItem(CommonStrings.MenuBarFile)
|
||||
{
|
||||
Items = new OsuMenuItem[]
|
||||
{
|
||||
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
||||
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new OsuMenuItemSpacer(),
|
||||
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
|
||||
new OsuMenuItemSpacer(),
|
||||
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
|
||||
},
|
||||
},
|
||||
new MenuItem(CommonStrings.MenuBarEdit)
|
||||
{
|
||||
Items = new OsuMenuItem[]
|
||||
{
|
||||
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
|
||||
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
|
||||
new OsuMenuItemSpacer(),
|
||||
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
|
||||
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
|
||||
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
|
||||
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
headerText = new OsuTextFlowContainer
|
||||
{
|
||||
TextAnchor = Anchor.TopRight,
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new SkinEditorSceneLibrary
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
componentsSidebar = new EditorSidebar(),
|
||||
content = new Container
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Items = new[]
|
||||
{
|
||||
new MenuItem(CommonStrings.MenuBarFile)
|
||||
{
|
||||
Items = new OsuMenuItem[]
|
||||
{
|
||||
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
||||
new EditorMenuItem(CommonStrings.Export, MenuItemType.Standard, () => skins.ExportCurrentSkin()) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new OsuMenuItemSpacer(),
|
||||
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
|
||||
new OsuMenuItemSpacer(),
|
||||
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
|
||||
},
|
||||
},
|
||||
new MenuItem(CommonStrings.MenuBarEdit)
|
||||
{
|
||||
Items = new OsuMenuItem[]
|
||||
{
|
||||
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
|
||||
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
|
||||
new OsuMenuItemSpacer(),
|
||||
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
|
||||
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
|
||||
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
|
||||
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
settingsSidebar = new EditorSidebar(),
|
||||
headerText = new OsuTextFlowContainer
|
||||
{
|
||||
TextAnchor = Anchor.TopRight,
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new SkinEditorSceneLibrary
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
componentsSidebar = new EditorSidebar(),
|
||||
content = new Container
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
settingsSidebar = new EditorSidebar(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -46,7 +46,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private Drawable[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<Drawable, float>? originalRotations;
|
||||
private Dictionary<Drawable, Vector2>? originalPositions;
|
||||
|
||||
@ -60,7 +59,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
objectsInRotation = selectedItems.Cast<Drawable>().ToArray();
|
||||
originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation);
|
||||
originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition));
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
|
||||
DefaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
|
||||
|
||||
base.Begin();
|
||||
}
|
||||
@ -70,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null);
|
||||
Debug.Assert(originalRotations != null && originalPositions != null && DefaultOrigin != null);
|
||||
|
||||
if (objectsInRotation.Length == 1 && origin == null)
|
||||
{
|
||||
@ -79,7 +78,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
return;
|
||||
}
|
||||
|
||||
var actualOrigin = origin ?? defaultOrigin.Value;
|
||||
var actualOrigin = origin ?? DefaultOrigin.Value;
|
||||
|
||||
foreach (var drawableItem in objectsInRotation)
|
||||
{
|
||||
@ -100,7 +99,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalRotations = null;
|
||||
defaultOrigin = null;
|
||||
DefaultOrigin = null;
|
||||
|
||||
base.Commit();
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
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;
|
||||
defaultOrigin = ToLocalSpace(GeometryUtils.MinimumEnclosingCircle(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())).Item1);
|
||||
|
||||
isFlippedX = false;
|
||||
isFlippedY = false;
|
||||
|
@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping")
|
||||
{
|
||||
Name = "snapping",
|
||||
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -195,22 +196,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = OsuIcon.EditorDistanceSnap })
|
||||
};
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
handleToggleViaKey(e);
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
handleToggleViaKey(e);
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
private void handleToggleViaKey(KeyboardEvent key)
|
||||
public void HandleToggleViaKey(KeyboardEvent key)
|
||||
{
|
||||
bool altPressed = key.AltPressed;
|
||||
|
||||
@ -295,22 +281,36 @@ namespace osu.Game.Rulesets.Edit
|
||||
public virtual double FindSnappedDuration(HitObject referenceObject, float distance)
|
||||
=> beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
|
||||
|
||||
public virtual float FindSnappedDistance(HitObject referenceObject, float distance)
|
||||
public virtual float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target)
|
||||
{
|
||||
double startTime = referenceObject.StartTime;
|
||||
double referenceTime;
|
||||
|
||||
double actualDuration = startTime + DistanceToDuration(referenceObject, distance);
|
||||
switch (target)
|
||||
{
|
||||
case DistanceSnapTarget.Start:
|
||||
referenceTime = referenceObject.StartTime;
|
||||
break;
|
||||
|
||||
double snappedEndTime = beatSnapProvider.SnapTime(actualDuration, startTime);
|
||||
case DistanceSnapTarget.End:
|
||||
referenceTime = referenceObject.GetEndTime();
|
||||
break;
|
||||
|
||||
double beatLength = beatSnapProvider.GetBeatLengthAtTime(startTime);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(target), target, $"Unknown {nameof(DistanceSnapTarget)} value");
|
||||
}
|
||||
|
||||
double actualDuration = referenceTime + DistanceToDuration(referenceObject, distance);
|
||||
|
||||
double snappedTime = beatSnapProvider.SnapTime(actualDuration, referenceTime);
|
||||
|
||||
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
|
||||
|
||||
// we don't want to exceed the actual duration and snap to a point in the future.
|
||||
// as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it.
|
||||
if (snappedEndTime > actualDuration + 1)
|
||||
snappedEndTime -= beatLength;
|
||||
if (snappedTime > actualDuration + 1)
|
||||
snappedTime -= beatLength;
|
||||
|
||||
return DurationToDistance(referenceObject, snappedEndTime - startTime);
|
||||
return DurationToDistance(referenceObject, snappedTime - referenceTime);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -11,7 +11,7 @@ using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
internal partial class ExpandableButton : RoundedButton, IExpandable
|
||||
public partial class ExpandableButton : RoundedButton, IExpandable
|
||||
{
|
||||
private float actualHeight;
|
||||
|
||||
|
@ -363,7 +363,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
|
||||
return false;
|
||||
|
||||
if (checkLeftToggleFromKey(e.Key, out int leftIndex))
|
||||
if (checkToolboxMappingFromKey(e.Key, out int leftIndex))
|
||||
{
|
||||
var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex);
|
||||
|
||||
@ -375,7 +375,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
}
|
||||
|
||||
if (checkRightToggleFromKey(e.Key, out int rightIndex))
|
||||
if (checkToggleMappingFromKey(e.Key, out int rightIndex))
|
||||
{
|
||||
var item = e.ShiftPressed
|
||||
? sampleBankTogglesCollection.ElementAtOrDefault(rightIndex)
|
||||
@ -391,7 +391,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
private bool checkLeftToggleFromKey(Key key, out int index)
|
||||
private bool checkToolboxMappingFromKey(Key key, out int index)
|
||||
{
|
||||
if (key < Key.Number1 || key > Key.Number9)
|
||||
{
|
||||
@ -403,7 +403,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool checkRightToggleFromKey(Key key, out int index)
|
||||
private bool checkToggleMappingFromKey(Key key, out int index)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
|
@ -58,10 +58,17 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </summary>
|
||||
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
|
||||
/// <param name="distance">The distance to convert.</param>
|
||||
/// <param name="target">Whether the distance measured should be from the start or the end of <paramref name="referenceObject"/>.</param>
|
||||
/// <returns>
|
||||
/// A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.
|
||||
/// The distance will always be less than or equal to the provided <paramref name="distance"/>.
|
||||
/// </returns>
|
||||
float FindSnappedDistance(HitObject referenceObject, float distance);
|
||||
float FindSnappedDistance(HitObject referenceObject, float distance, DistanceSnapTarget target);
|
||||
}
|
||||
|
||||
public enum DistanceSnapTarget
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
|
||||
where THitObject : HitObject, IHasPath
|
||||
{
|
||||
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance;
|
||||
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -181,6 +181,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<HitResult, int> Statistics => ScoreResultCounts;
|
||||
|
||||
private bool beatmapApplied;
|
||||
|
||||
protected readonly Dictionary<HitResult, int> ScoreResultCounts = new Dictionary<HitResult, int>();
|
||||
|
@ -7,7 +7,10 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
@ -78,8 +81,11 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
|
||||
protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableEditorBarMenuItem(item);
|
||||
|
||||
private partial class DrawableEditorBarMenuItem : DrawableOsuMenuItem
|
||||
internal partial class DrawableEditorBarMenuItem : DrawableMenuItem
|
||||
{
|
||||
private HoverClickSounds hoverClickSounds = null!;
|
||||
private TextContainer text = null!;
|
||||
|
||||
public DrawableEditorBarMenuItem(MenuItem item)
|
||||
: base(item)
|
||||
{
|
||||
@ -92,6 +98,8 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
BackgroundColour = colourProvider.Background2;
|
||||
ForegroundColourHover = colourProvider.Content1;
|
||||
BackgroundColourHover = colourProvider.Background1;
|
||||
|
||||
AddInternal(hoverClickSounds = new HoverClickSounds());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -100,6 +108,36 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
|
||||
Foreground.Anchor = Anchor.CentreLeft;
|
||||
Foreground.Origin = Anchor.CentreLeft;
|
||||
Item.Action.BindDisabledChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
hoverClickSounds.Enabled.Value = IsActionable;
|
||||
Alpha = IsActionable ? 1 : 0.2f;
|
||||
|
||||
if (IsHovered && IsActionable)
|
||||
{
|
||||
text.BoldText.FadeIn(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
text.NormalText.FadeOut(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
text.BoldText.FadeOut(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
text.NormalText.FadeIn(DrawableOsuMenuItem.TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateBackgroundColour()
|
||||
@ -118,16 +156,56 @@ namespace osu.Game.Screens.Edit.Components.Menus
|
||||
base.UpdateForegroundColour();
|
||||
}
|
||||
|
||||
protected override DrawableOsuMenuItem.TextContainer CreateTextContainer() => new TextContainer();
|
||||
protected sealed override Drawable CreateContent() => text = new TextContainer();
|
||||
}
|
||||
|
||||
private new partial class TextContainer : DrawableOsuMenuItem.TextContainer
|
||||
private partial class TextContainer : Container, IHasText
|
||||
{
|
||||
public LocalisableString Text
|
||||
{
|
||||
public TextContainer()
|
||||
get => NormalText.Text;
|
||||
set
|
||||
{
|
||||
NormalText.Font = OsuFont.TorusAlternate;
|
||||
BoldText.Font = OsuFont.TorusAlternate.With(weight: FontWeight.Bold);
|
||||
NormalText.Text = value;
|
||||
BoldText.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly SpriteText NormalText;
|
||||
public readonly SpriteText BoldText;
|
||||
|
||||
public TextContainer()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Child = new Container
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = 17, Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL, },
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
NormalText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: DrawableOsuMenuItem.TEXT_SIZE),
|
||||
},
|
||||
BoldText = new OsuSpriteText
|
||||
{
|
||||
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: DrawableOsuMenuItem.TEXT_SIZE, weight: FontWeight.Bold),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class SubMenu : OsuMenu
|
||||
|
@ -196,6 +196,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
DragBox.HandleDrag(e);
|
||||
DragBox.Show();
|
||||
|
||||
selectionBeforeDrag.Clear();
|
||||
if (e.ControlPressed)
|
||||
selectionBeforeDrag.UnionWith(SelectedItems);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -217,6 +222,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
DragBox.Hide();
|
||||
selectionBeforeDrag.Clear();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -227,7 +233,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
lastDragEvent.Target = this;
|
||||
DragBox.HandleDrag(lastDragEvent);
|
||||
UpdateSelectionFromDragBox();
|
||||
UpdateSelectionFromDragBox(selectionBeforeDrag);
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,7 +432,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private bool endClickSelection(MouseButtonEvent e)
|
||||
{
|
||||
// If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action.
|
||||
if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint) return true;
|
||||
if (clickSelectionHandled || doubleClickHandled || isDraggingBlueprint || wasDragStarted) return true;
|
||||
|
||||
if (e.Button != MouseButton.Left) return false;
|
||||
|
||||
@ -442,7 +448,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wasDragStarted && selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1)
|
||||
if (selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1)
|
||||
{
|
||||
// If a click occurred and was handled by the currently selected blueprint but didn't result in a drag,
|
||||
// cycle between other blueprints which are also under the cursor.
|
||||
@ -472,7 +478,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <summary>
|
||||
/// Select all blueprints in a selection area specified by <see cref="DragBox"/>.
|
||||
/// </summary>
|
||||
protected virtual void UpdateSelectionFromDragBox()
|
||||
protected virtual void UpdateSelectionFromDragBox(HashSet<T> selectionBeforeDrag)
|
||||
{
|
||||
var quad = DragBox.Box.ScreenSpaceDrawQuad;
|
||||
|
||||
@ -482,7 +488,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
case SelectionState.Selected:
|
||||
// Selection is preserved even after blueprint becomes dead.
|
||||
if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint))
|
||||
if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint) && !selectionBeforeDrag.Contains(blueprint.Item))
|
||||
blueprint.Deselect();
|
||||
break;
|
||||
|
||||
@ -535,6 +541,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
private bool wasDragStarted;
|
||||
|
||||
private readonly HashSet<T> selectionBeforeDrag = new HashSet<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to begin the movement of any selected blueprints.
|
||||
/// </summary>
|
||||
|
@ -59,7 +59,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
// Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to
|
||||
// 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the
|
||||
// fact that the 1/2 snap reference object is not valid for 1/3 snapping.
|
||||
float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0);
|
||||
float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0, DistanceSnapTarget.End);
|
||||
|
||||
for (int i = 0; i < requiredCircles; i++)
|
||||
{
|
||||
@ -104,7 +104,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime())
|
||||
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
|
||||
// to allow for snapping at a non-multiplied ratio.
|
||||
: SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier);
|
||||
: SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End);
|
||||
|
||||
double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance);
|
||||
|
||||
|
@ -155,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime);
|
||||
double beatLength = timingPoint.BeatLength / beatDivisor.Value;
|
||||
int beatIndex = (int)Math.Round((StartTime - timingPoint.Time) / beatLength);
|
||||
int beatIndex = (int)Math.Floor((StartTime - timingPoint.Time) / beatLength);
|
||||
|
||||
var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours);
|
||||
|
||||
|
@ -8,6 +8,7 @@ using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -350,19 +351,69 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
if (SelectedBlueprints.All(b => b.Item is IHasComboInformation))
|
||||
{
|
||||
yield return new TernaryStateToggleMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } };
|
||||
yield return new TernaryStateToggleMenuItem("New combo")
|
||||
{
|
||||
State = { BindTarget = SelectionNewComboState },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Q))
|
||||
};
|
||||
}
|
||||
|
||||
yield return new OsuMenuItem("Sample")
|
||||
yield return new OsuMenuItem("Sample") { Items = getSampleSubmenuItems().ToArray(), };
|
||||
yield return new OsuMenuItem("Bank") { Items = getBankSubmenuItems().ToArray(), };
|
||||
}
|
||||
|
||||
private IEnumerable<MenuItem> getSampleSubmenuItems()
|
||||
{
|
||||
var whistle = SelectionSampleStates[HitSampleInfo.HIT_WHISTLE];
|
||||
yield return new TernaryStateToggleMenuItem(whistle.Description)
|
||||
{
|
||||
Items = SelectionSampleStates.Select(kvp =>
|
||||
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||
State = { BindTarget = whistle },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.W))
|
||||
};
|
||||
|
||||
yield return new OsuMenuItem("Bank")
|
||||
var finish = SelectionSampleStates[HitSampleInfo.HIT_FINISH];
|
||||
yield return new TernaryStateToggleMenuItem(finish.Description)
|
||||
{
|
||||
Items = SelectionBankStates.Select(kvp =>
|
||||
new TernaryStateToggleMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||
State = { BindTarget = finish },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.E))
|
||||
};
|
||||
|
||||
var clap = SelectionSampleStates[HitSampleInfo.HIT_CLAP];
|
||||
yield return new TernaryStateToggleMenuItem(clap.Description)
|
||||
{
|
||||
State = { BindTarget = clap },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.R))
|
||||
};
|
||||
}
|
||||
|
||||
private IEnumerable<MenuItem> getBankSubmenuItems()
|
||||
{
|
||||
var auto = SelectionBankStates[HIT_BANK_AUTO];
|
||||
yield return new TernaryStateToggleMenuItem(auto.Description)
|
||||
{
|
||||
State = { BindTarget = auto },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.Q))
|
||||
};
|
||||
|
||||
var normal = SelectionBankStates[HitSampleInfo.BANK_NORMAL];
|
||||
yield return new TernaryStateToggleMenuItem(normal.Description)
|
||||
{
|
||||
State = { BindTarget = normal },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.W))
|
||||
};
|
||||
|
||||
var soft = SelectionBankStates[HitSampleInfo.BANK_SOFT];
|
||||
yield return new TernaryStateToggleMenuItem(soft.Description)
|
||||
{
|
||||
State = { BindTarget = soft },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.E))
|
||||
};
|
||||
|
||||
var drum = SelectionBankStates[HitSampleInfo.BANK_DRUM];
|
||||
yield return new TernaryStateToggleMenuItem(drum.Description)
|
||||
{
|
||||
State = { BindTarget = drum },
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Shift, InputKey.R))
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// 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.Graphics;
|
||||
@ -284,8 +285,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Action = action
|
||||
};
|
||||
|
||||
button.OperationStarted += freezeButtonPosition;
|
||||
button.HoverLost += unfreezeButtonPosition;
|
||||
|
||||
button.OperationStarted += operationStarted;
|
||||
button.OperationEnded += operationEnded;
|
||||
|
||||
buttons.Add(button);
|
||||
|
||||
return button;
|
||||
@ -357,9 +362,35 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
OperationStarted?.Invoke();
|
||||
}
|
||||
|
||||
private void ensureButtonsOnScreen()
|
||||
private Vector2? frozenButtonsPosition;
|
||||
|
||||
private void freezeButtonPosition()
|
||||
{
|
||||
buttons.Position = Vector2.Zero;
|
||||
frozenButtonsPosition = buttons.ScreenSpaceDrawQuad.TopLeft;
|
||||
}
|
||||
|
||||
private void unfreezeButtonPosition()
|
||||
{
|
||||
if (frozenButtonsPosition != null)
|
||||
{
|
||||
frozenButtonsPosition = null;
|
||||
ensureButtonsOnScreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureButtonsOnScreen(bool animated = false)
|
||||
{
|
||||
if (frozenButtonsPosition != null)
|
||||
{
|
||||
buttons.Anchor = Anchor.TopLeft;
|
||||
buttons.Origin = Anchor.TopLeft;
|
||||
|
||||
buttons.Position = ToLocalSpace(frozenButtonsPosition.Value) - new Vector2(button_padding);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!animated && buttons.Transforms.Any())
|
||||
return;
|
||||
|
||||
var thisQuad = ScreenSpaceDrawQuad;
|
||||
|
||||
@ -374,24 +405,51 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
float minHeight = buttons.ScreenSpaceDrawQuad.Height;
|
||||
|
||||
Anchor targetAnchor;
|
||||
Anchor targetOrigin;
|
||||
Vector2 targetPosition = Vector2.Zero;
|
||||
|
||||
if (topExcess < minHeight && bottomExcess < minHeight)
|
||||
{
|
||||
buttons.Anchor = Anchor.BottomCentre;
|
||||
buttons.Origin = Anchor.BottomCentre;
|
||||
buttons.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight);
|
||||
targetAnchor = Anchor.BottomCentre;
|
||||
targetOrigin = Anchor.BottomCentre;
|
||||
targetPosition.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight);
|
||||
}
|
||||
else if (topExcess > bottomExcess)
|
||||
{
|
||||
buttons.Anchor = Anchor.TopCentre;
|
||||
buttons.Origin = Anchor.BottomCentre;
|
||||
targetAnchor = Anchor.TopCentre;
|
||||
targetOrigin = Anchor.BottomCentre;
|
||||
}
|
||||
else
|
||||
{
|
||||
buttons.Anchor = Anchor.BottomCentre;
|
||||
buttons.Origin = Anchor.TopCentre;
|
||||
targetAnchor = Anchor.BottomCentre;
|
||||
targetOrigin = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
buttons.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X;
|
||||
targetPosition.X += ToLocalSpace(thisQuad.TopLeft - new Vector2(Math.Min(0, leftExcess)) + new Vector2(Math.Min(0, rightExcess))).X;
|
||||
|
||||
if (animated)
|
||||
{
|
||||
var originalPosition = ToLocalSpace(buttons.ScreenSpaceDrawQuad.TopLeft);
|
||||
|
||||
buttons.Origin = targetOrigin;
|
||||
buttons.Anchor = targetAnchor;
|
||||
buttons.Position = targetPosition;
|
||||
|
||||
var newPosition = ToLocalSpace(buttons.ScreenSpaceDrawQuad.TopLeft);
|
||||
|
||||
var delta = newPosition - originalPosition;
|
||||
|
||||
buttons.Position -= delta;
|
||||
|
||||
buttons.MoveTo(targetPosition, 300, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttons.Anchor = targetAnchor;
|
||||
buttons.Origin = targetOrigin;
|
||||
buttons.Position = targetPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public Action? Action;
|
||||
|
||||
public event Action? HoverLost;
|
||||
|
||||
public SelectionBoxButton(IconUsage iconUsage, string tooltip)
|
||||
{
|
||||
this.iconUsage = iconUsage;
|
||||
@ -61,6 +63,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
|
||||
HoverLost?.Invoke();
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText { get; }
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
base.OnDrag(e);
|
||||
|
||||
if (rotationHandler == null || !rotationHandler.OperationInProgress.Value) return;
|
||||
|
||||
rawCumulativeRotation += convertDragEventToAngleOfRotation(e);
|
||||
|
||||
applyRotation(shouldSnap: e.ShiftPressed);
|
||||
@ -113,9 +115,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private float convertDragEventToAngleOfRotation(DragEvent e)
|
||||
{
|
||||
// Adjust coordinate system to the center of SelectionBox
|
||||
float startAngle = MathF.Atan2(e.LastMousePosition.Y - selectionBox.DrawHeight / 2, e.LastMousePosition.X - selectionBox.DrawWidth / 2);
|
||||
float endAngle = MathF.Atan2(e.MousePosition.Y - selectionBox.DrawHeight / 2, e.MousePosition.X - selectionBox.DrawWidth / 2);
|
||||
// Adjust coordinate system to the center of the selection
|
||||
Vector2 center = selectionBox.ToLocalSpace(rotationHandler!.ToScreenSpace(rotationHandler!.DefaultOrigin!.Value));
|
||||
|
||||
float startAngle = MathF.Atan2(e.LastMousePosition.Y - center.Y, e.LastMousePosition.X - center.X);
|
||||
float endAngle = MathF.Atan2(e.MousePosition.Y - center.Y, e.MousePosition.X - center.X);
|
||||
|
||||
return (endAngle - startAngle) * 180 / MathF.PI;
|
||||
}
|
||||
|
@ -50,14 +50,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
rawScale = convertDragEventToScaleMultiplier(e);
|
||||
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (IsDragged)
|
||||
{
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (IsDragged)
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
|
||||
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed, useDefaultOrigin: e.AltPressed);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
@ -100,13 +100,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
private void applyScale(bool shouldLockAspectRatio)
|
||||
private void applyScale(bool shouldLockAspectRatio, bool useDefaultOrigin = false)
|
||||
{
|
||||
var newScale = shouldLockAspectRatio
|
||||
? new Vector2((rawScale.X + rawScale.Y) * 0.5f)
|
||||
: rawScale;
|
||||
|
||||
var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value);
|
||||
Vector2? scaleOrigin = useDefaultOrigin ? null : originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value);
|
||||
scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis());
|
||||
}
|
||||
|
||||
|
@ -415,7 +415,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (SelectedBlueprints.Count == 1)
|
||||
items.AddRange(SelectedBlueprints[0].ContextMenuItems);
|
||||
|
||||
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected));
|
||||
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected)
|
||||
{
|
||||
Hotkey = new Hotkey { PlatformAction = PlatformAction.Delete, KeyCombinations = [new KeyCombination(InputKey.Shift, InputKey.MouseRight), new KeyCombination(InputKey.MouseMiddle)] }
|
||||
});
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
public partial class SelectionRotationHandler : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether there is any ongoing scale operation right now.
|
||||
/// Whether there is any ongoing rotation operation right now.
|
||||
/// </summary>
|
||||
public Bindable<bool> OperationInProgress { get; private set; } = new BindableBool();
|
||||
|
||||
@ -27,6 +27,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public Bindable<bool> CanRotateAroundPlayfieldOrigin { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Implementation-defined origin point to rotate around when no explicit origin is provided.
|
||||
/// This field is only assigned during a rotation operation.
|
||||
/// </summary>
|
||||
public Vector2? DefaultOrigin { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Performs a single, instant, atomic rotation operation.
|
||||
/// </summary>
|
||||
|
@ -173,7 +173,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
protected sealed override DragBox CreateDragBox() => new TimelineDragBox();
|
||||
|
||||
protected override void UpdateSelectionFromDragBox()
|
||||
protected override void UpdateSelectionFromDragBox(HashSet<HitObject> selectionBeforeDrag)
|
||||
{
|
||||
Composer.BlueprintContainer.CommitIfPlacementActive();
|
||||
|
||||
@ -191,6 +191,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
bool shouldBeSelected(HitObject hitObject)
|
||||
{
|
||||
if (selectionBeforeDrag.Contains(hitObject))
|
||||
return true;
|
||||
|
||||
double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2;
|
||||
return minTime <= midTime && midTime <= maxTime;
|
||||
}
|
||||
|
@ -362,13 +362,13 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
|
||||
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
|
||||
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo) { Hotkey = new Hotkey(PlatformAction.Undo) },
|
||||
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo) { Hotkey = new Hotkey(PlatformAction.Redo) },
|
||||
new OsuMenuItemSpacer(),
|
||||
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
|
||||
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
|
||||
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
|
||||
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
|
||||
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut) { Hotkey = new Hotkey(PlatformAction.Cut) },
|
||||
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy) { Hotkey = new Hotkey(PlatformAction.Copy) },
|
||||
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste) { Hotkey = new Hotkey(PlatformAction.Paste) },
|
||||
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone) { Hotkey = new Hotkey(GlobalAction.EditorCloneSelection) },
|
||||
}
|
||||
},
|
||||
new MenuItem(CommonStrings.MenuBarView)
|
||||
@ -1194,7 +1194,7 @@ namespace osu.Game.Screens.Edit
|
||||
yield return new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } };
|
||||
yield return new OsuMenuItemSpacer();
|
||||
|
||||
var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save));
|
||||
var save = new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => attemptMutationOperation(Save)) { Hotkey = new Hotkey(PlatformAction.Save) };
|
||||
saveRelatedMenuItems.Add(save);
|
||||
yield return save;
|
||||
|
||||
|
@ -118,13 +118,20 @@ namespace osu.Game.Screens
|
||||
{
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR));
|
||||
|
||||
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
|
||||
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "FastCircle"));
|
||||
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
|
||||
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"TriangleBorder"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"FastCircle"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"CircularProgress"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPath"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"ArgonBarPathBackground"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SaturationSelectorBackground"));
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"HueSelectorBackground"));
|
||||
loadTargets.Add(manager.Load(@"LogoAnimation", @"LogoAnimation"));
|
||||
|
||||
// Ruleset local shader usage (should probably move somewhere else).
|
||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, @"SpinnerGlow"));
|
||||
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
|
||||
}
|
||||
|
||||
protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded);
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -24,6 +26,21 @@ namespace osu.Game.Screens.Play
|
||||
private readonly Storyboard storyboard;
|
||||
private readonly IReadOnlyList<Mod> mods;
|
||||
|
||||
/// <summary>
|
||||
/// In certain circumstances, the storyboard cannot be hidden entirely even if it is fully dimmed. Such circumstances include:
|
||||
/// <list type="bullet">
|
||||
/// <item>
|
||||
/// cases where the storyboard has an overlay layer sprite, as it should continue to display fully dimmed
|
||||
/// <i>in front of</i> the playfield (https://github.com/ppy/osu/issues/29867),
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// cases where the storyboard includes samples - as they are played back via drawable samples,
|
||||
/// they must be present for the playback to occur (https://github.com/ppy/osu/issues/9315).
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private readonly Lazy<bool> storyboardMustAlwaysBePresent;
|
||||
|
||||
private DrawableStoryboard drawableStoryboard;
|
||||
|
||||
/// <summary>
|
||||
@ -38,6 +55,8 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
this.storyboard = storyboard;
|
||||
this.mods = mods;
|
||||
|
||||
storyboardMustAlwaysBePresent = new Lazy<bool>(() => storyboard.GetLayer(@"Overlay").Elements.Any() || storyboard.Layers.Any(l => l.Elements.OfType<StoryboardSampleInfo>().Any()));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -54,7 +73,7 @@ namespace osu.Game.Screens.Play
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
||||
protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && DimLevel < 1);
|
||||
protected override bool ShowDimContent => IgnoreUserSettings.Value || (ShowStoryboard.Value && (DimLevel < 1 || storyboardMustAlwaysBePresent.Value));
|
||||
|
||||
private void initializeStoryboard(bool async)
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation.HUD;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
@ -29,6 +30,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
|
||||
public Bindable<bool> ShowTime { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||
|
||||
[Resolved]
|
||||
private Player? player { get; set; }
|
||||
|
||||
@ -94,6 +98,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Interactive.BindValueChanged(_ => bar.Interactive = Interactive.Value, true);
|
||||
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
|
||||
ShowTime.BindValueChanged(_ => info.FadeTo(ShowTime.Value ? 1 : 0, 200, Easing.In), true);
|
||||
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
|
||||
}
|
||||
|
||||
protected override void UpdateObjects(IEnumerable<HitObject> objects)
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -21,6 +22,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[SettingSource("Inverted shear")]
|
||||
public BindableBool InvertShear { get; } = new BindableBool();
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Color4Extensions.FromHex("#66CCFF"));
|
||||
|
||||
public ArgonWedgePiece()
|
||||
{
|
||||
CornerRadius = 10f;
|
||||
@ -37,7 +41,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
InternalChild = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#66CCFF").Opacity(0.0f), Color4Extensions.FromHex("#66CCFF").Opacity(0.25f)),
|
||||
};
|
||||
}
|
||||
|
||||
@ -46,6 +49,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
base.LoadComplete();
|
||||
|
||||
InvertShear.BindValueChanged(v => Shear = new Vector2(0.8f, 0f) * (v.NewValue ? -1 : 1), true);
|
||||
AccentColour.BindValueChanged(c => InternalChild.Colour = ColourInfo.GradientVertical(AccentColour.Value.Opacity(0.0f), AccentColour.Value.Opacity(0.25f)), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation.HUD;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
|
||||
@ -36,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[SettingSource(typeof(SongProgressStrings), nameof(SongProgressStrings.ShowTime), nameof(SongProgressStrings.ShowTimeDescription))]
|
||||
public Bindable<bool> ShowTime { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||
|
||||
[Resolved]
|
||||
private Player? player { get; set; }
|
||||
|
||||
@ -86,6 +90,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Interactive.BindValueChanged(_ => updateBarVisibility(), true);
|
||||
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
|
||||
ShowTime.BindValueChanged(_ => updateTimeVisibility(), true);
|
||||
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
|
||||
|
||||
base.LoadComplete();
|
||||
}
|
||||
|
18
osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs
Normal file
18
osu.Game/Screens/Play/HUD/JudgementCounter/JudgementCount.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
{
|
||||
public struct JudgementCount
|
||||
{
|
||||
public LocalisableString DisplayName { get; set; }
|
||||
|
||||
public HitResult[] Types { get; set; }
|
||||
|
||||
public BindableInt ResultCount { get; set; }
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -53,10 +52,29 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
scoreProcessor.OnResetFromReplayFrame += updateAllCountsFromReplayFrame;
|
||||
scoreProcessor.NewJudgement += judgement => updateCount(judgement, false);
|
||||
scoreProcessor.JudgementReverted += judgement => updateCount(judgement, true);
|
||||
}
|
||||
|
||||
private bool hasUpdatedCountsFromReplayFrame;
|
||||
|
||||
private void updateAllCountsFromReplayFrame()
|
||||
{
|
||||
if (hasUpdatedCountsFromReplayFrame)
|
||||
return;
|
||||
|
||||
foreach (var kvp in scoreProcessor.Statistics)
|
||||
{
|
||||
if (!results.TryGetValue(kvp.Key, out var count))
|
||||
continue;
|
||||
|
||||
count.ResultCount.Value = kvp.Value;
|
||||
}
|
||||
|
||||
hasUpdatedCountsFromReplayFrame = true;
|
||||
}
|
||||
|
||||
private void updateCount(JudgementResult judgement, bool revert)
|
||||
{
|
||||
if (!results.TryGetValue(judgement.Type, out var count))
|
||||
@ -67,14 +85,5 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
else
|
||||
count.ResultCount.Value++;
|
||||
}
|
||||
|
||||
public struct JudgementCount
|
||||
{
|
||||
public LocalisableString DisplayName { get; set; }
|
||||
|
||||
public HitResult[] Types { get; set; }
|
||||
|
||||
public BindableInt ResultCount { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
@ -19,16 +18,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
public BindableBool ShowName = new BindableBool();
|
||||
public Bindable<FillDirection> Direction = new Bindable<FillDirection>();
|
||||
|
||||
public readonly JudgementCountController.JudgementCount Result;
|
||||
public readonly JudgementCount Result;
|
||||
|
||||
public JudgementCounter(JudgementCountController.JudgementCount result) => Result = result;
|
||||
public JudgementCounter(JudgementCount result) => Result = result;
|
||||
|
||||
public OsuSpriteText ResultName = null!;
|
||||
private FillFlowContainer flowContainer = null!;
|
||||
private JudgementRollingCounter counter = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, IBindable<RulesetInfo> ruleset)
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
||||
}
|
||||
}
|
||||
|
||||
private JudgementCounter createCounter(JudgementCountController.JudgementCount info) =>
|
||||
private JudgementCounter createCounter(JudgementCount info) =>
|
||||
new JudgementCounter(info)
|
||||
{
|
||||
State = { Value = Visibility.Hidden },
|
||||
|
@ -27,6 +27,7 @@ namespace osu.Game.Screens.Select
|
||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||
|
||||
private ModRateAdjust? lastActiveRateAdjustMod;
|
||||
private ModSettingChangeTracker? settingChangeTracker;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@ -34,10 +35,19 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
selectedMods.BindValueChanged(val =>
|
||||
{
|
||||
lastActiveRateAdjustMod = val.NewValue.OfType<ModRateAdjust>().SingleOrDefault() ?? lastActiveRateAdjustMod;
|
||||
storeLastActiveRateAdjustMod();
|
||||
|
||||
settingChangeTracker?.Dispose();
|
||||
settingChangeTracker = new ModSettingChangeTracker(val.NewValue);
|
||||
settingChangeTracker.SettingChanged += _ => storeLastActiveRateAdjustMod();
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void storeLastActiveRateAdjustMod()
|
||||
{
|
||||
lastActiveRateAdjustMod = (ModRateAdjust?)selectedMods.Value.OfType<ModRateAdjust>().SingleOrDefault()?.DeepClone() ?? lastActiveRateAdjustMod;
|
||||
}
|
||||
|
||||
public bool ChangeSpeed(double delta, IEnumerable<Mod> availableMods)
|
||||
{
|
||||
double targetSpeed = (selectedMods.Value.OfType<ModRateAdjust>().SingleOrDefault()?.SpeedChange.Value ?? 1) + delta;
|
||||
|
@ -123,6 +123,8 @@ namespace osu.Game.Skinning.Components
|
||||
}
|
||||
|
||||
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
||||
|
||||
protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour;
|
||||
}
|
||||
|
||||
// WARNING: DO NOT ADD ANY VALUES TO THIS ENUM ANYWHERE ELSE THAN AT THE END.
|
||||
|
@ -27,6 +27,9 @@ namespace osu.Game.Skinning.Components
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Colour), nameof(SkinnableComponentStrings.ColourDescription))]
|
||||
public BindableColour4 AccentColour { get; } = new BindableColour4(Colour4.White);
|
||||
|
||||
public BoxElement()
|
||||
{
|
||||
Size = new Vector2(400, 80);
|
||||
@ -43,6 +46,13 @@ namespace osu.Game.Skinning.Components
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AccentColour.BindValueChanged(_ => Colour = AccentColour.Value, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
@ -53,5 +53,7 @@ namespace osu.Game.Skinning.Components
|
||||
}
|
||||
|
||||
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
||||
|
||||
protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour;
|
||||
}
|
||||
}
|
||||
|
@ -36,5 +36,7 @@ namespace osu.Game.Skinning.Components
|
||||
}
|
||||
|
||||
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
||||
|
||||
protected override void SetTextColour(Colour4 textColour) => text.Colour = textColour;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user