diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f6354bc612..17541866ec 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Humanizer; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components @@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (action.ActionMethod) { case PlatformActionMethod.Delete: - return deleteSelected(); + return DeleteSelected(); } return false; @@ -126,7 +128,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - private bool deleteSelected() + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + + public bool DeleteSelected() { List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList(); @@ -134,7 +139,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (toRemove.Count == 0) return false; + changeHandler?.BeginChange(); RemoveControlPointsRequested?.Invoke(toRemove); + changeHandler?.EndChange(); // Since pieces are re-used, they will not point to the deleted control points while remaining selected foreach (var piece in Pieces) @@ -169,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return new MenuItem[] { - new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected()), + new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()), new OsuMenuItem("Curve type") { Items = items diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index f851c7bfc9..9b758ec898 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -72,6 +73,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BodyPiece.UpdateFrom(HitObject); } + public override bool HandleQuickDeletion() + { + var hoveredControlPoint = ControlPointVisualiser.Pieces.FirstOrDefault(p => p.IsHovered); + + if (hoveredControlPoint == null) + return false; + + hoveredControlPoint.IsSelected.Value = true; + ControlPointVisualiser.DeleteSelected(); + return true; + } + protected override void Update() { base.Update(); @@ -109,9 +122,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders rightClickPosition = e.MouseDownPosition; return false; // Allow right click to be handled by context menu - case MouseButton.Left when e.ControlPressed && IsSelected: - placementControlPointIndex = addControlPoint(e.MousePosition); - return true; // Stop input from being handled and modifying the selection + case MouseButton.Left: + if (e.ControlPressed && IsSelected) + { + placementControlPointIndex = addControlPoint(e.MousePosition); + return true; // Stop input from being handled and modifying the selection + } + + break; } return false; @@ -216,7 +234,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index d2f4b71f19..98432eb4fe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -42,8 +42,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private readonly IBindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); + [Resolved] + private DrawableHitObject drawableObject { get; set; } + [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load() { var drawableOsuObject = (DrawableOsuHitObject)drawableObject; @@ -64,32 +67,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void updateState(ValueChangedEvent state) { - glow.FadeOut(400); - - switch (state.NewValue) + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { - case ArmedState.Hit: - const double flash_in = 40; - const double flash_out = 100; + glow.FadeOut(400); - flash.FadeTo(0.8f, flash_in) - .Then() - .FadeOut(flash_out); + switch (state.NewValue) + { + case ArmedState.Hit: + const double flash_in = 40; + const double flash_out = 100; - explode.FadeIn(flash_in); - this.ScaleTo(1.5f, 400, Easing.OutQuad); + flash.FadeTo(0.8f, flash_in) + .Then() + .FadeOut(flash_out); - using (BeginDelayedSequence(flash_in, true)) - { - // after the flash, we can hide some elements that were behind it - ring.FadeOut(); - circle.FadeOut(); - number.FadeOut(); + explode.FadeIn(flash_in); + this.ScaleTo(1.5f, 400, Easing.OutQuad); - this.FadeOut(800); - } + using (BeginDelayedSequence(flash_in, true)) + { + // after the flash, we can hide some elements that were behind it + ring.FadeOut(); + circle.FadeOut(); + number.FadeOut(); - break; + this.FadeOut(800); + } + + break; + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 418bf124ab..1551d1c149 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -42,11 +42,14 @@ namespace osu.Game.Rulesets.Osu.Skinning private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); + [Resolved] + private DrawableHitObject drawableObject { get; set; } + [Resolved] private ISkinSource skin { get; set; } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load() { var drawableOsuObject = (DrawableOsuHitObject)drawableObject; @@ -144,28 +147,31 @@ namespace osu.Game.Rulesets.Osu.Skinning { const double legacy_fade_duration = 240; - switch (state.NewValue) + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { - case ArmedState.Hit: - circleSprites.FadeOut(legacy_fade_duration, Easing.Out); - circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + switch (state.NewValue) + { + case ArmedState.Hit: + circleSprites.FadeOut(legacy_fade_duration, Easing.Out); + circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - if (hasNumber) - { - var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - - if (legacyVersion >= 2.0m) - // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. - hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); - else + if (hasNumber) { - // old skins scale and fade it normally along other pieces. - hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); - hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - } - } + var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - break; + if (legacyVersion >= 2.0m) + // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. + hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); + else + { + // old skins scale and fade it normally along other pieces. + hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + } + } + + break; + } } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index 31350791e3..eb9fa85fde 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning protected DrawableSpinner DrawableSpinner { get; private set; } private Sprite spin; + private Sprite clear; [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) @@ -39,17 +41,58 @@ namespace osu.Game.Rulesets.Osu.Skinning Scale = new Vector2(SPRITE_SCALE), Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter }, + clear = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Depth = float.MinValue, + Alpha = 0, + Texture = source.GetTexture("spinner-clear"), + Scale = new Vector2(SPRITE_SCALE), + Y = -60 + }, }); } + private readonly Bindable completed = new Bindable(); + protected override void LoadComplete() { base.LoadComplete(); + completed.BindTo(DrawableSpinner.RotationTracker.Complete); + completed.BindValueChanged(onCompletedChanged, true); + DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; UpdateStateTransforms(DrawableSpinner, DrawableSpinner.State.Value); } + private void onCompletedChanged(ValueChangedEvent completed) + { + if (completed.NewValue) + { + double startTime = Math.Min(Time.Current, DrawableSpinner.HitStateUpdateTime - 400); + + using (BeginAbsoluteSequence(startTime, true)) + { + clear.FadeInFromZero(400, Easing.Out); + + clear.ScaleTo(SPRITE_SCALE * 2) + .Then().ScaleTo(SPRITE_SCALE * 0.8f, 240, Easing.Out) + .Then().ScaleTo(SPRITE_SCALE, 160); + } + + const double fade_out_duration = 50; + using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true)) + clear.FadeOut(fade_out_duration); + } + else + { + clear.ClearTransforms(); + clear.Alpha = 0; + } + } + protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { switch (drawableHitObject) @@ -59,7 +102,6 @@ namespace osu.Game.Rulesets.Osu.Skinning using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true)) spin.FadeOutFromOne(fadeOutLength); - break; case DrawableSpinnerTick d: diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 55adf02a45..9beb859f28 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -308,7 +308,6 @@ namespace osu.Game.Overlays if (disabled) playlist.Hide(); - playButton.Enabled.Value = !disabled; prevButton.Enabled.Value = !disabled; nextButton.Enabled.Value = !disabled; playlistButton.Enabled.Value = !disabled; diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index f3816f6218..99cdca045b 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -143,5 +143,11 @@ namespace osu.Game.Rulesets.Edit public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position; + + /// + /// Handle to perform a partial deletion when the user requests a quick delete (Shift+Right Click). + /// + /// True if the deletion operation was handled by this blueprint. Returning false will delete the full blueprint. + public virtual bool HandleQuickDeletion() => false; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index fa98358dbe..e7da220946 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -116,7 +116,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - beginClickSelection(e); + if (!beginClickSelection(e)) return true; + prepareSelectionMovement(); return e.Button == MouseButton.Left; @@ -291,19 +292,24 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Attempts to select any hovered blueprints. /// /// The input event that triggered this selection. - private void beginClickSelection(MouseButtonEvent e) + /// Whether a selection was performed. + private bool beginClickSelection(MouseButtonEvent e) { Debug.Assert(!clickSelectionBegan); + bool selectedPerformed = true; + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (blueprint.IsHovered) { - SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); + selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); clickSelectionBegan = true; break; } } + + return selectedPerformed; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 406ca07185..0bbbfaf5e8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -219,18 +219,28 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The blueprint. /// The input state at the point of selection. - internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) + /// Whether a selection was performed. + internal bool HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) + { handleQuickDeletion(blueprint); - else if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) + return false; + } + + if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) blueprint.ToggleSelection(); else ensureSelected(blueprint); + + return true; } private void handleQuickDeletion(SelectionBlueprint blueprint) { + if (blueprint.HandleQuickDeletion()) + return; + if (!blueprint.IsSelected) EditorBeatmap.Remove(blueprint.HitObject); else