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 ;
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 ;
2020-11-10 15:54:33 +08:00
using osu.Framework.Input.Events ;
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 ;
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>
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
{
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-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-10-09 20:44:10 +08:00
[Resolved]
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-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
}
} ;
}
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>
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>
2020-09-30 12:52:57 +08:00
public virtual bool HandleMovement ( MoveSelectionEvent moveEvent ) = > false ;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
/// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param>
2020-10-09 04:14:44 +08:00
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be rotated.</returns>
2020-09-30 12:52:57 +08:00
public virtual bool HandleRotation ( float angle ) = > false ;
/// <summary>
2020-09-30 14:08:56 +08:00
/// Handles the selected <see cref="DrawableHitObject"/>s 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>
2020-10-09 04:14:44 +08:00
/// <returns>Whether any <see cref="DrawableHitObject"/>s 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>
2020-10-09 04:14:44 +08:00
/// Handles the selected <see cref="DrawableHitObject"/>s being flipped.
2020-10-01 15:25:29 +08:00
/// </summary>
/// <param name="direction">The direction to flip</param>
2020-10-09 04:14:44 +08:00
/// <returns>Whether any <see cref="DrawableHitObject"/>s 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>
/// Handles the selected <see cref="DrawableHitObject"/>s being reversed pattern-wise.
/// </summary>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be reversed.</returns>
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 :
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-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
}
/// <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>
2020-11-10 15:54:33 +08:00
internal bool HandleSelectionRequested ( SelectionBlueprint 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 ) ;
2020-11-04 16:53:03 +08:00
return false ;
2020-11-03 19:45:48 +08:00
}
2020-11-10 15:54:33 +08:00
if ( e . ControlPressed & & e . Button = = MouseButton . Left )
2020-10-27 05:16:28 +08:00
blueprint . ToggleSelection ( ) ;
2018-04-13 17:19:50 +08:00
else
2020-10-27 05:16:28 +08:00
ensureSelected ( blueprint ) ;
2020-11-03 19:45:48 +08:00
2020-11-04 16:53:03 +08:00
return true ;
2018-04-13 17:19:50 +08:00
}
2020-10-31 18:35:25 +08:00
private void handleQuickDeletion ( SelectionBlueprint blueprint )
{
2020-11-03 19:45:48 +08:00
if ( blueprint . HandleQuickDeletion ( ) )
return ;
2020-10-31 18:35:25 +08:00
if ( ! blueprint . IsSelected )
EditorBeatmap . Remove ( blueprint . HitObject ) ;
else
deleteSelected ( ) ;
}
2020-10-27 05:16:28 +08:00
private void ensureSelected ( SelectionBlueprint blueprint )
2020-10-27 03:28:53 +08:00
{
2020-10-27 05:16:28 +08:00
if ( blueprint . IsSelected )
return ;
DeselectAll ? . Invoke ( ) ;
blueprint . Select ( ) ;
2020-10-27 03:28:53 +08:00
}
2019-11-08 18:44:47 +08:00
private void deleteSelected ( )
{
2020-10-09 20:44:10 +08:00
EditorBeatmap . RemoveRange ( selectedBlueprints . Select ( b = > b . HitObject ) ) ;
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>
2020-10-31 19:21:07 +08:00
private void updateVisibility ( )
2018-04-13 17:19:50 +08:00
{
2020-07-17 16:48:27 +08:00
int count = selectedBlueprints . Count ;
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 ;
// 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-10-09 20:44:10 +08:00
EditorBeatmap . BeginChange ( ) ;
2020-04-10 12:53:09 +08:00
2020-10-09 17:44:23 +08:00
foreach ( var h in EditorBeatmap . SelectedHitObjects )
2019-11-08 15:46:58 +08:00
{
// 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-10-09 20:44:10 +08:00
EditorBeatmap . 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-10-09 20:44:10 +08:00
EditorBeatmap . BeginChange ( ) ;
2020-09-23 15:38:16 +08:00
2020-10-09 17:44:23 +08:00
foreach ( var h in EditorBeatmap . SelectedHitObjects )
2020-09-23 15:38:16 +08:00
{
var comboInfo = h as IHasComboInformation ;
2020-09-28 13:45:36 +08:00
if ( comboInfo = = null | | comboInfo . NewCombo = = state ) continue ;
2020-09-23 15:38:16 +08:00
comboInfo . NewCombo = state ;
2020-10-09 20:44:10 +08:00
EditorBeatmap . Update ( h ) ;
2020-09-23 15:38:16 +08:00
}
2020-10-09 20:44:10 +08:00
EditorBeatmap . 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-10-09 20:44:10 +08:00
EditorBeatmap . BeginChange ( ) ;
2020-04-10 12:53:09 +08:00
2020-10-09 17:44:23 +08:00
foreach ( var h in EditorBeatmap . SelectedHitObjects )
2019-11-08 15:46:58 +08:00
h . SamplesBindable . RemoveAll ( s = > s . Name = = sampleName ) ;
2020-04-10 12:53:09 +08:00
2020-10-09 20:44:10 +08:00
EditorBeatmap . 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-11-03 00:08:35 +08:00
EditorBeatmap . HitObjectUpdated + = _ = > Scheduler . AddOnce ( UpdateTernaryStates ) ;
2020-10-31 19:21:07 +08:00
EditorBeatmap . SelectedHitObjects . CollectionChanged + = ( sender , args ) = >
{
2020-10-31 21:45:11 +08:00
Scheduler . AddOnce ( updateVisibility ) ;
2020-11-03 00:08:35 +08:00
Scheduler . AddOnce ( UpdateTernaryStates ) ;
2020-10-31 19:21:07 +08:00
} ;
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-10-09 17:44:23 +08:00
SelectionNewComboState . Value = GetStateFromSelection ( EditorBeatmap . 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-10-09 17:44:23 +08:00
bindable . Value = GetStateFromSelection ( EditorBeatmap . 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
}
}