mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 09:32:55 +08:00
Rewrite the way drag + click selections happen
The general idea here is that we need the masks to handle mouse down events, as they need to handle the drag (mousedown -> drag immediately). I've rewritten the editor selections to use events, as there are some 3 different components that handle/trigger selections in different ways. 1. All selections/deselections now propagate through `HitObjectMask.Select()`/`HitObjectMask.Deselect()`. 2. Components that react to changes in the selection bind to the masks' `Selected`/`Deselected` events, and track them/change their states locally. 3. Masks provide a `SingleSelectionRequested` event which is invoked on the mouse-down event. Various components bind to this event to perform state changes locally in this scenario. 4. `DragBox` now handles all drag input locally. It triggers `Select`/`Deselect` on the masks it needs to. 5. `SelectionBox` handles the display of itself locally. 6. `SelectionBox` handles movement of groups of masks locally. 7. `HitObjectMasks` handles movement of itself locally.
This commit is contained in:
parent
57e4281601
commit
6d4f94756e
@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
|
|||||||
Size = hitCircle.Size;
|
Size = hitCircle.Size;
|
||||||
Scale = hitCircle.Scale;
|
Scale = hitCircle.Scale;
|
||||||
|
|
||||||
|
CornerRadius = Size.X / 2;
|
||||||
|
|
||||||
AddInternal(new RingPiece());
|
AddInternal(new RingPiece());
|
||||||
|
|
||||||
hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position;
|
hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
@ -59,5 +60,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos);
|
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos);
|
||||||
|
|
||||||
|
public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
|
||||||
|
public override Quad SelectionQuad => body.PathDrawQuad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Framework.Graphics.Primitives;
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using OpenTK.Graphics;
|
using OpenTK.Graphics;
|
||||||
@ -177,8 +176,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
|
public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
|
||||||
|
|
||||||
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos);
|
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos);
|
||||||
|
|
||||||
public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
|
|
||||||
public override Quad SelectionQuad => Body.PathDrawQuad;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ namespace osu.Game.Tests.Visual
|
|||||||
{
|
{
|
||||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||||
{
|
{
|
||||||
typeof(SelectionLayer),
|
|
||||||
typeof(SelectionBox),
|
typeof(SelectionBox),
|
||||||
typeof(HitObjectComposer),
|
typeof(HitObjectComposer),
|
||||||
typeof(OsuHitObjectComposer),
|
typeof(OsuHitObjectComposer),
|
||||||
|
@ -65,9 +65,6 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HitObjectMaskLayer hitObjectMaskLayer = new HitObjectMaskLayer(rulesetContainer.Playfield, this);
|
|
||||||
SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield);
|
|
||||||
|
|
||||||
var layerBelowRuleset = new BorderLayer
|
var layerBelowRuleset = new BorderLayer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -75,12 +72,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
};
|
};
|
||||||
|
|
||||||
var layerAboveRuleset = CreateLayerContainer();
|
var layerAboveRuleset = CreateLayerContainer();
|
||||||
layerAboveRuleset.Children = new Drawable[]
|
layerAboveRuleset.Child = new HitObjectMaskLayer(rulesetContainer.Playfield, this);
|
||||||
{
|
|
||||||
selectionLayer, // Below object overlays for input
|
|
||||||
hitObjectMaskLayer,
|
|
||||||
selectionLayer.CreateProxy() // Proxy above object overlays for selections
|
|
||||||
};
|
|
||||||
|
|
||||||
layerContainers.Add(layerBelowRuleset);
|
layerContainers.Add(layerBelowRuleset);
|
||||||
layerContainers.Add(layerAboveRuleset);
|
layerContainers.Add(layerAboveRuleset);
|
||||||
@ -259,10 +251,10 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="SelectionBox"/> which outlines <see cref="DrawableHitObject"/>s
|
/// Creates a <see cref="SelectionBox"/> which outlines <see cref="DrawableHitObject"/>s
|
||||||
/// and handles all hitobject movement/pattern adjustments.
|
/// and handles hitobject pattern adjustments.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="overlays">The <see cref="DrawableHitObject"/> overlays.</param>
|
/// <param name="overlays">The <see cref="DrawableHitObject"/> overlays.</param>
|
||||||
public virtual SelectionBox CreateSelectionBox(IReadOnlyList<HitObjectMask> overlays) => new SelectionBox(overlays);
|
public virtual SelectionBox CreateSelectionBox() => new SelectionBox();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="ScalableContainer"/> which provides a layer above or below the <see cref="Playfield"/>.
|
/// Creates a <see cref="ScalableContainer"/> which provides a layer above or below the <see cref="Playfield"/>.
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Game.Rulesets.Edit.Types;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using OpenTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Edit
|
namespace osu.Game.Rulesets.Edit
|
||||||
{
|
{
|
||||||
@ -11,15 +16,90 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class HitObjectMask : VisibilityContainer
|
public class HitObjectMask : VisibilityContainer
|
||||||
{
|
{
|
||||||
|
public event Action<HitObjectMask> Selected;
|
||||||
|
public event Action<HitObjectMask> Deselected;
|
||||||
|
public event Action<HitObjectMask> SingleSelectionRequested;
|
||||||
|
|
||||||
public readonly DrawableHitObject HitObject;
|
public readonly DrawableHitObject HitObject;
|
||||||
|
|
||||||
|
protected override bool ShouldBeAlive => HitObject.IsAlive || State == Visibility.Visible;
|
||||||
|
public override bool HandleMouseInput => true;
|
||||||
|
|
||||||
public HitObjectMask(DrawableHitObject hitObject)
|
public HitObjectMask(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
HitObject = hitObject;
|
HitObject = hitObject;
|
||||||
|
|
||||||
|
AlwaysPresent = true;
|
||||||
State = Visibility.Hidden;
|
State = Visibility.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Selects this <see cref="HitObjectMask"/>, causing it to become visible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the <see cref="HitObjectMask"/> was selected. False if the <see cref="HitObjectMask"/> was already selected.</returns>
|
||||||
|
public bool Select()
|
||||||
|
{
|
||||||
|
if (State == Visibility.Visible)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Show();
|
||||||
|
Selected?.Invoke(this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deselects this <see cref="HitObjectMask"/>, causing it to become invisible.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if the <see cref="HitObjectMask"/> was deselected. False if the <see cref="HitObjectMask"/> was already deselected.</returns>
|
||||||
|
public bool Deselect()
|
||||||
|
{
|
||||||
|
if (State == Visibility.Hidden)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Hide();
|
||||||
|
Deselected?.Invoke(this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
|
||||||
|
{
|
||||||
|
if (HitObject.IsPresent)
|
||||||
|
{
|
||||||
|
SingleSelectionRequested?.Invoke(this);
|
||||||
|
Select();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(InputState state) => true;
|
||||||
|
|
||||||
|
protected override bool OnDrag(InputState state)
|
||||||
|
{
|
||||||
|
// Todo: Various forms of snapping
|
||||||
|
switch (HitObject.HitObject)
|
||||||
|
{
|
||||||
|
case IHasEditablePosition editablePosition:
|
||||||
|
editablePosition.OffsetPosition(state.Mouse.Delta);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragEnd(InputState state) => true;
|
||||||
|
|
||||||
protected override void PopIn() => Alpha = 1;
|
protected override void PopIn() => Alpha = 1;
|
||||||
protected override void PopOut() => Alpha = 0;
|
protected override void PopOut() => Alpha = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The screen-space point that causes this <see cref="HitObjectMask"/> to be selected.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The screen-space quad that outlines this <see cref="HitObjectMask"/> for selections.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,12 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Configuration;
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Graphics.Primitives;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using OpenTK;
|
|
||||||
using OpenTK.Graphics;
|
using OpenTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Drawables
|
namespace osu.Game.Rulesets.Objects.Drawables
|
||||||
@ -231,16 +229,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
protected virtual void CheckForJudgements(bool userTriggered, double timeOffset)
|
protected virtual void CheckForJudgements(bool userTriggered, double timeOffset)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The screen-space point that causes this <see cref="DrawableHitObject"/> to be selected in the Editor.
|
|
||||||
/// </summary>
|
|
||||||
public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The screen-space quad that outlines this <see cref="DrawableHitObject"/> for selections in the Editor.
|
|
||||||
/// </summary>
|
|
||||||
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class DrawableHitObject<TObject> : DrawableHitObject
|
public abstract class DrawableHitObject<TObject> : DrawableHitObject
|
||||||
|
97
osu.Game/Screens/Edit/Screens/Compose/Layers/DragBox.cs
Normal file
97
osu.Game/Screens/Edit/Screens/Compose/Layers/DragBox.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using OpenTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A box that represents a drag selection.
|
||||||
|
/// </summary>
|
||||||
|
public class DragBox : CompositeDrawable
|
||||||
|
{
|
||||||
|
public event Action DragEnd;
|
||||||
|
|
||||||
|
private readonly IEnumerable<HitObjectMask> hitObjectMasks;
|
||||||
|
|
||||||
|
private Drawable box;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="DragBox"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObjectMasks">The selectable <see cref="HitObjectMask"/>s.</param>
|
||||||
|
public DragBox(IEnumerable<HitObjectMask> hitObjectMasks)
|
||||||
|
{
|
||||||
|
this.hitObjectMasks = hitObjectMasks;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
AlwaysPresent = true;
|
||||||
|
Alpha = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChild = box = new Container
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
BorderColour = Color4.White,
|
||||||
|
BorderThickness = SelectionBox.BORDER_RADIUS,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0.1f
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(InputState state)
|
||||||
|
{
|
||||||
|
this.FadeIn(250, Easing.OutQuint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDrag(InputState state)
|
||||||
|
{
|
||||||
|
var dragPosition = state.Mouse.NativeState.Position;
|
||||||
|
var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition;
|
||||||
|
|
||||||
|
var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
|
||||||
|
|
||||||
|
// We use AABBFloat instead of RectangleF since it handles negative sizes for us
|
||||||
|
SetDragRectangle(dragQuad.AABBFloat);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragEnd(InputState state)
|
||||||
|
{
|
||||||
|
this.FadeOut(250, Easing.OutQuint);
|
||||||
|
DragEnd?.Invoke();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetDragRectangle(RectangleF screenSpaceRectangle)
|
||||||
|
{
|
||||||
|
var topLeft = ToLocalSpace(screenSpaceRectangle.TopLeft);
|
||||||
|
var bottomRight = ToLocalSpace(screenSpaceRectangle.BottomRight);
|
||||||
|
|
||||||
|
box.Position = topLeft;
|
||||||
|
box.Size = bottomRight - topLeft;
|
||||||
|
|
||||||
|
foreach (var mask in hitObjectMasks)
|
||||||
|
{
|
||||||
|
if (mask.IsAlive && mask.IsPresent && screenSpaceRectangle.Contains(mask.SelectionPoint))
|
||||||
|
mask.Select();
|
||||||
|
else
|
||||||
|
mask.Deselect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,12 @@
|
|||||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -17,6 +19,10 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
|||||||
private readonly HitObjectComposer composer;
|
private readonly HitObjectComposer composer;
|
||||||
private readonly Container<HitObjectMask> overlayContainer;
|
private readonly Container<HitObjectMask> overlayContainer;
|
||||||
|
|
||||||
|
private readonly SelectionBox selectionBox;
|
||||||
|
|
||||||
|
private readonly HashSet<HitObjectMask> selectedObjects = new HashSet<HitObjectMask>();
|
||||||
|
|
||||||
public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer)
|
public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer)
|
||||||
{
|
{
|
||||||
this.playfield = playfield;
|
this.playfield = playfield;
|
||||||
@ -24,7 +30,19 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
|||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChild = overlayContainer = new Container<HitObjectMask> { RelativeSizeAxes = Axes.Both };
|
overlayContainer = new Container<HitObjectMask>();
|
||||||
|
selectionBox = composer.CreateSelectionBox();
|
||||||
|
|
||||||
|
var dragBox = new DragBox(overlayContainer);
|
||||||
|
dragBox.DragEnd += () => selectionBox.FinishSelection();
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
dragBox,
|
||||||
|
overlayContainer,
|
||||||
|
selectionBox,
|
||||||
|
dragBox.CreateProxy()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -44,7 +62,12 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
|||||||
if (overlay == null)
|
if (overlay == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
overlay.Selected += onSelected;
|
||||||
|
overlay.Deselected += onDeselected;
|
||||||
|
overlay.SingleSelectionRequested += onSingleSelectionRequested;
|
||||||
|
|
||||||
overlayContainer.Add(overlay);
|
overlayContainer.Add(overlay);
|
||||||
|
selectionBox.AddMask(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -57,22 +80,29 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
|||||||
if (existing == null)
|
if (existing == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
existing.Hide();
|
existing.Selected -= onSelected;
|
||||||
existing.Expire();
|
existing.Deselected -= onDeselected;
|
||||||
|
existing.SingleSelectionRequested -= onSingleSelectionRequested;
|
||||||
|
|
||||||
|
overlayContainer.Remove(existing);
|
||||||
|
selectionBox.RemoveMask(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
private SelectionBox currentSelectionBox;
|
private void onSelected(HitObjectMask mask) => selectedObjects.Add(mask);
|
||||||
|
|
||||||
private void addSelectionBox()
|
private void onDeselected(HitObjectMask mask) => selectedObjects.Remove(mask);
|
||||||
|
|
||||||
|
private void onSingleSelectionRequested(HitObjectMask mask) => DeselectAll();
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
|
||||||
{
|
{
|
||||||
if (overlayContainer.Count > 0)
|
DeselectAll();
|
||||||
AddInternal(currentSelectionBox = composer.CreateSelectionBox(overlayContainer));
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeSelectionBox()
|
/// <summary>
|
||||||
{
|
/// Deselects all selected <see cref="DrawableHitObject"/>s.
|
||||||
currentSelectionBox?.Hide();
|
/// </summary>
|
||||||
currentSelectionBox?.Expire();
|
public void DeselectAll() => overlayContainer.ToList().ForEach(m => m.Deselect());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -19,84 +20,138 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A box which surrounds <see cref="DrawableHitObject"/>s and provides interactive handles, context menus etc.
|
/// A box which surrounds <see cref="DrawableHitObject"/>s and provides interactive handles, context menus etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SelectionBox : VisibilityContainer
|
public class SelectionBox : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly IReadOnlyList<HitObjectMask> overlays;
|
|
||||||
|
|
||||||
public const float BORDER_RADIUS = 2;
|
public const float BORDER_RADIUS = 2;
|
||||||
|
|
||||||
public SelectionBox(IReadOnlyList<HitObjectMask> overlays)
|
private readonly HashSet<HitObjectMask> selectedMasks = new HashSet<HitObjectMask>();
|
||||||
|
|
||||||
|
private Drawable box;
|
||||||
|
|
||||||
|
public SelectionBox()
|
||||||
{
|
{
|
||||||
this.overlays = overlays;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
AlwaysPresent = true;
|
||||||
Masking = true;
|
Alpha = 0;
|
||||||
BorderThickness = BORDER_RADIUS;
|
|
||||||
|
|
||||||
InternalChild = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
AlwaysPresent = true,
|
|
||||||
Alpha = 0
|
|
||||||
};
|
|
||||||
|
|
||||||
State = Visibility.Visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
BorderColour = colours.Yellow;
|
InternalChild = box = new Container
|
||||||
|
{
|
||||||
|
Masking = true,
|
||||||
|
BorderThickness = BORDER_RADIUS,
|
||||||
|
BorderColour = colours.Yellow,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
AlwaysPresent = true,
|
||||||
|
Alpha = 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMask(HitObjectMask mask)
|
||||||
|
{
|
||||||
|
mask.Selected += onSelected;
|
||||||
|
mask.Deselected += onDeselected;
|
||||||
|
mask.SingleSelectionRequested += onSingleSelectionRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveMask(HitObjectMask mask)
|
||||||
|
{
|
||||||
|
mask.Selected -= onSelected;
|
||||||
|
mask.Deselected -= onDeselected;
|
||||||
|
mask.SingleSelectionRequested -= onSingleSelectionRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSelected(HitObjectMask mask) => selectedMasks.Add(mask);
|
||||||
|
|
||||||
|
private void onDeselected(HitObjectMask mask)
|
||||||
|
{
|
||||||
|
selectedMasks.Remove(mask);
|
||||||
|
|
||||||
|
if (selectedMasks.Count == 0)
|
||||||
|
FinishSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSingleSelectionRequested(HitObjectMask mask)
|
||||||
|
{
|
||||||
|
selectedMasks.Add(mask);
|
||||||
|
FinishSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only handle clicks on the selected masks
|
||||||
|
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => selectedMasks.Any(m => m.ReceiveMouseInputAt(screenSpacePos));
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true;
|
||||||
|
|
||||||
|
protected override bool OnClick(InputState state)
|
||||||
|
{
|
||||||
|
if (state.Mouse.NativeState.PositionMouseDown == null)
|
||||||
|
throw new InvalidOperationException("Click event received without a mouse down position.");
|
||||||
|
|
||||||
|
// If the mouse has moved slightly, but hasn't been dragged, select the mask which would've handled the mouse down
|
||||||
|
selectedMasks.First(m => m.ReceiveMouseInputAt(state.Mouse.NativeState.PositionMouseDown.Value)).TriggerOnMouseDown(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragStart(InputState state) => true;
|
||||||
|
|
||||||
|
protected override bool OnDrag(InputState state)
|
||||||
|
{
|
||||||
|
// Todo: Various forms of snapping
|
||||||
|
|
||||||
|
foreach (var mask in selectedMasks)
|
||||||
|
{
|
||||||
|
switch (mask.HitObject)
|
||||||
|
{
|
||||||
|
case IHasEditablePosition editablePosition:
|
||||||
|
editablePosition.OffsetPosition(state.Mouse.Delta);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnDragEnd(InputState state) => true;
|
||||||
|
|
||||||
|
public void FinishSelection()
|
||||||
|
{
|
||||||
|
if (selectedMasks.Count > 0)
|
||||||
|
Show();
|
||||||
|
else
|
||||||
|
Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
|
if (selectedMasks.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// Todo: We might need to optimise this
|
// Todo: We might need to optimise this
|
||||||
|
|
||||||
// Move the rectangle to cover the hitobjects
|
// Move the rectangle to cover the hitobjects
|
||||||
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
|
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
|
||||||
var bottomRight = new Vector2(float.MinValue, float.MinValue);
|
var bottomRight = new Vector2(float.MinValue, float.MinValue);
|
||||||
|
|
||||||
foreach (var obj in overlays)
|
bool hasSelection = false;
|
||||||
|
|
||||||
|
foreach (var mask in selectedMasks)
|
||||||
{
|
{
|
||||||
topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.TopLeft));
|
topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(mask.SelectionQuad.TopLeft));
|
||||||
bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.BottomRight));
|
bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(mask.SelectionQuad.BottomRight));
|
||||||
}
|
}
|
||||||
|
|
||||||
topLeft -= new Vector2(5);
|
topLeft -= new Vector2(5);
|
||||||
bottomRight += new Vector2(5);
|
bottomRight += new Vector2(5);
|
||||||
|
|
||||||
Size = bottomRight - topLeft;
|
box.Size = bottomRight - topLeft;
|
||||||
Position = topLeft;
|
box.Position = topLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => overlays.Any(o => o.ReceiveMouseInputAt(screenSpacePos));
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true;
|
|
||||||
|
|
||||||
protected override bool OnDragStart(InputState state) => true;
|
|
||||||
|
|
||||||
protected override bool OnDrag(InputState state)
|
|
||||||
{
|
|
||||||
// Todo: Various forms of snapping
|
|
||||||
foreach (var hitObject in overlays.Select(o => o.HitObject.HitObject))
|
|
||||||
{
|
|
||||||
switch (hitObject)
|
|
||||||
{
|
|
||||||
case IHasEditablePosition editablePosition:
|
|
||||||
editablePosition.OffsetPosition(state.Mouse.Delta);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDragEnd(InputState state) => true;
|
|
||||||
|
|
||||||
public override bool DisposeOnDeathRemoval => true;
|
|
||||||
|
|
||||||
protected override void PopIn() => this.FadeIn();
|
|
||||||
protected override void PopOut() => this.FadeOut();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,240 +0,0 @@
|
|||||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Primitives;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Input;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
|
||||||
using osu.Game.Rulesets.UI;
|
|
||||||
using OpenTK;
|
|
||||||
using OpenTK.Graphics;
|
|
||||||
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Screens.Compose.Layers
|
|
||||||
{
|
|
||||||
public class SelectionLayer : CompositeDrawable
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when a <see cref="DrawableHitObject"/> is selected.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<DrawableHitObject> ObjectSelected;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when a <see cref="DrawableHitObject"/> is deselected.
|
|
||||||
/// </summary>
|
|
||||||
public event Action<DrawableHitObject> ObjectDeselected;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when the selection has been cleared.
|
|
||||||
/// </summary>
|
|
||||||
public event Action SelectionCleared;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked when the user has finished selecting all <see cref="DrawableHitObject"/>s.
|
|
||||||
/// </summary>
|
|
||||||
public event Action SelectionFinished;
|
|
||||||
|
|
||||||
private readonly Playfield playfield;
|
|
||||||
|
|
||||||
public SelectionLayer(Playfield playfield)
|
|
||||||
{
|
|
||||||
this.playfield = playfield;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DragBox dragBox;
|
|
||||||
|
|
||||||
private readonly HashSet<DrawableHitObject> selectedHitObjects = new HashSet<DrawableHitObject>();
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
|
|
||||||
{
|
|
||||||
DeselectAll();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDragStart(InputState state)
|
|
||||||
{
|
|
||||||
AddInternal(dragBox = new DragBox());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDrag(InputState state)
|
|
||||||
{
|
|
||||||
dragBox.Show();
|
|
||||||
|
|
||||||
var dragPosition = state.Mouse.NativeState.Position;
|
|
||||||
var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition;
|
|
||||||
|
|
||||||
var screenSpaceDragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y);
|
|
||||||
|
|
||||||
dragBox.SetDragRectangle(screenSpaceDragQuad.AABBFloat);
|
|
||||||
selectQuad(screenSpaceDragQuad);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDragEnd(InputState state)
|
|
||||||
{
|
|
||||||
dragBox.Hide();
|
|
||||||
dragBox.Expire();
|
|
||||||
|
|
||||||
finishSelection();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnClick(InputState state)
|
|
||||||
{
|
|
||||||
selectPoint(state.Mouse.NativeState.Position);
|
|
||||||
finishSelection();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects a <see cref="DrawableHitObject"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to select.</param>
|
|
||||||
public void Select(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
if (!select(hitObject))
|
|
||||||
return;
|
|
||||||
|
|
||||||
clearSelection();
|
|
||||||
finishSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects a <see cref="DrawableHitObject"/> without performing capture updates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to select.</param>
|
|
||||||
/// <returns>Whether <paramref name="hitObject"/> was selected.</returns>
|
|
||||||
private bool select(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
if (!selectedHitObjects.Add(hitObject))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ObjectSelected?.Invoke(hitObject);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deselects a <see cref="DrawableHitObject"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to deselect.</param>
|
|
||||||
public void Deselect(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
if (!deselect(hitObject))
|
|
||||||
return;
|
|
||||||
|
|
||||||
clearSelection();
|
|
||||||
finishSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deselects a <see cref="DrawableHitObject"/> without performing capture updates.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to deselect.</param>
|
|
||||||
/// <returns>Whether the <see cref="DrawableHitObject"/> was deselected.</returns>
|
|
||||||
private bool deselect(DrawableHitObject hitObject)
|
|
||||||
{
|
|
||||||
if (!selectedHitObjects.Remove(hitObject))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
ObjectDeselected?.Invoke(hitObject);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deselects all selected <see cref="DrawableHitObject"/>s.
|
|
||||||
/// </summary>
|
|
||||||
public void DeselectAll()
|
|
||||||
{
|
|
||||||
selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h));
|
|
||||||
selectedHitObjects.Clear();
|
|
||||||
|
|
||||||
clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects all hitobjects that are present within the area of a <see cref="Quad"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="screenSpaceQuad">The selection <see cref="Quad"/>.</param>
|
|
||||||
// Todo: If needed we can severely reduce allocations in this method
|
|
||||||
private void selectQuad(Quad screenSpaceQuad)
|
|
||||||
{
|
|
||||||
var expectedSelection = playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint)).ToList();
|
|
||||||
|
|
||||||
var toRemove = selectedHitObjects.Except(expectedSelection).ToList();
|
|
||||||
foreach (var obj in toRemove)
|
|
||||||
deselect(obj);
|
|
||||||
|
|
||||||
expectedSelection.ForEach(h => select(h));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Selects the top-most hitobject that is present under a specific point.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="screenSpacePoint">The <see cref="Vector2"/> to select at.</param>
|
|
||||||
private void selectPoint(Vector2 screenSpacePoint)
|
|
||||||
{
|
|
||||||
var target = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint));
|
|
||||||
if (target == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
select(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearSelection() => SelectionCleared?.Invoke();
|
|
||||||
|
|
||||||
private void finishSelection()
|
|
||||||
{
|
|
||||||
if (selectedHitObjects.Count == 0)
|
|
||||||
return;
|
|
||||||
SelectionFinished?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A box that represents a drag selection.
|
|
||||||
/// </summary>
|
|
||||||
private class DragBox : VisibilityContainer
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new <see cref="DragBox"/>.
|
|
||||||
/// </summary>
|
|
||||||
public DragBox()
|
|
||||||
{
|
|
||||||
Masking = true;
|
|
||||||
BorderColour = Color4.White;
|
|
||||||
BorderThickness = SelectionBox.BORDER_RADIUS;
|
|
||||||
|
|
||||||
Child = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Alpha = 0.1f
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetDragRectangle(RectangleF rectangle)
|
|
||||||
{
|
|
||||||
var topLeft = Parent.ToLocalSpace(rectangle.TopLeft);
|
|
||||||
var bottomRight = Parent.ToLocalSpace(rectangle.BottomRight);
|
|
||||||
|
|
||||||
Position = topLeft;
|
|
||||||
Size = bottomRight - topLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool DisposeOnDeathRemoval => true;
|
|
||||||
|
|
||||||
protected override void PopIn() => this.FadeIn(250, Easing.OutQuint);
|
|
||||||
protected override void PopOut() => this.FadeOut(250, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user