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

438 lines
15 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;
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;
using osu.Framework.Graphics.Primitives;
2021-04-28 12:43:16 +08:00
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Cursor;
2021-04-28 12:43:16 +08:00
using osu.Game.Graphics.UserInterface;
2022-01-05 15:46:34 +08:00
using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
2018-11-20 15:51:59 +08:00
using osuTK;
using osuTK.Input;
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>
/// A component which outlines items and handles movement of selections.
2018-04-13 17:19:50 +08:00
/// </summary>
2022-11-24 13:32:20 +08:00
public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu
2018-04-13 17:19:50 +08:00
{
2023-02-17 16:51:52 +08:00
/// <summary>
/// How much padding around the selection area is added.
/// </summary>
public const float INFLATE_SIZE = 5;
/// <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="SelectedItems"/> instead.
/// </summary>
public IReadOnlyList<SelectionBlueprint<T>> SelectedBlueprints => selectedBlueprints;
2018-04-13 17:19:50 +08:00
/// <summary>
/// The currently selected items.
/// </summary>
public readonly BindableList<T> SelectedItems = new BindableList<T>();
private readonly List<SelectionBlueprint<T>> selectedBlueprints;
2024-08-06 14:17:52 +08:00
protected SelectionBox SelectionBox { get; private set; } = null!;
[Resolved(CanBeNull = true)]
2024-08-06 14:17:52 +08:00
protected IEditorChangeHandler? ChangeHandler { get; private set; }
2024-08-06 14:17:52 +08:00
public SelectionRotationHandler RotationHandler { get; private set; } = null!;
2024-08-06 14:17:52 +08:00
public SelectionScaleHandler ScaleHandler { get; private set; } = null!;
[Resolved(CanBeNull = true)]
protected OsuContextMenuContainer? ContextMenuContainer { get; private set; }
protected SelectionHandler()
2018-04-13 17:19:50 +08:00
{
selectedBlueprints = new List<SelectionBlueprint<T>>();
2018-04-13 17:19:50 +08:00
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(RotationHandler = CreateRotationHandler());
dependencies.CacheAs(ScaleHandler = CreateScaleHandler());
return dependencies;
}
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader]
2022-01-15 08:06:39 +08:00
private void load()
2018-04-13 17:19:50 +08:00
{
AddRangeInternal(new Drawable[]
{
RotationHandler,
ScaleHandler,
SelectionBox = CreateSelectionBox(),
});
SelectedItems.BindCollectionChanged((_, _) => Scheduler.AddOnce(updateVisibility), true);
2018-04-13 17:19:50 +08:00
}
public SelectionBox CreateSelectionBox()
=> new SelectionBox
{
OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded,
OnFlip = HandleFlip,
OnReverse = HandleReverse,
};
/// <summary>
/// Fired when a drag operation ends from the selection box.
/// </summary>
protected virtual void OnOperationBegan()
{
2020-11-14 22:52:12 +08:00
ChangeHandler?.BeginChange();
}
/// <summary>
/// Fired when a drag operation begins from the selection box.
/// </summary>
protected virtual void OnOperationEnded()
{
2020-11-14 22:52:12 +08:00
ChangeHandler?.EndChange();
}
2020-09-29 18:43:50 +08:00
2018-04-13 17:19:50 +08:00
#region User Input Handling
/// <remarks>
/// Positional input must be received outside the container's bounds,
/// in order to handle blueprints which are partially offscreen.
/// </remarks>
/// <seealso cref="ComposeBlueprintContainer.ReceivePositionalInputAt"/>
/// <seealso cref="TimelineBlueprintContainer.ReceivePositionalInputAt"/>
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
2018-11-19 15:58:11 +08:00
/// <summary>
/// Handles the selected items 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 default movement to take place.
2020-05-26 16:00:55 +08:00
/// 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 items could be moved.
2020-05-26 16:00:55 +08:00
/// </returns>
public virtual bool HandleMovement(MoveSelectionEvent<T> moveEvent) => false;
/// <summary>
/// Handles the selected items being rotated.
/// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param>
/// <returns>Whether any items could be rotated.</returns>
public virtual bool HandleRotation(float angle) => false;
/// <summary>
/// Creates the handler to use for rotation operations.
/// </summary>
public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler();
/// <summary>
/// Handles the selected items being scaled.
/// </summary>
/// <param name="scale">The delta scale to apply, in local coordinates.</param>
/// <param name="anchor">The point of reference where the scale is originating from.</param>
/// <returns>Whether any items could be scaled.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
2018-04-13 17:19:50 +08:00
/// <summary>
/// Creates the handler to use for scale operations.
/// </summary>
public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler();
/// <summary>
/// Handles the selected items being flipped.
/// </summary>
2022-01-05 15:46:34 +08:00
/// <param name="direction">The direction to flip.</param>
/// <param name="flipOverOrigin">Whether the flip operation should be global to the playfield's origin or local to the selected pattern.</param>
/// <returns>Whether any items could be flipped.</returns>
2022-01-05 15:46:34 +08:00
public virtual bool HandleFlip(Direction direction, bool flipOverOrigin) => false;
/// <summary>
/// Handles the selected items being reversed pattern-wise.
/// </summary>
/// <returns>Whether any items could be reversed.</returns>
public virtual bool HandleReverse() => false;
2022-01-05 15:46:34 +08:00
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
bool handled;
2022-01-05 15:46:34 +08:00
switch (e.Action)
{
case GlobalAction.EditorFlipHorizontally:
ChangeHandler?.BeginChange();
handled = HandleFlip(Direction.Horizontal, true);
ChangeHandler?.EndChange();
return handled;
2022-01-05 15:46:34 +08:00
case GlobalAction.EditorFlipVertically:
ChangeHandler?.BeginChange();
handled = HandleFlip(Direction.Vertical, true);
ChangeHandler?.EndChange();
return handled;
2022-01-05 15:46:34 +08:00
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
2021-09-16 17:26:12 +08:00
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
2018-10-18 15:36:06 +08:00
{
2021-09-16 17:26:12 +08:00
switch (e.Action)
2018-10-18 15:36:06 +08:00
{
2021-07-20 13:23:34 +08:00
case PlatformAction.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
}
2021-09-16 17:26:12 +08:00
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
{
}
2018-04-13 17:19:50 +08:00
#endregion
#region Selection Handling
/// <summary>
/// Deselect all selected items.
2018-04-13 17:19:50 +08:00
/// </summary>
protected void DeselectAll()
{
SelectedItems.Clear();
ContextMenuContainer?.CloseMenu();
}
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 virtual void HandleSelected(SelectionBlueprint<T> blueprint)
{
// there are potentially multiple SelectionHandlers active, but we only want to add items to the selected list once.
if (!SelectedItems.Contains(blueprint.Item))
SelectedItems.Add(blueprint.Item);
2020-09-11 21:03:19 +08:00
selectedBlueprints.Add(blueprint);
ContextMenuContainer?.CloseMenu();
}
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 virtual void HandleDeselected(SelectionBlueprint<T> blueprint)
2018-04-13 17:19:50 +08:00
{
SelectedItems.Remove(blueprint.Item);
2020-09-14 14:47:04 +08:00
selectedBlueprints.Remove(blueprint);
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>
/// <param name="e">The mouse event responsible for selection.</param>
2020-11-04 16:53:03 +08:00
/// <returns>Whether a selection was performed.</returns>
2021-09-27 01:07:38 +08:00
internal virtual bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
{
if (e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right))
{
handleQuickDeletion(blueprint);
return true;
}
// while holding control, we only want to add to selection, not replace an existing selection.
if (e.ControlPressed && e.Button == MouseButton.Left && !blueprint.IsSelected)
{
2020-10-27 05:16:28 +08:00
blueprint.ToggleSelection();
return true;
}
return ensureSelected(blueprint);
}
/// <summary>
/// Handle a blueprint requesting selection.
/// </summary>
/// <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<T> blueprint, MouseButtonEvent e)
{
if (blueprint.IsSelected)
{
blueprint.ToggleSelection();
return true;
}
return false;
2018-04-13 17:19:50 +08:00
}
private void handleQuickDeletion(SelectionBlueprint<T> blueprint)
{
if (blueprint.HandleQuickDeletion())
return;
if (!blueprint.IsSelected)
DeleteItems(new[] { blueprint.Item });
else
DeleteSelected();
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
public static TernaryState GetStateFromSelection<TObject>(IEnumerable<TObject> selection, Func<TObject, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
/// <summary>
/// Called whenever the deletion of items has been requested.
/// </summary>
/// <param name="items">The items to be deleted.</param>
protected abstract void DeleteItems(IEnumerable<T> items);
/// <summary>
/// Ensure the blueprint is in a selected state.
/// </summary>
/// <param name="blueprint">The blueprint to select.</param>
/// <returns>Whether selection state was changed.</returns>
private bool ensureSelected(SelectionBlueprint<T> blueprint)
{
2020-10-27 05:16:28 +08:00
if (blueprint.IsSelected)
return false;
2020-10-27 05:16:28 +08:00
DeselectAll();
2020-10-27 05:16:28 +08:00
blueprint.Select();
return true;
}
protected void DeleteSelected()
{
DeleteItems(SelectedItems.ToArray());
DeselectAll();
}
2018-04-13 17:19:50 +08:00
#endregion
#region Outline Display
2018-04-13 17:19:50 +08:00
/// <summary>
/// Updates whether this <see cref="SelectionHandler{T}"/> is visible.
2018-04-13 17:19:50 +08:00
/// </summary>
private void updateVisibility()
2018-04-13 17:19:50 +08:00
{
int count = SelectedItems.Count;
SelectionBox.Text = count > 0 ? count.ToString() : string.Empty;
SelectionBox.FadeTo(count > 0 ? 1 : 0);
OnSelectionChanged();
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Triggered whenever the set of selected items changes.
/// 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 items
2021-05-02 11:21:14 +08:00
RectangleF selectionRect = ToLocalSpace(selectedBlueprints[0].SelectionQuad).AABBFloat;
2018-04-13 17:19:50 +08:00
2021-05-02 11:21:14 +08:00
for (int i = 1; i < selectedBlueprints.Count; i++)
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
2018-04-13 17:19:50 +08:00
2023-02-17 16:51:52 +08:00
selectionRect = selectionRect.Inflate(INFLATE_SIZE);
2018-04-13 17:19:50 +08:00
SelectionBox.Position = selectionRect.Location;
SelectionBox.Size = selectionRect.Size;
2018-04-13 17:19:50 +08:00
}
#endregion
2021-04-28 12:43:16 +08:00
#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.Count == 1)
items.AddRange(SelectedBlueprints[0].ContextMenuItems);
2024-07-18 17:20:31 +08:00
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected)
{
Hotkey = new Hotkey { PlatformAction = PlatformAction.Delete, KeyCombinations = [new KeyCombination(InputKey.Shift, InputKey.MouseRight), new KeyCombination(InputKey.MouseMiddle)] }
});
return items.ToArray();
}
}
2021-04-28 12:43:16 +08:00
/// <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<T>> selection)
2021-04-28 12:43:16 +08:00
=> Enumerable.Empty<MenuItem>();
#endregion
2018-04-13 17:19:50 +08:00
}
}