mirror of
https://github.com/ppy/osu.git
synced 2024-11-12 03:57:25 +08:00
2e63c2ce20
Could lead to crashes after reversing a note cluster and playing it back. The root cause of the crash was that the hotkey operations were not ran inside of an editor change handler operation. This, in turn, caused the autoplay replay to not be regenerated after flipping an object cluster, therefore finally manifesting as a hard crash due to negative time offsets appearing in judgement results, which interfered with the default implementation of note lock. Note that this incidentally also fixes the fact that selection box hotkey operations (reverse and flip) did not handle undo/redo.
283 lines
8.6 KiB
C#
283 lines
8.6 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Shapes;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Game.Graphics;
|
|
using osuTK;
|
|
using osuTK.Input;
|
|
|
|
namespace osu.Game.Screens.Edit.Compose.Components
|
|
{
|
|
public class SelectionBox : CompositeDrawable
|
|
{
|
|
public Func<float, bool> OnRotation;
|
|
public Func<Vector2, Anchor, bool> OnScale;
|
|
public Func<Direction, bool> OnFlip;
|
|
public Func<bool> OnReverse;
|
|
|
|
public Action OperationStarted;
|
|
public Action OperationEnded;
|
|
|
|
private bool canReverse;
|
|
|
|
/// <summary>
|
|
/// Whether pattern reversing support should be enabled.
|
|
/// </summary>
|
|
public bool CanReverse
|
|
{
|
|
get => canReverse;
|
|
set
|
|
{
|
|
if (canReverse == value) return;
|
|
|
|
canReverse = value;
|
|
recreate();
|
|
}
|
|
}
|
|
|
|
private bool canRotate;
|
|
|
|
/// <summary>
|
|
/// Whether rotation support should be enabled.
|
|
/// </summary>
|
|
public bool CanRotate
|
|
{
|
|
get => canRotate;
|
|
set
|
|
{
|
|
if (canRotate == value) return;
|
|
|
|
canRotate = value;
|
|
recreate();
|
|
}
|
|
}
|
|
|
|
private bool canScaleX;
|
|
|
|
/// <summary>
|
|
/// Whether vertical scale support should be enabled.
|
|
/// </summary>
|
|
public bool CanScaleX
|
|
{
|
|
get => canScaleX;
|
|
set
|
|
{
|
|
if (canScaleX == value) return;
|
|
|
|
canScaleX = value;
|
|
recreate();
|
|
}
|
|
}
|
|
|
|
private bool canScaleY;
|
|
|
|
/// <summary>
|
|
/// Whether horizontal scale support should be enabled.
|
|
/// </summary>
|
|
public bool CanScaleY
|
|
{
|
|
get => canScaleY;
|
|
set
|
|
{
|
|
if (canScaleY == value) return;
|
|
|
|
canScaleY = value;
|
|
recreate();
|
|
}
|
|
}
|
|
|
|
private Container dragHandles;
|
|
private FillFlowContainer buttons;
|
|
|
|
public const float BORDER_RADIUS = 3;
|
|
|
|
[Resolved]
|
|
private OsuColour colours { get; set; }
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
recreate();
|
|
}
|
|
|
|
protected override bool OnKeyDown(KeyDownEvent e)
|
|
{
|
|
if (e.Repeat || !e.ControlPressed)
|
|
return false;
|
|
|
|
bool runOperationFromHotkey(Func<bool> operation)
|
|
{
|
|
operationStarted();
|
|
bool result = operation?.Invoke() ?? false;
|
|
operationEnded();
|
|
|
|
return result;
|
|
}
|
|
|
|
switch (e.Key)
|
|
{
|
|
case Key.G:
|
|
return CanReverse && runOperationFromHotkey(OnReverse);
|
|
|
|
case Key.H:
|
|
return CanScaleX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false);
|
|
|
|
case Key.J:
|
|
return CanScaleY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false);
|
|
}
|
|
|
|
return base.OnKeyDown(e);
|
|
}
|
|
|
|
private void recreate()
|
|
{
|
|
if (LoadState < LoadState.Loading)
|
|
return;
|
|
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
new Container
|
|
{
|
|
Masking = true,
|
|
BorderThickness = BORDER_RADIUS,
|
|
BorderColour = colours.YellowDark,
|
|
RelativeSizeAxes = Axes.Both,
|
|
Children = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
|
|
AlwaysPresent = true,
|
|
Alpha = 0
|
|
},
|
|
}
|
|
},
|
|
dragHandles = new Container
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
// ensures that the centres of all drag handles line up with the middle of the selection box border.
|
|
Padding = new MarginPadding(BORDER_RADIUS / 2)
|
|
},
|
|
buttons = new FillFlowContainer
|
|
{
|
|
Y = 20,
|
|
AutoSizeAxes = Axes.Both,
|
|
Direction = FillDirection.Horizontal,
|
|
Anchor = Anchor.BottomCentre,
|
|
Origin = Anchor.Centre
|
|
}
|
|
};
|
|
|
|
if (CanScaleX) addXScaleComponents();
|
|
if (CanScaleX && CanScaleY) addFullScaleComponents();
|
|
if (CanScaleY) addYScaleComponents();
|
|
if (CanRotate) addRotationComponents();
|
|
if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
|
|
}
|
|
|
|
private void addRotationComponents()
|
|
{
|
|
const float separation = 40;
|
|
|
|
addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90));
|
|
addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90));
|
|
|
|
AddRangeInternal(new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
Depth = float.MaxValue,
|
|
Colour = colours.YellowLight,
|
|
Blending = BlendingParameters.Additive,
|
|
Alpha = 0.3f,
|
|
Size = new Vector2(BORDER_RADIUS, separation),
|
|
Anchor = Anchor.TopCentre,
|
|
Origin = Anchor.BottomCentre,
|
|
},
|
|
new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate")
|
|
{
|
|
Anchor = Anchor.TopCentre,
|
|
Y = -separation,
|
|
HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e)),
|
|
OperationStarted = operationStarted,
|
|
OperationEnded = operationEnded
|
|
}
|
|
});
|
|
}
|
|
|
|
private void addYScaleComponents()
|
|
{
|
|
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical));
|
|
|
|
addDragHandle(Anchor.TopCentre);
|
|
addDragHandle(Anchor.BottomCentre);
|
|
}
|
|
|
|
private void addFullScaleComponents()
|
|
{
|
|
addDragHandle(Anchor.TopLeft);
|
|
addDragHandle(Anchor.TopRight);
|
|
addDragHandle(Anchor.BottomLeft);
|
|
addDragHandle(Anchor.BottomRight);
|
|
}
|
|
|
|
private void addXScaleComponents()
|
|
{
|
|
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal));
|
|
|
|
addDragHandle(Anchor.CentreLeft);
|
|
addDragHandle(Anchor.CentreRight);
|
|
}
|
|
|
|
private void addButton(IconUsage icon, string tooltip, Action action)
|
|
{
|
|
buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip)
|
|
{
|
|
OperationStarted = operationStarted,
|
|
OperationEnded = operationEnded,
|
|
Action = action
|
|
});
|
|
}
|
|
|
|
private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle
|
|
{
|
|
Anchor = anchor,
|
|
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
|
|
OperationStarted = operationStarted,
|
|
OperationEnded = operationEnded
|
|
});
|
|
|
|
private int activeOperations;
|
|
|
|
private float convertDragEventToAngleOfRotation(DragEvent e)
|
|
{
|
|
// Adjust coordinate system to the center of SelectionBox
|
|
float startAngle = MathF.Atan2(e.LastMousePosition.Y - DrawHeight / 2, e.LastMousePosition.X - DrawWidth / 2);
|
|
float endAngle = MathF.Atan2(e.MousePosition.Y - DrawHeight / 2, e.MousePosition.X - DrawWidth / 2);
|
|
|
|
return (endAngle - startAngle) * 180 / MathF.PI;
|
|
}
|
|
|
|
private void operationEnded()
|
|
{
|
|
if (--activeOperations == 0)
|
|
OperationEnded?.Invoke();
|
|
}
|
|
|
|
private void operationStarted()
|
|
{
|
|
if (activeOperations++ == 0)
|
|
OperationStarted?.Invoke();
|
|
}
|
|
}
|
|
}
|