diff --git a/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs index 00fd8247d8..fed1013ae1 100644 --- a/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs @@ -2,11 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Runtime.CompilerServices; -using osu.Framework.Testing; // We publish our internal attributes to other sub-projects of the framework. // Note, that we omit visual tests as they are meant to test the framework // behavior "in the wild". [assembly: InternalsVisibleTo("osu.Game.Rulesets.Catch.Tests")] -[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)] +[assembly: InternalsVisibleTo("osu.Game.Rulesets.Catch.Tests.Dynamic")] diff --git a/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs index c2c65433ec..515aeab9df 100644 --- a/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Mania/Properties/AssemblyInfo.cs @@ -2,11 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Runtime.CompilerServices; -using osu.Framework.Testing; // We publish our internal attributes to other sub-projects of the framework. // Note, that we omit visual tests as they are meant to test the framework // behavior "in the wild". [assembly: InternalsVisibleTo("osu.Game.Rulesets.Mania.Tests")] -[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)] +[assembly: InternalsVisibleTo("osu.Game.Rulesets.Mania.Tests.Dynamic")] diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs index b48dd73bb5..89a7686581 100644 --- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays Size = hitCircle.Size; Scale = hitCircle.Scale; + CornerRadius = Size.X / 2; + AddInternal(new RingPiece()); hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs index 586b516a11..96ff14205e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays Scale = slider.HeadCircle.Scale; AddInternal(new RingPiece()); + + Select(); } [BackgroundDependencyLoader] @@ -52,5 +54,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays RelativeAnchorPosition = hitObject.RelativeAnchorPosition; } + + // Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input. + public override bool HandleMouseInput => false; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs index 53f02617cd..629bce1847 100644 --- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Objects; @@ -59,5 +60,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays } public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos); + + public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition); + public override Quad SelectionQuad => body.PathDrawQuad; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3872821b96..5373926138 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -10,7 +10,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Judgements; -using osu.Framework.Graphics.Primitives; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using OpenTK.Graphics; @@ -177,8 +176,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Drawable ProxiedLayer => HeadCircle.ApproachCircle; public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos); - - public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition); - public override Quad SelectionQuad => Body.PathDrawQuad; } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index c00c30ced9..f64db6ba9e 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -7,10 +7,11 @@ using osu.Game.Rulesets.Objects; using OpenTK; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit.Types; namespace osu.Game.Rulesets.Osu.Objects { - public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition + public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasEditablePosition { public const double OBJECT_RADIUS = 64; diff --git a/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs index 7532646a32..ea2c2c6729 100644 --- a/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Osu/Properties/AssemblyInfo.cs @@ -2,11 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Runtime.CompilerServices; -using osu.Framework.Testing; // We publish our internal attributes to other sub-projects of the framework. // Note, that we omit visual tests as they are meant to test the framework // behavior "in the wild". [assembly: InternalsVisibleTo("osu.Game.Rulesets.Osu.Tests")] -[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)] +[assembly: InternalsVisibleTo("osu.Game.Rulesets.Osu.Tests.Dynamic")] diff --git a/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs index b7ed9f86b0..77218af5e1 100644 --- a/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Taiko/Properties/AssemblyInfo.cs @@ -2,11 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Runtime.CompilerServices; -using osu.Framework.Testing; // We publish our internal attributes to other sub-projects of the framework. // Note, that we omit visual tests as they are meant to test the framework // behavior "in the wild". [assembly: InternalsVisibleTo("osu.Game.Rulesets.Taiko.Tests")] -[assembly: InternalsVisibleTo(DynamicClassCompiler.DYNAMIC_ASSEMBLY_NAME)] +[assembly: InternalsVisibleTo("osu.Game.Rulesets.Taiko.Tests.Dynamic")] diff --git a/osu.Game.Tests/Visual/TestCaseCursors.cs b/osu.Game.Tests/Visual/TestCaseCursors.cs index 72e699c54b..4f4fdbeb5b 100644 --- a/osu.Game.Tests/Visual/TestCaseCursors.cs +++ b/osu.Game.Tests/Visual/TestCaseCursors.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.MathUtils; -using osu.Framework.Testing.Input; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Sprites; using OpenTK; @@ -18,88 +17,74 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { [TestFixture] - public class TestCaseCursors : OsuTestCase + public class TestCaseCursors : ManualInputManagerTestCase { - private readonly ManualInputManager inputManager; private readonly CursorOverrideContainer cursorOverrideContainer; private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6]; public TestCaseCursors() { - Child = inputManager = new ManualInputManager + Child = cursorOverrideContainer = new CursorOverrideContainer { - Child = cursorOverrideContainer = new CursorOverrideContainer + RelativeSizeAxes = Axes.Both, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Children = new[] + // Middle user + cursorBoxes[0] = new CustomCursorBox(Color4.Green) { - // Middle user - cursorBoxes[0] = new CustomCursorBox(Color4.Green) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - }, - // Top-left user - cursorBoxes[1] = new CustomCursorBox(Color4.Blue) - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.4f) - }, - // Bottom-right user - cursorBoxes[2] = new CustomCursorBox(Color4.Red) - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.4f) - }, - // Bottom-left local - cursorBoxes[3] = new CustomCursorBox(Color4.Magenta, false) - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.4f) - }, - // Top-right local - cursorBoxes[4] = new CustomCursorBox(Color4.Cyan, false) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.4f) - }, - // Left-local - cursorBoxes[5] = new CustomCursorBox(Color4.Yellow, false) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.2f, 1), - }, - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + }, + // Top-left user + cursorBoxes[1] = new CustomCursorBox(Color4.Blue) + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.4f) + }, + // Bottom-right user + cursorBoxes[2] = new CustomCursorBox(Color4.Red) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.4f) + }, + // Bottom-left local + cursorBoxes[3] = new CustomCursorBox(Color4.Magenta, false) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.4f) + }, + // Top-right local + cursorBoxes[4] = new CustomCursorBox(Color4.Cyan, false) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.4f) + }, + // Left-local + cursorBoxes[5] = new CustomCursorBox(Color4.Yellow, false) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.2f, 1), + }, } }; - returnUserInput(); - AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b)); testUserCursor(); testLocalCursor(); testUserCursorOverride(); testMultipleLocalCursors(); - returnUserInput(); - } - - /// - /// Returns input back to the user. - /// - private void returnUserInput() - { - AddStep("Return user input", () => inputManager.UseParentState = true); + ReturnUserInput(); } /// @@ -109,7 +94,7 @@ namespace osu.Game.Tests.Visual /// private void testUserCursor() { - AddStep("Move to green area", () => inputManager.MoveMouseTo(cursorBoxes[0])); + AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0])); AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor)); AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor)); AddStep("Move out", moveOut); @@ -124,7 +109,7 @@ namespace osu.Game.Tests.Visual /// private void testLocalCursor() { - AddStep("Move to purple area", () => inputManager.MoveMouseTo(cursorBoxes[3])); + AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3])); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor)); AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor)); @@ -141,7 +126,7 @@ namespace osu.Game.Tests.Visual /// private void testUserCursorOverride() { - AddStep("Move to blue-green boundary", () => inputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor)); @@ -156,7 +141,7 @@ namespace osu.Game.Tests.Visual /// private void testMultipleLocalCursors() { - AddStep("Move to yellow-purple boundary", () => inputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); + AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor)); @@ -172,7 +157,7 @@ namespace osu.Game.Tests.Visual /// private void testUserOverrideWithLocal() { - AddStep("Move to yellow-blue boundary", () => inputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10))); + AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10))); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor)); @@ -186,7 +171,7 @@ namespace osu.Game.Tests.Visual /// Moves the cursor to a point not covered by any cursor containers. /// private void moveOut() - => inputManager.MoveMouseTo(new Vector2(inputManager.ScreenSpaceDrawQuad.Centre.X, inputManager.ScreenSpaceDrawQuad.TopLeft.Y)); + => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.TopLeft.Y)); /// /// Checks if a cursor is visible. @@ -199,7 +184,7 @@ namespace osu.Game.Tests.Visual /// /// The cursor to check. private bool checkAtMouse(CursorContainer cursorContainer) - => Precision.AlmostEquals(inputManager.CurrentState.Mouse.NativeState.Position, cursorContainer.ToScreenSpace(cursorContainer.ActiveCursor.DrawPosition)); + => Precision.AlmostEquals(InputManager.CurrentState.Mouse.NativeState.Position, cursorContainer.ToScreenSpace(cursorContainer.ActiveCursor.DrawPosition)); private class CustomCursorBox : Container, IProvideCursor { diff --git a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs similarity index 71% rename from osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs rename to osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs index 24f5905131..72d60d8e01 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs +++ b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs @@ -3,15 +3,16 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Timing; using OpenTK; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; -using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Screens.Compose.Layers; using osu.Game.Tests.Beatmaps; @@ -19,21 +20,23 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { [TestFixture] - public class TestCaseEditorSelectionLayer : EditorClockTestCase + public class TestCaseHitObjectComposer : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { - typeof(SelectionLayer), - typeof(SelectionBox), + typeof(MaskSelection), + typeof(DragLayer), typeof(HitObjectComposer), typeof(OsuHitObjectComposer), typeof(HitObjectMaskLayer), - typeof(HitObjectMask), - typeof(HitCircleMask), - typeof(SliderMask), - typeof(SliderCircleMask) + typeof(NotNullAttribute) }; + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + [BackgroundDependencyLoader] private void load(OsuGameBase osuGame) { @@ -59,6 +62,10 @@ namespace osu.Game.Tests.Visual }, }); + var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); + Child = new OsuHitObjectComposer(new OsuRuleset()); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 7050e34712..3afb22f0ad 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -35,11 +35,13 @@ namespace osu.Game.Database /// /// Fired when a new becomes available in the database. + /// This is not guaranteed to run on the update thread. /// public event Action ItemAdded; /// /// Fired when a is removed from the database. + /// This is not guaranteed to run on the update thread. /// public event Action ItemRemoved; diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index b9a35ec1f0..8056dbff3c 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -246,11 +246,11 @@ namespace osu.Game.Overlays.BeatmapSet if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd; } - private void handleBeatmapAdd(BeatmapSetInfo beatmap) + private void handleBeatmapAdd(BeatmapSetInfo beatmap) => Schedule(() => { if (beatmap.OnlineBeatmapSetID == BeatmapSet?.OnlineBeatmapSetID) downloadButtonsContainer.FadeOut(transition_duration); - } + }); private void download(bool noVideo) { diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 8d8a4aebaa..3f1aa04c36 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -188,12 +188,12 @@ namespace osu.Game.Overlays beatmaps.ItemAdded += setAdded; } - private void setAdded(BeatmapSetInfo set) + private void setAdded(BeatmapSetInfo set) => Schedule(() => { // if a new map was imported, we should remove it from search results (download completed etc.) panels?.FirstOrDefault(p => p.SetInfo.OnlineBeatmapSetID == set.OnlineBeatmapSetID)?.FadeOut(400).Expire(); BeatmapSets = BeatmapSets?.Where(b => b.OnlineBeatmapSetID != set.OnlineBeatmapSetID); - } + }); private void updateResultCounts() { @@ -323,6 +323,14 @@ namespace osu.Game.Overlays private int distinctCount(List list) => list.Distinct().ToArray().Length; + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmaps != null) + beatmaps.ItemAdded -= setAdded; + } + public class ResultCounts { public readonly int Artists; diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index ac7ec6257b..c981e5f493 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.ItemAdded += list.AddBeatmapSet; - beatmaps.ItemRemoved += list.RemoveBeatmapSet; + beatmaps.ItemAdded += handleBeatmapAdded; + beatmaps.ItemRemoved += handleBeatmapRemoved; list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); @@ -95,6 +95,9 @@ namespace osu.Game.Overlays.Music beatmapBacking.TriggerChange(); } + private void handleBeatmapAdded(BeatmapSetInfo setInfo) => Schedule(() => list.AddBeatmapSet(setInfo)); + private void handleBeatmapRemoved(BeatmapSetInfo setInfo) => Schedule(() => list.RemoveBeatmapSet(setInfo)); + protected override void PopIn() { filter.Search.HoldFocus = true; @@ -153,6 +156,17 @@ namespace osu.Game.Overlays.Music track.Restart(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmaps != null) + { + beatmaps.ItemAdded -= handleBeatmapAdded; + beatmaps.ItemRemoved -= handleBeatmapRemoved; + } + } } //todo: placeholder diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5df5304751..a2215035dd 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -21,9 +21,13 @@ namespace osu.Game.Overlays.Settings.Sections public override FontAwesome Icon => FontAwesome.fa_paint_brush; + private SkinManager skins; + [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skins) { + this.skins = skins; + FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { @@ -47,15 +51,29 @@ namespace osu.Game.Overlays.Settings.Sections }, }; - void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); - skins.ItemAdded += _ => reloadSkins(); - skins.ItemRemoved += _ => reloadSkins(); + skins.ItemAdded += onItemsChanged; + skins.ItemRemoved += onItemsChanged; reloadSkins(); skinDropdown.Bindable = config.GetBindable(OsuSetting.Skin); } + private void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); + + private void onItemsChanged(SkinInfo _) => Schedule(reloadSkins); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (skins != null) + { + skins.ItemAdded -= onItemsChanged; + skins.ItemRemoved -= onItemsChanged; + } + } + private class SizeSlider : OsuSliderBar { public override string TooltipText => Current.Value.ToString(@"0.##x"); diff --git a/osu.Game/Properties/AssemblyInfo.cs b/osu.Game/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9384740308 --- /dev/null +++ b/osu.Game/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Runtime.CompilerServices; + +// We publish our internal attributes to other sub-projects of the framework. +// Note, that we omit visual tests as they are meant to test the framework +// behavior "in the wild". + +[assembly: InternalsVisibleTo("osu.Game.Tests")] +[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")] diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 9d0246b2fc..1e7416e63d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -53,9 +53,6 @@ namespace osu.Game.Rulesets.Edit return; } - HitObjectMaskLayer hitObjectMaskLayer = new HitObjectMaskLayer(this); - SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield); - var layerBelowRuleset = new BorderLayer { RelativeSizeAxes = Axes.Both, @@ -63,12 +60,7 @@ namespace osu.Game.Rulesets.Edit }; var layerAboveRuleset = CreateLayerContainer(); - layerAboveRuleset.Children = new Drawable[] - { - selectionLayer, // Below object overlays for input - hitObjectMaskLayer, - selectionLayer.CreateProxy() // Proxy above object overlays for selections - }; + layerAboveRuleset.Child = new HitObjectMaskLayer(rulesetContainer.Playfield, this); layerContainers.Add(layerBelowRuleset); layerContainers.Add(layerAboveRuleset); @@ -110,11 +102,6 @@ namespace osu.Game.Rulesets.Edit } }; - selectionLayer.ObjectSelected += hitObjectMaskLayer.AddOverlay; - selectionLayer.ObjectDeselected += hitObjectMaskLayer.RemoveOverlay; - selectionLayer.SelectionCleared += hitObjectMaskLayer.RemoveSelectionOverlay; - selectionLayer.SelectionFinished += hitObjectMaskLayer.AddSelectionOverlay; - toolboxCollection.Items = CompositionTools.Select(t => new RadioButton(t.Name, () => setCompositionTool(t))) .Prepend(new RadioButton("Select", () => setCompositionTool(null))) @@ -149,11 +136,10 @@ namespace osu.Game.Rulesets.Edit public virtual HitObjectMask CreateMaskFor(DrawableHitObject hitObject) => null; /// - /// Creates a which outlines s - /// and handles all hitobject movement/pattern adjustments. + /// Creates a which outlines s + /// and handles hitobject pattern adjustments. /// - /// The overlays. - public virtual SelectionBox CreateSelectionOverlay(IReadOnlyList overlays) => new SelectionBox(overlays); + public virtual MaskSelection CreateMaskSelection() => new MaskSelection(); /// /// Creates a which provides a layer above or below the . diff --git a/osu.Game/Rulesets/Edit/HitObjectMask.cs b/osu.Game/Rulesets/Edit/HitObjectMask.cs index 051b42fec6..9f055ffc5d 100644 --- a/osu.Game/Rulesets/Edit/HitObjectMask.cs +++ b/osu.Game/Rulesets/Edit/HitObjectMask.cs @@ -1,21 +1,146 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using osu.Framework; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input; using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; namespace osu.Game.Rulesets.Edit { /// /// A mask placed above a adding editing functionality. /// - public class HitObjectMask : Container + public class HitObjectMask : CompositeDrawable, IStateful { + /// + /// Invoked when this has been selected. + /// + public event Action Selected; + + /// + /// Invoked when this has been deselected. + /// + public event Action Deselected; + + /// + /// Invoked when this has requested selection. + /// Will fire even if already selected. Does not actually perform selection. + /// + public event Action SelectionRequested; + + /// + /// Invoked when this has requested drag. + /// + public event Action DragRequested; + + /// + /// The which this applies to. + /// public readonly DrawableHitObject HitObject; + protected override bool ShouldBeAlive => HitObject.IsAlive && HitObject.IsPresent || State == SelectionState.Selected; + public override bool HandleMouseInput => ShouldBeAlive; + public override bool RemoveWhenNotAlive => false; + public HitObjectMask(DrawableHitObject hitObject) { HitObject = hitObject; + + AlwaysPresent = true; + Alpha = 0; } + + private SelectionState state; + + public event Action StateChanged; + + public SelectionState State + { + get => state; + set + { + if (state == value) return; + + state = value; + switch (state) + { + case SelectionState.Selected: + Show(); + Selected?.Invoke(this); + break; + case SelectionState.NotSelected: + Hide(); + Deselected?.Invoke(this); + break; + } + } + } + + /// + /// Selects this , causing it to become visible. + /// + public void Select() => State = SelectionState.Selected; + + /// + /// Deselects this , causing it to become invisible. + /// + public void Deselect() => State = SelectionState.NotSelected; + + public bool IsSelected => State == SelectionState.Selected; + + private bool selectionRequested; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + selectionRequested = false; + + if (State == SelectionState.NotSelected) + { + SelectionRequested?.Invoke(this, state); + selectionRequested = true; + } + + return IsSelected; + } + + protected override bool OnClick(InputState state) + { + if (State == SelectionState.Selected && !selectionRequested) + { + selectionRequested = true; + SelectionRequested?.Invoke(this, state); + return true; + } + + return base.OnClick(state); + } + + protected override bool OnDragStart(InputState state) => true; + + protected override bool OnDrag(InputState state) + { + DragRequested?.Invoke(this, state); + return true; + } + + /// + /// The screen-space point that causes this to be selected. + /// + public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; + + /// + /// The screen-space quad that outlines this for selections. + /// + public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; + } + + public enum SelectionState + { + NotSelected, + Selected } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 348364a2bf..fdfef14a88 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -6,14 +6,12 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Graphics.Primitives; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using OpenTK; using OpenTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -231,16 +229,6 @@ namespace osu.Game.Rulesets.Objects.Drawables protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) { } - - /// - /// The screen-space point that causes this to be selected in the Editor. - /// - public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; - - /// - /// The screen-space quad that outlines this for selections in the Editor. - /// - public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } public abstract class DrawableHitObject : DrawableHitObject diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/DragLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/DragLayer.cs new file mode 100644 index 0000000000..51bb61b607 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/DragLayer.cs @@ -0,0 +1,92 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Rulesets.Edit; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + /// + /// A layer that handles and displays drag selection for a collection of s. + /// + public class DragLayer : CompositeDrawable + { + private readonly Action performSelection; + + /// + /// Invoked when the drag selection has finished. + /// + public event Action DragEnd; + + private Drawable box; + + /// + /// Creates a new . + /// + /// The selectable s. + public DragLayer(Action performSelection) + { + this.performSelection = performSelection; + + RelativeSizeAxes = Axes.Both; + AlwaysPresent = true; + Alpha = 0; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = box = new Container + { + Masking = true, + BorderColour = Color4.White, + BorderThickness = MaskSelection.BORDER_RADIUS, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + } + }; + } + + protected override bool OnDragStart(InputState state) + { + this.FadeIn(250, Easing.OutQuint); + return true; + } + + protected override bool OnDrag(InputState state) + { + var dragPosition = state.Mouse.NativeState.Position; + var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition; + + var dragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y); + + // We use AABBFloat instead of RectangleF since it handles negative sizes for us + var dragRectangle = dragQuad.AABBFloat; + + var topLeft = ToLocalSpace(dragRectangle.TopLeft); + var bottomRight = ToLocalSpace(dragRectangle.BottomRight); + + box.Position = topLeft; + box.Size = bottomRight - topLeft; + + performSelection?.Invoke(dragRectangle); + return true; + } + + protected override bool OnDragEnd(InputState state) + { + this.FadeOut(250, Easing.OutQuint); + DragEnd?.Invoke(); + return true; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs index 46b09e2c23..423cf0ed29 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs @@ -2,65 +2,92 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Edit.Screens.Compose.Layers { public class HitObjectMaskLayer : CompositeDrawable { + private readonly Playfield playfield; private readonly HitObjectComposer composer; - private readonly Container overlayContainer; - public HitObjectMaskLayer(HitObjectComposer composer) + private MaskContainer maskContainer; + + public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer) { + // we need the playfield as HitObjects may not be initialised until its BDL. + this.playfield = playfield; + this.composer = composer; + RelativeSizeAxes = Axes.Both; + } - InternalChild = overlayContainer = new Container { RelativeSizeAxes = Axes.Both }; + [BackgroundDependencyLoader] + private void load() + { + maskContainer = new MaskContainer(); + + var maskSelection = composer.CreateMaskSelection(); + + maskContainer.MaskSelected += maskSelection.HandleSelected; + maskContainer.MaskDeselected += maskSelection.HandleDeselected; + maskContainer.MaskSelectionRequested += maskSelection.HandleSelectionRequested; + maskContainer.MaskDragRequested += maskSelection.HandleDrag; + + maskSelection.DeselectAll = maskContainer.DeselectAll; + + var dragLayer = new DragLayer(maskContainer.Select); + dragLayer.DragEnd += () => maskSelection.UpdateVisibility(); + + InternalChildren = new Drawable[] + { + dragLayer, + maskSelection, + maskContainer, + dragLayer.CreateProxy() + }; + + foreach (var obj in playfield.HitObjects.Objects) + addMask(obj); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + maskContainer.DeselectAll(); + return true; } /// - /// Adds an overlay for a which adds movement support. + /// Adds a mask for a which adds movement support. /// - /// The to create an overlay for. - public void AddOverlay(DrawableHitObject hitObject) + /// The to create a mask for. + private void addMask(DrawableHitObject hitObject) { - var overlay = composer.CreateMaskFor(hitObject); - if (overlay == null) + var mask = composer.CreateMaskFor(hitObject); + if (mask == null) return; - overlayContainer.Add(overlay); + maskContainer.Add(mask); } /// - /// Removes the overlay for a . + /// Removes the mask for a . /// - /// The to remove the overlay for. - public void RemoveOverlay(DrawableHitObject hitObject) + /// The to remove the mask for. + private void removeMask(DrawableHitObject hitObject) { - var existing = overlayContainer.FirstOrDefault(h => h.HitObject == hitObject); - if (existing == null) + var mask = maskContainer.FirstOrDefault(h => h.HitObject == hitObject); + if (mask == null) return; - existing.Hide(); - existing.Expire(); - } - - private SelectionBox currentSelectionBox; - - public void AddSelectionOverlay() - { - if (overlayContainer.Count > 0) - AddInternal(currentSelectionBox = composer.CreateSelectionOverlay(overlayContainer)); - } - - public void RemoveSelectionOverlay() - { - currentSelectionBox?.Hide(); - currentSelectionBox?.Expire(); + maskContainer.Remove(mask); } } } diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/MaskContainer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/MaskContainer.cs new file mode 100644 index 0000000000..b631628c9e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/MaskContainer.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Rulesets.Edit; +using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + public class MaskContainer : Container + { + /// + /// Invoked when any is selected. + /// + public event Action MaskSelected; + + /// + /// Invoked when any is deselected. + /// + public event Action MaskDeselected; + + /// + /// Invoked when any requests selection. + /// + public event Action MaskSelectionRequested; + + /// + /// Invoked when any requests drag. + /// + public event Action MaskDragRequested; + + private IEnumerable aliveMasks => AliveInternalChildren.Cast(); + + public MaskContainer() + { + RelativeSizeAxes = Axes.Both; + } + + public override void Add(HitObjectMask drawable) + { + base.Add(drawable); + + drawable.Selected += onMaskSelected; + drawable.Deselected += onMaskDeselected; + drawable.SelectionRequested += onSelectionRequested; + drawable.DragRequested += onDragRequested; + } + + public override bool Remove(HitObjectMask drawable) + { + var result = base.Remove(drawable); + + if (result) + { + drawable.Selected -= onMaskSelected; + drawable.Deselected -= onMaskDeselected; + drawable.SelectionRequested -= onSelectionRequested; + drawable.DragRequested -= onDragRequested; + } + + return result; + } + + /// + /// Select all masks in a given rectangle selection area. + /// + /// The rectangle to perform a selection on in screen-space coordinates. + public void Select(RectangleF rect) + { + foreach (var mask in aliveMasks.ToList()) + { + if (mask.IsPresent && rect.Contains(mask.SelectionPoint)) + mask.Select(); + else + mask.Deselect(); + } + } + + /// + /// Deselects all selected s. + /// + public void DeselectAll() => aliveMasks.ToList().ForEach(m => m.Deselect()); + + private void onMaskSelected(HitObjectMask mask) + { + MaskSelected?.Invoke(mask); + ChangeChildDepth(mask, 1); + } + + private void onMaskDeselected(HitObjectMask mask) + { + MaskDeselected?.Invoke(mask); + ChangeChildDepth(mask, 0); + } + + private void onSelectionRequested(HitObjectMask mask, InputState state) => MaskSelectionRequested?.Invoke(mask, state); + private void onDragRequested(HitObjectMask mask, InputState state) => MaskDragRequested?.Invoke(mask, state); + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is HitObjectMask xMask) || !(y is HitObjectMask yMask)) + return base.Compare(x, y); + return Compare(xMask, yMask); + } + + public int Compare(HitObjectMask x, HitObjectMask y) + { + // dpeth is used to denote selected status (we always want selected masks to handle input first). + int d = x.Depth.CompareTo(y.Depth); + if (d != 0) + return d; + + // Put earlier hitobjects towards the end of the list, so they handle input first + int i = y.HitObject.HitObject.StartTime.CompareTo(x.HitObject.HitObject.StartTime); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/MaskSelection.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/MaskSelection.cs new file mode 100644 index 0000000000..76b8027b07 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/MaskSelection.cs @@ -0,0 +1,164 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Types; +using OpenTK; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + /// + /// A box which surrounds s and provides interactive handles, context menus etc. + /// + public class MaskSelection : CompositeDrawable + { + public const float BORDER_RADIUS = 2; + + private readonly List selectedMasks; + + private Drawable outline; + + public MaskSelection() + { + selectedMasks = new List(); + + RelativeSizeAxes = Axes.Both; + AlwaysPresent = true; + Alpha = 0; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChild = outline = new Container + { + Masking = true, + BorderThickness = BORDER_RADIUS, + BorderColour = colours.Yellow, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0 + } + }; + } + + #region User Input Handling + + public void HandleDrag(HitObjectMask m, InputState state) + { + // Todo: Various forms of snapping + + foreach (var mask in selectedMasks) + { + switch (mask.HitObject.HitObject) + { + case IHasEditablePosition editablePosition: + editablePosition.OffsetPosition(state.Mouse.Delta); + break; + } + } + } + + #endregion + + #region Selection Handling + + /// + /// Bind an action to deselect all selected masks. + /// + public Action DeselectAll { private get; set; } + + /// + /// Handle a mask becoming selected. + /// + /// The mask. + public void HandleSelected(HitObjectMask mask) => selectedMasks.Add(mask); + + /// + /// Handle a mask becoming deselected. + /// + /// The mask. + public void HandleDeselected(HitObjectMask mask) + { + selectedMasks.Remove(mask); + + // We don't want to update visibility if > 0, since we may be deselecting masks during drag-selection + if (selectedMasks.Count == 0) + UpdateVisibility(); + } + + /// + /// Handle a mask requesting selection. + /// + /// The mask. + public void HandleSelectionRequested(HitObjectMask mask, InputState state) + { + if (state.Keyboard.ControlPressed) + { + if (mask.IsSelected) + mask.Deselect(); + else + mask.Select(); + } + else + { + if (mask.IsSelected) + return; + + DeselectAll?.Invoke(); + mask.Select(); + } + + UpdateVisibility(); + } + + #endregion + + /// + /// Updates whether this is visible. + /// + internal void UpdateVisibility() + { + if (selectedMasks.Count > 0) + Show(); + else + Hide(); + } + + protected override void Update() + { + base.Update(); + + if (selectedMasks.Count == 0) + return; + + // Move the rectangle to cover the hitobjects + var topLeft = new Vector2(float.MaxValue, float.MaxValue); + var bottomRight = new Vector2(float.MinValue, float.MinValue); + + bool hasSelection = false; + + foreach (var mask in selectedMasks) + { + topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(mask.SelectionQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(mask.SelectionQuad.BottomRight)); + } + + topLeft -= new Vector2(5); + bottomRight += new Vector2(5); + + outline.Size = bottomRight - topLeft; + outline.Position = topLeft; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs deleted file mode 100644 index 0e5d824559..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Game.Graphics; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Edit.Types; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; - -namespace osu.Game.Screens.Edit.Screens.Compose.Layers -{ - /// - /// A box which surrounds s and provides interactive handles, context menus etc. - /// - public class SelectionBox : VisibilityContainer - { - private readonly IReadOnlyList overlays; - - public const float BORDER_RADIUS = 2; - - public SelectionBox(IReadOnlyList overlays) - { - this.overlays = overlays; - - Masking = true; - BorderThickness = BORDER_RADIUS; - - InternalChild = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 - }; - - State = Visibility.Visible; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BorderColour = colours.Yellow; - } - - protected override void Update() - { - base.Update(); - - // Todo: We might need to optimise this - - // Move the rectangle to cover the hitobjects - var topLeft = new Vector2(float.MaxValue, float.MaxValue); - var bottomRight = new Vector2(float.MinValue, float.MinValue); - - foreach (var obj in overlays) - { - topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.TopLeft)); - bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.BottomRight)); - } - - topLeft -= new Vector2(5); - bottomRight += new Vector2(5); - - Size = bottomRight - topLeft; - Position = topLeft; - } - - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => overlays.Any(o => o.ReceiveMouseInputAt(screenSpacePos)); - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; - - protected override bool OnDragStart(InputState state) => true; - - protected override bool OnDrag(InputState state) - { - // Todo: Various forms of snapping - foreach (var hitObject in overlays.Select(o => o.HitObject.HitObject)) - { - switch (hitObject) - { - case IHasEditablePosition editablePosition: - editablePosition.OffsetPosition(state.Mouse.Delta); - break; - } - } - return true; - } - - protected override bool OnDragEnd(InputState state) => true; - - public override bool DisposeOnDeathRemoval => true; - - protected override void PopIn() => this.FadeIn(); - protected override void PopOut() => this.FadeOut(); - } -} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs deleted file mode 100644 index ab51385980..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI; -using OpenTK; -using OpenTK.Graphics; -using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; - -namespace osu.Game.Screens.Edit.Screens.Compose.Layers -{ - public class SelectionLayer : CompositeDrawable - { - /// - /// Invoked when a is selected. - /// - public event Action ObjectSelected; - - /// - /// Invoked when a is deselected. - /// - public event Action ObjectDeselected; - - /// - /// Invoked when the selection has been cleared. - /// - public event Action SelectionCleared; - - /// - /// Invoked when the user has finished selecting all s. - /// - public event Action SelectionFinished; - - private readonly Playfield playfield; - - public SelectionLayer(Playfield playfield) - { - this.playfield = playfield; - - RelativeSizeAxes = Axes.Both; - } - - private DragBox dragBox; - - private readonly HashSet selectedHitObjects = new HashSet(); - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - DeselectAll(); - return true; - } - - protected override bool OnDragStart(InputState state) - { - AddInternal(dragBox = new DragBox()); - return true; - } - - protected override bool OnDrag(InputState state) - { - dragBox.Show(); - - var dragPosition = state.Mouse.NativeState.Position; - var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition; - - var screenSpaceDragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y); - - dragBox.SetDragRectangle(screenSpaceDragQuad.AABBFloat); - selectQuad(screenSpaceDragQuad); - - return true; - } - - protected override bool OnDragEnd(InputState state) - { - dragBox.Hide(); - dragBox.Expire(); - - finishSelection(); - - return true; - } - - protected override bool OnClick(InputState state) - { - selectPoint(state.Mouse.NativeState.Position); - finishSelection(); - - return true; - } - - /// - /// Selects a . - /// - /// The to select. - public void Select(DrawableHitObject hitObject) - { - if (!select(hitObject)) - return; - - clearSelection(); - finishSelection(); - } - - /// - /// Selects a without performing capture updates. - /// - /// The to select. - /// Whether was selected. - private bool select(DrawableHitObject hitObject) - { - if (!selectedHitObjects.Add(hitObject)) - return false; - - ObjectSelected?.Invoke(hitObject); - return true; - } - - /// - /// Deselects a . - /// - /// The to deselect. - public void Deselect(DrawableHitObject hitObject) - { - if (!deselect(hitObject)) - return; - - clearSelection(); - finishSelection(); - } - - /// - /// Deselects a without performing capture updates. - /// - /// The to deselect. - /// Whether the was deselected. - private bool deselect(DrawableHitObject hitObject) - { - if (!selectedHitObjects.Remove(hitObject)) - return false; - - ObjectDeselected?.Invoke(hitObject); - return true; - } - - /// - /// Deselects all selected s. - /// - public void DeselectAll() - { - selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h)); - selectedHitObjects.Clear(); - - clearSelection(); - } - - /// - /// Selects all hitobjects that are present within the area of a . - /// - /// The selection . - // Todo: If needed we can severely reduce allocations in this method - private void selectQuad(Quad screenSpaceQuad) - { - var expectedSelection = playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint)).ToList(); - - var toRemove = selectedHitObjects.Except(expectedSelection).ToList(); - foreach (var obj in toRemove) - deselect(obj); - - expectedSelection.ForEach(h => select(h)); - } - - /// - /// Selects the top-most hitobject that is present under a specific point. - /// - /// The to select at. - private void selectPoint(Vector2 screenSpacePoint) - { - var target = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint)); - if (target == null) - return; - - select(target); - } - - private void clearSelection() => SelectionCleared?.Invoke(); - - private void finishSelection() - { - if (selectedHitObjects.Count == 0) - return; - SelectionFinished?.Invoke(); - } - - /// - /// A box that represents a drag selection. - /// - private class DragBox : VisibilityContainer - { - /// - /// Creates a new . - /// - public DragBox() - { - Masking = true; - BorderColour = Color4.White; - BorderThickness = SelectionBox.BORDER_RADIUS; - - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f - }; - } - - public void SetDragRectangle(RectangleF rectangle) - { - var topLeft = Parent.ToLocalSpace(rectangle.TopLeft); - var bottomRight = Parent.ToLocalSpace(rectangle.BottomRight); - - Position = topLeft; - Size = bottomRight - topLeft; - } - - public override bool DisposeOnDeathRemoval => true; - - protected override void PopIn() => this.FadeIn(250, Easing.OutQuint); - protected override void PopOut() => this.FadeOut(250, Easing.OutQuint); - } - } -} diff --git a/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs b/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs new file mode 100644 index 0000000000..c2595231c9 --- /dev/null +++ b/osu.Game/Tests/Visual/ManualInputManagerTestCase.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing.Input; + +namespace osu.Game.Tests.Visual +{ + public abstract class ManualInputManagerTestCase : OsuTestCase + { + protected override Container Content => InputManager; + protected readonly ManualInputManager InputManager; + + protected ManualInputManagerTestCase() + { + base.Content.Add(InputManager = new ManualInputManager()); + ReturnUserInput(); + } + + /// + /// Returns input back to the user. + /// + protected void ReturnUserInput() + { + AddStep("Return user input", () => InputManager.UseParentState = true); + } + } +}