mirror of
https://github.com/ppy/osu.git
synced 2025-01-31 04:32:57 +08:00
Implement precise movement tool
As mentioned in one of the points in https://github.com/ppy/osu/discussions/31263.
This commit is contained in:
parent
a8456ce9ac
commit
ebca2e4b4f
190
osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs
Normal file
190
osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit
|
||||||
|
{
|
||||||
|
public partial class PreciseMovementPopover : OsuPopover
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
private readonly Dictionary<HitObject, Vector2> initialPositions = new Dictionary<HitObject, Vector2>();
|
||||||
|
private RectangleF initialSurroundingQuad;
|
||||||
|
|
||||||
|
private BindableNumber<float> xBindable = null!;
|
||||||
|
private BindableNumber<float> yBindable = null!;
|
||||||
|
|
||||||
|
private SliderWithTextBoxInput<float> xInput = null!;
|
||||||
|
private OsuCheckbox relativeCheckbox = null!;
|
||||||
|
|
||||||
|
public PreciseMovementPopover()
|
||||||
|
{
|
||||||
|
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 220,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
xInput = new SliderWithTextBoxInput<float>("X:")
|
||||||
|
{
|
||||||
|
Current = xBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
new SliderWithTextBoxInput<float>("Y:")
|
||||||
|
{
|
||||||
|
Current = yBindable = new BindableNumber<float>
|
||||||
|
{
|
||||||
|
Precision = 1,
|
||||||
|
},
|
||||||
|
Instantaneous = true,
|
||||||
|
TabbableContentContainer = this,
|
||||||
|
},
|
||||||
|
relativeCheckbox = new OsuCheckbox(false)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
LabelText = "Relative movement",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
xInput.TakeFocus();
|
||||||
|
xInput.SelectAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
editorBeatmap.BeginChange();
|
||||||
|
initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair<HitObject, Vector2>(ho, ((IHasPosition)ho).Position)));
|
||||||
|
initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast<IHasPosition>()).AABBFloat;
|
||||||
|
|
||||||
|
Debug.Assert(initialPositions.Count > 0);
|
||||||
|
|
||||||
|
if (initialPositions.Count > 1)
|
||||||
|
{
|
||||||
|
relativeCheckbox.Current.Value = true;
|
||||||
|
relativeCheckbox.Current.Disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true);
|
||||||
|
xBindable.BindValueChanged(_ => applyPosition());
|
||||||
|
yBindable.BindValueChanged(_ => applyPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
if (IsLoaded) editorBeatmap.EndChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void relativeChanged()
|
||||||
|
{
|
||||||
|
// reset bindable bounds to something that is guaranteed to be larger than any previous value.
|
||||||
|
// this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic -
|
||||||
|
// if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue.
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue);
|
||||||
|
|
||||||
|
float previousX = xBindable.Value;
|
||||||
|
float previousY = yBindable.Value;
|
||||||
|
|
||||||
|
if (relativeCheckbox.Current.Value)
|
||||||
|
{
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||||
|
|
||||||
|
xBindable.Default = yBindable.Default = 0;
|
||||||
|
|
||||||
|
if (initialPositions.Count == 1)
|
||||||
|
{
|
||||||
|
var initialPosition = initialPositions.Single().Value;
|
||||||
|
xBindable.Value = previousX - initialPosition.X;
|
||||||
|
yBindable.Value = previousY - initialPosition.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Assert(initialPositions.Count == 1);
|
||||||
|
var initialPosition = initialPositions.Single().Value;
|
||||||
|
|
||||||
|
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||||
|
|
||||||
|
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||||
|
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||||
|
|
||||||
|
xBindable.Default = initialPosition.X;
|
||||||
|
yBindable.Default = initialPosition.Y;
|
||||||
|
|
||||||
|
xBindable.Value = xBindable.Default + previousX;
|
||||||
|
yBindable.Value = yBindable.Default + previousY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPosition()
|
||||||
|
{
|
||||||
|
editorBeatmap.PerformOnSelection(ho =>
|
||||||
|
{
|
||||||
|
if (!initialPositions.TryGetValue(ho, out var initialPosition))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pos = new Vector2(xBindable.Value, yBindable.Value);
|
||||||
|
if (relativeCheckbox.Current.Value)
|
||||||
|
((IHasPosition)ho).Position = initialPosition + pos;
|
||||||
|
else
|
||||||
|
((IHasPosition)ho).Position = pos;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
|
{
|
||||||
|
if (e.Action == GlobalAction.Select && !e.Repeat)
|
||||||
|
{
|
||||||
|
this.HidePopover();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnPressed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
|
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||||
|
private readonly BindableBool canMove = new BindableBool();
|
||||||
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
|
||||||
|
|
||||||
|
private EditorToolButton moveButton = null!;
|
||||||
private EditorToolButton rotateButton = null!;
|
private EditorToolButton rotateButton = null!;
|
||||||
private EditorToolButton scaleButton = null!;
|
private EditorToolButton scaleButton = null!;
|
||||||
|
|
||||||
@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
{
|
{
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
Spacing = new Vector2(5),
|
Spacing = new Vector2(5),
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
moveButton = new EditorToolButton("Move",
|
||||||
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
||||||
|
() => new PreciseMovementPopover()),
|
||||||
rotateButton = new EditorToolButton("Rotate",
|
rotateButton = new EditorToolButton("Rotate",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
|
||||||
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
() => new PreciseRotationPopover(RotationHandler, GridToolbox)),
|
||||||
scaleButton = new EditorToolButton("Scale",
|
scaleButton = new EditorToolButton("Scale",
|
||||||
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
|
() => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt },
|
||||||
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
() => new PreciseScalePopover(ScaleHandler, GridToolbox))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true);
|
||||||
|
|
||||||
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
|
||||||
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
|
||||||
|
|
||||||
@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||||
|
canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true);
|
||||||
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
|
||||||
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
|
||||||
}
|
}
|
||||||
@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
|
case GlobalAction.EditorToggleMoveControl:
|
||||||
|
{
|
||||||
|
moveButton.TriggerClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case GlobalAction.EditorToggleRotateControl:
|
case GlobalAction.EditorToggleRotateControl:
|
||||||
{
|
{
|
||||||
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
|
||||||
|
@ -32,6 +32,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
set => slider.Current = value;
|
set => slider.Current = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompositeDrawable TabbableContentContainer
|
||||||
|
{
|
||||||
|
set => textBox.TabbableContentContainer = value;
|
||||||
|
}
|
||||||
|
|
||||||
private bool instantaneous;
|
private bool instantaneous;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||||
|
new KeyBinding(InputKey.None, GlobalAction.EditorToggleMoveControl),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
||||||
@ -493,7 +494,10 @@ namespace osu.Game.Input.Bindings
|
|||||||
EditorSeekToNextBookmark,
|
EditorSeekToNextBookmark,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))]
|
||||||
AbsoluteScrollSongList
|
AbsoluteScrollSongList,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))]
|
||||||
|
EditorToggleMoveControl,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GlobalActionCategory
|
public enum GlobalActionCategory
|
||||||
|
@ -454,6 +454,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list");
|
public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Toggle movement control"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control");
|
||||||
|
|
||||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user