2019-01-24 16:43:03 +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 ;
2020-09-25 14:09:47 +08:00
using Humanizer ;
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 ;
2019-11-07 21:51:49 +08:00
using osu.Framework.Graphics.Cursor ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.Shapes ;
2019-11-07 21:51:49 +08:00
using osu.Framework.Graphics.UserInterface ;
2019-11-05 12:26:44 +08:00
using osu.Framework.Input ;
using osu.Framework.Input.Bindings ;
2018-07-21 10:38:28 +08:00
using osu.Framework.Input.States ;
2019-11-07 21:51:49 +08:00
using osu.Game.Audio ;
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 ;
2019-11-07 21:51:49 +08:00
using osu.Game.Graphics.UserInterface ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Edit ;
2018-11-16 16:12:24 +08:00
using osu.Game.Rulesets.Objects ;
2018-11-26 15:08:56 +08:00
using osu.Game.Rulesets.Objects.Drawables ;
2020-09-23 15:38:16 +08:00
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>
2019-11-07 21:51:49 +08:00
public class SelectionHandler : CompositeDrawable , IKeyBindingHandler < PlatformAction > , IHasContextMenu
2018-04-13 17:19:50 +08:00
{
public const float BORDER_RADIUS = 2 ;
2019-10-16 19:07:11 +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
2020-09-25 13:10:30 +08:00
public int SelectedCount = > selectedBlueprints . Count ;
2020-01-20 23:53:59 +08:00
public IEnumerable < HitObject > SelectedHitObjects = > selectedBlueprints . Select ( b = > b . HitObject ) ;
2018-11-16 16:12:24 +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-01-15 18:09:49 +08:00
[Resolved(CanBeNull = true)]
2020-05-26 15:58:28 +08:00
protected EditorBeatmap EditorBeatmap { get ; private set ; }
2018-10-18 15:36:06 +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
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 )
{
2020-09-25 14:09:47 +08:00
createStateBindables ( ) ;
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-07-17 16:48:27 +08:00
new Container
{
RelativeSizeAxes = Axes . Both ,
Masking = true ,
BorderThickness = BORDER_RADIUS ,
2020-07-17 16:51:39 +08:00
BorderColour = colours . YellowDark ,
2020-07-17 16:48:27 +08:00
Child = new Box
{
RelativeSizeAxes = Axes . Both ,
AlwaysPresent = true ,
Alpha = 0
}
} ,
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
}
}
}
2018-04-13 17:19:50 +08:00
}
} ;
}
#region User Input Handling
2018-11-19 15:58:11 +08:00
/// <summary>
2019-10-08 18:08:23 +08:00
/// 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>
2019-10-08 18:08:23 +08:00
/// <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 ) = > true ;
2018-04-13 17:19:50 +08:00
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 :
2019-11-08 18:44:47 +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>
2020-01-21 19:46:39 +08:00
internal void HandleSelected ( SelectionBlueprint blueprint )
{
2020-09-11 21:03:19 +08:00
selectedBlueprints . Add ( blueprint ) ;
2020-09-11 19:13:54 +08:00
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-01-22 00:32:11 +08:00
2020-09-11 21:03:19 +08:00
UpdateVisibility ( ) ;
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>
2018-11-16 16:12:24 +08:00
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>
2018-11-16 16:12:24 +08:00
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
}
}
2019-11-08 18:44:47 +08:00
private void deleteSelected ( )
{
2020-09-23 15:58:22 +08:00
ChangeHandler ? . BeginChange ( ) ;
2020-04-09 21:00:56 +08:00
2019-11-08 18:44:47 +08:00
foreach ( var h in selectedBlueprints . ToList ( ) )
2020-05-26 15:58:28 +08:00
EditorBeatmap ? . Remove ( h . HitObject ) ;
2020-04-09 21:00:56 +08:00
2020-09-23 15:58:22 +08:00
ChangeHandler ? . EndChange ( ) ;
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>
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 ( )
{
2020-07-17 16:48:27 +08:00
int count = selectedBlueprints . Count ;
selectionDetailsText . Text = count > 0 ? count . ToString ( ) : string . Empty ;
if ( count > 0 )
2018-04-13 17:19:50 +08:00
Show ( ) ;
else
Hide ( ) ;
}
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 ) ;
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
2019-11-08 15:46:58 +08: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-09-23 15:58:22 +08:00
ChangeHandler ? . BeginChange ( ) ;
2020-04-10 12:53:09 +08:00
2019-11-08 15:46:58 +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
2020-09-23 15:58:22 +08:00
ChangeHandler ? . EndChange ( ) ;
2019-11-08 15:46:58 +08:00
}
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>
2020-09-23 15:38:16 +08:00
public void SetNewCombo ( bool state )
{
2020-09-23 15:58:22 +08:00
ChangeHandler ? . BeginChange ( ) ;
2020-09-23 15:38:16 +08:00
foreach ( var h in SelectedHitObjects )
{
var comboInfo = h as IHasComboInformation ;
if ( comboInfo = = null )
2020-09-25 13:19:35 +08:00
continue ;
2020-09-23 15:38:16 +08:00
comboInfo . NewCombo = state ;
EditorBeatmap ? . UpdateHitObject ( h ) ;
}
2020-09-23 15:58:22 +08:00
ChangeHandler ? . EndChange ( ) ;
2020-09-23 15:38:16 +08:00
}
2019-11-08 15:46:58 +08: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-09-23 15:58:22 +08:00
ChangeHandler ? . BeginChange ( ) ;
2020-04-10 12:53:09 +08:00
2019-11-08 15:46:58 +08:00
foreach ( var h in SelectedHitObjects )
h . SamplesBindable . RemoveAll ( s = > s . Name = = sampleName ) ;
2020-04-10 12:53:09 +08:00
2020-09-23 15:58:22 +08:00
ChangeHandler ? . EndChange ( ) ;
2019-11-08 15:46:58 +08:00
}
#endregion
2020-09-25 14:09:47 +08:00
#region Selection State
2019-11-07 21:51:49 +08:00
2020-09-25 15:33:22 +08:00
/// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable < TernaryState > SelectionNewComboState = new Bindable < TernaryState > ( ) ;
2020-05-26 12:00:32 +08:00
2020-09-25 15:33:22 +08:00
/// <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 > > ( ) ;
2020-05-26 12:00:32 +08:00
2020-09-25 14:09:47 +08:00
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables ( )
{
2020-09-25 16:30:31 +08:00
foreach ( var sampleName in HitSampleInfo . AllAdditions )
2020-09-25 14:09:47 +08:00
{
var bindable = new Bindable < TernaryState >
{
Description = sampleName . Replace ( "hit" , string . Empty ) . Titleize ( )
} ;
2020-05-26 12:00:32 +08:00
2020-09-25 14:09:47 +08:00
bindable . ValueChanged + = state = >
2019-11-07 21:51:49 +08:00
{
2020-09-25 14:09:47 +08:00
switch ( state . NewValue )
2019-11-07 21:51:49 +08:00
{
2020-09-25 14:09:47 +08:00
case TernaryState . False :
RemoveHitSample ( sampleName ) ;
break ;
2019-11-07 21:51:49 +08:00
2020-09-25 14:09:47 +08:00
case TernaryState . True :
AddHitSample ( sampleName ) ;
break ;
}
} ;
2020-05-26 12:00:32 +08:00
2020-09-25 15:33:22 +08:00
SelectionSampleStates [ sampleName ] = bindable ;
2020-09-25 14:09:47 +08:00
}
2020-09-23 15:38:16 +08:00
2020-09-25 14:09:47 +08:00
// new combo
2020-09-25 15:33:22 +08:00
SelectionNewComboState . ValueChanged + = state = >
2020-09-23 15:38:16 +08:00
{
2020-09-25 14:09:47 +08:00
switch ( state . NewValue )
2020-09-23 15:38:16 +08:00
{
case TernaryState . False :
SetNewCombo ( false ) ;
break ;
case TernaryState . True :
SetNewCombo ( true ) ;
break ;
}
2020-09-25 14:09:47 +08:00
} ;
// bring in updates from selection changes
2020-09-25 14:27:45 +08:00
EditorBeatmap . HitObjectUpdated + = _ = > UpdateTernaryStates ( ) ;
EditorBeatmap . SelectedHitObjects . CollectionChanged + = ( sender , args ) = > UpdateTernaryStates ( ) ;
2020-09-25 14:09:47 +08:00
}
2020-09-25 14:27:45 +08:00
/// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary>
protected virtual void UpdateTernaryStates ( )
2020-09-25 14:09:47 +08:00
{
2020-09-25 15:33:22 +08:00
SelectionNewComboState . Value = GetStateFromSelection ( SelectedHitObjects . OfType < IHasComboInformation > ( ) , h = > h . NewCombo ) ;
2020-09-23 15:38:16 +08:00
2020-09-25 15:33:22 +08:00
foreach ( var ( sampleName , bindable ) in SelectionSampleStates )
2020-09-23 15:38:16 +08:00
{
2020-09-25 14:27:45 +08:00
bindable . Value = GetStateFromSelection ( SelectedHitObjects , h = > h . Samples . Any ( s = > s . Name = = sampleName ) ) ;
2020-09-25 14:09:47 +08:00
}
}
2020-09-23 15:38:16 +08:00
2020-09-25 14:09:47 +08:00
/// <summary>
2020-09-25 14:27:45 +08:00
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
2020-09-25 14:09:47 +08:00
/// </summary>
2020-09-25 14:27:45 +08:00
protected TernaryState GetStateFromSelection < T > ( IEnumerable < T > selection , Func < T , bool > func )
2020-09-25 14:09:47 +08:00
{
2020-09-25 14:27:45 +08:00
if ( selection . Any ( func ) )
return selection . All ( func ) ? TernaryState . True : TernaryState . Indeterminate ;
2020-09-23 15:38:16 +08:00
2020-09-25 14:27:45 +08:00
return TernaryState . False ;
2020-09-23 15:38:16 +08:00
}
2020-09-25 14:09:47 +08:00
#endregion
2019-11-07 21:51:49 +08:00
#region Context Menu
2020-05-26 12:00:32 +08:00
public MenuItem [ ] ContextMenuItems
2019-11-07 21:51:49 +08:00
{
get
{
2019-11-12 13:42:30 +08:00
if ( ! selectedBlueprints . Any ( b = > b . IsHovered ) )
2019-11-07 21:51:49 +08:00
return Array . Empty < MenuItem > ( ) ;
2020-05-26 12:00:32 +08:00
var items = new List < MenuItem > ( ) ;
items . AddRange ( GetContextMenuItemsForSelection ( selectedBlueprints ) ) ;
2020-09-23 15:38:16 +08:00
if ( selectedBlueprints . All ( b = > b . HitObject is IHasComboInformation ) )
2020-09-25 14:09:47 +08:00
{
2020-09-25 15:33:22 +08:00
items . Add ( new TernaryStateMenuItem ( "New combo" ) { State = { BindTarget = SelectionNewComboState } } ) ;
2020-09-25 14:09:47 +08:00
}
2020-09-23 15:38:16 +08:00
2020-05-26 12:00:32 +08:00
if ( selectedBlueprints . Count = = 1 )
items . AddRange ( selectedBlueprints [ 0 ] . ContextMenuItems ) ;
items . AddRange ( new [ ]
2019-11-07 21:51:49 +08:00
{
2019-11-08 12:52:36 +08:00
new OsuMenuItem ( "Sound" )
2019-11-07 21:51:49 +08:00
{
2020-09-25 15:33:22 +08:00
Items = SelectionSampleStates . Select ( kvp = >
2020-09-25 14:09:47 +08:00
new TernaryStateMenuItem ( kvp . Value . Description ) { State = { BindTarget = kvp . Value } } ) . ToArray ( )
2019-11-08 18:44:47 +08:00
} ,
new OsuMenuItem ( "Delete" , MenuItemType . Destructive , deleteSelected ) ,
2020-05-26 12:00:32 +08:00
} ) ;
2019-11-12 12:38:42 +08:00
return items . ToArray ( ) ;
2019-11-07 21:51:49 +08:00
}
}
2020-05-26 12:00:32 +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 > selection )
= > Enumerable . Empty < MenuItem > ( ) ;
2019-11-07 21:51:49 +08:00
#endregion
2018-04-13 17:19:50 +08:00
}
}