diff --git a/osu.Android.props b/osu.Android.props
index ff76e17184..c73c643d4b 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
index 5f22ef5c12..1e63d32c41 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private double placementStartTime;
private double placementEndTime;
+ protected override bool IsValidForPlacement => HitObject.Duration > 0;
+
public BananaShowerPlacementBlueprint()
{
InternalChild = outline = new TimeSpanOutline();
@@ -49,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
case PlacementState.Active:
if (e.Button != MouseButton.Right) break;
- EndPlacement(HitObject.Duration > 0);
+ EndPlacement(true);
return true;
}
diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
index 03ec674abb..9e50b5a80f 100644
--- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs
@@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private InputManager inputManager = null!;
+ protected override bool IsValidForPlacement => HitObject.Duration > 0;
+
public JuiceStreamPlacementBlueprint()
{
InternalChildren = new Drawable[]
@@ -70,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return true;
case MouseButton.Right:
- EndPlacement(HitObject.Duration > 0);
+ EndPlacement(true);
return true;
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index 21beee0769..381af8be7f 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
+ protected override bool IsValidForPlacement => HitObject.Duration > 0;
+
public HoldNotePlacementBlueprint()
: base(new HoldNote())
{
@@ -75,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return;
base.OnMouseUp(e);
- EndPlacement(HitObject.Duration > 0);
+ EndPlacement(true);
}
private double originalStartTime;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index 50514865e1..28ceb80627 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -41,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
+ protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
+
public SliderPlacementBlueprint()
: base(new Slider())
{
@@ -150,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void endCurve()
{
updateSlider();
- EndPlacement(HitObject.Path.HasValidLength);
+ EndPlacement(true);
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index fcf2573d64..bc4129c982 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private readonly IHasDuration spanPlacementObject;
+ protected override bool IsValidForPlacement => spanPlacementObject.Duration > 0;
+
public TaikoSpanPlacementBlueprint(HitObject hitObject)
: base(hitObject)
{
@@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
return;
base.OnMouseUp(e);
- EndPlacement(spanPlacementObject.Duration > 0);
+ EndPlacement(true);
}
public override void UpdateTimeAndPosition(SnapResult result)
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs
new file mode 100644
index 0000000000..58eff9ade7
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs
@@ -0,0 +1,83 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Screens;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public partial class TestScenePlacementBlueprint : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ private GlobalActionContainer globalActionContainer => this.ChildrenOfType().Single();
+
+ [Test]
+ public void TestCommitPlacementViaGlobalAction()
+ {
+ Playfield playfield = null!;
+
+ AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
+ AddStep("move mouse to top left of playfield", () =>
+ {
+ playfield = this.ChildrenOfType().Single();
+ var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
+ InputManager.MoveMouseTo(location);
+ });
+ AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
+ AddStep("move mouse to bottom right of playfield", () =>
+ {
+ var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
+ InputManager.MoveMouseTo(location);
+ });
+ AddStep("confirm via global action", () =>
+ {
+ globalActionContainer.TriggerPressed(GlobalAction.Select);
+ globalActionContainer.TriggerReleased(GlobalAction.Select);
+ });
+ AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
+ }
+
+ [Test]
+ public void TestAbortPlacementViaGlobalAction()
+ {
+ Playfield playfield = null!;
+
+ AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
+ AddStep("move mouse to top left of playfield", () =>
+ {
+ playfield = this.ChildrenOfType().Single();
+ var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
+ InputManager.MoveMouseTo(location);
+ });
+ AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
+ AddStep("move mouse to bottom right of playfield", () =>
+ {
+ var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
+ InputManager.MoveMouseTo(location);
+ });
+ AddStep("abort via global action", () =>
+ {
+ globalActionContainer.TriggerPressed(GlobalAction.Back);
+ globalActionContainer.TriggerReleased(GlobalAction.Back);
+ });
+ AddAssert("editor is still current", () => Editor.IsCurrentScreen());
+ AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0));
+ AddAssert("no active placement", () => this.ChildrenOfType().Single().CurrentPlacement.PlacementActive,
+ () => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting));
+ }
+ }
+}
diff --git a/osu.Game/Input/OsuUserInputManager.cs b/osu.Game/Input/OsuUserInputManager.cs
index ab43497156..c205636ab9 100644
--- a/osu.Game/Input/OsuUserInputManager.cs
+++ b/osu.Game/Input/OsuUserInputManager.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Bindables;
using osu.Framework.Input;
using osuTK.Input;
@@ -10,6 +11,10 @@ namespace osu.Game.Input
{
public partial class OsuUserInputManager : UserInputManager
{
+ protected override bool AllowRightClickFromLongTouch => !LocalUserPlaying.Value;
+
+ public readonly BindableBool LocalUserPlaying = new BindableBool();
+
internal OsuUserInputManager()
{
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 7c9b03bd5b..fe6e479d19 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -269,6 +269,13 @@ namespace osu.Game
if (hideToolbar) Toolbar.Hide();
}
+ protected override UserInputManager CreateUserInputManager()
+ {
+ var userInputManager = base.CreateUserInputManager();
+ (userInputManager as OsuUserInputManager)?.LocalUserPlaying.BindTo(LocalUserPlaying);
+ return userInputManager;
+ }
+
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index 951cf3802f..4c9320c2a6 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -3,6 +3,8 @@
#nullable disable
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -23,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
public partial class TabletSettings : SettingsSubsection
{
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "area" });
+
public TabletAreaSelection AreaSelection { get; private set; }
private readonly ITabletHandler tabletHandler;
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index bdcb334738..12c0ea1807 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -9,10 +9,12 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
@@ -24,7 +26,7 @@ namespace osu.Game.Rulesets.Edit
///
/// A blueprint which governs the creation of a new to actualisation.
///
- public abstract partial class PlacementBlueprint : CompositeDrawable
+ public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler
{
///
/// Whether the is currently mid-placement, but has not necessarily finished being placed.
@@ -47,6 +49,15 @@ namespace osu.Game.Rulesets.Edit
[Resolved]
private IPlacementHandler placementHandler { get; set; }
+ ///
+ /// Whether this blueprint is currently in a state that can be committed.
+ ///
+ ///
+ /// Override this with any preconditions that should be double-checked on committing.
+ /// If false is returned and a commit is attempted, the blueprint will be destroyed instead.
+ ///
+ protected virtual bool IsValidForPlacement => true;
+
protected PlacementBlueprint(HitObject hitObject)
{
HitObject = hitObject;
@@ -88,7 +99,7 @@ namespace osu.Game.Rulesets.Edit
/// Signals that the placement of has finished.
/// This will destroy this , and add the HitObject.StartTime to the .
///
- /// Whether the object should be committed.
+ /// Whether the object should be committed. Note that a commit may fail if is false.
public void EndPlacement(bool commit)
{
switch (PlacementActive)
@@ -102,10 +113,34 @@ namespace osu.Game.Rulesets.Edit
break;
}
- placementHandler.EndPlacement(HitObject, commit);
+ placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit);
PlacementActive = PlacementState.Finished;
}
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ if (PlacementActive == PlacementState.Waiting)
+ return false;
+
+ switch (e.Action)
+ {
+ case GlobalAction.Select:
+ EndPlacement(true);
+ return true;
+
+ case GlobalAction.Back:
+ EndPlacement(false);
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+
///
/// Updates the time and position of this based on the provided snap information.
///
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 2ae54a3afe..a24e22f22b 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.UI
public abstract partial class RulesetInputManager : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler
where T : struct
{
+ protected override bool AllowRightClickFromLongTouch => false;
+
public readonly KeyBindingContainer KeyBindingContainer;
[Resolved(CanBeNull = true)]
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 4315f44e07..3ea4a57c2c 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index c5477f765e..a240dec963 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -16,6 +16,6 @@
iossimulator-x64
-
+