1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-12 06:47:25 +08:00
osu-lazer/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs

498 lines
17 KiB
C#
Raw Normal View History

2020-09-30 14:35:25 +08:00
// 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.
2018-04-13 17:19:50 +08:00
using System;
using System.Collections.Generic;
2018-10-18 15:36:06 +08:00
using System.Linq;
using Humanizer;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation;
using osu.Framework.Bindables;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
2018-07-21 10:38:28 +08:00
using osu.Framework.Input.States;
using osu.Game.Audio;
2018-04-13 17:19:50 +08:00
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
2018-11-20 15:51:59 +08:00
using osuTK;
2018-04-13 17:19:50 +08:00
2018-11-06 17:28:22 +08:00
namespace osu.Game.Screens.Edit.Compose.Components
2018-04-13 17:19:50 +08:00
{
/// <summary>
2018-11-19 15:58:11 +08:00
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
2018-04-13 17:19:50 +08:00
/// </summary>
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
2018-04-13 17:19:50 +08:00
{
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
2018-11-06 17:06:34 +08:00
private readonly List<SelectionBlueprint> selectedBlueprints;
2018-04-13 17:19:50 +08:00
public int SelectedCount => selectedBlueprints.Count;
public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject);
private Drawable content;
private OsuSpriteText selectionDetailsText;
2018-04-13 17:19:50 +08:00
protected SelectionBox SelectionBox { get; private set; }
[Resolved(CanBeNull = true)]
protected EditorBeatmap EditorBeatmap { get; private set; }
2018-10-18 15:36:06 +08:00
[Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; }
2018-11-19 15:58:11 +08:00
public SelectionHandler()
2018-04-13 17:19:50 +08:00
{
2018-11-06 17:06:34 +08:00
selectedBlueprints = new List<SelectionBlueprint>();
2018-04-13 17:19:50 +08:00
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
createStateBindables();
InternalChild = content = new Container
2018-04-13 17:19:50 +08:00
{
Children = new Drawable[]
2018-04-13 17:19:50 +08:00
{
// todo: should maybe be inside the SelectionBox?
new Container
{
Name = "info text",
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colours.YellowDark,
RelativeSizeAxes = Axes.Both,
},
selectionDetailsText = new OsuSpriteText
{
Padding = new MarginPadding(2),
Colour = colours.Gray0,
Font = OsuFont.Default.With(size: 11)
}
}
},
SelectionBox = CreateSelectionBox(),
2018-04-13 17:19:50 +08:00
}
};
}
public SelectionBox CreateSelectionBox()
=> new SelectionBox
{
OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded,
OnRotation = angle => HandleRotation(angle),
OnScale = (amount, anchor) => HandleScale(amount, anchor),
OnFlip = direction => HandleFlip(direction),
};
/// <summary>
/// Fired when a drag operation ends from the selection box.
/// </summary>
protected virtual void OnOperationBegan()
{
ChangeHandler.BeginChange();
}
/// <summary>
/// Fired when a drag operation begins from the selection box.
/// </summary>
protected virtual void OnOperationEnded()
{
ChangeHandler.EndChange();
}
2020-09-29 18:43:50 +08:00
2018-04-13 17:19:50 +08:00
#region User Input Handling
2018-11-19 15:58:11 +08:00
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being moved.
2018-11-19 15:58:11 +08:00
/// </summary>
2020-05-26 16:00:55 +08:00
/// <remarks>
/// Just returning true is enough to allow <see cref="HitObject.StartTime"/> updates to take place.
/// Custom implementation is only required if other attributes are to be considered, like changing columns.
/// </remarks>
/// <param name="moveEvent">The move event.</param>
2020-05-26 16:00:55 +08:00
/// <returns>
/// 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;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
/// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
public virtual bool HandleRotation(float angle) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being scaled.
/// </summary>
/// <param name="scale">The delta scale to apply, in playfield local coordinates.</param>
/// <param name="anchor">The point of reference where the scale is originating from.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Handled the selected <see cref="DrawableHitObject"/>s being flipped.
/// </summary>
/// <param name="direction">The direction to flip</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
public virtual bool HandleFlip(Direction direction) => false;
public bool OnPressed(PlatformAction action)
2018-10-18 15:36:06 +08:00
{
switch (action.ActionMethod)
2018-10-18 15:36:06 +08:00
{
case PlatformActionMethod.Delete:
deleteSelected();
2018-10-18 15:36:06 +08:00
return true;
}
2018-10-31 11:07:06 +08:00
return false;
2018-10-18 15:36:06 +08:00
}
public void OnReleased(PlatformAction action)
{
}
2018-04-13 17:19:50 +08:00
#endregion
#region Selection Handling
/// <summary>
2018-11-06 17:06:34 +08:00
/// Bind an action to deselect all selected blueprints.
2018-04-13 17:19:50 +08:00
/// </summary>
internal Action DeselectAll { private get; set; }
2018-04-13 17:19:50 +08:00
/// <summary>
2018-11-06 17:06:34 +08:00
/// Handle a blueprint becoming selected.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-11-06 17:06:34 +08:00
/// <param name="blueprint">The blueprint.</param>
internal void HandleSelected(SelectionBlueprint blueprint)
{
2020-09-11 21:03:19 +08:00
selectedBlueprints.Add(blueprint);
2020-09-14 14:47:10 +08:00
// there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once.
2020-09-11 21:03:19 +08:00
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject))
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
2020-09-11 21:03:19 +08:00
UpdateVisibility();
}
2018-04-13 17:19:50 +08:00
/// <summary>
2018-11-06 17:06:34 +08:00
/// Handle a blueprint becoming deselected.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-11-06 17:06:34 +08:00
/// <param name="blueprint">The blueprint.</param>
internal void HandleDeselected(SelectionBlueprint blueprint)
2018-04-13 17:19:50 +08:00
{
2020-09-14 14:47:04 +08:00
selectedBlueprints.Remove(blueprint);
2020-09-11 21:03:19 +08:00
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
2018-04-13 17:19:50 +08:00
2020-09-11 21:03:19 +08:00
UpdateVisibility();
2018-04-13 17:19:50 +08:00
}
/// <summary>
2018-11-06 17:06:34 +08:00
/// Handle a blueprint requesting selection.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-11-06 17:06:34 +08:00
/// <param name="blueprint">The blueprint.</param>
2019-04-25 16:36:17 +08:00
/// <param name="state">The input state at the point of selection.</param>
internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state)
2018-04-13 17:19:50 +08:00
{
if (state.Keyboard.ControlPressed)
{
2018-11-06 16:56:04 +08:00
if (blueprint.IsSelected)
blueprint.Deselect();
2018-04-13 17:19:50 +08:00
else
2018-11-06 16:56:04 +08:00
blueprint.Select();
2018-04-13 17:19:50 +08:00
}
else
{
2018-11-06 16:56:04 +08:00
if (blueprint.IsSelected)
2018-04-13 17:19:50 +08:00
return;
DeselectAll?.Invoke();
2018-11-06 16:56:04 +08:00
blueprint.Select();
2018-04-13 17:19:50 +08:00
}
}
private void deleteSelected()
{
ChangeHandler?.BeginChange();
foreach (var h in selectedBlueprints.ToList())
EditorBeatmap?.Remove(h.HitObject);
ChangeHandler?.EndChange();
}
2018-04-13 17:19:50 +08:00
#endregion
#region Outline Display
2018-04-13 17:19:50 +08:00
/// <summary>
2018-11-19 15:58:11 +08:00
/// Updates whether this <see cref="SelectionHandler"/> is visible.
2018-04-13 17:19:50 +08:00
/// </summary>
internal void UpdateVisibility()
{
int count = selectedBlueprints.Count;
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
if (count > 0)
{
2018-04-13 17:19:50 +08:00
Show();
OnSelectionChanged();
}
2018-04-13 17:19:50 +08:00
else
Hide();
}
/// <summary>
/// Triggered whenever more than one object is selected, on each change.
/// Should update the selection box's state to match supported operations.
/// </summary>
protected virtual void OnSelectionChanged()
{
}
2018-04-13 17:19:50 +08:00
protected override void Update()
{
base.Update();
2018-11-06 17:06:34 +08:00
if (selectedBlueprints.Count == 0)
2018-04-13 17:19:50 +08:00
return;
// Move the rectangle to cover the hitobjects
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
var bottomRight = new Vector2(float.MinValue, float.MinValue);
2018-11-06 17:06:34 +08:00
foreach (var blueprint in selectedBlueprints)
2018-04-13 17:19:50 +08:00
{
2018-11-06 17:06:34 +08:00
topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(blueprint.SelectionQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(blueprint.SelectionQuad.BottomRight));
2018-04-13 17:19:50 +08:00
}
topLeft -= new Vector2(5);
bottomRight += new Vector2(5);
content.Size = bottomRight - topLeft;
content.Position = topLeft;
2018-04-13 17:19:50 +08:00
}
#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)
{
ChangeHandler?.BeginChange();
2020-04-10 12:53:09 +08:00
foreach (var h in SelectedHitObjects)
{
// Make sure there isn't already an existing sample
if (h.Samples.Any(s => s.Name == sampleName))
continue;
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
2020-04-10 12:53:09 +08:00
ChangeHandler?.EndChange();
}
2020-09-23 15:40:56 +08:00
/// <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)
{
ChangeHandler?.BeginChange();
foreach (var h in SelectedHitObjects)
{
var comboInfo = h as IHasComboInformation;
if (comboInfo == null || comboInfo.NewCombo == state) continue;
comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h);
}
ChangeHandler?.EndChange();
}
/// <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)
{
ChangeHandler?.BeginChange();
2020-04-10 12:53:09 +08:00
foreach (var h in SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
2020-04-10 12:53:09 +08:00
ChangeHandler?.EndChange();
}
#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 += _ => UpdateTernaryStates();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => 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(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(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),
});
2019-11-12 12:38:42 +08:00
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
2018-04-13 17:19:50 +08:00
}
}