mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:33:30 +08:00
Refactor editor selection/blueprint components to be generic
This commit is contained in:
parent
ec1c336b0a
commit
f2e56bd306
@ -4,6 +4,7 @@
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
@ -30,6 +31,6 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
return base.CreateBlueprintFor(hitObject);
|
||||
}
|
||||
|
||||
protected override SelectionHandler CreateSelectionHandler() => new ManiaSelectionHandler();
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,13 @@ using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class ManiaSelectionHandler : SelectionHandler
|
||||
public class ManiaSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; }
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var maniaBlueprint = (ManiaSelectionBlueprint)moveEvent.Blueprint;
|
||||
int lastColumn = maniaBlueprint.DrawableObject.HitObject.Column;
|
||||
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
return true;
|
||||
}
|
||||
|
||||
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
|
||||
private void performColumnMovement(int lastColumn, MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
|
||||
|
||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
public abstract class OsuSelectionBlueprint<T> : OverlaySelectionBlueprint
|
||||
where T : OsuHitObject
|
||||
{
|
||||
protected new T HitObject => (T)DrawableObject.HitObject;
|
||||
protected T HitObject => (T)DrawableObject.HitObject;
|
||||
|
||||
protected override bool AlwaysShowWhenSelected => true;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler();
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
|
||||
|
||||
public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
|
||||
{
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if (b.IsSelected)
|
||||
continue;
|
||||
|
||||
var hitObject = (OsuHitObject)b.HitObject;
|
||||
var hitObject = (OsuHitObject)b.Item;
|
||||
|
||||
Vector2? snap = checkSnap(hitObject.Position);
|
||||
if (snap == null && hitObject.Position != hitObject.EndPosition)
|
||||
|
@ -15,7 +15,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuSelectionHandler : SelectionHandler
|
||||
public class OsuSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
}
|
||||
|
||||
protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler();
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TaikoSelectionHandler();
|
||||
|
||||
public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) =>
|
||||
new TaikoSelectionBlueprint(hitObject);
|
||||
|
@ -8,12 +8,13 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public class TaikoSelectionHandler : SelectionHandler
|
||||
public class TaikoSelectionHandler : EditorSelectionHandler
|
||||
{
|
||||
private readonly Bindable<TernaryState> selectionRimState = new Bindable<TernaryState>();
|
||||
private readonly Bindable<TernaryState> selectionStrongState = new Bindable<TernaryState>();
|
||||
@ -72,16 +73,16 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
});
|
||||
}
|
||||
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
|
||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
{
|
||||
if (selection.All(s => s.HitObject is Hit))
|
||||
if (selection.All(s => s.Item is Hit))
|
||||
yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } };
|
||||
|
||||
if (selection.All(s => s.HitObject is TaikoHitObject))
|
||||
if (selection.All(s => s.Item is TaikoHitObject))
|
||||
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
|
||||
}
|
||||
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
|
||||
|
||||
protected override void UpdateTernaryStates()
|
||||
{
|
||||
|
@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
private BlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<BlueprintContainer>().First();
|
||||
private EditorBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
|
||||
|
||||
[Test]
|
||||
public void TestSelectedObjectHasPriorityWhenOverlapping()
|
||||
|
@ -132,8 +132,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionHandler>().First().Alpha == 0);
|
||||
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionHandler>().First().Alpha == 0);
|
||||
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0);
|
||||
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0);
|
||||
}
|
||||
|
||||
AddStep("paste hitobject", () => Editor.Paste());
|
||||
@ -142,8 +142,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000);
|
||||
|
||||
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionHandler>().First().Alpha > 0);
|
||||
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionHandler>().First().Alpha > 0);
|
||||
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
|
||||
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -26,15 +26,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||
|
||||
private BlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<BlueprintContainer>().First();
|
||||
private EditorBlueprintContainer blueprintContainer
|
||||
=> Editor.ChildrenOfType<EditorBlueprintContainer>().First();
|
||||
|
||||
private void moveMouseToObject(Func<HitObject> targetFunc)
|
||||
{
|
||||
AddStep("move mouse to object", () =>
|
||||
{
|
||||
var pos = blueprintContainer.SelectionBlueprints
|
||||
.First(s => s.HitObject == targetFunc())
|
||||
.First(s => s.Item == targetFunc())
|
||||
.ChildrenOfType<HitCirclePiece>()
|
||||
.First().ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
|
@ -3,12 +3,13 @@
|
||||
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public abstract class OverlaySelectionBlueprint : SelectionBlueprint
|
||||
public abstract class OverlaySelectionBlueprint : SelectionBlueprint<HitObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableHitObject"/> which this <see cref="OverlaySelectionBlueprint"/> applies to.
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osuTK;
|
||||
|
||||
@ -17,26 +16,26 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <summary>
|
||||
/// A blueprint placed above a <see cref="DrawableHitObject"/> adding editing functionality.
|
||||
/// </summary>
|
||||
public abstract class SelectionBlueprint : CompositeDrawable, IStateful<SelectionState>
|
||||
public abstract class SelectionBlueprint<T> : CompositeDrawable, IStateful<SelectionState>
|
||||
{
|
||||
public readonly HitObject HitObject;
|
||||
public readonly T Item;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="SelectionBlueprint"/> has been selected.
|
||||
/// Invoked when this <see cref="SelectionBlueprint{T}"/> has been selected.
|
||||
/// </summary>
|
||||
public event Action<SelectionBlueprint> Selected;
|
||||
public event Action<SelectionBlueprint<T>> Selected;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="SelectionBlueprint"/> has been deselected.
|
||||
/// Invoked when this <see cref="SelectionBlueprint{T}"/> has been deselected.
|
||||
/// </summary>
|
||||
public event Action<SelectionBlueprint> Deselected;
|
||||
public event Action<SelectionBlueprint<T>> Deselected;
|
||||
|
||||
public override bool HandlePositionalInput => ShouldBeAlive;
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
|
||||
protected SelectionBlueprint(HitObject hitObject)
|
||||
protected SelectionBlueprint(T item)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
Item = item;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AlwaysPresent = true;
|
||||
|
@ -3,11 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
@ -25,37 +23,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which provides a "blueprint" display of hitobjects.
|
||||
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler"/>.
|
||||
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
|
||||
/// </summary>
|
||||
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
protected DragBox DragBox { get; private set; }
|
||||
|
||||
public Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
|
||||
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
|
||||
|
||||
protected SelectionHandler SelectionHandler { get; private set; }
|
||||
protected SelectionHandler<T> SelectionHandler { get; private set; }
|
||||
|
||||
protected readonly HitObjectComposer Composer;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected EditorClock EditorClock { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; }
|
||||
|
||||
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||
private readonly Dictionary<HitObject, SelectionBlueprint> blueprintMap = new Dictionary<HitObject, SelectionBlueprint>();
|
||||
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IPositionSnapProvider snapProvider { get; set; }
|
||||
|
||||
protected BlueprintContainer(HitObjectComposer composer)
|
||||
{
|
||||
Composer = composer;
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
protected BlueprintContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
@ -73,66 +60,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
SelectionHandler.CreateProxy(),
|
||||
DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
|
||||
});
|
||||
|
||||
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
|
||||
if (Composer != null)
|
||||
{
|
||||
foreach (var obj in Composer.HitObjects)
|
||||
addBlueprintFor(obj.HitObject);
|
||||
}
|
||||
|
||||
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
|
||||
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
|
||||
{
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (var o in args.NewItems)
|
||||
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Select();
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var o in args.OldItems)
|
||||
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Beatmap.HitObjectAdded += addBlueprintFor;
|
||||
Beatmap.HitObjectRemoved += removeBlueprintFor;
|
||||
|
||||
if (Composer != null)
|
||||
{
|
||||
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
|
||||
foreach (var obj in Composer.HitObjects)
|
||||
addBlueprintFor(obj.HitObject);
|
||||
|
||||
Composer.Playfield.HitObjectUsageBegan += addBlueprintFor;
|
||||
Composer.Playfield.HitObjectUsageFinished += removeBlueprintFor;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
|
||||
protected virtual Container<SelectionBlueprint<T>> CreateSelectionBlueprintContainer() => new Container<SelectionBlueprint<T>> { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
|
||||
/// Creates a <see cref="Components.SelectionHandler{T}"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
|
||||
/// </summary>
|
||||
protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
|
||||
protected virtual SelectionHandler<T> CreateSelectionHandler() => new SelectionHandler<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="SelectionBlueprint"/> for a specific <see cref="DrawableHitObject"/>.
|
||||
/// Creates a <see cref="SelectionBlueprint{T}"/> for a specific <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create the overlay for.</param>
|
||||
protected virtual SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => null;
|
||||
protected virtual SelectionBlueprint<T> CreateBlueprintFor(T hitObject) => null;
|
||||
|
||||
protected virtual DragBox CreateDragBox(Action<RectangleF> performSelect) => new DragBox(performSelect);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this component is in a state where deselection should be allowed. If false, selection will only be added to.
|
||||
/// </summary>
|
||||
protected virtual bool AllowDeselection => true;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
bool selectionPerformed = performMouseDownActions(e);
|
||||
@ -143,7 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return selectionPerformed || e.Button == MouseButton.Left;
|
||||
}
|
||||
|
||||
private SelectionBlueprint clickedBlueprint;
|
||||
protected SelectionBlueprint<T> ClickedBlueprint { get; private set; }
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
@ -151,11 +100,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return false;
|
||||
|
||||
// store for double-click handling
|
||||
clickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
|
||||
ClickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
|
||||
|
||||
// Deselection should only occur if no selected blueprints are hovered
|
||||
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
|
||||
if (endClickSelection(e) || clickedBlueprint != null)
|
||||
if (endClickSelection(e) || ClickedBlueprint != null)
|
||||
return true;
|
||||
|
||||
deselectAll();
|
||||
@ -168,10 +117,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return false;
|
||||
|
||||
// ensure the blueprint which was hovered for the first click is still the hovered blueprint.
|
||||
if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
|
||||
if (ClickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != ClickedBlueprint)
|
||||
return false;
|
||||
|
||||
EditorClock?.SeekSmoothlyTo(clickedBlueprint.HitObject.StartTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -227,9 +175,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
if (isDraggingBlueprint)
|
||||
{
|
||||
// handle positional change etc.
|
||||
foreach (var obj in selectedHitObjects)
|
||||
Beatmap.Update(obj);
|
||||
UpdateSelection();
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
@ -238,6 +184,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
DragBox.Hide();
|
||||
}
|
||||
|
||||
protected virtual void UpdateSelection()
|
||||
{
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
switch (e.Key)
|
||||
@ -258,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
switch (action.ActionType)
|
||||
{
|
||||
case PlatformActionType.SelectAll:
|
||||
selectAll();
|
||||
SelectAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -271,61 +221,55 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
#region Blueprint Addition/Removal
|
||||
|
||||
private void addBlueprintFor(HitObject hitObject)
|
||||
protected virtual void AddBlueprintFor(T item)
|
||||
{
|
||||
if (hitObject is IBarLine)
|
||||
if (blueprintMap.ContainsKey(item))
|
||||
return;
|
||||
|
||||
if (blueprintMap.ContainsKey(hitObject))
|
||||
return;
|
||||
|
||||
var blueprint = CreateBlueprintFor(hitObject);
|
||||
var blueprint = CreateBlueprintFor(item);
|
||||
if (blueprint == null)
|
||||
return;
|
||||
|
||||
blueprintMap[hitObject] = blueprint;
|
||||
blueprintMap[item] = blueprint;
|
||||
|
||||
blueprint.Selected += onBlueprintSelected;
|
||||
blueprint.Deselected += onBlueprintDeselected;
|
||||
|
||||
if (Beatmap.SelectedHitObjects.Contains(hitObject))
|
||||
blueprint.Select();
|
||||
blueprint.Selected += OnBlueprintSelected;
|
||||
blueprint.Deselected += OnBlueprintDeselected;
|
||||
|
||||
SelectionBlueprints.Add(blueprint);
|
||||
|
||||
OnBlueprintAdded(hitObject);
|
||||
OnBlueprintAdded(blueprint);
|
||||
}
|
||||
|
||||
private void removeBlueprintFor(HitObject hitObject)
|
||||
protected void RemoveBlueprintFor(T item)
|
||||
{
|
||||
if (!blueprintMap.Remove(hitObject, out var blueprint))
|
||||
if (!blueprintMap.Remove(item, out var blueprint))
|
||||
return;
|
||||
|
||||
blueprint.Deselect();
|
||||
blueprint.Selected -= onBlueprintSelected;
|
||||
blueprint.Deselected -= onBlueprintDeselected;
|
||||
blueprint.Selected -= OnBlueprintSelected;
|
||||
blueprint.Deselected -= OnBlueprintDeselected;
|
||||
|
||||
SelectionBlueprints.Remove(blueprint);
|
||||
|
||||
if (movementBlueprints?.Contains(blueprint) == true)
|
||||
finishSelectionMovement();
|
||||
|
||||
OnBlueprintRemoved(hitObject);
|
||||
OnBlueprintRemoved(blueprint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after a <see cref="HitObject"/> blueprint has been added.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been added.</param>
|
||||
protected virtual void OnBlueprintAdded(HitObject hitObject)
|
||||
/// <param name="blueprint">The <see cref="HitObject"/> for which the blueprint has been added.</param>
|
||||
protected virtual void OnBlueprintAdded(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after a <see cref="HitObject"/> blueprint has been removed.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The <see cref="HitObject"/> for which the blueprint has been removed.</param>
|
||||
protected virtual void OnBlueprintRemoved(HitObject hitObject)
|
||||
/// <param name="item">The <see cref="HitObject"/> for which the blueprint has been removed.</param>
|
||||
protected virtual void OnBlueprintRemoved(SelectionBlueprint<T> item)
|
||||
{
|
||||
}
|
||||
|
||||
@ -347,7 +291,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
|
||||
// Priority is given to already-selected blueprints.
|
||||
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
|
||||
foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
|
||||
{
|
||||
if (!blueprint.IsHovered) continue;
|
||||
|
||||
@ -371,7 +315,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
|
||||
// Priority is given to already-selected blueprints.
|
||||
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
|
||||
foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
|
||||
{
|
||||
if (!blueprint.IsHovered) continue;
|
||||
|
||||
@ -405,7 +349,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
case SelectionState.Selected:
|
||||
// if the editor is playing, we generally don't want to deselect objects even if outside the selection area.
|
||||
if (!EditorClock.IsRunning && !isValidForSelection())
|
||||
if (AllowDeselection && !isValidForSelection())
|
||||
blueprint.Deselect();
|
||||
break;
|
||||
}
|
||||
@ -413,35 +357,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects all <see cref="SelectionBlueprint"/>s.
|
||||
/// Selects all <see cref="SelectionBlueprint{T}"/>s.
|
||||
/// </summary>
|
||||
private void selectAll()
|
||||
protected virtual void SelectAll()
|
||||
{
|
||||
Composer.Playfield.KeepAllAlive();
|
||||
|
||||
// Scheduled to allow the change in lifetime to take place.
|
||||
Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deselects all selected <see cref="SelectionBlueprint"/>s.
|
||||
/// Deselects all selected <see cref="SelectionBlueprint{T}"/>s.
|
||||
/// </summary>
|
||||
private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
|
||||
|
||||
private void onBlueprintSelected(SelectionBlueprint blueprint)
|
||||
protected virtual void OnBlueprintSelected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
SelectionHandler.HandleSelected(blueprint);
|
||||
SelectionBlueprints.ChangeChildDepth(blueprint, 1);
|
||||
|
||||
Composer.Playfield.SetKeepAlive(blueprint.HitObject, true);
|
||||
}
|
||||
|
||||
private void onBlueprintDeselected(SelectionBlueprint blueprint)
|
||||
protected virtual void OnBlueprintDeselected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
|
||||
SelectionHandler.HandleDeselected(blueprint);
|
||||
|
||||
Composer.Playfield.SetKeepAlive(blueprint.HitObject, false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -449,7 +387,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
#region Selection Movement
|
||||
|
||||
private Vector2[] movementBlueprintOriginalPositions;
|
||||
private SelectionBlueprint[] movementBlueprints;
|
||||
private SelectionBlueprint<T>[] movementBlueprints;
|
||||
private bool isDraggingBlueprint;
|
||||
|
||||
/// <summary>
|
||||
@ -466,10 +404,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return;
|
||||
|
||||
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
|
||||
movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray();
|
||||
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
|
||||
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<SelectionBlueprint<T>> SortForMovement(IReadOnlyList<SelectionBlueprint<T>> blueprints) => blueprints;
|
||||
|
||||
/// <summary>
|
||||
/// Moves the current selected blueprints.
|
||||
/// </summary>
|
||||
@ -497,7 +437,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||
|
||||
// attempt to move the objects, and abort any time based snapping if we can.
|
||||
if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition)))
|
||||
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints[i], positionalResult.ScreenSpacePosition)))
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -510,20 +450,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
// Retrieve a snapped position.
|
||||
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
|
||||
|
||||
// Move the hitobjects.
|
||||
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition)))
|
||||
return true;
|
||||
return ApplySnapResult(movementBlueprints, result);
|
||||
}
|
||||
|
||||
if (result.Time.HasValue)
|
||||
{
|
||||
// Apply the start time at the newly snapped-to position
|
||||
double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime;
|
||||
|
||||
if (offset != 0)
|
||||
Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
|
||||
}
|
||||
|
||||
return true;
|
||||
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result)
|
||||
{
|
||||
return !SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -542,22 +474,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (Beatmap != null)
|
||||
{
|
||||
Beatmap.HitObjectAdded -= addBlueprintFor;
|
||||
Beatmap.HitObjectRemoved -= removeBlueprintFor;
|
||||
}
|
||||
|
||||
if (Composer != null)
|
||||
{
|
||||
Composer.Playfield.HitObjectUsageBegan -= addBlueprintFor;
|
||||
Composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,12 +27,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <summary>
|
||||
/// A blueprint container generally displayed as an overlay to a ruleset's playfield.
|
||||
/// </summary>
|
||||
public class ComposeBlueprintContainer : BlueprintContainer
|
||||
public class ComposeBlueprintContainer : EditorBlueprintContainer
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
private readonly Container<PlacementBlueprint> placementBlueprintContainer;
|
||||
|
||||
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
|
||||
|
||||
private PlacementBlueprint currentPlacement;
|
||||
private InputManager inputManager;
|
||||
|
||||
@ -113,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
// convert to game space coordinates
|
||||
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
|
||||
|
||||
SelectionHandler.HandleMovement(new MoveSelectionEvent(firstBlueprint, firstBlueprint.ScreenSpaceSelectionPoint + delta));
|
||||
SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(firstBlueprint, firstBlueprint.ScreenSpaceSelectionPoint + delta));
|
||||
}
|
||||
|
||||
private void updatePlacementNewCombo()
|
||||
@ -237,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
updatePlacementPosition();
|
||||
}
|
||||
|
||||
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
|
||||
protected sealed override SelectionBlueprint<HitObject> CreateBlueprintFor(HitObject hitObject)
|
||||
{
|
||||
var drawable = Composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject);
|
||||
|
||||
@ -249,9 +251,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null;
|
||||
|
||||
protected override void OnBlueprintAdded(HitObject hitObject)
|
||||
protected override void OnBlueprintAdded(SelectionBlueprint<HitObject> item)
|
||||
{
|
||||
base.OnBlueprintAdded(hitObject);
|
||||
base.OnBlueprintAdded(item);
|
||||
|
||||
refreshTool();
|
||||
|
||||
|
@ -0,0 +1,176 @@
|
||||
// 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.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class EditorBlueprintContainer : BlueprintContainer<HitObject>
|
||||
{
|
||||
[Resolved]
|
||||
protected EditorClock EditorClock { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; }
|
||||
|
||||
protected readonly HitObjectComposer Composer;
|
||||
|
||||
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||
|
||||
protected EditorBlueprintContainer(HitObjectComposer composer)
|
||||
{
|
||||
Composer = composer;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
|
||||
if (Composer != null)
|
||||
{
|
||||
foreach (var obj in Composer.HitObjects)
|
||||
AddBlueprintFor(obj.HitObject);
|
||||
}
|
||||
|
||||
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
|
||||
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
|
||||
{
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (var o in args.NewItems)
|
||||
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var o in args.OldItems)
|
||||
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Beatmap.HitObjectAdded += AddBlueprintFor;
|
||||
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
|
||||
|
||||
if (Composer != null)
|
||||
{
|
||||
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
|
||||
foreach (var obj in Composer.HitObjects)
|
||||
AddBlueprintFor(obj.HitObject);
|
||||
|
||||
Composer.Playfield.HitObjectUsageBegan += AddBlueprintFor;
|
||||
Composer.Playfield.HitObjectUsageFinished += RemoveBlueprintFor;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
|
||||
=> blueprints.OrderBy(b => b.Item.StartTime);
|
||||
|
||||
protected override bool AllowDeselection => !EditorClock.IsRunning;
|
||||
|
||||
protected override bool ApplySnapResult(SelectionBlueprint<HitObject>[] blueprints, SnapResult result)
|
||||
{
|
||||
if (!base.ApplySnapResult(blueprints, result))
|
||||
return false;
|
||||
|
||||
if (result.Time.HasValue)
|
||||
{
|
||||
// Apply the start time at the newly snapped-to position
|
||||
double offset = result.Time.Value - blueprints.First().Item.StartTime;
|
||||
|
||||
if (offset != 0)
|
||||
Beatmap.PerformOnSelection(obj => obj.StartTime += offset);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void AddBlueprintFor(HitObject item)
|
||||
{
|
||||
if (item is IBarLine)
|
||||
return;
|
||||
|
||||
base.AddBlueprintFor(item);
|
||||
}
|
||||
|
||||
protected override void OnBlueprintAdded(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
base.OnBlueprintAdded(blueprint);
|
||||
if (Beatmap.SelectedHitObjects.Contains(blueprint.Item))
|
||||
blueprint.Select();
|
||||
}
|
||||
|
||||
protected override void UpdateSelection()
|
||||
{
|
||||
base.UpdateSelection();
|
||||
|
||||
// handle positional change etc.
|
||||
foreach (var blueprint in SelectionBlueprints)
|
||||
Beatmap.Update(blueprint.Item);
|
||||
}
|
||||
|
||||
protected override bool OnDoubleClick(DoubleClickEvent e)
|
||||
{
|
||||
if (!base.OnDoubleClick(e))
|
||||
return false;
|
||||
|
||||
EditorClock?.SeekSmoothlyTo(ClickedBlueprint.Item.StartTime);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
protected override void SelectAll()
|
||||
{
|
||||
Composer.Playfield.KeepAllAlive();
|
||||
|
||||
base.SelectAll();
|
||||
}
|
||||
|
||||
protected override void OnBlueprintSelected(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
base.OnBlueprintSelected(blueprint);
|
||||
|
||||
Composer.Playfield.SetKeepAlive(blueprint.Item, true);
|
||||
}
|
||||
|
||||
protected override void OnBlueprintDeselected(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
base.OnBlueprintDeselected(blueprint);
|
||||
|
||||
Composer.Playfield.SetKeepAlive(blueprint.Item, false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (Beatmap != null)
|
||||
{
|
||||
Beatmap.HitObjectAdded -= AddBlueprintFor;
|
||||
Beatmap.HitObjectRemoved -= RemoveBlueprintFor;
|
||||
}
|
||||
|
||||
if (Composer != null)
|
||||
{
|
||||
Composer.Playfield.HitObjectUsageBegan -= AddBlueprintFor;
|
||||
Composer.Playfield.HitObjectUsageFinished -= RemoveBlueprintFor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,233 @@
|
||||
// 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.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class EditorSelectionHandler : SelectionHandler<HitObject>, IHasContextMenu
|
||||
{
|
||||
[Resolved]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
createStateBindables();
|
||||
|
||||
// bring in updates from selection changes
|
||||
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
|
||||
EditorBeatmap.SelectedHitObjects.BindTo(SelectedItems);
|
||||
|
||||
SelectedItems.CollectionChanged += (sender, args) =>
|
||||
{
|
||||
Scheduler.AddOnce(UpdateTernaryStates);
|
||||
};
|
||||
}
|
||||
|
||||
internal override void HandleSelected(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
base.HandleSelected(blueprint);
|
||||
|
||||
// there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once.
|
||||
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.Item))
|
||||
EditorBeatmap.SelectedHitObjects.Add(blueprint.Item);
|
||||
}
|
||||
|
||||
internal override void HandleDeselected(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
base.HandleDeselected(blueprint);
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Remove(blueprint.Item);
|
||||
}
|
||||
|
||||
protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items);
|
||||
|
||||
#region Selection State
|
||||
|
||||
/// <summary>
|
||||
/// The state of "new combo" for all selected hitobjects.
|
||||
/// </summary>
|
||||
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
|
||||
|
||||
/// <summary>
|
||||
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
|
||||
|
||||
/// <summary>
|
||||
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
|
||||
/// </summary>
|
||||
private void createStateBindables()
|
||||
{
|
||||
foreach (var sampleName in HitSampleInfo.AllAdditions)
|
||||
{
|
||||
var bindable = new Bindable<TernaryState>
|
||||
{
|
||||
Description = sampleName.Replace("hit", string.Empty).Titleize()
|
||||
};
|
||||
|
||||
bindable.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case TernaryState.False:
|
||||
RemoveHitSample(sampleName);
|
||||
break;
|
||||
|
||||
case TernaryState.True:
|
||||
AddHitSample(sampleName);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SelectionSampleStates[sampleName] = bindable;
|
||||
}
|
||||
|
||||
// new combo
|
||||
SelectionNewComboState.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case TernaryState.False:
|
||||
SetNewCombo(false);
|
||||
break;
|
||||
|
||||
case TernaryState.True:
|
||||
SetNewCombo(true);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
|
||||
/// </summary>
|
||||
protected virtual void UpdateTernaryStates()
|
||||
{
|
||||
SelectionNewComboState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
|
||||
|
||||
foreach (var (sampleName, bindable) in SelectionSampleStates)
|
||||
{
|
||||
bindable.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
|
||||
/// </summary>
|
||||
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
|
||||
{
|
||||
if (selection.Any(func))
|
||||
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
|
||||
|
||||
return TernaryState.False;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample Changes
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="sampleName">The name of the hit sample.</param>
|
||||
public void AddHitSample(string sampleName)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
// Make sure there isn't already an existing sample
|
||||
if (h.Samples.Any(s => s.Name == sampleName))
|
||||
return;
|
||||
|
||||
h.Samples.Add(new HitSampleInfo(sampleName));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the new combo state of all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="state">Whether to set or unset.</param>
|
||||
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
|
||||
public void SetNewCombo(bool state)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
var comboInfo = h as IHasComboInformation;
|
||||
|
||||
if (comboInfo == null || comboInfo.NewCombo == state) return;
|
||||
|
||||
comboInfo.NewCombo = state;
|
||||
EditorBeatmap.Update(h);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a hit sample from all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="sampleName">The name of the hit sample.</param>
|
||||
public void RemoveHitSample(string sampleName)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context Menu
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!SelectedBlueprints.Any(b => b.IsHovered))
|
||||
return Array.Empty<MenuItem>();
|
||||
|
||||
var items = new List<MenuItem>();
|
||||
|
||||
items.AddRange(GetContextMenuItemsForSelection(SelectedBlueprints));
|
||||
|
||||
if (SelectedBlueprints.All(b => b.Item is IHasComboInformation))
|
||||
{
|
||||
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
|
||||
}
|
||||
|
||||
if (SelectedBlueprints.Count == 1)
|
||||
items.AddRange(SelectedBlueprints[0].ContextMenuItems);
|
||||
|
||||
items.AddRange(new[]
|
||||
{
|
||||
new OsuMenuItem("Sound")
|
||||
{
|
||||
Items = SelectionSampleStates.Select(kvp =>
|
||||
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||
},
|
||||
new OsuMenuItem("Delete", MenuItemType.Destructive, DeleteSelected),
|
||||
});
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provide context menu items relevant to current selection. Calling base is not required.
|
||||
/// </summary>
|
||||
/// <param name="selection">The current selection.</param>
|
||||
/// <returns>The relevant menu items.</returns>
|
||||
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)
|
||||
=> Enumerable.Empty<MenuItem>();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -11,17 +11,17 @@ using osu.Game.Rulesets.Objects;
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A container for <see cref="SelectionBlueprint"/> ordered by their <see cref="HitObject"/> start times.
|
||||
/// A container for <see cref="SelectionBlueprint{HitObject}"/> ordered by their <see cref="HitObject"/> start times.
|
||||
/// </summary>
|
||||
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint>
|
||||
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint<HitObject>>
|
||||
{
|
||||
public override void Add(SelectionBlueprint drawable)
|
||||
public override void Add(SelectionBlueprint<HitObject> drawable)
|
||||
{
|
||||
base.Add(drawable);
|
||||
bindStartTime(drawable);
|
||||
}
|
||||
|
||||
public override bool Remove(SelectionBlueprint drawable)
|
||||
public override bool Remove(SelectionBlueprint<HitObject> drawable)
|
||||
{
|
||||
if (!base.Remove(drawable))
|
||||
return false;
|
||||
@ -36,11 +36,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
unbindAllStartTimes();
|
||||
}
|
||||
|
||||
private readonly Dictionary<SelectionBlueprint, IBindable> startTimeMap = new Dictionary<SelectionBlueprint, IBindable>();
|
||||
private readonly Dictionary<SelectionBlueprint<HitObject>, IBindable> startTimeMap = new Dictionary<SelectionBlueprint<HitObject>, IBindable>();
|
||||
|
||||
private void bindStartTime(SelectionBlueprint blueprint)
|
||||
private void bindStartTime(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
|
||||
var bindable = blueprint.Item.StartTimeBindable.GetBoundCopy();
|
||||
|
||||
bindable.BindValueChanged(_ =>
|
||||
{
|
||||
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
startTimeMap[blueprint] = bindable;
|
||||
}
|
||||
|
||||
private void unbindStartTime(SelectionBlueprint blueprint)
|
||||
private void unbindStartTime(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
startTimeMap[blueprint].UnbindAll();
|
||||
startTimeMap.Remove(blueprint);
|
||||
@ -66,16 +66,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
{
|
||||
var xObj = (SelectionBlueprint)x;
|
||||
var yObj = (SelectionBlueprint)y;
|
||||
var xObj = (SelectionBlueprint<HitObject>)x;
|
||||
var yObj = (SelectionBlueprint<HitObject>)y;
|
||||
|
||||
// Put earlier blueprints towards the end of the list, so they handle input first
|
||||
int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
|
||||
int i = yObj.Item.StartTime.CompareTo(xObj.Item.StartTime);
|
||||
|
||||
if (i != 0) return i;
|
||||
|
||||
// Fall back to end time if the start time is equal.
|
||||
i = yObj.HitObject.GetEndTime().CompareTo(xObj.HitObject.GetEndTime());
|
||||
i = yObj.Item.GetEndTime().CompareTo(xObj.Item.GetEndTime());
|
||||
|
||||
return i == 0 ? CompareReverseChildID(y, x) : i;
|
||||
}
|
||||
|
@ -9,12 +9,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <summary>
|
||||
/// An event which occurs when a <see cref="OverlaySelectionBlueprint"/> is moved.
|
||||
/// </summary>
|
||||
public class MoveSelectionEvent
|
||||
public class MoveSelectionEvent<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="SelectionBlueprint"/> that triggered this <see cref="MoveSelectionEvent"/>.
|
||||
/// The <see cref="SelectionBlueprint{T}"/> that triggered this <see cref="MoveSelectionEvent{T}"/>.
|
||||
/// </summary>
|
||||
public readonly SelectionBlueprint Blueprint;
|
||||
public readonly SelectionBlueprint<T> Blueprint;
|
||||
|
||||
/// <summary>
|
||||
/// The expected screen-space position of the hitobject at the current cursor position.
|
||||
@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public readonly Vector2 InstantDelta;
|
||||
|
||||
public MoveSelectionEvent(SelectionBlueprint blueprint, Vector2 screenSpacePosition)
|
||||
public MoveSelectionEvent(SelectionBlueprint<T> blueprint, Vector2 screenSpacePosition)
|
||||
{
|
||||
Blueprint = blueprint;
|
||||
ScreenSpacePosition = screenSpacePosition;
|
||||
|
@ -4,25 +4,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -31,16 +25,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <summary>
|
||||
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
|
||||
/// </summary>
|
||||
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
public class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
/// <summary>
|
||||
/// The currently selected blueprints.
|
||||
/// Should be used when operations are dealing directly with the visible blueprints.
|
||||
/// For more general selection operations, use <see cref="osu.Game.Screens.Edit.EditorBeatmap.SelectedHitObjects"/> instead.
|
||||
/// </summary>
|
||||
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
|
||||
public IReadOnlyList<SelectionBlueprint<T>> SelectedBlueprints => selectedBlueprints;
|
||||
|
||||
private readonly List<SelectionBlueprint> selectedBlueprints;
|
||||
protected BindableList<T> SelectedItems = new BindableList<T>();
|
||||
|
||||
private readonly List<SelectionBlueprint<T>> selectedBlueprints;
|
||||
|
||||
private Drawable content;
|
||||
|
||||
@ -48,15 +44,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected SelectionBox SelectionBox { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
||||
|
||||
public SelectionHandler()
|
||||
{
|
||||
selectedBlueprints = new List<SelectionBlueprint>();
|
||||
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AlwaysPresent = true;
|
||||
@ -66,8 +59,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
createStateBindables();
|
||||
|
||||
InternalChild = content = new Container
|
||||
{
|
||||
Children = new Drawable[]
|
||||
@ -95,6 +86,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
}
|
||||
};
|
||||
|
||||
SelectedItems.CollectionChanged += (sender, args) =>
|
||||
{
|
||||
Scheduler.AddOnce(updateVisibility);
|
||||
};
|
||||
}
|
||||
|
||||
public SelectionBox CreateSelectionBox()
|
||||
@ -139,7 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Whether any <see cref="DrawableHitObject"/>s could be moved.
|
||||
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
|
||||
/// </returns>
|
||||
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
|
||||
public virtual bool HandleMovement(MoveSelectionEvent<T> moveEvent) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
|
||||
@ -174,7 +170,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
switch (action.ActionMethod)
|
||||
{
|
||||
case PlatformActionMethod.Delete:
|
||||
deleteSelected();
|
||||
DeleteSelected();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -198,24 +194,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Handle a blueprint becoming selected.
|
||||
/// </summary>
|
||||
/// <param name="blueprint">The blueprint.</param>
|
||||
internal void HandleSelected(SelectionBlueprint blueprint)
|
||||
internal virtual void HandleSelected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
selectedBlueprints.Add(blueprint);
|
||||
|
||||
// there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once.
|
||||
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject))
|
||||
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle a blueprint becoming deselected.
|
||||
/// </summary>
|
||||
/// <param name="blueprint">The blueprint.</param>
|
||||
internal void HandleDeselected(SelectionBlueprint blueprint)
|
||||
internal virtual void HandleDeselected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
selectedBlueprints.Remove(blueprint);
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -224,7 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <param name="blueprint">The blueprint.</param>
|
||||
/// <param name="e">The mouse event responsible for selection.</param>
|
||||
/// <returns>Whether a selection was performed.</returns>
|
||||
internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
|
||||
internal bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
|
||||
{
|
||||
if (e.ShiftPressed && e.Button == MouseButton.Right)
|
||||
{
|
||||
@ -248,7 +238,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <param name="blueprint">The blueprint.</param>
|
||||
/// <param name="e">The mouse event responsible for deselection.</param>
|
||||
/// <returns>Whether a deselection was performed.</returns>
|
||||
internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
|
||||
internal bool MouseUpSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
|
||||
{
|
||||
if (blueprint.IsSelected)
|
||||
{
|
||||
@ -259,15 +249,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleQuickDeletion(SelectionBlueprint blueprint)
|
||||
private void handleQuickDeletion(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
if (blueprint.HandleQuickDeletion())
|
||||
return;
|
||||
|
||||
if (!blueprint.IsSelected)
|
||||
EditorBeatmap.Remove(blueprint.HitObject);
|
||||
DeleteItems(new[] { blueprint.Item });
|
||||
else
|
||||
deleteSelected();
|
||||
DeleteSelected();
|
||||
}
|
||||
|
||||
protected virtual void DeleteItems(IEnumerable<T> items)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -275,7 +269,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
/// <param name="blueprint">The blueprint to select.</param>
|
||||
/// <returns>Whether selection state was changed.</returns>
|
||||
private bool ensureSelected(SelectionBlueprint blueprint)
|
||||
private bool ensureSelected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
if (blueprint.IsSelected)
|
||||
return false;
|
||||
@ -285,9 +279,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private void deleteSelected()
|
||||
protected void DeleteSelected()
|
||||
{
|
||||
EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject));
|
||||
DeleteItems(selectedBlueprints.Select(b => b.Item));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -295,11 +289,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
#region Outline Display
|
||||
|
||||
/// <summary>
|
||||
/// Updates whether this <see cref="SelectionHandler"/> is visible.
|
||||
/// Updates whether this <see cref="SelectionHandler{T}"/> is visible.
|
||||
/// </summary>
|
||||
private void updateVisibility()
|
||||
{
|
||||
int count = EditorBeatmap.SelectedHitObjects.Count;
|
||||
int count = SelectedItems.Count;
|
||||
|
||||
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
|
||||
|
||||
@ -340,188 +334,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample Changes
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hit sample to all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="sampleName">The name of the hit sample.</param>
|
||||
public void AddHitSample(string sampleName)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
// Make sure there isn't already an existing sample
|
||||
if (h.Samples.Any(s => s.Name == sampleName))
|
||||
return;
|
||||
|
||||
h.Samples.Add(new HitSampleInfo(sampleName));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the new combo state of all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="state">Whether to set or unset.</param>
|
||||
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
|
||||
public void SetNewCombo(bool state)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
var comboInfo = h as IHasComboInformation;
|
||||
|
||||
if (comboInfo == null || comboInfo.NewCombo == state) return;
|
||||
|
||||
comboInfo.NewCombo = state;
|
||||
EditorBeatmap.Update(h);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a hit sample from all selected <see cref="HitObject"/>s.
|
||||
/// </summary>
|
||||
/// <param name="sampleName">The name of the hit sample.</param>
|
||||
public void RemoveHitSample(string sampleName)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Selection State
|
||||
|
||||
/// <summary>
|
||||
/// The state of "new combo" for all selected hitobjects.
|
||||
/// </summary>
|
||||
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
|
||||
|
||||
/// <summary>
|
||||
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
|
||||
|
||||
/// <summary>
|
||||
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
|
||||
/// </summary>
|
||||
private void createStateBindables()
|
||||
{
|
||||
foreach (var sampleName in HitSampleInfo.AllAdditions)
|
||||
{
|
||||
var bindable = new Bindable<TernaryState>
|
||||
{
|
||||
Description = sampleName.Replace("hit", string.Empty).Titleize()
|
||||
};
|
||||
|
||||
bindable.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case TernaryState.False:
|
||||
RemoveHitSample(sampleName);
|
||||
break;
|
||||
|
||||
case TernaryState.True:
|
||||
AddHitSample(sampleName);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SelectionSampleStates[sampleName] = bindable;
|
||||
}
|
||||
|
||||
// new combo
|
||||
SelectionNewComboState.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case TernaryState.False:
|
||||
SetNewCombo(false);
|
||||
break;
|
||||
|
||||
case TernaryState.True:
|
||||
SetNewCombo(true);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// bring in updates from selection changes
|
||||
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) =>
|
||||
{
|
||||
Scheduler.AddOnce(updateVisibility);
|
||||
Scheduler.AddOnce(UpdateTernaryStates);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
|
||||
/// </summary>
|
||||
protected virtual void UpdateTernaryStates()
|
||||
{
|
||||
SelectionNewComboState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
|
||||
|
||||
foreach (var (sampleName, bindable) in SelectionSampleStates)
|
||||
{
|
||||
bindable.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
|
||||
/// </summary>
|
||||
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
|
||||
{
|
||||
if (selection.Any(func))
|
||||
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
|
||||
|
||||
return TernaryState.False;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context Menu
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!selectedBlueprints.Any(b => b.IsHovered))
|
||||
return Array.Empty<MenuItem>();
|
||||
|
||||
var items = new List<MenuItem>();
|
||||
|
||||
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
|
||||
|
||||
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
|
||||
{
|
||||
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
|
||||
}
|
||||
|
||||
if (selectedBlueprints.Count == 1)
|
||||
items.AddRange(selectedBlueprints[0].ContextMenuItems);
|
||||
|
||||
items.AddRange(new[]
|
||||
{
|
||||
new OsuMenuItem("Sound")
|
||||
{
|
||||
Items = SelectionSampleStates.Select(kvp =>
|
||||
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
|
||||
},
|
||||
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),
|
||||
});
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provide context menu items relevant to current selection. Calling base is not required.
|
||||
/// </summary>
|
||||
/// <param name="selection">The current selection.</param>
|
||||
/// <returns>The relevant menu items.</returns>
|
||||
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
|
||||
=> Enumerable.Empty<MenuItem>();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -25,20 +25,17 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
internal class TimelineBlueprintContainer : BlueprintContainer
|
||||
internal class TimelineBlueprintContainer : EditorBlueprintContainer
|
||||
{
|
||||
[Resolved(CanBeNull = true)]
|
||||
private Timeline timeline { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private DragEvent lastDragEvent;
|
||||
private Bindable<HitObject> placement;
|
||||
private SelectionBlueprint placementBlueprint;
|
||||
private SelectionBlueprint<HitObject> placementBlueprint;
|
||||
|
||||
private SelectableAreaBackground backgroundBox;
|
||||
|
||||
@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
base.LoadComplete();
|
||||
DragBox.Alpha = 0;
|
||||
|
||||
placement = beatmap.PlacementObject.GetBoundCopy();
|
||||
placement = Beatmap.PlacementObject.GetBoundCopy();
|
||||
placement.ValueChanged += placementChanged;
|
||||
}
|
||||
|
||||
@ -100,7 +97,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
}
|
||||
|
||||
protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
|
||||
protected override Container<SelectionBlueprint<HitObject>> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
@ -160,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
// remove objects from the stack as long as their end time is in the past.
|
||||
while (currentConcurrentObjects.TryPeek(out HitObject hitObject))
|
||||
{
|
||||
if (Precision.AlmostBigger(hitObject.GetEndTime(), b.HitObject.StartTime, 1))
|
||||
if (Precision.AlmostBigger(hitObject.GetEndTime(), b.Item.StartTime, 1))
|
||||
break;
|
||||
|
||||
currentConcurrentObjects.Pop();
|
||||
@ -168,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
// if the stack gets too high, we should have space below it to display the next batch of objects.
|
||||
// importantly, we only do this if time has incremented, else a stack of hitobjects all at the same time value would start to overlap themselves.
|
||||
if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.HitObject.StartTime, 1))
|
||||
if (currentConcurrentObjects.TryPeek(out HitObject h) && !Precision.AlmostEquals(h.StartTime, b.Item.StartTime, 1))
|
||||
{
|
||||
if (currentConcurrentObjects.Count >= stack_reset_count)
|
||||
currentConcurrentObjects.Clear();
|
||||
@ -176,13 +173,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
b.Y = -(stack_offset * currentConcurrentObjects.Count);
|
||||
|
||||
currentConcurrentObjects.Push(b.HitObject);
|
||||
currentConcurrentObjects.Push(b.Item);
|
||||
}
|
||||
}
|
||||
|
||||
protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler();
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TimelineSelectionHandler();
|
||||
|
||||
protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
|
||||
protected override SelectionBlueprint<HitObject> CreateBlueprintFor(HitObject hitObject)
|
||||
{
|
||||
return new TimelineHitObjectBlueprint(hitObject)
|
||||
{
|
||||
@ -239,10 +236,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
}
|
||||
|
||||
internal class TimelineSelectionHandler : SelectionHandler, IKeyBindingHandler<GlobalAction>
|
||||
internal class TimelineSelectionHandler : EditorSelectionHandler, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
|
||||
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
|
||||
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
@ -344,13 +341,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
}
|
||||
|
||||
protected class TimelineSelectionBlueprintContainer : Container<SelectionBlueprint>
|
||||
protected class TimelineSelectionBlueprintContainer : Container<SelectionBlueprint<HitObject>>
|
||||
{
|
||||
protected override Container<SelectionBlueprint> Content { get; }
|
||||
protected override Container<SelectionBlueprint<HitObject>> Content { get; }
|
||||
|
||||
public TimelineSelectionBlueprintContainer()
|
||||
{
|
||||
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
|
||||
AddInternal(new TimelinePart<SelectionBlueprint<HitObject>>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimelineHitObjectBlueprint : SelectionBlueprint
|
||||
public class TimelineHitObjectBlueprint : SelectionBlueprint<HitObject>
|
||||
{
|
||||
private const float circle_size = 38;
|
||||
|
||||
@ -49,13 +49,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
|
||||
public TimelineHitObjectBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
public TimelineHitObjectBlueprint(HitObject item)
|
||||
: base(item)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
|
||||
startTime = hitObject.StartTimeBindable.GetBoundCopy();
|
||||
startTime = item.StartTimeBindable.GetBoundCopy();
|
||||
startTime.BindValueChanged(time => X = (float)time.NewValue, true);
|
||||
|
||||
RelativePositionAxes = Axes.X;
|
||||
@ -95,9 +95,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
},
|
||||
});
|
||||
|
||||
if (hitObject is IHasDuration)
|
||||
if (item is IHasDuration)
|
||||
{
|
||||
colouredComponents.Add(new DragArea(hitObject)
|
||||
colouredComponents.Add(new DragArea(item)
|
||||
{
|
||||
OnDragHandled = e => OnDragHandled?.Invoke(e)
|
||||
});
|
||||
@ -108,7 +108,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (HitObject is IHasComboInformation comboInfo)
|
||||
if (Item is IHasComboInformation comboInfo)
|
||||
{
|
||||
indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy();
|
||||
indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true);
|
||||
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private void updateComboColour()
|
||||
{
|
||||
if (!(HitObject is IHasComboInformation combo))
|
||||
if (!(Item is IHasComboInformation combo))
|
||||
return;
|
||||
|
||||
var comboColours = skin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
|
||||
@ -152,7 +152,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
border.Hide();
|
||||
}
|
||||
|
||||
if (HitObject is IHasDuration duration && duration.Duration > 0)
|
||||
if (Item is IHasDuration duration && duration.Duration > 0)
|
||||
circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
|
||||
else
|
||||
circle.Colour = comboColour;
|
||||
@ -166,14 +166,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
base.Update();
|
||||
|
||||
// no bindable so we perform this every update
|
||||
float duration = (float)(HitObject.GetEndTime() - HitObject.StartTime);
|
||||
float duration = (float)(Item.GetEndTime() - Item.StartTime);
|
||||
|
||||
if (Width != duration)
|
||||
{
|
||||
Width = duration;
|
||||
|
||||
// kind of haphazard but yeah, no bindables.
|
||||
if (HitObject is IHasRepeats repeats)
|
||||
if (Item is IHasRepeats repeats)
|
||||
updateRepeats(repeats);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user