From 038021f682c5fcbc9fd4f6a34bda5e5f9db5e659 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Jun 2022 20:24:50 +0300 Subject: [PATCH 1/6] Add failing test case --- .../Visual/Editing/TestSceneComposerSelection.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index dddd9f07ab..7fe98b9f09 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; @@ -61,6 +62,21 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("context menu is visible", () => contextMenuContainer.ChildrenOfType().Single().State == MenuState.Open); } + [Test] + public void TestSelectAndShowContextMenuOutsideBounds() + { + var addedObject = new HitCircle { StartTime = 100, Position = OsuPlayfield.BASE_SIZE }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("descale blueprint container", () => this.ChildrenOfType().Single().Scale = new Vector2(0.5f)); + AddStep("move mouse to bottom-right", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight + new Vector2(20))); + + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + + AddUntilStep("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + AddUntilStep("context menu is visible", () => contextMenuContainer.ChildrenOfType().Single().State == MenuState.Open); + } + [Test] public void TestNudgeSelection() { From 85fc218edf0d8f2c688af226265ef3e9373c5a4c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 11 Jun 2022 20:53:53 +0300 Subject: [PATCH 2/6] Provide context menu items at `BlueprintContainer` for out-of-bounds support --- .../Compose/Components/BlueprintContainer.cs | 30 ++++++++++++++++++- .../Components/EditorSelectionHandler.cs | 2 +- .../Compose/Components/SelectionHandler.cs | 28 ++--------------- .../Skinning/Editor/SkinSelectionHandler.cs | 2 +- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index d56dc176f6..5fdd7c0e81 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -11,11 +11,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; using osuTK; using osuTK.Input; @@ -26,7 +29,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// A container which provides a "blueprint" display of items. /// Includes selection and manipulation support via a . /// - public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler + public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler, IHasContextMenu where T : class { protected DragBox DragBox { get; private set; } @@ -534,5 +537,30 @@ namespace osu.Game.Screens.Edit.Compose.Components } #endregion + + #region Context Menu + + public MenuItem[] ContextMenuItems + { + get + { + var selectedBlueprints = SelectionHandler.SelectedBlueprints; + if (!selectedBlueprints.Any(b => b.IsHovered)) + return Array.Empty(); + + var items = new List(); + + items.AddRange(SelectionHandler.GetContextMenuItemsForSelection(selectedBlueprints)); + + if (selectedBlueprints.Count == 1) + items.AddRange(selectedBlueprints[0].ContextMenuItems); + + items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, SelectionHandler.DeleteSelected)); + + return items.ToArray(); + } + } + + #endregion } } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 7f693996a4..5fa98aea01 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The current selection. /// The relevant menu items. - protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected internal override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { if (SelectedBlueprints.All(b => b.Item is IHasComboInformation)) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 78b98a3649..d4ffb67a51 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; @@ -17,7 +16,6 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; using osuTK; using osuTK.Input; @@ -27,7 +25,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A component which outlines items and handles movement of selections. /// - public abstract class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler, IHasContextMenu + public abstract class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler { /// /// The currently selected blueprints. @@ -292,7 +290,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - protected void DeleteSelected() + protected internal void DeleteSelected() { DeleteItems(selectedBlueprints.Select(b => b.Item)); } @@ -345,32 +343,12 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Context Menu - public MenuItem[] ContextMenuItems - { - get - { - if (!SelectedBlueprints.Any(b => b.IsHovered)) - return Array.Empty(); - - var items = new List(); - - items.AddRange(GetContextMenuItemsForSelection(SelectedBlueprints)); - - if (SelectedBlueprints.Count == 1) - items.AddRange(SelectedBlueprints[0].ContextMenuItems); - - items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected)); - - return items.ToArray(); - } - } - /// /// Provide context menu items relevant to current selection. Calling base is not required. /// /// The current selection. /// The relevant menu items. - protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected internal virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) => Enumerable.Empty(); #endregion diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 943425e099..3ead8fee17 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -180,7 +180,7 @@ namespace osu.Game.Skinning.Editor protected override void DeleteItems(IEnumerable items) => skinEditor.DeleteItems(items.ToArray()); - protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected internal override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) { From 20e7f32fd9b495f2437048a0cb1bddb549a3f92d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 12 Jun 2022 17:53:10 +0300 Subject: [PATCH 3/6] Improve test case to work in headless --- osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 7fe98b9f09..17ca9da8f8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); AddStep("descale blueprint container", () => this.ChildrenOfType().Single().Scale = new Vector2(0.5f)); - AddStep("move mouse to bottom-right", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.BottomRight + new Vector2(20))); + AddStep("move mouse to bottom-right", () => InputManager.MoveMouseTo(blueprintContainer.ToScreenSpace(blueprintContainer.LayoutRectangle.BottomRight + new Vector2(10)))); AddStep("right click", () => InputManager.Click(MouseButton.Right)); From 9fc04924eba411f1bcdf6794fa694718abac34ff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 14 Jun 2022 00:02:02 +0300 Subject: [PATCH 4/6] Revert "Provide context menu items at `BlueprintContainer` for out-of-bounds support" This reverts commit 85fc218edf0d8f2c688af226265ef3e9373c5a4c. --- .../Compose/Components/BlueprintContainer.cs | 30 +------------------ .../Components/EditorSelectionHandler.cs | 2 +- .../Compose/Components/SelectionHandler.cs | 28 +++++++++++++++-- .../Skinning/Editor/SkinSelectionHandler.cs | 2 +- 4 files changed, 28 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5fdd7c0e81..d56dc176f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -11,14 +11,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; -using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; using osuTK; using osuTK.Input; @@ -29,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// A container which provides a "blueprint" display of items. /// Includes selection and manipulation support via a . /// - public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler, IHasContextMenu + public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler where T : class { protected DragBox DragBox { get; private set; } @@ -537,30 +534,5 @@ namespace osu.Game.Screens.Edit.Compose.Components } #endregion - - #region Context Menu - - public MenuItem[] ContextMenuItems - { - get - { - var selectedBlueprints = SelectionHandler.SelectedBlueprints; - if (!selectedBlueprints.Any(b => b.IsHovered)) - return Array.Empty(); - - var items = new List(); - - items.AddRange(SelectionHandler.GetContextMenuItemsForSelection(selectedBlueprints)); - - if (selectedBlueprints.Count == 1) - items.AddRange(selectedBlueprints[0].ContextMenuItems); - - items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, SelectionHandler.DeleteSelected)); - - return items.ToArray(); - } - } - - #endregion } } diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 5fa98aea01..7f693996a4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The current selection. /// The relevant menu items. - protected internal override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { if (SelectedBlueprints.All(b => b.Item is IHasComboInformation)) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index d4ffb67a51..78b98a3649 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; @@ -16,6 +17,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.Resources.Localisation.Web; using osu.Game.Rulesets.Edit; using osuTK; using osuTK.Input; @@ -25,7 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A component which outlines items and handles movement of selections. /// - public abstract class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler + public abstract class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IKeyBindingHandler, IHasContextMenu { /// /// The currently selected blueprints. @@ -290,7 +292,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - protected internal void DeleteSelected() + protected void DeleteSelected() { DeleteItems(selectedBlueprints.Select(b => b.Item)); } @@ -343,12 +345,32 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Context Menu + public MenuItem[] ContextMenuItems + { + get + { + if (!SelectedBlueprints.Any(b => b.IsHovered)) + return Array.Empty(); + + var items = new List(); + + items.AddRange(GetContextMenuItemsForSelection(SelectedBlueprints)); + + if (SelectedBlueprints.Count == 1) + items.AddRange(SelectedBlueprints[0].ContextMenuItems); + + items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, DeleteSelected)); + + return items.ToArray(); + } + } + /// /// Provide context menu items relevant to current selection. Calling base is not required. /// /// The current selection. /// The relevant menu items. - protected internal virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) => Enumerable.Empty(); #endregion diff --git a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs index 3ead8fee17..943425e099 100644 --- a/osu.Game/Skinning/Editor/SkinSelectionHandler.cs +++ b/osu.Game/Skinning/Editor/SkinSelectionHandler.cs @@ -180,7 +180,7 @@ namespace osu.Game.Skinning.Editor protected override void DeleteItems(IEnumerable items) => skinEditor.DeleteItems(items.ToArray()); - protected internal override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) + protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable> selection) { var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) { From c49b8e4a5cc4ae14c9dd25d2030c7410bb1f1dba Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 14 Jun 2022 00:19:15 +0300 Subject: [PATCH 5/6] Allow `BlueprintContainer` and `SelectionHandler` to receive input outside bounds --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 7 +++++++ .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 -- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index d56dc176f6..fbec80a63b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -106,6 +106,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual bool AllowDeselectionDuringDrag => true; + /// + /// Positional input must be received outside the container's bounds, + /// in order to handle blueprints which are partially offscreen. + /// + /// + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + protected override bool OnMouseDown(MouseDownEvent e) { bool selectionPerformed = performMouseDownActions(e); diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 68be20720d..0be2cb4462 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -30,8 +30,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class ComposeBlueprintContainer : EditorBlueprintContainer { - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - private readonly Container placementBlueprintContainer; protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 78b98a3649..e5020afcde 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -97,6 +97,13 @@ namespace osu.Game.Screens.Edit.Compose.Components #region User Input Handling + /// + /// Positional input must be received outside the container's bounds, + /// in order to handle blueprints which are partially offscreen. + /// + /// + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + /// /// Handles the selected items being moved. /// From fa4930c5865a414bb3d1d811cbca69b9a08f2c13 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 14 Jun 2022 00:19:47 +0300 Subject: [PATCH 6/6] Remove no longer necessary `ReceivePositionalInputAt` overrides in timeline components --- .../Components/Timeline/TimelineBlueprintContainer.cs | 3 --- .../Components/Timeline/TimelineSelectionHandler.cs | 7 ------- 2 files changed, 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index a9e9ef5001..1c0c2fb215 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -33,9 +33,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable placement; private SelectionBlueprint placementBlueprint; - // We want children within the timeline to be interactable - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ScreenSpaceDrawQuad.Contains(screenSpacePos); - public TimelineBlueprintContainer(HitObjectComposer composer) : base(composer) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs index e98cf8332f..da12ab521f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs @@ -6,23 +6,16 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; -using osuTK; using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { internal class TimelineSelectionHandler : EditorSelectionHandler { - [Resolved] - private Timeline timeline { get; set; } - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => timeline.ScreenSpaceDrawQuad.Contains(screenSpacePos); - // for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;