// Copyright (c) ppy Pty Ltd . 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 initialPositions = new Dictionary(); private RectangleF initialSurroundingQuad; private BindableNumber xBindable = null!; private BindableNumber yBindable = null!; private SliderWithTextBoxInput 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("X:") { Current = xBindable = new BindableNumber { Precision = 1, }, Instantaneous = true, TabbableContentContainer = this, }, new SliderWithTextBoxInput("Y:") { Current = yBindable = new BindableNumber { 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()); } protected override void PopIn() { base.PopIn(); editorBeatmap.BeginChange(); initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair(ho, ((IHasPosition)ho).Position))); initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast()).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 e) { if (e.Action == GlobalAction.Select && !e.Repeat) { this.HidePopover(); return true; } return base.OnPressed(e); } } }