From 6d4f94756e339f1c06d11f3ba077b6aae006394d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 29 Mar 2018 22:06:45 +0900 Subject: [PATCH] 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. --- .../Selection/Overlays/HitCircleMask.cs | 2 + .../Layers/Selection/Overlays/SliderMask.cs | 4 + .../Objects/Drawables/DrawableSlider.cs | 4 - .../Visual/TestCaseEditorSelectionLayer.cs | 1 - osu.Game/Rulesets/Edit/HitObjectComposer.cs | 14 +- osu.Game/Rulesets/Edit/HitObjectMask.cs | 80 ++++++ .../Objects/Drawables/DrawableHitObject.cs | 12 - .../Edit/Screens/Compose/Layers/DragBox.cs | 97 +++++++ .../Compose/Layers/HitObjectMaskLayer.cs | 54 +++- .../Screens/Compose/Layers/SelectionBox.cs | 157 ++++++++---- .../Screens/Compose/Layers/SelectionLayer.cs | 240 ------------------ 11 files changed, 334 insertions(+), 331 deletions(-) create mode 100644 osu.Game/Screens/Edit/Screens/Compose/Layers/DragBox.cs delete mode 100644 osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs index b48dd73bb5..89a7686581 100644 --- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays Size = hitCircle.Size; Scale = hitCircle.Scale; + CornerRadius = Size.X / 2; + AddInternal(new RingPiece()); hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs index 53f02617cd..629bce1847 100644 --- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; 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 Vector2 SelectionPoint => ToScreenSpace(OriginPosition); + public override Quad SelectionQuad => body.PathDrawQuad; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3872821b96..5373926138 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -10,7 +10,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Judgements; -using osu.Framework.Graphics.Primitives; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using OpenTK.Graphics; @@ -177,8 +176,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => HeadCircle.ApproachCircle; public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos); - - public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition); - public override Quad SelectionQuad => Body.PathDrawQuad; } } diff --git a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs index d56417f144..4e39548b5b 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs @@ -25,7 +25,6 @@ namespace osu.Game.Tests.Visual { public override IReadOnlyList RequiredTypes => new[] { - typeof(SelectionLayer), typeof(SelectionBox), typeof(HitObjectComposer), typeof(OsuHitObjectComposer), diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 93a8980aa7..1a587bf8f5 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -65,9 +65,6 @@ namespace osu.Game.Rulesets.Edit return; } - HitObjectMaskLayer hitObjectMaskLayer = new HitObjectMaskLayer(rulesetContainer.Playfield, this); - SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield); - var layerBelowRuleset = new BorderLayer { RelativeSizeAxes = Axes.Both, @@ -75,12 +72,7 @@ namespace osu.Game.Rulesets.Edit }; var layerAboveRuleset = CreateLayerContainer(); - layerAboveRuleset.Children = new Drawable[] - { - selectionLayer, // Below object overlays for input - hitObjectMaskLayer, - selectionLayer.CreateProxy() // Proxy above object overlays for selections - }; + layerAboveRuleset.Child = new HitObjectMaskLayer(rulesetContainer.Playfield, this); layerContainers.Add(layerBelowRuleset); layerContainers.Add(layerAboveRuleset); @@ -259,10 +251,10 @@ namespace osu.Game.Rulesets.Edit /// /// Creates a which outlines s - /// and handles all hitobject movement/pattern adjustments. + /// and handles hitobject pattern adjustments. /// /// The overlays. - public virtual SelectionBox CreateSelectionBox(IReadOnlyList overlays) => new SelectionBox(overlays); + public virtual SelectionBox CreateSelectionBox() => new SelectionBox(); /// /// Creates a which provides a layer above or below the . diff --git a/osu.Game/Rulesets/Edit/HitObjectMask.cs b/osu.Game/Rulesets/Edit/HitObjectMask.cs index e79e540bce..44ee981783 100644 --- a/osu.Game/Rulesets/Edit/HitObjectMask.cs +++ b/osu.Game/Rulesets/Edit/HitObjectMask.cs @@ -1,8 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; 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 OpenTK; namespace osu.Game.Rulesets.Edit { @@ -11,15 +16,90 @@ namespace osu.Game.Rulesets.Edit /// public class HitObjectMask : VisibilityContainer { + public event Action Selected; + public event Action Deselected; + public event Action SingleSelectionRequested; + public readonly DrawableHitObject HitObject; + protected override bool ShouldBeAlive => HitObject.IsAlive || State == Visibility.Visible; + public override bool HandleMouseInput => true; + public HitObjectMask(DrawableHitObject hitObject) { HitObject = hitObject; + + AlwaysPresent = true; State = Visibility.Hidden; } + /// + /// Selects this , causing it to become visible. + /// + /// True if the was selected. False if the was already selected. + public bool Select() + { + if (State == Visibility.Visible) + return false; + + Show(); + Selected?.Invoke(this); + return true; + } + + /// + /// Deselects this , causing it to become invisible. + /// + /// True if the was deselected. False if the was already deselected. + 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 PopOut() => Alpha = 0; + + /// + /// The screen-space point that causes this to be selected. + /// + public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; + + /// + /// The screen-space quad that outlines this for selections. + /// + public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 348364a2bf..fdfef14a88 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -6,14 +6,12 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Graphics.Primitives; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using OpenTK; using OpenTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -231,16 +229,6 @@ namespace osu.Game.Rulesets.Objects.Drawables protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) { } - - /// - /// The screen-space point that causes this to be selected in the Editor. - /// - public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; - - /// - /// The screen-space quad that outlines this for selections in the Editor. - /// - public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } public abstract class DrawableHitObject : DrawableHitObject diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/DragBox.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/DragBox.cs new file mode 100644 index 0000000000..f70696f0f1 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/DragBox.cs @@ -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 +{ + /// + /// A box that represents a drag selection. + /// + public class DragBox : CompositeDrawable + { + public event Action DragEnd; + + private readonly IEnumerable hitObjectMasks; + + private Drawable box; + + /// + /// Creates a new . + /// + /// The selectable s. + public DragBox(IEnumerable 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(); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs index 1412d98f31..ac7ba76220 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -17,6 +19,10 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers private readonly HitObjectComposer composer; private readonly Container overlayContainer; + private readonly SelectionBox selectionBox; + + private readonly HashSet selectedObjects = new HashSet(); + public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer) { this.playfield = playfield; @@ -24,7 +30,19 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers RelativeSizeAxes = Axes.Both; - InternalChild = overlayContainer = new Container { RelativeSizeAxes = Axes.Both }; + overlayContainer = new Container(); + selectionBox = composer.CreateSelectionBox(); + + var dragBox = new DragBox(overlayContainer); + dragBox.DragEnd += () => selectionBox.FinishSelection(); + + InternalChildren = new Drawable[] + { + dragBox, + overlayContainer, + selectionBox, + dragBox.CreateProxy() + }; } [BackgroundDependencyLoader] @@ -44,7 +62,12 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers if (overlay == null) return; + overlay.Selected += onSelected; + overlay.Deselected += onDeselected; + overlay.SingleSelectionRequested += onSingleSelectionRequested; + overlayContainer.Add(overlay); + selectionBox.AddMask(overlay); } /// @@ -57,22 +80,29 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers if (existing == null) return; - existing.Hide(); - existing.Expire(); + existing.Selected -= onSelected; + 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) - AddInternal(currentSelectionBox = composer.CreateSelectionBox(overlayContainer)); + DeselectAll(); + return true; } - private void removeSelectionBox() - { - currentSelectionBox?.Hide(); - currentSelectionBox?.Expire(); - } + /// + /// Deselects all selected s. + /// + public void DeselectAll() => overlayContainer.ToList().ForEach(m => m.Deselect()); } } diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs index 0e5d824559..8249c08a7a 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // 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.Allocation; @@ -19,84 +20,138 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers /// /// A box which surrounds s and provides interactive handles, context menus etc. /// - public class SelectionBox : VisibilityContainer + public class SelectionBox : CompositeDrawable { - private readonly IReadOnlyList overlays; - public const float BORDER_RADIUS = 2; - public SelectionBox(IReadOnlyList overlays) + private readonly HashSet selectedMasks = new HashSet(); + + private Drawable box; + + public SelectionBox() { - this.overlays = overlays; - - Masking = true; - BorderThickness = BORDER_RADIUS; - - InternalChild = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 - }; - - State = Visibility.Visible; + RelativeSizeAxes = Axes.Both; + AlwaysPresent = true; + Alpha = 0; } [BackgroundDependencyLoader] 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() { base.Update(); + if (selectedMasks.Count == 0) + return; + // Todo: We might need to optimise this // Move the rectangle to cover the hitobjects var topLeft = new Vector2(float.MaxValue, float.MaxValue); 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)); - bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.BottomRight)); + topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(mask.SelectionQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(mask.SelectionQuad.BottomRight)); } topLeft -= new Vector2(5); bottomRight += new Vector2(5); - Size = bottomRight - topLeft; - Position = topLeft; + box.Size = bottomRight - 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(); } } diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs deleted file mode 100644 index ab51385980..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// 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 - { - /// - /// Invoked when a is selected. - /// - public event Action ObjectSelected; - - /// - /// Invoked when a is deselected. - /// - public event Action ObjectDeselected; - - /// - /// Invoked when the selection has been cleared. - /// - public event Action SelectionCleared; - - /// - /// Invoked when the user has finished selecting all s. - /// - public event Action SelectionFinished; - - private readonly Playfield playfield; - - public SelectionLayer(Playfield playfield) - { - this.playfield = playfield; - - RelativeSizeAxes = Axes.Both; - } - - private DragBox dragBox; - - private readonly HashSet selectedHitObjects = new HashSet(); - - 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; - } - - /// - /// Selects a . - /// - /// The to select. - public void Select(DrawableHitObject hitObject) - { - if (!select(hitObject)) - return; - - clearSelection(); - finishSelection(); - } - - /// - /// Selects a without performing capture updates. - /// - /// The to select. - /// Whether was selected. - private bool select(DrawableHitObject hitObject) - { - if (!selectedHitObjects.Add(hitObject)) - return false; - - ObjectSelected?.Invoke(hitObject); - return true; - } - - /// - /// Deselects a . - /// - /// The to deselect. - public void Deselect(DrawableHitObject hitObject) - { - if (!deselect(hitObject)) - return; - - clearSelection(); - finishSelection(); - } - - /// - /// Deselects a without performing capture updates. - /// - /// The to deselect. - /// Whether the was deselected. - private bool deselect(DrawableHitObject hitObject) - { - if (!selectedHitObjects.Remove(hitObject)) - return false; - - ObjectDeselected?.Invoke(hitObject); - return true; - } - - /// - /// Deselects all selected s. - /// - public void DeselectAll() - { - selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h)); - selectedHitObjects.Clear(); - - clearSelection(); - } - - /// - /// Selects all hitobjects that are present within the area of a . - /// - /// The selection . - // 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)); - } - - /// - /// Selects the top-most hitobject that is present under a specific point. - /// - /// The to select at. - 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(); - } - - /// - /// A box that represents a drag selection. - /// - private class DragBox : VisibilityContainer - { - /// - /// Creates a new . - /// - 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); - } - } -}