From 8d29e9e76bc781d8814e70a7a1f0317cfe7a2920 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 20:23:59 +0900 Subject: [PATCH 01/12] Move selection logic from DragBox to BlueprintContainer --- .../Compose/Components/BlueprintContainer.cs | 49 +++++++------------ .../Edit/Compose/Components/DragBox.cs | 46 +++-------------- .../Timeline/TimelineBlueprintContainer.cs | 12 +++-- .../Components/Timeline/TimelineDragBox.cs | 16 +----- 4 files changed, 34 insertions(+), 89 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8b38d9c612..787959d214 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; @@ -13,11 +12,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osuTK; using osuTK.Input; @@ -79,7 +76,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AddRangeInternal(new[] { - DragBox = CreateDragBox(selectBlueprintsFromDragRectangle), + DragBox = CreateDragBox(), SelectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), SelectionHandler.CreateProxy(), @@ -101,7 +98,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [CanBeNull] protected virtual SelectionBlueprint CreateBlueprintFor(T item) => null; - protected virtual DragBox CreateDragBox(Action performSelect) => new DragBox(performSelect); + protected virtual DragBox CreateDragBox() => new DragBox(); /// /// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to. @@ -183,13 +180,9 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - if (DragBox.HandleDrag(e)) - { - DragBox.Show(); - return true; - } - - return false; + DragBox.HandleDrag(e); + DragBox.Show(); + return true; } protected override void OnDrag(DragEvent e) @@ -198,7 +191,10 @@ namespace osu.Game.Screens.Edit.Compose.Components return; if (DragBox.State == Visibility.Visible) + { DragBox.HandleDrag(e); + UpdateSelectionFromDragBox(); + } moveCurrentSelection(e); } @@ -214,8 +210,7 @@ namespace osu.Game.Screens.Edit.Compose.Components changeHandler?.EndChange(); } - if (DragBox.State == Visibility.Visible) - DragBox.Hide(); + DragBox.Hide(); } /// @@ -380,28 +375,20 @@ namespace osu.Game.Screens.Edit.Compose.Components } /// - /// Select all masks in a given rectangle selection area. + /// Select all blueprints in a selection area specified by . /// - /// The rectangle to perform a selection on in screen-space coordinates. - private void selectBlueprintsFromDragRectangle(RectangleF rect) + protected virtual void UpdateSelectionFromDragBox() { + var quad = DragBox.Box.ScreenSpaceDrawQuad; + foreach (var blueprint in SelectionBlueprints) { - // only run when utmost necessary to avoid unnecessary rect computations. - bool isValidForSelection() => blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint); + if (blueprint.IsSelected && !AllowDeselectionDuringDrag) + continue; - switch (blueprint.State) - { - case SelectionState.NotSelected: - if (isValidForSelection()) - blueprint.Select(); - break; - - case SelectionState.Selected: - if (AllowDeselectionDuringDrag && !isValidForSelection()) - blueprint.Deselect(); - break; - } + bool shouldBeSelected = blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint); + if (blueprint.IsSelected != shouldBeSelected) + blueprint.ToggleSelection(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 838562719d..905d47533a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -8,7 +8,6 @@ using osu.Framework; 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.Events; using osu.Framework.Layout; @@ -21,18 +20,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class DragBox : CompositeDrawable, IStateful { - protected readonly Action PerformSelection; - - protected Drawable Box; + public Drawable Box { get; private set; } /// /// Creates a new . /// - /// A delegate that performs drag selection. - public DragBox(Action performSelection) + public DragBox() { - PerformSelection = performSelection; - RelativeSizeAxes = Axes.Both; AlwaysPresent = true; Alpha = 0; @@ -46,30 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual Drawable CreateBox() => new BoxWithBorders(); - private RectangleF? dragRectangle; - /// /// Handle a forwarded mouse event. /// /// The mouse event. - /// Whether the event should be handled and blocking. - public virtual bool HandleDrag(MouseButtonEvent e) + public virtual void HandleDrag(MouseButtonEvent e) { - var dragPosition = e.ScreenSpaceMousePosition; - var dragStartPosition = e.ScreenSpaceMouseDownPosition; - - 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 - var rec = dragQuad.AABBFloat; - dragRectangle = rec; - - var topLeft = ToLocalSpace(rec.TopLeft); - var bottomRight = ToLocalSpace(rec.BottomRight); - - Box.Position = topLeft; - Box.Size = bottomRight - topLeft; - return true; + Box.Position = Vector2.ComponentMin(e.MouseDownPosition, e.MousePosition); + Box.Size = Vector2.ComponentMax(e.MouseDownPosition, e.MousePosition) - Box.Position; } private Visibility state; @@ -87,19 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - protected override void Update() - { - base.Update(); - - if (dragRectangle != null) - PerformSelection?.Invoke(dragRectangle.Value); - } - - public override void Hide() - { - State = Visibility.Hidden; - dragRectangle = null; - } + public override void Hide() => State = Visibility.Hidden; public override void Show() => State = Visibility.Visible; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 590f92d281..da80ed5ad6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -13,7 +13,6 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -65,7 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override void LoadComplete() { base.LoadComplete(); - DragBox.Alpha = 0; placement = Beatmap.PlacementObject.GetBoundCopy(); placement.ValueChanged += placementChanged; @@ -93,6 +91,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Container> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override bool OnDragStart(DragStartEvent e) + { + if (!base.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition)) + return false; + + return base.OnDragStart(e); + } + protected override void OnDrag(DragEvent e) { handleScrollViaDrag(e); @@ -169,7 +175,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }; } - protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); + protected override DragBox CreateDragBox() => new TimelineDragBox(); private void handleScrollViaDrag(DragEvent e) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs index c026c169d6..8b901c8958 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs @@ -6,7 +6,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -24,24 +23,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private Timeline timeline { get; set; } - public TimelineDragBox(Action performSelect) - : base(performSelect) - { - } - protected override Drawable CreateBox() => new Box { RelativeSizeAxes = Axes.Y, Alpha = 0.3f }; - public override bool HandleDrag(MouseButtonEvent e) + public override void HandleDrag(MouseButtonEvent e) { - // The dragbox should only be active if the mouseDownPosition.Y is within this drawable's bounds. - float localY = ToLocalSpace(e.ScreenSpaceMouseDownPosition).Y; - if (DrawRectangle.Top > localY || DrawRectangle.Bottom < localY) - return false; - selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom; // only calculate end when a transition is not in progress to avoid bouncing. @@ -49,7 +38,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline selectionEnd = e.MousePosition.X / timeline.CurrentZoom; updateDragBoxPosition(); - return true; } private void updateDragBoxPosition() @@ -68,8 +56,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment. boxScreenRect.Y -= boxScreenRect.Height; boxScreenRect.Height *= 2; - - PerformSelection?.Invoke(boxScreenRect); } public override void Hide() From 0ffde02f79e8d67538e941e312abfa1611e5e712 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 20:43:02 +0900 Subject: [PATCH 02/12] Use hit object time for timeline selection --- .../Compose/Components/Timeline/Timeline.cs | 18 ++++++-- .../Timeline/TimelineBlueprintContainer.cs | 24 ++++++++++- .../Components/Timeline/TimelineDragBox.cs | 42 ++++++------------- 3 files changed, 49 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 721f0c4e3b..a73ada76f5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -304,10 +304,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => editorClock.TrackLength / Zoom; - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) => - new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); + public double TimeAtPosition(float x) + { + return x / Content.DrawWidth * editorClock.TrackLength; + } - private double getTimeFromPosition(Vector2 localPosition) => - (localPosition.X / Content.DrawWidth) * editorClock.TrackLength; + public float PositionAtTime(double time) + { + return (float)(time / editorClock.TrackLength * Content.DrawWidth); + } + + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) + { + double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X); + return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time)); + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index da80ed5ad6..05897e6d97 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -175,7 +175,29 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }; } - protected override DragBox CreateDragBox() => new TimelineDragBox(); + protected sealed override DragBox CreateDragBox() => new TimelineDragBox(); + + protected override void UpdateSelectionFromDragBox() + { + var dragBox = (TimelineDragBox)DragBox; + double minTime = dragBox.MinTime; + double maxTime = dragBox.MaxTime; + Console.WriteLine($"{minTime}, {maxTime}"); + + // TODO: performance + foreach (var hitObject in Beatmap.HitObjects) + { + bool shouldBeSelected = minTime <= hitObject.StartTime && hitObject.StartTime <= maxTime; + bool isSelected = SelectedItems.Contains(hitObject); + if (isSelected != shouldBeSelected) + { + if (!isSelected) + SelectedItems.Add(hitObject); + else + SelectedItems.Remove(hitObject); + } + } + } private void handleScrollViaDrag(DragEvent e) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs index 8b901c8958..4b16848c58 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs @@ -14,11 +14,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineDragBox : DragBox { - // the following values hold the start and end X positions of the drag box in the timeline's local space, - // but with zoom unapplied in order to be able to compensate for positional changes - // while the timeline is being zoomed in/out. - private float? selectionStart; - private float selectionEnd; + public double MinTime => Math.Min(startTime.Value, endTime); + + public double MaxTime => Math.Max(startTime.Value, endTime); + + private double? startTime; + + private double endTime; [Resolved] private Timeline timeline { get; set; } @@ -31,37 +33,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override void HandleDrag(MouseButtonEvent e) { - selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom; + startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X); + endTime = timeline.TimeAtPosition(e.MousePosition.X); - // only calculate end when a transition is not in progress to avoid bouncing. - if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom)) - selectionEnd = e.MousePosition.X / timeline.CurrentZoom; - - updateDragBoxPosition(); - } - - private void updateDragBoxPosition() - { - if (selectionStart == null) - return; - - float rescaledStart = selectionStart.Value * timeline.CurrentZoom; - float rescaledEnd = selectionEnd * timeline.CurrentZoom; - - Box.X = Math.Min(rescaledStart, rescaledEnd); - Box.Width = Math.Abs(rescaledStart - rescaledEnd); - - var boxScreenRect = Box.ScreenSpaceDrawQuad.AABBFloat; - - // we don't care about where the hitobjects are vertically. in cases like stacking display, they may be outside the box without this adjustment. - boxScreenRect.Y -= boxScreenRect.Height; - boxScreenRect.Height *= 2; + Box.X = timeline.PositionAtTime(MinTime); + Box.Width = timeline.PositionAtTime(MaxTime) - Box.X; } public override void Hide() { base.Hide(); - selectionStart = null; + startTime = null; } } } From 0613388aaa8bb5ae656d1237816e898c5f59b855 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 20:48:35 +0900 Subject: [PATCH 03/12] Make sure all selected items get deleted --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 8419d3b380..269c19f846 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -305,7 +305,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected void DeleteSelected() { - DeleteItems(selectedBlueprints.Select(b => b.Item)); + DeleteItems(SelectedItems.ToArray()); } #endregion diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 16c0064e80..839535b99f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -352,6 +352,8 @@ namespace osu.Game.Screens.Edit var updates = batchPendingUpdates.ToArray(); batchPendingUpdates.Clear(); + foreach (var h in deletes) SelectedHitObjects.Remove(h); + foreach (var h in deletes) HitObjectRemoved?.Invoke(h); foreach (var h in inserts) HitObjectAdded?.Invoke(h); foreach (var h in updates) HitObjectUpdated?.Invoke(h); From 00b3d97f69abaaed8a29cca1770aca91966e6fc6 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 21:26:00 +0900 Subject: [PATCH 04/12] Improve timeline selection performance But selecting a large number of hit objects is still very slow because all DHOs must be added and also `AddBlueprintFor` has quadratic behaviors --- .../Compose/Components/BlueprintContainer.cs | 10 ++++++++-- .../Timeline/TimelineBlueprintContainer.cs | 18 ++++-------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 787959d214..da7a8e662b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -58,13 +58,19 @@ namespace osu.Game.Screens.Edit.Compose.Components { case NotifyCollectionChangedAction.Add: foreach (object o in args.NewItems) - SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select(); + { + if (blueprintMap.TryGetValue((T)o, out var blueprint)) + blueprint.Select(); + } break; case NotifyCollectionChangedAction.Remove: foreach (object o in args.OldItems) - SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect(); + { + if (blueprintMap.TryGetValue((T)o, out var blueprint)) + blueprint.Deselect(); + } break; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 05897e6d97..2d6dc797ca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -182,21 +182,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var dragBox = (TimelineDragBox)DragBox; double minTime = dragBox.MinTime; double maxTime = dragBox.MaxTime; - Console.WriteLine($"{minTime}, {maxTime}"); - // TODO: performance - foreach (var hitObject in Beatmap.HitObjects) - { - bool shouldBeSelected = minTime <= hitObject.StartTime && hitObject.StartTime <= maxTime; - bool isSelected = SelectedItems.Contains(hitObject); - if (isSelected != shouldBeSelected) - { - if (!isSelected) - SelectedItems.Add(hitObject); - else - SelectedItems.Remove(hitObject); - } - } + SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject)); + SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).Where(hitObject => shouldBeSelected(hitObject))); + + bool shouldBeSelected(HitObject hitObject) => minTime <= hitObject.StartTime && hitObject.StartTime <= maxTime; } private void handleScrollViaDrag(DragEvent e) From 3108c42ecea1e08ac5a66e98ab7f798c42757fd6 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 22:04:43 +0900 Subject: [PATCH 05/12] Fix inspect issues --- .../Timeline/TimelineBlueprintContainer.cs | 3 +-- .../Compose/Components/Timeline/TimelineDragBox.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2d6dc797ca..04575e55b1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -3,7 +3,6 @@ #nullable disable -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -184,7 +183,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double maxTime = dragBox.MaxTime; SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject)); - SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).Where(hitObject => shouldBeSelected(hitObject))); + SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected)); bool shouldBeSelected(HitObject hitObject) => minTime <= hitObject.StartTime && hitObject.StartTime <= maxTime; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs index 4b16848c58..65d9293b7e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineDragBox.cs @@ -8,20 +8,17 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineDragBox : DragBox { - public double MinTime => Math.Min(startTime.Value, endTime); + public double MinTime { get; private set; } - public double MaxTime => Math.Max(startTime.Value, endTime); + public double MaxTime { get; private set; } private double? startTime; - private double endTime; - [Resolved] private Timeline timeline { get; set; } @@ -34,7 +31,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override void HandleDrag(MouseButtonEvent e) { startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X); - endTime = timeline.TimeAtPosition(e.MousePosition.X); + double endTime = timeline.TimeAtPosition(e.MousePosition.X); + + MinTime = Math.Min(startTime.Value, endTime); + MaxTime = Math.Max(startTime.Value, endTime); Box.X = timeline.PositionAtTime(MinTime); Box.Width = timeline.PositionAtTime(MaxTime) - Box.X; From 6753f6b01ab353baaa6575400d074292e2831705 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 22:14:11 +0900 Subject: [PATCH 06/12] Move `AllowDeselectionDuringDrag` down Because it is now ignored in the timeline implementation anyways --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 ++ .../Screens/Edit/Compose/Components/EditorBlueprintContainer.cs | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 4c37d200bc..43ead88d54 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -83,6 +83,8 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning; + protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject) { base.TransferBlueprintFor(hitObject, drawableObject); diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 6a4fe27f04..879ac58887 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -66,8 +66,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override IEnumerable> SortForMovement(IReadOnlyList> blueprints) => blueprints.OrderBy(b => b.Item.StartTime); - protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning; - protected override bool ApplySnapResult(SelectionBlueprint[] blueprints, SnapResult result) { if (!base.ApplySnapResult(blueprints, result)) From b0213c29e98b3b59efdb16c3ff6457c592f5385b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 22:19:22 +0900 Subject: [PATCH 07/12] Use mid time instead of start time It is closer to the old blueprint-based behavior --- .../Components/Timeline/TimelineBlueprintContainer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 04575e55b1..08682ae05f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -185,7 +185,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject)); SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected)); - bool shouldBeSelected(HitObject hitObject) => minTime <= hitObject.StartTime && hitObject.StartTime <= maxTime; + bool shouldBeSelected(HitObject hitObject) + { + double midTime = (hitObject.StartTime + hitObject.GetEndTime()) / 2; + return minTime <= midTime && midTime <= maxTime; + } } private void handleScrollViaDrag(DragEvent e) From 2a7476cc4a820015898ac9104bacde3e0dc8e10d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 5 Oct 2022 23:29:45 +0900 Subject: [PATCH 08/12] Add test for timeline drag selection --- .../Editing/TestSceneTimelineSelection.cs | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 7e0981ce69..a4f4c375bf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -29,16 +29,18 @@ namespace osu.Game.Tests.Visual.Editing private TimelineBlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); + private Vector2 getPosition(HitObject hitObject) => + blueprintContainer.SelectionBlueprints.First(s => s.Item == hitObject).ScreenSpaceDrawQuad.Centre; + + private Vector2 getMiddlePosition(HitObject hitObject1, HitObject hitObject2) => + (getPosition(hitObject1) + getPosition(hitObject2)) / 2; + private void moveMouseToObject(Func targetFunc) { AddStep("move mouse to object", () => { - var pos = blueprintContainer.SelectionBlueprints - .First(s => s.Item == targetFunc()) - .ChildrenOfType() - .First().ScreenSpaceDrawQuad.Centre; - - InputManager.MoveMouseTo(pos); + var hitObject = targetFunc(); + InputManager.MoveMouseTo(getPosition(hitObject)); }); } @@ -262,6 +264,55 @@ namespace osu.Game.Tests.Visual.Editing AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } + [Test] + public void TestBasicDragSelection() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 500, Position = new Vector2(100) }, + new HitCircle { StartTime = 1000, Position = new Vector2(200) }, + new HitCircle { StartTime = 1500, Position = new Vector2(300) }, + }; + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1]))); + AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("drag to select", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[2], addedObjects[3]))); + assertSelectionIs(new[] { addedObjects[1], addedObjects[2] }); + + AddStep("drag to deselect", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[1], addedObjects[2]))); + assertSelectionIs(new[] { addedObjects[1] }); + + AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left)); + assertSelectionIs(new[] { addedObjects[1] }); + } + + [Test] + public void TestFastDragSelection() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 20000, Position = new Vector2(100) }, + new HitCircle { StartTime = 31000, Position = new Vector2(200) }, + new HitCircle { StartTime = 60000, Position = new Vector2(300) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1]))); + AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left)); + AddStep("start drag", () => InputManager.MoveMouseTo(getPosition(addedObjects[1]))); + + AddStep("jump editor clock", () => EditorClock.Seek(30000)); + AddStep("jump editor clock", () => EditorClock.Seek(60000)); + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + assertSelectionIs(addedObjects.Skip(1)); + } + private void assertSelectionIs(IEnumerable hitObjects) => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } From 0d448e6cc898e181ab65f31280e3be15ee761f42 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 6 Oct 2022 13:50:56 +0900 Subject: [PATCH 09/12] Fix items without blueprints are not deselected --- .../Compose/Components/BlueprintContainer.cs | 18 +++++++----------- .../Components/EditorBlueprintContainer.cs | 4 ++-- .../Skinning/Editor/SkinBlueprintContainer.cs | 6 ++++++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index da7a8e662b..8aecc75824 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }; SelectionHandler = CreateSelectionHandler(); - SelectionHandler.DeselectAll = deselectAll; + SelectionHandler.DeselectAll = DeselectAll; SelectionHandler.SelectedItems.BindTo(SelectedItems); AddRangeInternal(new[] @@ -145,7 +145,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (endClickSelection(e) || ClickedBlueprint != null) return true; - deselectAll(); + DeselectAll(); return true; } @@ -234,7 +234,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!SelectionHandler.SelectedBlueprints.Any()) return false; - deselectAll(); + DeselectAll(); return true; } @@ -399,18 +399,14 @@ namespace osu.Game.Screens.Edit.Compose.Components } /// - /// Selects all s. + /// Select all currently-present items. /// - protected virtual void SelectAll() - { - // Scheduled to allow the change in lifetime to take place. - Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); - } + protected abstract void SelectAll(); /// - /// Deselects all selected s. + /// Deselect all selected items. /// - private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); + protected void DeselectAll() => SelectedItems.Clear(); protected virtual void OnBlueprintSelected(SelectionBlueprint blueprint) { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 879ac58887..6682748253 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -131,8 +131,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void SelectAll() { Composer.Playfield.KeepAllAlive(); - - base.SelectAll(); + SelectedItems.Clear(); + SelectedItems.AddRange(Beatmap.HitObjects); } protected override void OnBlueprintSelected(SelectionBlueprint blueprint) diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 5a1ef34151..97522ddff8 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -117,6 +117,12 @@ namespace osu.Game.Skinning.Editor return false; } + protected override void SelectAll() + { + SelectedItems.Clear(); + SelectedItems.AddRange(targetComponents.SelectMany(list => list)); + } + /// /// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints). /// From 29cc55463268c40b688d1125ea7b69886c046135 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 6 Oct 2022 13:59:54 +0900 Subject: [PATCH 10/12] Ensure blueprint is added for selected hit object --- .../Components/Timeline/TimelineBlueprintContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 08682ae05f..31990bfd35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -183,7 +183,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline double maxTime = dragBox.MaxTime; SelectedItems.RemoveAll(hitObject => !shouldBeSelected(hitObject)); - SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected)); + + foreach (var hitObject in Beatmap.HitObjects.Except(SelectedItems).Where(shouldBeSelected)) + { + Composer.Playfield.SetKeepAlive(hitObject, true); + SelectedItems.Add(hitObject); + } bool shouldBeSelected(HitObject hitObject) { From 0ade0492526b184b14f64e59a66e88a71fc1e5d3 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 6 Oct 2022 14:02:49 +0900 Subject: [PATCH 11/12] Add test for selected hit object blueprint --- osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index a4f4c375bf..54ad4e25e4 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("jump editor clock", () => EditorClock.Seek(60000)); AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); assertSelectionIs(addedObjects.Skip(1)); + AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count); } private void assertSelectionIs(IEnumerable hitObjects) From 6164e0896a8ec69e87329f790e22624bba85225d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 7 Oct 2022 10:46:07 +0900 Subject: [PATCH 12/12] Don't reselect already selected items in SelectAll --- .../Edit/Compose/Components/EditorBlueprintContainer.cs | 3 +-- osu.Game/Skinning/Editor/SkinBlueprintContainer.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 6682748253..6adaeb1a83 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -131,8 +131,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void SelectAll() { Composer.Playfield.KeepAllAlive(); - SelectedItems.Clear(); - SelectedItems.AddRange(Beatmap.HitObjects); + SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray()); } protected override void OnBlueprintSelected(SelectionBlueprint blueprint) diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index 97522ddff8..2937b62eec 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -119,8 +119,7 @@ namespace osu.Game.Skinning.Editor protected override void SelectAll() { - SelectedItems.Clear(); - SelectedItems.AddRange(targetComponents.SelectMany(list => list)); + SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray()); } ///