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
2019-10-16 19:07:11 +08:00
using System ;
2018-11-06 16:51:26 +08:00
using System.Collections.Generic ;
2019-10-23 17:58:15 +08:00
using System.Diagnostics ;
2018-10-18 15:36:06 +08:00
using System.Linq ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Allocation ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2018-11-06 16:51:26 +08:00
using osu.Framework.Graphics.Primitives ;
2019-10-03 15:14:42 +08:00
using osu.Framework.Input ;
2018-10-02 11:02:47 +08:00
using osu.Framework.Input.Events ;
2019-11-06 17:15:57 +08:00
using osu.Framework.Timing ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Edit ;
2018-11-06 17:02:55 +08:00
using osu.Game.Rulesets.Edit.Tools ;
2019-08-29 15:06:40 +08:00
using osu.Game.Rulesets.Objects ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects.Drawables ;
2019-10-16 19:34:16 +08:00
using osuTK ;
2019-11-07 21:51:49 +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
{
2018-11-06 16:36:10 +08:00
public class BlueprintContainer : CompositeDrawable
2018-04-13 17:19:50 +08:00
{
2019-10-16 19:07:11 +08:00
public event Action < IEnumerable < HitObject > > SelectionChanged ;
2018-11-13 11:52:04 +08:00
2019-10-23 17:37:57 +08:00
private DragBox dragBox ;
2019-10-16 19:07:11 +08:00
private SelectionBlueprintContainer selectionBlueprints ;
2018-11-06 17:04:03 +08:00
private Container < PlacementBlueprint > placementBlueprintContainer ;
2018-11-13 11:52:04 +08:00
private PlacementBlueprint currentPlacement ;
2018-11-19 15:58:11 +08:00
private SelectionHandler selectionHandler ;
2019-10-03 15:14:42 +08:00
private InputManager inputManager ;
2018-11-06 16:51:26 +08:00
2019-11-06 17:15:57 +08:00
[Resolved]
private IAdjustableClock adjustableClock { get ; set ; }
2018-10-17 15:17:12 +08:00
[Resolved]
2018-10-17 16:41:17 +08:00
private HitObjectComposer composer { get ; set ; }
2018-10-17 15:17:12 +08:00
2019-08-29 15:06:40 +08:00
[Resolved]
private IEditorBeatmap beatmap { get ; set ; }
2018-11-06 16:36:10 +08:00
public BlueprintContainer ( )
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes . Both ;
}
[BackgroundDependencyLoader]
2018-10-17 16:41:17 +08:00
private void load ( )
2018-04-13 17:19:50 +08:00
{
2018-11-19 15:58:11 +08:00
selectionHandler = composer . CreateSelectionHandler ( ) ;
selectionHandler . DeselectAll = deselectAll ;
2018-04-13 17:19:50 +08:00
2018-04-20 17:19:17 +08:00
InternalChildren = new [ ]
2018-04-13 17:19:50 +08:00
{
2019-10-23 17:37:57 +08:00
dragBox = new DragBox ( select ) ,
2018-11-19 15:58:11 +08:00
selectionHandler ,
2018-11-06 16:51:26 +08:00
selectionBlueprints = new SelectionBlueprintContainer { RelativeSizeAxes = Axes . Both } ,
2018-11-06 17:04:03 +08:00
placementBlueprintContainer = new Container < PlacementBlueprint > { RelativeSizeAxes = Axes . Both } ,
2018-11-06 16:24:38 +08:00
dragBox . CreateProxy ( )
2018-04-13 17:19:50 +08:00
} ;
2018-06-07 15:27:49 +08:00
2018-07-17 14:35:32 +08:00
foreach ( var obj in composer . HitObjects )
2019-08-29 15:06:40 +08:00
addBlueprintFor ( obj ) ;
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
beatmap . HitObjectAdded + = addBlueprintFor ;
beatmap . HitObjectRemoved + = removeBlueprintFor ;
2019-10-03 15:14:42 +08:00
inputManager = GetContainingInputManager ( ) ;
2018-04-13 17:19:50 +08:00
}
2018-11-06 17:02:55 +08:00
private HitObjectCompositionTool currentTool ;
/// <summary>
/// The current placement tool.
/// </summary>
public HitObjectCompositionTool CurrentTool
2018-04-13 17:19:50 +08:00
{
2018-11-06 17:02:55 +08:00
get = > currentTool ;
set
{
if ( currentTool = = value )
return ;
2019-02-28 12:31:40 +08:00
2018-11-06 17:02:55 +08:00
currentTool = value ;
refreshTool ( ) ;
}
2018-04-13 17:19:50 +08:00
}
2019-10-24 13:58:02 +08:00
protected override bool OnMouseDown ( MouseDownEvent e )
{
2019-10-24 14:58:22 +08:00
beginClickSelection ( e ) ;
2019-11-08 12:40:47 +08:00
return e . Button = = MouseButton . Left ;
2019-10-24 13:58:02 +08:00
}
2018-11-06 17:02:55 +08:00
protected override bool OnClick ( ClickEvent e )
{
2019-11-07 21:51:49 +08:00
if ( e . Button = = MouseButton . Right )
return false ;
2019-10-24 15:14:29 +08:00
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
if ( endClickSelection ( ) | | selectionHandler . SelectedBlueprints . Any ( b = > b . IsHovered ) )
2019-10-24 13:58:02 +08:00
return true ;
2018-11-06 17:02:55 +08:00
deselectAll ( ) ;
return true ;
}
2019-11-06 17:15:57 +08:00
protected override bool OnDoubleClick ( DoubleClickEvent e )
{
2019-11-07 21:51:49 +08:00
if ( e . Button = = MouseButton . Right )
return false ;
2019-11-06 17:15:57 +08:00
SelectionBlueprint clickedBlueprint = selectionHandler . SelectedBlueprints . FirstOrDefault ( b = > b . IsHovered ) ;
if ( clickedBlueprint = = null )
return false ;
adjustableClock ? . Seek ( clickedBlueprint . DrawableObject . HitObject . StartTime ) ;
return true ;
}
2019-10-24 14:58:22 +08:00
protected override bool OnMouseUp ( MouseUpEvent e )
{
2019-10-24 15:14:29 +08:00
// Special case for when a drag happened instead of a click
Schedule ( ( ) = > endClickSelection ( ) ) ;
2019-11-08 12:40:47 +08:00
return e . Button = = MouseButton . Left ;
2019-10-24 14:58:22 +08:00
}
2019-10-03 15:14:42 +08:00
protected override bool OnMouseMove ( MouseMoveEvent e )
{
if ( currentPlacement ! = null )
{
2019-10-18 12:48:59 +08:00
updatePlacementPosition ( e . ScreenSpaceMousePosition ) ;
2019-10-03 15:14:42 +08:00
return true ;
}
return base . OnMouseMove ( e ) ;
}
2019-10-24 14:11:54 +08:00
protected override bool OnDragStart ( DragStartEvent e )
{
2019-11-07 21:51:49 +08:00
if ( e . Button = = MouseButton . Right )
return false ;
2019-10-24 14:11:54 +08:00
if ( ! beginSelectionMovement ( ) )
2019-10-24 16:22:14 +08:00
{
dragBox . UpdateDrag ( e ) ;
2019-10-24 14:11:54 +08:00
dragBox . FadeIn ( 250 , Easing . OutQuint ) ;
2019-10-24 16:22:14 +08:00
}
2019-10-24 14:11:54 +08:00
return true ;
}
protected override bool OnDrag ( DragEvent e )
{
2019-11-07 21:51:49 +08:00
if ( e . Button = = MouseButton . Right )
return false ;
2019-10-24 14:11:54 +08:00
if ( ! moveCurrentSelection ( e ) )
dragBox . UpdateDrag ( e ) ;
return true ;
}
protected override bool OnDragEnd ( DragEndEvent e )
{
2019-11-07 21:51:49 +08:00
if ( e . Button = = MouseButton . Right )
return false ;
2019-10-24 14:11:54 +08:00
if ( ! finishSelectionMovement ( ) )
{
dragBox . FadeOut ( 250 , Easing . OutQuint ) ;
selectionHandler . UpdateVisibility ( ) ;
}
return true ;
}
2018-11-13 11:52:04 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2018-11-13 12:00:00 +08:00
if ( currentPlacement ! = null )
{
2018-11-14 17:34:13 +08:00
if ( composer . CursorInPlacementArea )
2018-11-13 12:00:00 +08:00
currentPlacement . State = PlacementState . Shown ;
else if ( currentPlacement ? . PlacementBegun = = false )
currentPlacement . State = PlacementState . Hidden ;
}
2018-11-13 11:52:04 +08:00
}
2019-10-24 15:17:48 +08:00
#region Blueprint Addition / Removal
2019-08-29 15:06:40 +08:00
private void addBlueprintFor ( HitObject hitObject )
2018-04-13 17:19:50 +08:00
{
2019-08-29 15:06:40 +08:00
var drawable = composer . HitObjects . FirstOrDefault ( d = > d . HitObject = = hitObject ) ;
if ( drawable = = null )
2018-04-13 17:19:50 +08:00
return ;
2019-08-29 15:06:40 +08:00
addBlueprintFor ( drawable ) ;
2018-04-13 17:19:50 +08:00
}
2018-10-18 15:36:06 +08:00
2019-08-29 15:06:40 +08:00
private void removeBlueprintFor ( HitObject hitObject )
2018-10-18 15:36:06 +08:00
{
2019-10-21 16:04:56 +08:00
var blueprint = selectionBlueprints . Single ( m = > m . DrawableObject . HitObject = = hitObject ) ;
2018-11-06 17:02:55 +08:00
if ( blueprint = = null )
2018-10-18 15:36:06 +08:00
return ;
2018-11-06 17:02:55 +08:00
blueprint . Deselect ( ) ;
2018-11-06 16:51:26 +08:00
2018-11-06 17:02:55 +08:00
blueprint . Selected - = onBlueprintSelected ;
blueprint . Deselected - = onBlueprintDeselected ;
2018-11-06 16:51:26 +08:00
2018-11-06 17:02:55 +08:00
selectionBlueprints . Remove ( blueprint ) ;
2018-11-06 16:51:26 +08:00
}
2019-08-29 15:06:40 +08:00
private void addBlueprintFor ( DrawableHitObject hitObject )
{
refreshTool ( ) ;
var blueprint = composer . CreateBlueprintFor ( hitObject ) ;
if ( blueprint = = null )
return ;
blueprint . Selected + = onBlueprintSelected ;
blueprint . Deselected + = onBlueprintDeselected ;
selectionBlueprints . Add ( blueprint ) ;
}
2019-10-24 15:17:48 +08:00
#endregion
2019-08-29 15:06:40 +08:00
2019-10-24 15:17:48 +08:00
#region Placement
2018-11-13 11:52:04 +08:00
2018-11-06 17:02:55 +08:00
/// <summary>
/// Refreshes the current placement tool.
/// </summary>
private void refreshTool ( )
{
placementBlueprintContainer . Clear ( ) ;
2018-11-13 11:52:04 +08:00
currentPlacement = null ;
2018-11-06 17:02:55 +08:00
2018-11-06 17:06:34 +08:00
var blueprint = CurrentTool ? . CreatePlacementBlueprint ( ) ;
2019-10-03 15:14:42 +08:00
2018-11-06 17:02:55 +08:00
if ( blueprint ! = null )
2019-10-03 15:14:42 +08:00
{
2018-11-13 11:52:04 +08:00
placementBlueprintContainer . Child = currentPlacement = blueprint ;
2019-10-03 15:14:42 +08:00
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
2019-10-18 12:48:59 +08:00
updatePlacementPosition ( inputManager . CurrentState . Mouse . Position ) ;
2019-10-03 15:14:42 +08:00
}
2018-11-06 17:02:55 +08:00
}
2019-10-21 13:08:28 +08:00
private void updatePlacementPosition ( Vector2 screenSpacePosition )
{
2019-10-25 11:34:49 +08:00
Vector2 snappedGridPosition = composer . GetSnappedPosition ( ToLocalSpace ( screenSpacePosition ) , 0 ) . position ;
2019-10-21 13:08:28 +08:00
Vector2 snappedScreenSpacePosition = ToScreenSpace ( snappedGridPosition ) ;
currentPlacement . UpdatePosition ( snappedScreenSpacePosition ) ;
}
2019-10-18 12:48:59 +08:00
2019-10-24 15:17:48 +08:00
#endregion
2019-10-24 14:58:22 +08:00
#region Selection
/// <summary>
/// Whether a blueprint was selected by a previous click event.
/// </summary>
private bool clickSelectionBegan ;
/// <summary>
/// Attempts to select any hovered blueprints.
/// </summary>
/// <param name="e">The input event that triggered this selection.</param>
private void beginClickSelection ( UIEvent e )
{
Debug . Assert ( ! clickSelectionBegan ) ;
2019-11-05 17:25:38 +08:00
// If a select blueprint is already hovered, disallow changes in selection.
2019-11-05 17:29:08 +08:00
// Exception is made when holding control, as deselection should still be allowed.
if ( ! e . CurrentState . Keyboard . ControlPressed & &
selectionHandler . SelectedBlueprints . Any ( s = > s . IsHovered ) )
2019-11-05 17:25:38 +08:00
return ;
2019-11-05 16:28:42 +08:00
2019-11-05 17:25:38 +08:00
foreach ( SelectionBlueprint blueprint in selectionBlueprints . AliveBlueprints )
2019-10-24 14:58:22 +08:00
{
2019-11-05 17:25:38 +08:00
if ( blueprint . IsHovered )
2019-10-24 14:58:22 +08:00
{
2019-11-05 17:25:38 +08:00
selectionHandler . HandleSelectionRequested ( blueprint , e . CurrentState ) ;
clickSelectionBegan = true ;
2019-10-24 14:58:22 +08:00
break ;
}
}
}
/// <summary>
/// Finishes the current blueprint selection.
/// </summary>
2019-10-24 15:14:29 +08:00
/// <returns>Whether a click selection was active.</returns>
private bool endClickSelection ( )
2019-10-24 14:58:22 +08:00
{
2019-10-24 15:14:29 +08:00
if ( ! clickSelectionBegan )
return false ;
2019-10-24 14:58:22 +08:00
clickSelectionBegan = false ;
2019-10-24 15:14:29 +08:00
return true ;
2019-10-24 14:58:22 +08:00
}
2018-11-06 16:51:26 +08:00
/// <summary>
/// Select all masks in a given rectangle selection area.
/// </summary>
/// <param name="rect">The rectangle to perform a selection on in screen-space coordinates.</param>
private void select ( RectangleF rect )
{
2019-10-16 19:07:11 +08:00
foreach ( var blueprint in selectionBlueprints )
2018-11-06 16:51:26 +08:00
{
2019-10-16 19:07:11 +08:00
if ( blueprint . IsAlive & & blueprint . IsPresent & & rect . Contains ( blueprint . SelectionPoint ) )
2018-11-06 17:02:55 +08:00
blueprint . Select ( ) ;
2018-11-06 16:51:26 +08:00
else
2018-11-06 17:02:55 +08:00
blueprint . Deselect ( ) ;
2018-11-06 16:51:26 +08:00
}
}
/// <summary>
2018-11-06 16:56:04 +08:00
/// Deselects all selected <see cref="SelectionBlueprint"/>s.
2018-11-06 16:51:26 +08:00
/// </summary>
2019-10-16 19:07:11 +08:00
private void deselectAll ( ) = > selectionHandler . SelectedBlueprints . ToList ( ) . ForEach ( m = > m . Deselect ( ) ) ;
2018-11-06 16:51:26 +08:00
2018-11-06 16:56:04 +08:00
private void onBlueprintSelected ( SelectionBlueprint blueprint )
2018-11-06 16:51:26 +08:00
{
2018-11-19 15:58:11 +08:00
selectionHandler . HandleSelected ( blueprint ) ;
2018-11-06 16:56:04 +08:00
selectionBlueprints . ChangeChildDepth ( blueprint , 1 ) ;
2019-10-16 19:07:11 +08:00
SelectionChanged ? . Invoke ( selectionHandler . SelectedHitObjects ) ;
2018-11-06 16:51:26 +08:00
}
2018-11-06 16:56:04 +08:00
private void onBlueprintDeselected ( SelectionBlueprint blueprint )
2018-11-06 16:51:26 +08:00
{
2018-11-19 15:58:11 +08:00
selectionHandler . HandleDeselected ( blueprint ) ;
2018-11-06 16:56:04 +08:00
selectionBlueprints . ChangeChildDepth ( blueprint , 0 ) ;
2019-10-16 19:07:11 +08:00
SelectionChanged ? . Invoke ( selectionHandler . SelectedHitObjects ) ;
2018-11-06 16:51:26 +08:00
}
2019-10-24 14:58:22 +08:00
#endregion
#region Selection Movement
2019-10-23 17:58:15 +08:00
private Vector2 ? screenSpaceMovementStartPosition ;
private SelectionBlueprint movementBlueprint ;
2019-10-24 14:11:54 +08:00
/// <summary>
/// Attempts to begin the movement of any selected blueprints.
/// </summary>
/// <returns>Whether movement began.</returns>
private bool beginSelectionMovement ( )
2019-10-23 17:37:57 +08:00
{
2019-10-24 14:11:54 +08:00
Debug . Assert ( movementBlueprint = = null ) ;
2019-10-24 15:14:29 +08:00
// Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement
// A special case is added for when a click selection occurred before the drag
if ( ! clickSelectionBegan & & ! selectionHandler . SelectedBlueprints . Any ( b = > b . IsHovered ) )
2019-10-24 14:11:54 +08:00
return false ;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = selectionHandler . SelectedBlueprints . OrderBy ( b = > b . DrawableObject . HitObject . StartTime ) . First ( ) ;
screenSpaceMovementStartPosition = movementBlueprint . DrawableObject . ToScreenSpace ( movementBlueprint . DrawableObject . OriginPosition ) ;
2019-10-23 17:37:57 +08:00
return true ;
}
2018-11-06 16:51:26 +08:00
2019-10-24 14:11:54 +08:00
/// <summary>
/// Moves the current selected blueprints.
/// </summary>
/// <param name="e">The <see cref="DragEvent"/> defining the movement event.</param>
/// <returns>Whether a movement was active.</returns>
private bool moveCurrentSelection ( DragEvent e )
2019-10-08 17:57:03 +08:00
{
2019-10-24 14:11:54 +08:00
if ( movementBlueprint = = null )
return false ;
2019-10-23 17:37:57 +08:00
2019-10-24 14:11:54 +08:00
Debug . Assert ( screenSpaceMovementStartPosition ! = null ) ;
2019-10-23 17:58:15 +08:00
2019-10-24 14:11:54 +08:00
Vector2 startPosition = screenSpaceMovementStartPosition . Value ;
HitObject draggedObject = movementBlueprint . DrawableObject . HitObject ;
2019-10-25 11:34:49 +08:00
2019-10-24 14:11:54 +08:00
// The final movement position, relative to screenSpaceMovementStartPosition
Vector2 movePosition = startPosition + e . ScreenSpaceMousePosition - e . ScreenSpaceMouseDownPosition ;
2019-10-25 11:34:49 +08:00
( Vector2 snappedPosition , double snappedTime ) = composer . GetSnappedPosition ( ToLocalSpace ( movePosition ) , draggedObject . StartTime ) ;
2019-10-16 19:34:02 +08:00
// Move the hitobjects
2019-11-06 16:27:41 +08:00
if ( ! selectionHandler . HandleMovement ( new MoveSelectionEvent ( movementBlueprint , startPosition , ToScreenSpace ( snappedPosition ) ) ) )
return true ;
2019-10-16 19:34:16 +08:00
// Apply the start time at the newly snapped-to position
2019-10-25 11:34:49 +08:00
double offset = snappedTime - draggedObject . StartTime ;
2019-10-16 19:34:16 +08:00
foreach ( HitObject obj in selectionHandler . SelectedHitObjects )
obj . StartTime + = offset ;
2019-10-23 17:37:57 +08:00
return true ;
2019-10-08 17:57:03 +08:00
}
2018-11-06 16:51:26 +08:00
2019-10-24 14:11:54 +08:00
/// <summary>
/// Finishes the current movement of selected blueprints.
/// </summary>
/// <returns>Whether a movement was active.</returns>
private bool finishSelectionMovement ( )
2019-10-08 17:57:03 +08:00
{
2019-10-24 14:11:54 +08:00
if ( movementBlueprint = = null )
return false ;
screenSpaceMovementStartPosition = null ;
movementBlueprint = null ;
2019-10-16 19:34:16 +08:00
2019-10-23 17:58:15 +08:00
return true ;
2019-10-08 17:57:03 +08:00
}
2018-11-06 16:51:26 +08:00
2019-10-24 14:58:22 +08:00
#endregion
2019-08-29 15:06:40 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
if ( beatmap ! = null )
{
beatmap . HitObjectAdded - = addBlueprintFor ;
beatmap . HitObjectRemoved - = removeBlueprintFor ;
}
}
2018-11-06 16:56:04 +08:00
private class SelectionBlueprintContainer : Container < SelectionBlueprint >
2018-11-06 16:51:26 +08:00
{
2019-10-24 13:58:02 +08:00
public IEnumerable < SelectionBlueprint > AliveBlueprints = > AliveInternalChildren . Cast < SelectionBlueprint > ( ) ;
2018-11-06 16:51:26 +08:00
protected override int Compare ( Drawable x , Drawable y )
{
2018-11-06 16:56:04 +08:00
if ( ! ( x is SelectionBlueprint xBlueprint ) | | ! ( y is SelectionBlueprint yBlueprint ) )
2018-11-06 16:51:26 +08:00
return base . Compare ( x , y ) ;
2019-02-28 12:31:40 +08:00
2018-11-06 16:56:04 +08:00
return Compare ( xBlueprint , yBlueprint ) ;
2018-11-06 16:51:26 +08:00
}
2018-11-06 16:56:04 +08:00
public int Compare ( SelectionBlueprint x , SelectionBlueprint y )
2018-11-06 16:51:26 +08:00
{
2018-11-06 16:56:04 +08:00
// dpeth is used to denote selected status (we always want selected blueprints to handle input first).
2018-11-06 16:51:26 +08:00
int d = x . Depth . CompareTo ( y . Depth ) ;
if ( d ! = 0 )
return d ;
// Put earlier hitobjects towards the end of the list, so they handle input first
2019-10-21 16:04:56 +08:00
int i = y . DrawableObject . HitObject . StartTime . CompareTo ( x . DrawableObject . HitObject . StartTime ) ;
2018-11-06 16:51:26 +08:00
return i = = 0 ? CompareReverseChildID ( x , y ) : i ;
}
2018-10-18 15:36:06 +08:00
}
2018-04-13 17:19:50 +08:00
}
}