From cc2acf5e548fb11873f1d20d480b27e35ced57fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 19:05:23 +0900 Subject: [PATCH 1/6] Fix ctrl-dragging on an existing selection causing deselection of the hovered object --- .../Compose/Components/BlueprintContainer.cs | 38 ++++++++++++++----- .../Compose/Components/SelectionHandler.cs | 37 +++++++++++++++--- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5699be4560..64cf0e7512 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -135,11 +135,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - if (!beginClickSelection(e)) return true; + bool selectionPerformed = beginClickSelection(e); + // even if a selection didn't occur, a drag event may still move the selection. prepareSelectionMovement(); - return e.Button == MouseButton.Left; + return selectionPerformed || e.Button == MouseButton.Left; } private SelectionBlueprint clickedBlueprint; @@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Deselection should only occur if no selected blueprints are hovered // A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection - if (endClickSelection() || clickedBlueprint != null) + if (endClickSelection(e) || clickedBlueprint != null) return true; deselectAll(); @@ -177,7 +178,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnMouseUp(MouseUpEvent e) { // Special case for when a drag happened instead of a click - Schedule(() => endClickSelection()); + Schedule(() => + { + endClickSelection(e); + clickSelectionBegan = false; + isDraggingBlueprint = false; + }); finishSelectionMovement(); } @@ -226,7 +232,6 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.Update(obj); changeHandler?.EndChange(); - isDraggingBlueprint = false; } if (DragBox.State == Visibility.Visible) @@ -355,13 +360,28 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Finishes the current blueprint selection. /// + /// The mouse event which triggered end of selection. /// Whether a click selection was active. - private bool endClickSelection() + private bool endClickSelection(MouseButtonEvent e) { - if (!clickSelectionBegan) - return false; + if (!clickSelectionBegan && !isDraggingBlueprint) + { + // if a selection didn't occur, we may want to trigger a deselection. + if (e.ControlPressed && e.Button == MouseButton.Left) + { + // Iterate from the top of the input stack (blueprints closest to the front of the screen first). + // Priority is given to already-selected blueprints. + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected)) + { + if (!blueprint.IsHovered) continue; + + return clickSelectionBegan = SelectionHandler.HandleDeselectionRequested(blueprint, e); + } + } + + return false; + } - clickSelectionBegan = false; return true; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 018d4d081c..e5e1100797 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -228,12 +228,31 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; } - if (e.ControlPressed && e.Button == MouseButton.Left) + // while holding control, we only want to add to selection, not replace an existing selection. + if (e.ControlPressed && e.Button == MouseButton.Left && !blueprint.IsSelected) + { blueprint.ToggleSelection(); - else - ensureSelected(blueprint); + return true; + } - return true; + return ensureSelected(blueprint); + } + + /// + /// Handle a blueprint requesting selection. + /// + /// The blueprint. + /// The mouse event responsible for deselection. + /// Whether a deselection was performed. + internal bool HandleDeselectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + { + if (blueprint.IsSelected) + { + blueprint.ToggleSelection(); + return true; + } + + return false; } private void handleQuickDeletion(SelectionBlueprint blueprint) @@ -247,13 +266,19 @@ namespace osu.Game.Screens.Edit.Compose.Components deleteSelected(); } - private void ensureSelected(SelectionBlueprint blueprint) + /// + /// Ensure the blueprint is in a selected state. + /// + /// The blueprint to select. + /// Whether selection state was changed. + private bool ensureSelected(SelectionBlueprint blueprint) { if (blueprint.IsSelected) - return; + return false; DeselectAll?.Invoke(); blueprint.Select(); + return true; } private void deleteSelected() From b4c75ba3c6eb81a8cf88f36edc8738ee7e2e09df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Apr 2021 19:19:25 +0900 Subject: [PATCH 2/6] Fix TestQuickDeleteRemovesObject failing on second run --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 9efd299fba..8a0f27b851 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestQuickDeleteRemovesSliderControlPoint() { - Slider slider = new Slider { StartTime = 1000 }; + Slider slider = null; PathControlPoint[] points = { @@ -62,7 +62,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add slider", () => { - slider.Path = new SliderPath(points); + slider = new Slider + { + StartTime = 1000, + Path = new SliderPath(points) + }; + EditorBeatmap.Add(slider); }); From 66e74da2b74a10ed695b073bf1a92c5c1b7b7d32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:03:14 +0900 Subject: [PATCH 3/6] Fix regression in quick delete mouse action blocking --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 3 ++- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 8 ++++---- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 8a0f27b851..25e12b7a88 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -83,8 +83,9 @@ namespace osu.Game.Tests.Visual.Editing AddStep("right click", () => InputManager.Click(MouseButton.Right)); AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2); - // second click should nuke the object completely. AddStep("right click", () => InputManager.Click(MouseButton.Right)); + + // second click should nuke the object completely. AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 64cf0e7512..b5a28dc022 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - bool selectionPerformed = beginClickSelection(e); + bool selectionPerformed = performMouseDownActions(e); // even if a selection didn't occur, a drag event may still move the selection. prepareSelectionMovement(); @@ -343,7 +343,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The input event that triggered this selection. /// Whether a selection was performed. - private bool beginClickSelection(MouseButtonEvent e) + private bool performMouseDownActions(MouseButtonEvent e) { // Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Priority is given to already-selected blueprints. @@ -351,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (!blueprint.IsHovered) continue; - return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e); + return clickSelectionBegan = SelectionHandler.MouseDownSelectionRequested(blueprint, e); } return false; @@ -375,7 +375,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (!blueprint.IsHovered) continue; - return clickSelectionBegan = SelectionHandler.HandleDeselectionRequested(blueprint, e); + return clickSelectionBegan = SelectionHandler.MouseUpSelectionRequested(blueprint, e); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index e5e1100797..389ef78ed5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -220,12 +220,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The mouse event responsible for selection. /// Whether a selection was performed. - internal bool HandleSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (e.ShiftPressed && e.Button == MouseButton.Right) { handleQuickDeletion(blueprint); - return false; + return true; } // while holding control, we only want to add to selection, not replace an existing selection. @@ -244,7 +244,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The mouse event responsible for deselection. /// Whether a deselection was performed. - internal bool HandleDeselectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (blueprint.IsSelected) { From 05d7fe289f3e6a48b855edc629e163fe7ea26fff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:09:18 +0900 Subject: [PATCH 4/6] Rename test scene in preparation for increasing scope --- ...estSceneEditorQuickDelete.cs => TestSceneEditorSelection.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Editing/{TestSceneEditorQuickDelete.cs => TestSceneEditorSelection.cs} (98%) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs similarity index 98% rename from osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs rename to osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 25e12b7a88..36c4357432 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -18,7 +18,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { - public class TestSceneEditorQuickDelete : EditorTestScene + public class TestSceneEditorSelection : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); From 7c975359d9b6b06a797a7121c38700e09c2f08b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:29:37 +0900 Subject: [PATCH 5/6] Add basic select/deselect tests --- .../Editing/TestSceneEditorSelection.cs | 98 ++++++++++++++++++- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 36c4357432..4f386972fa 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -27,6 +27,97 @@ namespace osu.Game.Tests.Visual.Editing private BlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); + private void moveMouseToObject(HitObject obj) + { + AddStep("move mouse to object", () => + { + var pos = blueprintContainer.SelectionBlueprints + .First(s => s.HitObject == obj) + .ChildrenOfType() + .First().ScreenSpaceDrawQuad.Centre; + + InputManager.MoveMouseTo(pos); + }); + } + + [Test] + public void TestBasicSelect() + { + var addedObject = new HitCircle { StartTime = 100 }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(addedObject); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + var addedObject2 = new HitCircle + { + StartTime = 100, + Position = new Vector2(100), + }; + + AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2)); + AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + moveMouseToObject(addedObject2); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2); + } + + [Test] + public void TestMultiSelect() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(50) }, + new HitCircle { StartTime = 300, Position = new Vector2(100) }, + new HitCircle { StartTime = 400, Position = new Vector2(150) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + moveMouseToObject(addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]); + + AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); + + moveMouseToObject(addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + + moveMouseToObject(addedObjects[2]); + AddStep("click third", () => InputManager.Click(MouseButton.Left)); + AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2])); + + moveMouseToObject(addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + } + + [Test] + public void TestBasicDeselect() + { + var addedObject = new HitCircle { StartTime = 100 }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(addedObject); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + AddStep("click away", () => + { + InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0); + } + [Test] public void TestQuickDeleteRemovesObject() { @@ -36,11 +127,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); - AddStep("move mouse to object", () => - { - var pos = blueprintContainer.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; - InputManager.MoveMouseTo(pos); - }); + moveMouseToObject(addedObject); + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("right click", () => InputManager.Click(MouseButton.Right)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); From 516bd138e32ba1bd3e4ae35a150d4ae1e9b0d8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Apr 2021 13:46:38 +0900 Subject: [PATCH 6/6] Add (previously failing) test coverage of drag from selection --- .../Editing/TestSceneEditorSelection.cs | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 4f386972fa..99f31b0c2a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; @@ -27,12 +28,12 @@ namespace osu.Game.Tests.Visual.Editing private BlueprintContainer blueprintContainer => Editor.ChildrenOfType().First(); - private void moveMouseToObject(HitObject obj) + private void moveMouseToObject(Func targetFunc) { AddStep("move mouse to object", () => { var pos = blueprintContainer.SelectionBlueprints - .First(s => s.HitObject == obj) + .First(s => s.HitObject == targetFunc()) .ChildrenOfType() .First().ScreenSpaceDrawQuad.Centre; @@ -46,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing var addedObject = new HitCircle { StartTime = 100 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); - moveMouseToObject(addedObject); + moveMouseToObject(() => addedObject); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); @@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2)); AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); - moveMouseToObject(addedObject2); + moveMouseToObject(() => addedObject2); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2); } @@ -78,33 +79,69 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); - moveMouseToObject(addedObjects[0]); + moveMouseToObject(() => addedObjects[0]); AddStep("click first", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]); AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); - moveMouseToObject(addedObjects[1]); + moveMouseToObject(() => addedObjects[1]); AddStep("click second", () => InputManager.Click(MouseButton.Left)); AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); - moveMouseToObject(addedObjects[2]); + moveMouseToObject(() => addedObjects[2]); AddStep("click third", () => InputManager.Click(MouseButton.Left)); AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2])); - moveMouseToObject(addedObjects[1]); + moveMouseToObject(() => addedObjects[1]); AddStep("click second", () => InputManager.Click(MouseButton.Left)); AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); } + [TestCase(false)] + [TestCase(true)] + public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag) + { + HitCircle[] addedObjects = null; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(50) }, + new HitCircle { StartTime = 300, Position = new Vector2(100) }, + new HitCircle { StartTime = 400, Position = new Vector2(150) }, + })); + + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + + AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); + + moveMouseToObject(() => addedObjects[1]); + + if (alreadySelectedBeforeDrag) + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + + AddStep("mouse down on second", () => InputManager.PressButton(MouseButton.Left)); + + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + + AddStep("drag to centre", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre)); + + AddAssert("positions changed", () => addedObjects[0].Position != Vector2.Zero && addedObjects[1].Position != new Vector2(50)); + + AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft)); + AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left)); + } + [Test] public void TestBasicDeselect() { var addedObject = new HitCircle { StartTime = 100 }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); - moveMouseToObject(addedObject); + moveMouseToObject(() => addedObject); AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); @@ -127,7 +164,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); - moveMouseToObject(addedObject); + moveMouseToObject(() => addedObject); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); AddStep("right click", () => InputManager.Click(MouseButton.Right));