2020-09-30 14:35:25 +08:00
|
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
2019-01-24 16:43:03 +08:00
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-06-17 15:37:17 +08:00
|
|
|
|
#nullable disable
|
|
|
|
|
|
2018-04-04 19:05:01 +08:00
|
|
|
|
using System;
|
2018-04-05 20:03:39 +08:00
|
|
|
|
using System.Collections.Generic;
|
2018-10-18 15:36:06 +08:00
|
|
|
|
using System.Linq;
|
2018-02-12 17:19:55 +08:00
|
|
|
|
using osu.Framework.Allocation;
|
2020-09-25 14:09:47 +08:00
|
|
|
|
using osu.Framework.Bindables;
|
2018-02-12 17:19:55 +08:00
|
|
|
|
using osu.Framework.Graphics;
|
|
|
|
|
using osu.Framework.Graphics.Containers;
|
2022-06-14 05:02:02 +08:00
|
|
|
|
using osu.Framework.Graphics.Cursor;
|
2021-05-01 20:05:12 +08:00
|
|
|
|
using osu.Framework.Graphics.Primitives;
|
2021-04-28 12:43:16 +08:00
|
|
|
|
using osu.Framework.Graphics.UserInterface;
|
2019-11-05 12:26:44 +08:00
|
|
|
|
using osu.Framework.Input;
|
|
|
|
|
using osu.Framework.Input.Bindings;
|
2020-11-10 15:54:33 +08:00
|
|
|
|
using osu.Framework.Input.Events;
|
2021-05-20 17:21:16 +08:00
|
|
|
|
using osu.Framework.Utils;
|
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;
|
2022-06-14 05:02:02 +08:00
|
|
|
|
using osu.Game.Resources.Localisation.Web;
|
2018-03-14 14:18:21 +08:00
|
|
|
|
using osu.Game.Rulesets.Edit;
|
2022-06-18 23:28:36 +08:00
|
|
|
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
2018-11-20 15:51:59 +08:00
|
|
|
|
using osuTK;
|
2020-10-29 05:03:59 +08:00
|
|
|
|
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-02-12 17:19:55 +08:00
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// A component which outlines items and handles movement of selections.
|
2018-02-12 17:19:55 +08:00
|
|
|
|
/// </summary>
|
2022-06-14 05:02:02 +08:00
|
|
|
|
public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu
|
2018-02-12 17:19:55 +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;
|
|
|
|
|
|
2021-04-18 19:11:05 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The currently selected blueprints.
|
|
|
|
|
/// Should be used when operations are dealing directly with the visible blueprints.
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// For more general selection operations, use <see cref="SelectedItems"/> instead.
|
2021-04-18 19:11:05 +08:00
|
|
|
|
/// </summary>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
public IReadOnlyList<SelectionBlueprint<T>> SelectedBlueprints => selectedBlueprints;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The currently selected items.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public readonly BindableList<T> SelectedItems = new BindableList<T>();
|
2021-04-27 14:40:35 +08:00
|
|
|
|
|
|
|
|
|
private readonly List<SelectionBlueprint<T>> selectedBlueprints;
|
2020-09-25 13:10:30 +08:00
|
|
|
|
|
2020-10-01 15:34:34 +08:00
|
|
|
|
protected SelectionBox SelectionBox { get; private set; }
|
2020-09-30 12:52:57 +08:00
|
|
|
|
|
2020-04-09 21:00:56 +08:00
|
|
|
|
[Resolved(CanBeNull = true)]
|
2020-09-23 15:58:22 +08:00
|
|
|
|
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
2020-04-09 21:00:56 +08:00
|
|
|
|
|
2021-04-27 17:06:27 +08:00
|
|
|
|
protected SelectionHandler()
|
2018-03-29 21:06:45 +08:00
|
|
|
|
{
|
2021-04-27 14:40:35 +08:00
|
|
|
|
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-03-29 21:06:45 +08:00
|
|
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
|
AlwaysPresent = true;
|
2018-02-12 17:19:55 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-02-12 17:19:55 +08:00
|
|
|
|
[BackgroundDependencyLoader]
|
2022-01-15 08:06:39 +08:00
|
|
|
|
private void load()
|
2018-02-12 17:19:55 +08:00
|
|
|
|
{
|
2021-05-03 16:34:34 +08:00
|
|
|
|
InternalChild = SelectionBox = CreateSelectionBox();
|
2021-04-27 14:40:35 +08:00
|
|
|
|
|
2022-06-24 20:25:23 +08:00
|
|
|
|
SelectedItems.CollectionChanged += (_, _) =>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
{
|
|
|
|
|
Scheduler.AddOnce(updateVisibility);
|
|
|
|
|
};
|
2018-03-09 22:11:48 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-10-01 15:34:34 +08:00
|
|
|
|
public SelectionBox CreateSelectionBox()
|
|
|
|
|
=> new SelectionBox
|
2020-09-30 12:52:57 +08:00
|
|
|
|
{
|
2020-10-01 15:24:04 +08:00
|
|
|
|
OperationStarted = OnOperationBegan,
|
|
|
|
|
OperationEnded = OnOperationEnded,
|
2020-09-30 12:52:57 +08:00
|
|
|
|
|
2020-11-03 20:10:31 +08:00
|
|
|
|
OnRotation = HandleRotation,
|
|
|
|
|
OnScale = HandleScale,
|
|
|
|
|
OnFlip = HandleFlip,
|
|
|
|
|
OnReverse = HandleReverse,
|
2020-09-30 12:52:57 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fired when a drag operation ends from the selection box.
|
|
|
|
|
/// </summary>
|
2020-10-01 15:24:04 +08:00
|
|
|
|
protected virtual void OnOperationBegan()
|
2020-09-30 12:52:57 +08:00
|
|
|
|
{
|
2020-11-14 22:52:12 +08:00
|
|
|
|
ChangeHandler?.BeginChange();
|
2020-09-30 12:52:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Fired when a drag operation begins from the selection box.
|
|
|
|
|
/// </summary>
|
2020-10-01 15:24:04 +08:00
|
|
|
|
protected virtual void OnOperationEnded()
|
2020-09-30 12:52:57 +08:00
|
|
|
|
{
|
2020-11-14 22:52:12 +08:00
|
|
|
|
ChangeHandler?.EndChange();
|
2020-09-30 12:52:57 +08:00
|
|
|
|
}
|
2020-09-29 18:43:50 +08:00
|
|
|
|
|
2018-03-30 14:50:55 +08:00
|
|
|
|
#region User Input Handling
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-06-14 05:19:15 +08:00
|
|
|
|
/// <remarks>
|
|
|
|
|
/// Positional input must be received outside the container's bounds,
|
|
|
|
|
/// in order to handle blueprints which are partially offscreen.
|
|
|
|
|
/// </remarks>
|
2022-06-18 23:28:36 +08:00
|
|
|
|
/// <seealso cref="ComposeBlueprintContainer.ReceivePositionalInputAt"/>
|
|
|
|
|
/// <seealso cref="TimelineBlueprintContainer.ReceivePositionalInputAt"/>
|
2022-06-14 05:19:15 +08:00
|
|
|
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
|
|
|
|
|
2018-11-19 15:58:11 +08:00
|
|
|
|
/// <summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// Handles the selected items being moved.
|
2018-11-19 15:58:11 +08:00
|
|
|
|
/// </summary>
|
2020-05-26 16:00:55 +08:00
|
|
|
|
/// <remarks>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// 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>
|
2019-10-08 18:08:23 +08:00
|
|
|
|
/// <param name="moveEvent">The move event.</param>
|
2020-05-26 16:00:55 +08:00
|
|
|
|
/// <returns>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// Whether any items could be moved.
|
2020-05-26 16:00:55 +08:00
|
|
|
|
/// </returns>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
public virtual bool HandleMovement(MoveSelectionEvent<T> moveEvent) => false;
|
2020-09-30 12:52:57 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// Handles the selected items being rotated.
|
2020-09-30 12:52:57 +08:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="angle">The delta angle to apply to the selection.</param>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// <returns>Whether any items could be rotated.</returns>
|
2020-09-30 12:52:57 +08:00
|
|
|
|
public virtual bool HandleRotation(float angle) => false;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// Handles the selected items being scaled.
|
2020-09-30 12:52:57 +08:00
|
|
|
|
/// </summary>
|
2021-04-28 10:44:50 +08:00
|
|
|
|
/// <param name="scale">The delta scale to apply, in local coordinates.</param>
|
2020-09-30 12:52:57 +08:00
|
|
|
|
/// <param name="anchor">The point of reference where the scale is originating from.</param>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// <returns>Whether any items could be scaled.</returns>
|
2020-09-30 14:08:56 +08:00
|
|
|
|
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-10-01 15:25:29 +08:00
|
|
|
|
/// <summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// Handles the selected items being flipped.
|
2020-10-01 15:25:29 +08:00
|
|
|
|
/// </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>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// <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;
|
2020-10-01 15:25:29 +08:00
|
|
|
|
|
2020-10-09 05:31:59 +08:00
|
|
|
|
/// <summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// Handles the selected items being reversed pattern-wise.
|
2020-10-09 05:31:59 +08:00
|
|
|
|
/// </summary>
|
2021-04-27 17:33:47 +08:00
|
|
|
|
/// <returns>Whether any items could be reversed.</returns>
|
2020-10-09 05:31:59 +08:00
|
|
|
|
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;
|
|
|
|
|
|
2023-06-29 02:48:22 +08:00
|
|
|
|
bool handled;
|
|
|
|
|
|
2022-01-05 15:46:34 +08:00
|
|
|
|
switch (e.Action)
|
|
|
|
|
{
|
|
|
|
|
case GlobalAction.EditorFlipHorizontally:
|
2023-06-28 11:54:12 +08:00
|
|
|
|
ChangeHandler?.BeginChange();
|
2023-06-29 02:48:22 +08:00
|
|
|
|
handled = HandleFlip(Direction.Horizontal, true);
|
2023-06-28 11:54:12 +08:00
|
|
|
|
ChangeHandler?.EndChange();
|
|
|
|
|
|
2023-06-29 02:48:22 +08:00
|
|
|
|
return handled;
|
2022-01-05 15:46:34 +08:00
|
|
|
|
|
|
|
|
|
case GlobalAction.EditorFlipVertically:
|
2023-06-28 11:54:12 +08:00
|
|
|
|
ChangeHandler?.BeginChange();
|
2023-06-29 02:48:22 +08:00
|
|
|
|
handled = HandleFlip(Direction.Vertical, true);
|
2023-06-28 11:54:12 +08:00
|
|
|
|
ChangeHandler?.EndChange();
|
|
|
|
|
|
2023-06-29 02:48:22 +08:00
|
|
|
|
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:
|
2021-04-27 14:40:35 +08:00
|
|
|
|
DeleteSelected();
|
2018-10-18 15:36:06 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
2018-10-31 11:07:06 +08:00
|
|
|
|
|
2019-11-05 12:26:44 +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)
|
2020-01-22 12:22:34 +08:00
|
|
|
|
{
|
|
|
|
|
}
|
2019-11-05 12:26:44 +08:00
|
|
|
|
|
2018-03-30 14:50:55 +08:00
|
|
|
|
#endregion
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-03-30 14:50:55 +08:00
|
|
|
|
#region Selection Handling
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// <summary>
|
2023-05-25 17:46:31 +08:00
|
|
|
|
/// Deselect all selected items.
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// </summary>
|
2023-05-25 17:46:31 +08:00
|
|
|
|
protected void DeselectAll() => SelectedItems.Clear();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// <summary>
|
2018-11-06 17:06:34 +08:00
|
|
|
|
/// Handle a blueprint becoming selected.
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// </summary>
|
2018-11-06 17:06:34 +08:00
|
|
|
|
/// <param name="blueprint">The blueprint.</param>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
internal virtual void HandleSelected(SelectionBlueprint<T> blueprint)
|
2020-01-21 19:46:39 +08:00
|
|
|
|
{
|
2021-04-27 17:33:47 +08:00
|
|
|
|
// there are potentially multiple SelectionHandlers active, but we only want to add items to the selected list once.
|
2021-04-27 16:42:10 +08:00
|
|
|
|
if (!SelectedItems.Contains(blueprint.Item))
|
|
|
|
|
SelectedItems.Add(blueprint.Item);
|
|
|
|
|
|
2020-09-11 21:03:19 +08:00
|
|
|
|
selectedBlueprints.Add(blueprint);
|
2020-01-21 19:46:39 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// <summary>
|
2018-11-06 17:06:34 +08:00
|
|
|
|
/// Handle a blueprint becoming deselected.
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// </summary>
|
2018-11-06 17:06:34 +08:00
|
|
|
|
/// <param name="blueprint">The blueprint.</param>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
internal virtual void HandleDeselected(SelectionBlueprint<T> blueprint)
|
2018-03-30 14:50:55 +08:00
|
|
|
|
{
|
2021-04-27 16:42:10 +08:00
|
|
|
|
SelectedItems.Remove(blueprint.Item);
|
2020-09-14 14:47:04 +08:00
|
|
|
|
selectedBlueprints.Remove(blueprint);
|
2018-03-30 14:50:55 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// <summary>
|
2018-11-06 17:06:34 +08:00
|
|
|
|
/// Handle a blueprint requesting selection.
|
2018-04-04 19:05:01 +08:00
|
|
|
|
/// </summary>
|
2018-11-06 17:06:34 +08:00
|
|
|
|
/// <param name="blueprint">The blueprint.</param>
|
2020-11-10 15:54:33 +08:00
|
|
|
|
/// <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)
|
2020-10-27 03:28:53 +08:00
|
|
|
|
{
|
2020-11-10 15:54:33 +08:00
|
|
|
|
if (e.ShiftPressed && e.Button == MouseButton.Right)
|
2020-11-03 19:45:48 +08:00
|
|
|
|
{
|
2020-10-31 18:35:25 +08:00
|
|
|
|
handleQuickDeletion(blueprint);
|
2021-04-13 12:03:14 +08:00
|
|
|
|
return true;
|
2020-11-03 19:45:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-04-12 18:05:23 +08:00
|
|
|
|
// 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();
|
2021-04-12 18:05:23 +08:00
|
|
|
|
return true;
|
|
|
|
|
}
|
2020-11-03 19:45:48 +08:00
|
|
|
|
|
2021-04-12 18:05:23 +08:00
|
|
|
|
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>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
internal bool MouseUpSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
|
2021-04-12 18:05:23 +08:00
|
|
|
|
{
|
|
|
|
|
if (blueprint.IsSelected)
|
|
|
|
|
{
|
|
|
|
|
blueprint.ToggleSelection();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
2018-04-04 17:21:27 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-04-27 14:40:35 +08:00
|
|
|
|
private void handleQuickDeletion(SelectionBlueprint<T> blueprint)
|
2020-10-31 18:35:25 +08:00
|
|
|
|
{
|
2020-11-03 19:45:48 +08:00
|
|
|
|
if (blueprint.HandleQuickDeletion())
|
|
|
|
|
return;
|
|
|
|
|
|
2020-10-31 18:35:25 +08:00
|
|
|
|
if (!blueprint.IsSelected)
|
2021-04-27 14:40:35 +08:00
|
|
|
|
DeleteItems(new[] { blueprint.Item });
|
2020-10-31 18:35:25 +08:00
|
|
|
|
else
|
2021-04-27 14:40:35 +08:00
|
|
|
|
DeleteSelected();
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-03 14:13:32 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected 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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-27 17:06:27 +08:00
|
|
|
|
/// <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);
|
2020-10-31 18:35:25 +08:00
|
|
|
|
|
2021-04-12 18:05:23 +08:00
|
|
|
|
/// <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>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
private bool ensureSelected(SelectionBlueprint<T> blueprint)
|
2020-10-27 03:28:53 +08:00
|
|
|
|
{
|
2020-10-27 05:16:28 +08:00
|
|
|
|
if (blueprint.IsSelected)
|
2021-04-12 18:05:23 +08:00
|
|
|
|
return false;
|
2020-10-27 05:16:28 +08:00
|
|
|
|
|
2023-05-25 17:46:31 +08:00
|
|
|
|
DeselectAll();
|
2020-10-27 05:16:28 +08:00
|
|
|
|
blueprint.Select();
|
2021-04-12 18:05:23 +08:00
|
|
|
|
return true;
|
2020-10-27 03:28:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-14 05:02:02 +08:00
|
|
|
|
protected void DeleteSelected()
|
2019-11-08 18:44:47 +08:00
|
|
|
|
{
|
2022-10-05 19:48:35 +08:00
|
|
|
|
DeleteItems(SelectedItems.ToArray());
|
2023-05-25 22:19:55 +08:00
|
|
|
|
DeselectAll();
|
2019-11-08 18:44:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-30 14:50:55 +08:00
|
|
|
|
#endregion
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-11-07 21:51:49 +08:00
|
|
|
|
#region Outline Display
|
|
|
|
|
|
2018-03-29 21:20:56 +08:00
|
|
|
|
/// <summary>
|
2021-04-27 14:40:35 +08:00
|
|
|
|
/// Updates whether this <see cref="SelectionHandler{T}"/> is visible.
|
2018-03-29 21:20:56 +08:00
|
|
|
|
/// </summary>
|
2020-10-31 19:21:07 +08:00
|
|
|
|
private void updateVisibility()
|
2018-03-29 21:06:45 +08:00
|
|
|
|
{
|
2021-04-27 14:40:35 +08:00
|
|
|
|
int count = SelectedItems.Count;
|
2020-07-17 16:48:27 +08:00
|
|
|
|
|
2021-05-03 16:34:34 +08:00
|
|
|
|
SelectionBox.Text = count > 0 ? count.ToString() : string.Empty;
|
|
|
|
|
SelectionBox.FadeTo(count > 0 ? 1 : 0);
|
2020-07-17 16:48:27 +08:00
|
|
|
|
|
2020-10-31 19:25:02 +08:00
|
|
|
|
OnSelectionChanged();
|
2018-03-29 21:06:45 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-09-30 12:52:57 +08:00
|
|
|
|
/// <summary>
|
2021-04-28 12:49:41 +08:00
|
|
|
|
/// Triggered whenever the set of selected items changes.
|
2020-09-30 12:52:57 +08:00
|
|
|
|
/// Should update the selection box's state to match supported operations.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected virtual void OnSelectionChanged()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-29 21:06:45 +08:00
|
|
|
|
protected override void Update()
|
|
|
|
|
{
|
|
|
|
|
base.Update();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2018-11-06 17:06:34 +08:00
|
|
|
|
if (selectedBlueprints.Count == 0)
|
2018-03-29 21:06:45 +08:00
|
|
|
|
return;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-04-27 17:33:47 +08:00
|
|
|
|
// 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
|
|
|
|
|
2021-05-03 16:34:34 +08:00
|
|
|
|
SelectionBox.Position = selectionRect.Location;
|
|
|
|
|
SelectionBox.Size = selectionRect.Size;
|
2018-03-29 21:06:45 +08:00
|
|
|
|
}
|
2019-11-07 21:51:49 +08:00
|
|
|
|
|
|
|
|
|
#endregion
|
2021-04-28 12:43:16 +08:00
|
|
|
|
|
|
|
|
|
#region Context Menu
|
|
|
|
|
|
2022-06-14 05:02:02 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected));
|
|
|
|
|
|
|
|
|
|
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>
|
2022-06-14 05:02:02 +08:00
|
|
|
|
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<T>> selection)
|
2021-04-28 12:43:16 +08:00
|
|
|
|
=> Enumerable.Empty<MenuItem>();
|
|
|
|
|
|
|
|
|
|
#endregion
|
2021-05-18 17:34:06 +08:00
|
|
|
|
|
|
|
|
|
#region Helper Methods
|
|
|
|
|
|
2021-05-20 17:21:16 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Rotate a point around an arbitrary origin.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="point">The point.</param>
|
|
|
|
|
/// <param name="origin">The centre origin to rotate around.</param>
|
|
|
|
|
/// <param name="angle">The angle to rotate (in degrees).</param>
|
|
|
|
|
protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
|
|
|
|
|
{
|
|
|
|
|
angle = -angle;
|
|
|
|
|
|
|
|
|
|
point.X -= origin.X;
|
|
|
|
|
point.Y -= origin.Y;
|
|
|
|
|
|
|
|
|
|
Vector2 ret;
|
|
|
|
|
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
|
|
|
|
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
|
|
|
|
|
|
|
|
|
ret.X += origin.X;
|
|
|
|
|
ret.Y += origin.Y;
|
|
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-18 17:34:06 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
|
|
|
|
|
/// will return the flipped position in screen space coordinates.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
|
|
|
|
|
{
|
|
|
|
|
var centre = quad.Centre;
|
|
|
|
|
|
|
|
|
|
switch (direction)
|
|
|
|
|
{
|
|
|
|
|
case Direction.Horizontal:
|
|
|
|
|
position.X = centre.X - (position.X - centre.X);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Direction.Vertical:
|
|
|
|
|
position.Y = centre.Y - (position.Y - centre.Y);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return position;
|
|
|
|
|
}
|
2021-05-18 17:57:52 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
|
|
|
|
|
/// will return the scaled position in screen space coordinates.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
|
|
|
|
|
{
|
|
|
|
|
// adjust the direction of scale depending on which side the user is dragging.
|
|
|
|
|
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
|
|
|
|
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
|
|
|
|
|
|
|
|
|
// guard against no-ops and NaN.
|
|
|
|
|
if (scale.X != 0 && selectionQuad.Width > 0)
|
|
|
|
|
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
|
|
|
|
|
|
|
|
|
|
if (scale.Y != 0 && selectionQuad.Height > 0)
|
|
|
|
|
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
|
|
|
|
|
|
|
|
|
|
return position;
|
|
|
|
|
}
|
2021-05-18 17:34:06 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Returns a quad surrounding the provided points.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="points">The points to calculate a quad for.</param>
|
|
|
|
|
protected static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
|
|
|
|
|
{
|
|
|
|
|
if (!points.Any())
|
|
|
|
|
return new Quad();
|
|
|
|
|
|
|
|
|
|
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
|
|
|
|
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
|
|
|
|
|
|
|
|
|
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
|
|
|
|
foreach (var p in points)
|
|
|
|
|
{
|
|
|
|
|
minPosition = Vector2.ComponentMin(minPosition, p);
|
|
|
|
|
maxPosition = Vector2.ComponentMax(maxPosition, p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Vector2 size = maxPosition - minPosition;
|
|
|
|
|
|
|
|
|
|
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
2018-02-12 17:19:55 +08:00
|
|
|
|
}
|
|
|
|
|
}
|