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
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 ;
2020-09-25 14:09:47 +08:00
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.Shapes ;
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 ;
2018-04-13 17:19:50 +08:00
using osu.Game.Graphics ;
2020-07-17 16:48:27 +08:00
using osu.Game.Graphics.Sprites ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Edit ;
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-04-13 17:19:50 +08:00
{
/// <summary>
2021-04-27 17:33:47 +08:00
/// A component which outlines items and handles movement of selections.
2018-04-13 17:19:50 +08:00
/// </summary>
2021-04-27 17:06:27 +08:00
public abstract class SelectionHandler < T > : CompositeDrawable , IKeyBindingHandler < PlatformAction >
2018-04-13 17:19:50 +08:00
{
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-07-17 16:48:27 +08:00
private Drawable content ;
private OsuSpriteText selectionDetailsText ;
2018-04-13 17:19:50 +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-04-13 17:19:50 +08:00
{
2021-04-27 14:40:35 +08:00
selectedBlueprints = new List < SelectionBlueprint < T > > ( ) ;
2018-04-13 17:19:50 +08:00
RelativeSizeAxes = Axes . Both ;
AlwaysPresent = true ;
Alpha = 0 ;
}
[BackgroundDependencyLoader]
private void load ( OsuColour colours )
{
2020-07-17 16:48:27 +08:00
InternalChild = content = new Container
2018-04-13 17:19:50 +08:00
{
2020-07-17 16:48:27 +08:00
Children = new Drawable [ ]
2018-04-13 17:19:50 +08:00
{
2020-09-30 12:07:24 +08:00
// todo: should maybe be inside the SelectionBox?
2020-07-17 16:48:27 +08:00
new Container
{
Name = "info text" ,
AutoSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
new Box
{
2020-07-17 16:51:39 +08:00
Colour = colours . YellowDark ,
2020-07-17 16:48:27 +08:00
RelativeSizeAxes = Axes . Both ,
} ,
selectionDetailsText = new OsuSpriteText
{
Padding = new MarginPadding ( 2 ) ,
Colour = colours . Gray0 ,
2020-07-17 16:51:39 +08:00
Font = OsuFont . Default . With ( size : 11 )
2020-07-17 16:48:27 +08:00
}
}
2020-09-30 12:07:24 +08:00
} ,
2020-09-30 12:52:57 +08:00
SelectionBox = CreateSelectionBox ( ) ,
2018-04-13 17:19:50 +08:00
}
} ;
2021-04-27 14:40:35 +08:00
SelectedItems . CollectionChanged + = ( sender , args ) = >
{
Scheduler . AddOnce ( updateVisibility ) ;
} ;
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-04-13 17:19:50 +08:00
#region User Input Handling
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
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
/// </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>
2020-09-30 14:08:56 +08:00
/// <param name="scale">The delta scale to apply, in playfield 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>
/// <param name="direction">The direction to flip</param>
2021-04-27 17:33:47 +08:00
/// <returns>Whether any items could be flipped.</returns>
2020-10-01 15:25:29 +08:00
public virtual bool HandleFlip ( Direction direction ) = > false ;
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 ;
2019-11-05 12:26:44 +08:00
public bool OnPressed ( PlatformAction action )
2018-10-18 15:36:06 +08:00
{
2019-11-05 12:26:44 +08:00
switch ( action . ActionMethod )
2018-10-18 15:36:06 +08:00
{
2019-11-05 12:26:44 +08:00
case PlatformActionMethod . 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
}
2020-01-22 12:22:34 +08:00
public void OnReleased ( PlatformAction action )
{
}
2019-11-05 12:26:44 +08:00
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>
2018-11-16 16:12:24 +08:00
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>
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
/// <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>
2021-04-27 14:40:35 +08:00
internal virtual void HandleDeselected ( SelectionBlueprint < T > blueprint )
2018-04-13 17:19:50 +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-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>
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-04-27 14:40:35 +08:00
internal 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-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-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
DeselectAll ? . Invoke ( ) ;
blueprint . Select ( ) ;
2021-04-12 18:05:23 +08:00
return true ;
2020-10-27 03:28:53 +08:00
}
2021-04-27 14:40:35 +08:00
protected void DeleteSelected ( )
2019-11-08 18:44:47 +08:00
{
2021-04-27 14:40:35 +08:00
DeleteItems ( selectedBlueprints . Select ( b = > b . Item ) ) ;
2019-11-08 18:44:47 +08:00
}
2018-04-13 17:19:50 +08:00
#endregion
2019-11-07 21:51:49 +08:00
#region Outline Display
2018-04-13 17:19:50 +08:00
/// <summary>
2021-04-27 14:40:35 +08:00
/// Updates whether this <see cref="SelectionHandler{T}"/> is visible.
2018-04-13 17:19:50 +08:00
/// </summary>
2020-10-31 19:21:07 +08:00
private void updateVisibility ( )
2018-04-13 17:19:50 +08:00
{
2021-04-27 14:40:35 +08:00
int count = SelectedItems . Count ;
2020-07-17 16:48:27 +08:00
selectionDetailsText . Text = count > 0 ? count . ToString ( ) : string . Empty ;
2020-10-31 19:28:35 +08:00
this . FadeTo ( count > 0 ? 1 : 0 ) ;
2020-10-31 19:25:02 +08:00
OnSelectionChanged ( ) ;
2018-04-13 17:19:50 +08:00
}
2020-09-30 12:52:57 +08:00
/// <summary>
2020-10-31 19:25:02 +08:00
/// Triggered whenever the set of selected objects 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-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 ;
2021-04-27 17:33:47 +08:00
// Move the rectangle to cover the items
2018-04-13 17:19:50 +08:00
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 ) ;
2020-07-17 16:48:27 +08:00
content . Size = bottomRight - topLeft ;
content . Position = topLeft ;
2018-04-13 17:19:50 +08:00
}
2019-11-07 21:51:49 +08:00
#endregion
2018-04-13 17:19:50 +08:00
}
}