mirror of
https://github.com/ppy/osu.git
synced 2024-12-17 06:42:54 +08:00
75b8f2535f
Here we produce a local bound copy of the path version, and bind it to update the path type. This way, if the path version updates (i.e. any control point changes type or position), we check that all control points have a well-defined path. Additionally, if the control point piece is disposed of, the GB should also swoop up the subscription because of the local bound copy.
245 lines
8.7 KiB
C#
245 lines
8.7 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 System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using Humanizer;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Cursor;
|
|
using osu.Framework.Graphics.UserInterface;
|
|
using osu.Framework.Input;
|
|
using osu.Framework.Input.Bindings;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Game.Graphics.UserInterface;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
using osu.Game.Screens.Edit;
|
|
using osuTK;
|
|
using osuTK.Input;
|
|
|
|
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|
{
|
|
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
|
{
|
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
|
|
|
|
internal readonly Container<PathControlPointPiece> Pieces;
|
|
internal readonly Container<PathControlPointConnectionPiece> Connections;
|
|
|
|
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
|
private readonly Slider slider;
|
|
private readonly bool allowSelection;
|
|
|
|
private InputManager inputManager;
|
|
|
|
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
|
|
|
|
public PathControlPointVisualiser(Slider slider, bool allowSelection)
|
|
{
|
|
this.slider = slider;
|
|
this.allowSelection = allowSelection;
|
|
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
|
|
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }
|
|
};
|
|
}
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
inputManager = GetContainingInputManager();
|
|
|
|
controlPoints.CollectionChanged += onControlPointsChanged;
|
|
controlPoints.BindTo(slider.Path.ControlPoints);
|
|
}
|
|
|
|
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
switch (e.Action)
|
|
{
|
|
case NotifyCollectionChangedAction.Add:
|
|
// If inserting in the path (not appending),
|
|
// update indices of existing connections after insert location
|
|
if (e.NewStartingIndex < Pieces.Count)
|
|
{
|
|
foreach (var connection in Connections)
|
|
{
|
|
if (connection.ControlPointIndex >= e.NewStartingIndex)
|
|
connection.ControlPointIndex += e.NewItems.Count;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < e.NewItems.Count; i++)
|
|
{
|
|
var point = (PathControlPoint)e.NewItems[i];
|
|
|
|
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
|
|
{
|
|
if (allowSelection)
|
|
d.RequestSelection = selectPiece;
|
|
}));
|
|
|
|
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
|
|
}
|
|
|
|
break;
|
|
|
|
case NotifyCollectionChangedAction.Remove:
|
|
foreach (var point in e.OldItems.Cast<PathControlPoint>())
|
|
{
|
|
Pieces.RemoveAll(p => p.ControlPoint == point);
|
|
Connections.RemoveAll(c => c.ControlPoint == point);
|
|
}
|
|
|
|
// If removing before the end of the path,
|
|
// update indices of connections after remove location
|
|
if (e.OldStartingIndex < Pieces.Count)
|
|
{
|
|
foreach (var connection in Connections)
|
|
{
|
|
if (connection.ControlPointIndex >= e.OldStartingIndex)
|
|
connection.ControlPointIndex -= e.OldItems.Count;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected override bool OnClick(ClickEvent e)
|
|
{
|
|
foreach (var piece in Pieces)
|
|
{
|
|
piece.IsSelected.Value = false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool OnPressed(PlatformAction action)
|
|
{
|
|
switch (action.ActionMethod)
|
|
{
|
|
case PlatformActionMethod.Delete:
|
|
return DeleteSelected();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void OnReleased(PlatformAction action)
|
|
{
|
|
}
|
|
|
|
private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e)
|
|
{
|
|
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
|
|
piece.IsSelected.Toggle();
|
|
else
|
|
{
|
|
foreach (var p in Pieces)
|
|
p.IsSelected.Value = p == piece;
|
|
}
|
|
}
|
|
|
|
[Resolved(CanBeNull = true)]
|
|
private IEditorChangeHandler changeHandler { get; set; }
|
|
|
|
public bool DeleteSelected()
|
|
{
|
|
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
|
|
|
|
// Ensure that there are any points to be deleted
|
|
if (toRemove.Count == 0)
|
|
return false;
|
|
|
|
changeHandler?.BeginChange();
|
|
RemoveControlPointsRequested?.Invoke(toRemove);
|
|
changeHandler?.EndChange();
|
|
|
|
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
|
|
foreach (var piece in Pieces)
|
|
piece.IsSelected.Value = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public MenuItem[] ContextMenuItems
|
|
{
|
|
get
|
|
{
|
|
if (!Pieces.Any(p => p.IsHovered))
|
|
return null;
|
|
|
|
var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToList();
|
|
int count = selectedPieces.Count;
|
|
|
|
if (count == 0)
|
|
return null;
|
|
|
|
List<MenuItem> items = new List<MenuItem>();
|
|
|
|
if (!selectedPieces.Contains(Pieces[0]))
|
|
items.Add(createMenuItemForPathType(null));
|
|
|
|
// todo: hide/disable items which aren't valid for selected points
|
|
items.Add(createMenuItemForPathType(PathType.Linear));
|
|
items.Add(createMenuItemForPathType(PathType.PerfectCurve));
|
|
items.Add(createMenuItemForPathType(PathType.Bezier));
|
|
items.Add(createMenuItemForPathType(PathType.Catmull));
|
|
|
|
return new MenuItem[]
|
|
{
|
|
new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()),
|
|
new OsuMenuItem("Curve type")
|
|
{
|
|
Items = items
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
private MenuItem createMenuItemForPathType(PathType? type)
|
|
{
|
|
int totalCount = Pieces.Count(p => p.IsSelected.Value);
|
|
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type);
|
|
|
|
var item = new PathTypeMenuItem(type, () =>
|
|
{
|
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
|
p.ControlPoint.Type.Value = type;
|
|
});
|
|
|
|
if (countOfState == totalCount)
|
|
item.State.Value = TernaryState.True;
|
|
else if (countOfState > 0)
|
|
item.State.Value = TernaryState.Indeterminate;
|
|
else
|
|
item.State.Value = TernaryState.False;
|
|
|
|
return item;
|
|
}
|
|
|
|
private class PathTypeMenuItem : TernaryStateMenuItem
|
|
{
|
|
public PathTypeMenuItem(PathType? type, Action action)
|
|
: base(type == null ? "Inherit" : type.ToString().Humanize(), changeState, MenuItemType.Standard, _ => action?.Invoke())
|
|
{
|
|
}
|
|
|
|
private static TernaryState changeState(TernaryState state) => TernaryState.True;
|
|
}
|
|
}
|
|
}
|