2019-01-24 17:43:03 +09: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 18:19:50 +09:00
using System ;
using System.Collections.Generic ;
2018-10-18 16:36:06 +09:00
using System.Linq ;
2018-04-13 18:19:50 +09:00
using osu.Framework.Allocation ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2019-11-07 22:51:49 +09:00
using osu.Framework.Graphics.Cursor ;
2018-04-13 18:19:50 +09:00
using osu.Framework.Graphics.Shapes ;
2019-11-07 22:51:49 +09:00
using osu.Framework.Graphics.UserInterface ;
2019-11-05 13:26:44 +09:00
using osu.Framework.Input ;
using osu.Framework.Input.Bindings ;
2018-07-21 11:38:28 +09:00
using osu.Framework.Input.States ;
2019-11-07 22:51:49 +09:00
using osu.Game.Audio ;
2018-04-13 18:19:50 +09:00
using osu.Game.Graphics ;
2019-11-07 22:51:49 +09:00
using osu.Game.Graphics.UserInterface ;
2018-04-13 18:19:50 +09:00
using osu.Game.Rulesets.Edit ;
2018-11-16 17:12:24 +09:00
using osu.Game.Rulesets.Objects ;
2018-11-26 16:08:56 +09:00
using osu.Game.Rulesets.Objects.Drawables ;
2018-11-20 16:51:59 +09:00
using osuTK ;
2018-04-13 18:19:50 +09:00
2018-11-06 18:28:22 +09:00
namespace osu.Game.Screens.Edit.Compose.Components
2018-04-13 18:19:50 +09:00
{
/// <summary>
2018-11-19 16:58:11 +09:00
/// A component which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
2018-04-13 18:19:50 +09:00
/// </summary>
2019-11-07 22:51:49 +09:00
public class SelectionHandler : CompositeDrawable , IKeyBindingHandler < PlatformAction > , IHasContextMenu
2018-04-13 18:19:50 +09:00
{
public const float BORDER_RADIUS = 2 ;
2019-10-16 20:07:11 +09:00
public IEnumerable < SelectionBlueprint > SelectedBlueprints = > selectedBlueprints ;
2018-11-06 18:06:34 +09:00
private readonly List < SelectionBlueprint > selectedBlueprints ;
2018-04-13 18:19:50 +09:00
2020-01-21 00:53:59 +09:00
public IEnumerable < HitObject > SelectedHitObjects = > selectedBlueprints . Select ( b = > b . HitObject ) ;
2018-11-16 17:12:24 +09:00
2018-04-13 18:19:50 +09:00
private Drawable outline ;
2020-01-15 19:09:49 +09:00
[Resolved(CanBeNull = true)]
2020-05-26 16:58:28 +09:00
protected EditorBeatmap EditorBeatmap { get ; private set ; }
2018-10-18 16:36:06 +09:00
2020-04-09 22:00:56 +09:00
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get ; set ; }
2018-11-19 16:58:11 +09:00
public SelectionHandler ( )
2018-04-13 18:19:50 +09:00
{
2018-11-06 18:06:34 +09:00
selectedBlueprints = new List < SelectionBlueprint > ( ) ;
2018-04-13 18:19:50 +09:00
RelativeSizeAxes = Axes . Both ;
AlwaysPresent = true ;
Alpha = 0 ;
}
[BackgroundDependencyLoader]
private void load ( OsuColour colours )
{
InternalChild = outline = new Container
{
Masking = true ,
BorderThickness = BORDER_RADIUS ,
BorderColour = colours . Yellow ,
Child = new Box
{
RelativeSizeAxes = Axes . Both ,
AlwaysPresent = true ,
Alpha = 0
}
} ;
}
#region User Input Handling
2018-11-19 16:58:11 +09:00
/// <summary>
2019-10-08 19:08:23 +09:00
/// Handles the selected <see cref="DrawableHitObject"/>s being moved.
2018-11-19 16:58:11 +09:00
/// </summary>
2020-05-26 17:00:55 +09: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>
2019-10-08 19:08:23 +09:00
/// <param name="moveEvent">The move event.</param>
2020-05-26 17:00:55 +09: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 ) = > true ;
2018-04-13 18:19:50 +09:00
2019-11-05 13:26:44 +09:00
public bool OnPressed ( PlatformAction action )
2018-10-18 16:36:06 +09:00
{
2019-11-05 13:26:44 +09:00
switch ( action . ActionMethod )
2018-10-18 16:36:06 +09:00
{
2019-11-05 13:26:44 +09:00
case PlatformActionMethod . Delete :
2019-11-08 19:44:47 +09:00
deleteSelected ( ) ;
2018-10-18 16:36:06 +09:00
return true ;
}
2018-10-31 12:07:06 +09:00
2019-11-05 13:26:44 +09:00
return false ;
2018-10-18 16:36:06 +09:00
}
2020-01-22 13:22:34 +09:00
public void OnReleased ( PlatformAction action )
{
}
2019-11-05 13:26:44 +09:00
2018-04-13 18:19:50 +09:00
#endregion
#region Selection Handling
/// <summary>
2018-11-06 18:06:34 +09:00
/// Bind an action to deselect all selected blueprints.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-11-16 17:12:24 +09:00
internal Action DeselectAll { private get ; set ; }
2018-04-13 18:19:50 +09:00
/// <summary>
2018-11-06 18:06:34 +09:00
/// Handle a blueprint becoming selected.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-11-06 18:06:34 +09:00
/// <param name="blueprint">The blueprint.</param>
2020-01-21 20:46:39 +09:00
internal void HandleSelected ( SelectionBlueprint blueprint )
{
selectedBlueprints . Add ( blueprint ) ;
2020-05-26 16:58:28 +09:00
EditorBeatmap . SelectedHitObjects . Add ( blueprint . HitObject ) ;
2020-01-22 01:32:11 +09:00
UpdateVisibility ( ) ;
2020-01-21 20:46:39 +09:00
}
2018-04-13 18:19:50 +09:00
/// <summary>
2018-11-06 18:06:34 +09:00
/// Handle a blueprint becoming deselected.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-11-06 18:06:34 +09:00
/// <param name="blueprint">The blueprint.</param>
2018-11-16 17:12:24 +09:00
internal void HandleDeselected ( SelectionBlueprint blueprint )
2018-04-13 18:19:50 +09:00
{
2018-11-06 18:06:34 +09:00
selectedBlueprints . Remove ( blueprint ) ;
2020-05-26 16:58:28 +09:00
EditorBeatmap . SelectedHitObjects . Remove ( blueprint . HitObject ) ;
2018-04-13 18:19:50 +09:00
2018-11-06 18:06:34 +09:00
// We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection
if ( selectedBlueprints . Count = = 0 )
2018-04-13 18:19:50 +09:00
UpdateVisibility ( ) ;
}
/// <summary>
2018-11-06 18:06:34 +09:00
/// Handle a blueprint requesting selection.
2018-04-13 18:19:50 +09:00
/// </summary>
2018-11-06 18:06:34 +09:00
/// <param name="blueprint">The blueprint.</param>
2019-04-25 17:36:17 +09:00
/// <param name="state">The input state at the point of selection.</param>
2018-11-16 17:12:24 +09:00
internal void HandleSelectionRequested ( SelectionBlueprint blueprint , InputState state )
2018-04-13 18:19:50 +09:00
{
if ( state . Keyboard . ControlPressed )
{
2018-11-06 17:56:04 +09:00
if ( blueprint . IsSelected )
blueprint . Deselect ( ) ;
2018-04-13 18:19:50 +09:00
else
2018-11-06 17:56:04 +09:00
blueprint . Select ( ) ;
2018-04-13 18:19:50 +09:00
}
else
{
2018-11-06 17:56:04 +09:00
if ( blueprint . IsSelected )
2018-04-13 18:19:50 +09:00
return ;
DeselectAll ? . Invoke ( ) ;
2018-11-06 17:56:04 +09:00
blueprint . Select ( ) ;
2018-04-13 18:19:50 +09:00
}
}
2019-11-08 19:44:47 +09:00
private void deleteSelected ( )
{
2020-04-09 22:00:56 +09:00
changeHandler ? . BeginChange ( ) ;
2019-11-08 19:44:47 +09:00
foreach ( var h in selectedBlueprints . ToList ( ) )
2020-05-26 16:58:28 +09:00
EditorBeatmap ? . Remove ( h . HitObject ) ;
2020-04-09 22:00:56 +09:00
changeHandler ? . EndChange ( ) ;
2019-11-08 19:44:47 +09:00
}
2018-04-13 18:19:50 +09:00
#endregion
2019-11-07 22:51:49 +09:00
#region Outline Display
2018-04-13 18:19:50 +09:00
/// <summary>
2018-11-19 16:58:11 +09:00
/// Updates whether this <see cref="SelectionHandler"/> is visible.
2018-04-13 18:19:50 +09:00
/// </summary>
internal void UpdateVisibility ( )
{
2018-11-06 18:06:34 +09:00
if ( selectedBlueprints . Count > 0 )
2018-04-13 18:19:50 +09:00
Show ( ) ;
else
Hide ( ) ;
}
protected override void Update ( )
{
base . Update ( ) ;
2018-11-06 18:06:34 +09:00
if ( selectedBlueprints . Count = = 0 )
2018-04-13 18:19:50 +09: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 18:06:34 +09:00
foreach ( var blueprint in selectedBlueprints )
2018-04-13 18:19:50 +09:00
{
2018-11-06 18:06:34 +09:00
topLeft = Vector2 . ComponentMin ( topLeft , ToLocalSpace ( blueprint . SelectionQuad . TopLeft ) ) ;
bottomRight = Vector2 . ComponentMax ( bottomRight , ToLocalSpace ( blueprint . SelectionQuad . BottomRight ) ) ;
2018-04-13 18:19:50 +09:00
}
topLeft - = new Vector2 ( 5 ) ;
bottomRight + = new Vector2 ( 5 ) ;
outline . Size = bottomRight - topLeft ;
outline . Position = topLeft ;
}
2019-11-07 22:51:49 +09:00
#endregion
2019-11-08 16:46:58 +09:00
#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 )
{
2020-04-10 13:53:09 +09:00
changeHandler ? . BeginChange ( ) ;
2019-11-08 16:46:58 +09: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 13:53:09 +09:00
changeHandler ? . EndChange ( ) ;
2019-11-08 16:46:58 +09:00
}
/// <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 )
{
2020-04-10 13:53:09 +09:00
changeHandler ? . BeginChange ( ) ;
2019-11-08 16:46:58 +09:00
foreach ( var h in SelectedHitObjects )
h . SamplesBindable . RemoveAll ( s = > s . Name = = sampleName ) ;
2020-04-10 13:53:09 +09:00
changeHandler ? . EndChange ( ) ;
2019-11-08 16:46:58 +09:00
}
#endregion
2019-11-07 22:51:49 +09:00
#region Context Menu
2020-05-26 13:00:32 +09:00
public MenuItem [ ] ContextMenuItems
2019-11-07 22:51:49 +09:00
{
get
{
2019-11-12 14:42:30 +09:00
if ( ! selectedBlueprints . Any ( b = > b . IsHovered ) )
2019-11-07 22:51:49 +09:00
return Array . Empty < MenuItem > ( ) ;
2020-05-26 13:00:32 +09:00
var items = new List < MenuItem > ( ) ;
items . AddRange ( GetContextMenuItemsForSelection ( selectedBlueprints ) ) ;
if ( selectedBlueprints . Count = = 1 )
items . AddRange ( selectedBlueprints [ 0 ] . ContextMenuItems ) ;
items . AddRange ( new [ ]
2019-11-07 22:51:49 +09:00
{
2019-11-08 13:52:36 +09:00
new OsuMenuItem ( "Sound" )
2019-11-07 22:51:49 +09:00
{
Items = new [ ]
{
2019-11-08 13:52:36 +09:00
createHitSampleMenuItem ( "Whistle" , HitSampleInfo . HIT_WHISTLE ) ,
createHitSampleMenuItem ( "Clap" , HitSampleInfo . HIT_CLAP ) ,
createHitSampleMenuItem ( "Finish" , HitSampleInfo . HIT_FINISH )
2019-11-07 22:51:49 +09:00
}
2019-11-08 19:44:47 +09:00
} ,
new OsuMenuItem ( "Delete" , MenuItemType . Destructive , deleteSelected ) ,
2020-05-26 13:00:32 +09:00
} ) ;
2019-11-12 13:38:42 +09:00
return items . ToArray ( ) ;
2019-11-07 22:51:49 +09:00
}
}
2020-05-26 13:00:32 +09: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 > selection )
= > Enumerable . Empty < MenuItem > ( ) ;
2019-11-08 13:52:36 +09:00
private MenuItem createHitSampleMenuItem ( string name , string sampleName )
2019-11-07 22:51:49 +09:00
{
2019-11-12 10:45:46 +09:00
return new TernaryStateMenuItem ( name , MenuItemType . Standard , setHitSampleState )
2019-11-07 22:51:49 +09:00
{
State = { Value = getHitSampleState ( ) }
} ;
2019-11-12 10:45:46 +09:00
void setHitSampleState ( TernaryState state )
2019-11-07 22:51:49 +09:00
{
2019-11-08 13:52:36 +09:00
switch ( state )
2019-11-07 22:51:49 +09:00
{
2019-11-12 10:45:46 +09:00
case TernaryState . False :
2019-11-08 16:46:58 +09:00
RemoveHitSample ( sampleName ) ;
2019-11-08 13:52:36 +09:00
break ;
2019-11-07 22:51:49 +09:00
2019-11-12 10:45:46 +09:00
case TernaryState . True :
2019-11-08 16:46:58 +09:00
AddHitSample ( sampleName ) ;
2019-11-08 13:52:36 +09:00
break ;
2019-11-07 22:51:49 +09:00
}
}
2019-11-12 10:45:46 +09:00
TernaryState getHitSampleState ( )
2019-11-08 13:52:36 +09:00
{
int countExisting = SelectedHitObjects . Count ( h = > h . Samples . Any ( s = > s . Name = = sampleName ) ) ;
if ( countExisting = = 0 )
2019-11-12 10:45:46 +09:00
return TernaryState . False ;
2019-11-08 13:52:36 +09:00
if ( countExisting < SelectedHitObjects . Count ( ) )
2019-11-12 10:45:46 +09:00
return TernaryState . Indeterminate ;
2019-11-08 13:52:36 +09:00
2019-11-12 10:45:46 +09:00
return TernaryState . True ;
2019-11-08 13:52:36 +09:00
}
2019-11-07 22:51:49 +09:00
}
#endregion
2018-04-13 18:19:50 +09:00
}
}