1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-02 03:59:54 +08:00

Merge pull request #31148 from peppy/editor-right-click-as-people-expect

Add back right-click-for-new-combo and right-click-delete when in object placement mode
This commit is contained in:
Bartłomiej Dach
2025-03-13 08:24:08 +01:00
committed by GitHub
Unverified
7 changed files with 123 additions and 21 deletions
@@ -30,6 +30,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit
{
@@ -351,6 +352,35 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
protected override bool OnMouseDown(MouseDownEvent e)
{
// Why is this logic here and not in `OsuSelectionHandler`?
// Because we only want to handle this toggle after all other right-click handling completes.
//
// Consider that input is handled from the most nested child first:
//
// ComposeScreen
// |- OsuContextMenuContainer // right click for context
// |- TimelineBlueprintContainer
// |- TimelineSelectionHandler
// |- (Osu)HitObjectComposer // right click for toggle new combo
// |- (Osu)EditorBlueprintContainer // right click for select
// |- (Osu)EditorSelectionHandler // right click for delete
if (e.Button == MouseButton.Right)
{
var osuSelectionHandler = (OsuSelectionHandler)BlueprintContainer.SelectionHandler;
if (!osuSelectionHandler.SelectedItems.Any())
{
osuSelectionHandler.SelectionNewComboState.Value =
osuSelectionHandler.SelectionNewComboState.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
return true;
}
}
return base.OnMouseDown(e);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
@@ -3,9 +3,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Tests.Visual;
@@ -31,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Editor
AddStep("hover over first hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(1)));
AddStep("hover over second hit", () => InputManager.MoveMouseTo(Editor.ChildrenOfType<DrawableHit>().ElementAt(0)));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddUntilStep("context menu open", () => Editor.ChildrenOfType<OsuContextMenu>().Any(menu => menu.State == MenuState.Open));
AddUntilStep("second hit deleted", () => Editor.ChildrenOfType<DrawableHit>().Count(), () => Is.EqualTo(1));
}
}
}
@@ -7,6 +7,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
@@ -14,8 +15,10 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
@@ -58,19 +61,63 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
public void TestContextMenu()
public void TestRightClickDuringEmptyPlacementTogglesNewCombo()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("move mouse away from placed circle", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
AddAssert("new combo true", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.True));
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
}
[Test]
public void TestRightClickDuringPlacementDeletes()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(0).Items);
AddAssert("circle not selected", () => EditorBeatmap.SelectedHitObjects, () => Has.Exactly(0).Items);
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
}
[Test]
public void TestRightClickDuringSelectionShowsContextMenu()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with right mouse", () =>
{
InputManager.Click(MouseButton.Right);
});
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items);
// ensure the circle we're selecting is not a new combo so we can assert
// new combo doesn't happen to get toggled by right click.
AddStep("seek forward", () => EditorClock.Seek(1000));
AddStep("place second circle", () => InputManager.Click(MouseButton.Left));
AddAssert("two circles added", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items);
AddAssert("context menu not visible", () => !Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
AddStep("select selection tool", () => InputManager.Key(Key.Number1));
AddStep("click right mouse", () => InputManager.Click(MouseButton.Right));
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.Exactly(2).Items);
AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items);
AddAssert("context menu visible", () => Editor.ChildrenOfType<OsuContextMenu>().Any(c => c.IsPresent));
AddAssert("new combo false", () => this.ChildrenOfType<NewComboTernaryButton>().Single().Current.Value, () => Is.EqualTo(TernaryState.False));
}
[Test]
@@ -115,19 +115,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnMouseDown(MouseDownEvent e)
{
bool selectionPerformed = performMouseDownActions(e);
bool handled = performMouseDownActions(e);
bool movementPossible = prepareSelectionMovement(e);
// check if selection has occurred
if (selectionPerformed)
if (SelectedItems.Any())
{
// only unmodified right click should show context menu
// if there is a selection and there are no modifiers pressed, don't block so the context menu still shows.
bool shouldShowContextMenu = e.Button == MouseButton.Right && !e.ShiftPressed && !e.AltPressed && !e.SuperPressed;
// stop propagation if not showing context menu
return !shouldShowContextMenu;
}
if (handled)
return true;
// even if a selection didn't occur, a drag event may still move the selection.
return e.Button == MouseButton.Left && movementPossible;
}
@@ -387,6 +387,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
currentTool = value;
SelectionHandler.RightClickAlwaysQuickDeletes = currentTool is not SelectTool;
// As per stable editor, when changing tools, we should forcefully commit any pending placement.
CommitIfPlacementActive();
}
@@ -10,16 +10,23 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components
{
public partial class EditorSelectionHandler : SelectionHandler<HitObject>
{
/// <summary>
/// Whether right click should delete even when shift is not held.
/// </summary>
public bool RightClickAlwaysQuickDeletes { get; set; }
/// <summary>
/// A special bank name that is only used in the editor UI.
/// When selected and in placement mode, the bank of the last hit object will always be used.
@@ -40,6 +47,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectedItems.CollectionChanged += onSelectedItemsChanged;
}
protected override bool ShouldQuickDelete(MouseButtonEvent e)
{
if (RightClickAlwaysQuickDeletes && e.Button == MouseButton.Right)
return true;
return base.ShouldQuickDelete(e);
}
protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items);
#region Selection State
@@ -293,7 +308,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach ((string bankName, var bindable) in SelectionAdditionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank));
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL),
h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank));
}
}
@@ -378,14 +394,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
string normalBank = h.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList();
h.Samples = h.Samples.Select(s =>
s.Name != HitSampleInfo.HIT_NORMAL
? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false)
: s)
.ToList();
if (h is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i)
{
normalBank = hasRepeats.NodeSamples[i].FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList();
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s =>
s.Name != HitSampleInfo.HIT_NORMAL
? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false)
: s).ToList();
}
}
});
@@ -255,15 +255,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectedBlueprints.Remove(blueprint);
}
protected virtual bool ShouldQuickDelete(MouseButtonEvent e) => e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right);
/// <summary>
/// Handle a blueprint requesting selection.
/// </summary>
/// <param name="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for selection.</param>
/// <returns>Whether a selection was performed.</returns>
/// <returns>Whether an action was performed.</returns>
internal virtual bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
{
if (e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right))
if (ShouldQuickDelete(e))
{
handleQuickDeletion(blueprint);
return true;