diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 28a83fbbae..510b53054b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -111,7 +111,6 @@ platform :ios do souyuz( platform: "ios", - build_target: "osu_iOS", plist_path: "../osu.iOS/Info.plist" ) end diff --git a/osu.Android.props b/osu.Android.props index 2ccba60424..5497a82a7a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -54,6 +54,6 @@ - + diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index ff3dbe614a..9f57160f99 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { - public class ManiaSelectionBlueprint : SelectionBlueprint + public class ManiaSelectionBlueprint : OverlaySelectionBlueprint { public Vector2 ScreenSpaceDragPosition { get; private set; } public Vector2 DragPosition { get; private set; } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index 5f66ae7491..d744036b4c 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Edit { } - public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) + public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) { switch (hitObject) { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 618af3e772..9069a636a8 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Timing; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.UI; @@ -70,10 +71,12 @@ namespace osu.Game.Rulesets.Mania.Edit // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen. // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height. if (scrollingInfo.Direction.Value == ScrollingDirection.Down) - delta -= moveEvent.Blueprint.DrawableObject.Parent.DrawHeight; + delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: probably wrong - foreach (var b in SelectedBlueprints) + foreach (var selectionBlueprint in SelectedBlueprints) { + var b = (OverlaySelectionBlueprint)selectionBlueprint; + var hitObject = b.DrawableObject; var objectParent = (HitObjectContainer)hitObject.Parent; diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs index ff8882124f..433db79ae0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Edit.Masks { - public abstract class ManiaSelectionBlueprint : SelectionBlueprint + public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint { protected ManiaSelectionBlueprint(DrawableHitObject drawableObject) : base(drawableObject) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs index a864257274..b0e13808a5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs @@ -7,10 +7,10 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Edit.Blueprints { - public abstract class OsuSelectionBlueprint : SelectionBlueprint + public abstract class OsuSelectionBlueprint : OverlaySelectionBlueprint where T : OsuHitObject { - protected T HitObject => (T)DrawableObject.HitObject; + protected new T HitObject => (T)DrawableObject.HitObject; protected OsuSelectionBlueprint(DrawableHitObject drawableObject) : base(drawableObject) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs index f09279ed73..a0392fe536 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders : base(slider) { this.position = position; + InternalChild = CirclePiece = new HitCirclePiece(); Select(); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 4fdead512a..c18b3b0ff3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), }; - public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint; + public override Vector2 SelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 30682616e6..330f34b85c 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); - public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) + public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) { switch (hitObject) { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs similarity index 93% rename from osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs rename to osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs index e9372bd134..e7b2508ac7 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneTimelineBlueprintContainer.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -22,12 +23,11 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Editor { [TestFixture] - public class TestSceneEditorComposeTimeline : EditorClockTestScene + public class TestSceneTimelineBlueprintContainer : EditorClockTestScene { public override IReadOnlyList RequiredTypes => new[] { typeof(TimelineArea), - typeof(TimelineHitObjectDisplay), typeof(Timeline), typeof(TimelineButton), typeof(CentreMarker) @@ -38,9 +38,10 @@ namespace osu.Game.Tests.Visual.Editor { Beatmap.Value = new WaveformTestBeatmap(audio); - var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap); + var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap, BeatDivisor); Dependencies.Cache(editorBeatmap); + Dependencies.CacheAs(editorBeatmap); Children = new Drawable[] { @@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editor }, new TimelineArea { - Child = new TimelineHitObjectDisplay(editorBeatmap), + Child = new TimelineBlueprintContainer(), Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs new file mode 100644 index 0000000000..d7f23f5cc0 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Screens.Menu; + +namespace osu.Game.Tests.Visual.Menus +{ + public class TestSceneSongTicker : OsuTestScene + { + [Cached] + private MusicController musicController = new MusicController(); + + public TestSceneSongTicker() + { + AddRange(new Drawable[] + { + musicController, + new SongTicker + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new NowPlayingOverlay + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + State = { Value = Visibility.Visible } + } + }); + } + } +} diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 132b104afb..71ae47dc66 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -437,6 +437,53 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1); } + [Test] + public void TestFilteringByUserStarDifficulty() + { + BeatmapSetInfo set = null; + + loadBeatmaps(new List()); + + AddStep("add mixed difficulty set", () => + { + set = createTestBeatmapSet(1); + set.Beatmaps.Clear(); + + for (int i = 1; i <= 15; i++) + { + set.Beatmaps.Add(new BeatmapInfo + { + Version = $"Stars: {i}", + StarDifficulty = i, + }); + } + + carousel.UpdateBeatmapSet(set); + }); + + AddStep("select added set", () => carousel.SelectBeatmap(set.Beatmaps[0], false)); + + AddStep("filter [5..]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 5 } })); + AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask); + checkVisibleItemCount(true, 11); + + AddStep("filter to [0..7]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Max = 7 } })); + AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask); + checkVisibleItemCount(true, 7); + + AddStep("filter to [5..7]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 5, Max = 7 } })); + AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask); + checkVisibleItemCount(true, 3); + + AddStep("filter [2..2]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 2, Max = 2 } })); + AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask); + checkVisibleItemCount(true, 1); + + AddStep("filter to [0..]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 0 } })); + AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask); + checkVisibleItemCount(true, 15); + } + private void loadBeatmaps(List beatmapSets = null) { createCarousel(); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 447d52d980..009da0656b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -239,11 +239,11 @@ namespace osu.Game.Beatmaps.Formats break; case @"Source": - beatmap.BeatmapInfo.Metadata.Source = pair.Value; + metadata.Source = pair.Value; break; case @"Tags": - beatmap.BeatmapInfo.Metadata.Tags = pair.Value; + metadata.Tags = pair.Value; break; case @"BeatmapID": @@ -300,13 +300,11 @@ namespace osu.Game.Beatmaps.Formats switch (type) { case LegacyEventType.Background: - string bgFilename = split[2].Trim('"'); - beatmap.BeatmapInfo.Metadata.BackgroundFile = bgFilename.ToStandardisedPath(); + beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); break; case LegacyEventType.Video: - string videoFilename = split[2].Trim('"'); - beatmap.BeatmapInfo.Metadata.VideoFile = videoFilename.ToStandardisedPath(); + beatmap.BeatmapInfo.Metadata.VideoFile = CleanFilename(split[2]); break; case LegacyEventType.Break: diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index f55e24245b..0ec80eee41 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; @@ -115,7 +116,7 @@ namespace osu.Game.Beatmaps.Formats protected KeyValuePair SplitKeyVal(string line, char separator = ':') { - var split = line.Trim().Split(new[] { separator }, 2); + var split = line.Split(separator, 2); return new KeyValuePair ( @@ -124,6 +125,8 @@ namespace osu.Game.Beatmaps.Formats ); } + protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath(); + protected enum Section { None, diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index c46634e72f..35576e0f33 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.IO; using osuTK; using osuTK.Graphics; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.IO; using osu.Game.Storyboards; @@ -65,13 +64,16 @@ namespace osu.Game.Beatmaps.Formats private void handleEvents(string line) { var depth = 0; + var lineSpan = line.AsSpan(); - while (line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal)) + while (lineSpan.StartsWith(" ", StringComparison.Ordinal) || lineSpan.StartsWith("_", StringComparison.Ordinal)) { + lineSpan = lineSpan.Slice(1); ++depth; - line = line.Substring(1); } + line = lineSpan.ToString(); + decodeVariables(ref line); string[] split = line.Split(','); @@ -89,7 +91,7 @@ namespace osu.Game.Beatmaps.Formats { var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); - var path = cleanFilename(split[3]); + var path = CleanFilename(split[3]); var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); @@ -101,7 +103,7 @@ namespace osu.Game.Beatmaps.Formats { var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); - var path = cleanFilename(split[3]); + var path = CleanFilename(split[3]); var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); var frameCount = int.Parse(split[6]); @@ -116,7 +118,7 @@ namespace osu.Game.Beatmaps.Formats { var time = double.Parse(split[1], CultureInfo.InvariantCulture); var layer = parseLayer(split[2]); - var path = cleanFilename(split[3]); + var path = CleanFilename(split[3]); var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume)); break; @@ -331,7 +333,5 @@ namespace osu.Game.Beatmaps.Formats break; } } - - private string cleanFilename(string path) => path.Trim('"').ToStandardisedPath(); } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 7e4ab58e36..4155381790 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowConvertedBeatmaps, true); Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); - Set(OsuSetting.DisplayStarsMaximum, 10.0, 0, 10, 0.1); + Set(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1); Set(OsuSetting.SongSelectGroupingMode, GroupMode.All); Set(OsuSetting.SongSelectSortingMode, SortMode.Title); diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index deffa863dd..9bdd227957 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; @@ -85,14 +84,6 @@ namespace osu.Game.Graphics.UserInterface set => strip.Colour = value; } - protected override TabFillFlowContainer CreateTabFlow() => new OsuTabFillFlowContainer - { - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.Both, - Depth = -1, - Masking = true - }; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -284,10 +275,5 @@ namespace osu.Game.Graphics.UserInterface } } } - - private class OsuTabFillFlowContainer : TabFillFlowContainer - { - protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); - } } } diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs index 2e4d8ce729..104495ae01 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -9,6 +9,7 @@ using osuTK; using System; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; namespace osu.Game.Overlays.Chat.Tabs { @@ -113,5 +114,18 @@ namespace osu.Game.Overlays.Chat.Tabs OnRequestLeave?.Invoke(tab.Value); } + + protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.Both, + Depth = -1, + Masking = true + }; + + private class ChannelTabFillFlowContainer : TabFillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs index a5f56ae76e..0c42247993 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs @@ -1,7 +1,9 @@ // 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 osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -10,11 +12,20 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { public class SongSelectSettings : SettingsSubsection { + private Bindable minStars; + private Bindable maxStars; + protected override string Header => "Song Select"; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { + minStars = config.GetBindable(OsuSetting.DisplayStarsMinimum); + maxStars = config.GetBindable(OsuSetting.DisplayStarsMaximum); + + minStars.ValueChanged += min => maxStars.Value = Math.Max(min.NewValue, maxStars.Value); + maxStars.ValueChanged += max => minStars.Value = Math.Min(max.NewValue, minStars.Value); + Children = new Drawable[] { new SettingsCheckbox @@ -27,19 +38,19 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Show converted beatmaps", Bindable = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), }, - new SettingsSlider + new SettingsSlider { LabelText = "Display beatmaps from", Bindable = config.GetBindable(OsuSetting.DisplayStarsMinimum), KeyboardStep = 0.1f, - Keywords = new[] { "star", "difficulty" } + Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, - new SettingsSlider + new SettingsSlider { LabelText = "up to", Bindable = config.GetBindable(OsuSetting.DisplayStarsMaximum), KeyboardStep = 0.1f, - Keywords = new[] { "star", "difficulty" } + Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsEnumDropdown { @@ -49,7 +60,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }; } - private class StarSlider : OsuSliderBar + private class MaximumStarsSlider : StarsSlider + { + public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText; + } + + private class StarsSlider : OsuSliderBar { public override string TooltipText => Current.Value.ToString(@"0.## stars"); } diff --git a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs new file mode 100644 index 0000000000..b4ae3f3fba --- /dev/null +++ b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + public abstract class OverlaySelectionBlueprint : SelectionBlueprint + { + /// + /// The which this applies to. + /// + public readonly DrawableHitObject DrawableObject; + + protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected; + + protected OverlaySelectionBlueprint(DrawableHitObject drawableObject) + : base(drawableObject.HitObject) + { + DrawableObject = drawableObject; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos); + + public override Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre; + + public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad; + + public override Vector2 GetInstantDelta(Vector2 screenSpacePosition) => DrawableObject.Parent.ToLocalSpace(screenSpacePosition) - DrawableObject.Position; + } +} diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index bf99f83e0b..a972d28480 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Edit /// public abstract class SelectionBlueprint : CompositeDrawable, IStateful { + public readonly HitObject HitObject; + /// /// Invoked when this has been selected. /// @@ -30,26 +32,24 @@ namespace osu.Game.Rulesets.Edit /// public event Action Deselected; - /// - /// The which this applies to. - /// - public readonly DrawableHitObject DrawableObject; - - protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected; public override bool HandlePositionalInput => ShouldBeAlive; public override bool RemoveWhenNotAlive => false; [Resolved(CanBeNull = true)] private HitObjectComposer composer { get; set; } - protected SelectionBlueprint(DrawableHitObject drawableObject) + protected SelectionBlueprint(HitObject hitObject) { - DrawableObject = drawableObject; + HitObject = hitObject; RelativeSizeAxes = Axes.Both; - AlwaysPresent = true; - Alpha = 0; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); } private SelectionState state; @@ -66,58 +66,68 @@ namespace osu.Game.Rulesets.Edit state = value; - switch (state) - { - case SelectionState.Selected: - Show(); - Selected?.Invoke(this); - break; - - case SelectionState.NotSelected: - Hide(); - Deselected?.Invoke(this); - break; - } + if (IsLoaded) + updateState(); StateChanged?.Invoke(state); } } + private void updateState() + { + switch (state) + { + case SelectionState.Selected: + OnSelected(); + Selected?.Invoke(this); + break; + + case SelectionState.NotSelected: + OnDeselected(); + Deselected?.Invoke(this); + break; + } + } + + protected virtual void OnDeselected() => Hide(); + + protected virtual void OnSelected() => Show(); + // When not selected, input is only required for the blueprint itself to receive IsHovering protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected; /// - /// Selects this , causing it to become visible. + /// Selects this , causing it to become visible. /// public void Select() => State = SelectionState.Selected; /// - /// Deselects this , causing it to become invisible. + /// Deselects this , causing it to become invisible. /// public void Deselect() => State = SelectionState.NotSelected; public bool IsSelected => State == SelectionState.Selected; /// - /// Updates the , invoking and re-processing the beatmap. + /// Updates the , invoking and re-processing the beatmap. /// - protected void UpdateHitObject() => composer?.UpdateHitObject(DrawableObject.HitObject); - - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos); + protected void UpdateHitObject() => composer?.UpdateHitObject(HitObject); /// - /// The s to be displayed in the context menu for this . + /// The s to be displayed in the context menu for this . /// public virtual MenuItem[] ContextMenuItems => Array.Empty(); /// - /// The screen-space point that causes this to be selected. + /// The screen-space point that causes this to be selected. /// - public virtual Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre; + public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; /// - /// The screen-space quad that outlines this for selections. + /// The screen-space quad that outlines this for selections. /// - public virtual Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad; + public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; + + public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position; } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 119635ccd5..4a7c3f26bc 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -11,20 +11,24 @@ using osu.Game.Beatmaps; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { + public class TimelinePart : TimelinePart + { + } + /// /// Represents a part of the summary timeline.. /// - public class TimelinePart : Container + public class TimelinePart : Container where T : Drawable { protected readonly IBindable Beatmap = new Bindable(); - private readonly Container timeline; + private readonly Container content; - protected override Container Content => timeline; + protected override Container Content => content; - public TimelinePart() + public TimelinePart(Container content = null) { - AddInternal(timeline = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); Beatmap.ValueChanged += b => { @@ -44,17 +48,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts // the track may not be loaded completely (only has a length once it is). if (!Beatmap.Value.Track.IsLoaded) { - timeline.RelativeChildSize = Vector2.One; + content.RelativeChildSize = Vector2.One; Schedule(updateRelativeChildSize); return; } - timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); + content.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); } protected virtual void LoadBeatmap(WorkingBeatmap beatmap) { - timeline.Clear(); + content.Clear(); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index ce24eb579b..6b21f56567 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -31,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected DragBox DragBox { get; private set; } - private SelectionBlueprintContainer selectionBlueprints; + private Container selectionBlueprints; private SelectionHandler selectionHandler; @@ -41,6 +42,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [Resolved] private EditorBeatmap beatmap { get; set; } + private readonly BindableList selectedHitObjects = new BindableList(); + [Resolved(canBeNull: true)] private IDistanceSnapProvider snapProvider { get; set; } @@ -59,12 +62,25 @@ namespace osu.Game.Screens.Edit.Compose.Components { DragBox = CreateDragBox(select), selectionHandler, - selectionBlueprints = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }, + selectionBlueprints = CreateSelectionBlueprintContainer(), DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); foreach (var obj in beatmap.HitObjects) AddBlueprintFor(obj); + + selectedHitObjects.BindTo(beatmap.SelectedHitObjects); + selectedHitObjects.ItemsAdded += objects => + { + foreach (var o in objects) + selectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Select(); + }; + + selectedHitObjects.ItemsRemoved += objects => + { + foreach (var o in objects) + selectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect(); + }; } protected override void LoadComplete() @@ -75,6 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components beatmap.HitObjectRemoved += removeBlueprintFor; } + protected virtual Container CreateSelectionBlueprintContainer() => + new Container { RelativeSizeAxes = Axes.Both }; + /// /// Creates a which outlines s and handles movement of selections. /// @@ -91,6 +110,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { beginClickSelection(e); + prepareSelectionMovement(); + return e.Button == MouseButton.Left; } @@ -118,7 +139,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (clickedBlueprint == null) return false; - adjustableClock?.Seek(clickedBlueprint.DrawableObject.HitObject.StartTime); + adjustableClock?.Seek(clickedBlueprint.HitObject.StartTime); return true; } @@ -126,6 +147,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Special case for when a drag happened instead of a click Schedule(() => endClickSelection()); + + finishSelectionMovement(); } protected override bool OnDragStart(DragStartEvent e) @@ -133,15 +156,16 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return false; - if (!beginSelectionMovement()) - { - if (!DragBox.UpdateDrag(e)) - return false; + if (movementBlueprint != null) + return true; - DragBox.FadeIn(250, Easing.OutQuint); + if (DragBox.HandleDrag(e)) + { + DragBox.Show(); + return true; } - return true; + return false; } protected override void OnDrag(DragEvent e) @@ -149,8 +173,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return; - if (!moveCurrentSelection(e)) - DragBox.UpdateDrag(e); + if (DragBox.State == Visibility.Visible) + DragBox.HandleDrag(e); + + moveCurrentSelection(e); } protected override void OnDragEnd(DragEndEvent e) @@ -158,9 +184,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return; - if (!finishSelectionMovement()) + if (DragBox.State == Visibility.Visible) { - DragBox.FadeOut(250, Easing.OutQuint); + DragBox.Hide(); selectionHandler.UpdateVisibility(); } } @@ -200,7 +226,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void removeBlueprintFor(HitObject hitObject) { - var blueprint = selectionBlueprints.SingleOrDefault(m => m.DrawableObject.HitObject == hitObject); + var blueprint = selectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject); if (blueprint == null) return; @@ -248,7 +274,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) return; - foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveBlueprints) + foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveChildren) { if (blueprint.IsHovered) { @@ -305,6 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { selectionHandler.HandleSelected(blueprint); selectionBlueprints.ChangeChildDepth(blueprint, 1); + beatmap.SelectedHitObjects.Add(blueprint.HitObject); SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects); } @@ -313,6 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { selectionHandler.HandleDeselected(blueprint); selectionBlueprints.ChangeChildDepth(blueprint, 0); + beatmap.SelectedHitObjects.Remove(blueprint.HitObject); SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects); } @@ -321,27 +349,25 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2? screenSpaceMovementStartPosition; + private Vector2? movementBlueprintOriginalPosition; private SelectionBlueprint movementBlueprint; /// /// Attempts to begin the movement of any selected blueprints. /// - /// Whether movement began. - private bool beginSelectionMovement() + private void prepareSelectionMovement() { - Debug.Assert(movementBlueprint == null); + if (!selectionHandler.SelectedBlueprints.Any()) + return; // Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement // A special case is added for when a click selection occurred before the drag if (!clickSelectionBegan && !selectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) - return false; + return; // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject - movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.DrawableObject.HitObject.StartTime).First(); - screenSpaceMovementStartPosition = movementBlueprint.DrawableObject.ToScreenSpace(movementBlueprint.DrawableObject.OriginPosition); - - return true; + movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); + movementBlueprintOriginalPosition = movementBlueprint.SelectionPoint; // todo: unsure if correct } /// @@ -354,17 +380,17 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprint == null) return false; - Debug.Assert(screenSpaceMovementStartPosition != null); + Debug.Assert(movementBlueprintOriginalPosition != null); - Vector2 startPosition = screenSpaceMovementStartPosition.Value; - HitObject draggedObject = movementBlueprint.DrawableObject.HitObject; + HitObject draggedObject = movementBlueprint.HitObject; // The final movement position, relative to screenSpaceMovementStartPosition - Vector2 movePosition = startPosition + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); // Move the hitobjects - if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition)))) + if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition)))) return true; // Apply the start time at the newly snapped-to position @@ -384,7 +410,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprint == null) return false; - screenSpaceMovementStartPosition = null; + movementBlueprintOriginalPosition = null; movementBlueprint = null; return true; @@ -402,30 +428,5 @@ namespace osu.Game.Screens.Edit.Compose.Components beatmap.HitObjectRemoved -= removeBlueprintFor; } } - - private class SelectionBlueprintContainer : Container - { - public IEnumerable AliveBlueprints => AliveInternalChildren.Cast(); - - protected override int Compare(Drawable x, Drawable y) - { - if (!(x is SelectionBlueprint xBlueprint) || !(y is SelectionBlueprint yBlueprint)) - return base.Compare(x, y); - - return Compare(xBlueprint, yBlueprint); - } - - public int Compare(SelectionBlueprint x, SelectionBlueprint y) - { - // dpeth is used to denote selected status (we always want selected blueprints 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.DrawableObject.HitObject.StartTime.CompareTo(x.DrawableObject.HitObject.StartTime); - return i == 0 ? CompareReverseChildID(x, y) : i; - } - } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 1576def38e..3c41dead5d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -119,7 +119,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return CreateBlueprintFor(drawable); } - public virtual SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; + public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; protected override void AddBlueprintFor(HitObject hitObject) { diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index f522ca356f..c5f1bd1575 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,11 +16,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// A box that displays the drag selection and provides selection events for users to handle. /// - public class DragBox : CompositeDrawable + public class DragBox : CompositeDrawable, IStateful { - private readonly Action performSelection; + protected readonly Action PerformSelection; - private Drawable box; + protected Drawable Box; /// /// Creates a new . @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// A delegate that performs drag selection. public DragBox(Action performSelection) { - this.performSelection = performSelection; + PerformSelection = performSelection; RelativeSizeAxes = Axes.Both; AlwaysPresent = true; @@ -37,25 +38,27 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - InternalChild = box = new Container - { - Masking = true, - BorderColour = Color4.White, - BorderThickness = SelectionHandler.BORDER_RADIUS, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f - } - }; + InternalChild = Box = CreateBox(); } + protected virtual Drawable CreateBox() => new Container + { + Masking = true, + BorderColour = Color4.White, + BorderThickness = SelectionHandler.BORDER_RADIUS, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + } + }; + /// /// Handle a forwarded mouse event. /// /// The mouse event. /// Whether the event should be handled and blocking. - public virtual bool UpdateDrag(MouseButtonEvent e) + public virtual bool HandleDrag(MouseButtonEvent e) { var dragPosition = e.ScreenSpaceMousePosition; var dragStartPosition = e.ScreenSpaceMouseDownPosition; @@ -68,11 +71,32 @@ namespace osu.Game.Screens.Edit.Compose.Components var topLeft = ToLocalSpace(dragRectangle.TopLeft); var bottomRight = ToLocalSpace(dragRectangle.BottomRight); - box.Position = topLeft; - box.Size = bottomRight - topLeft; + Box.Position = topLeft; + Box.Size = bottomRight - topLeft; - performSelection?.Invoke(dragRectangle); + PerformSelection?.Invoke(dragRectangle); return true; } + + private Visibility state; + + public Visibility State + { + get => state; + set + { + if (value == state) return; + + state = value; + this.FadeTo(state == Visibility.Hidden ? 0 : 1, 250, Easing.OutQuint); + StateChanged?.Invoke(state); + } + } + + public override void Hide() => State = Visibility.Hidden; + + public override void Show() => State = Visibility.Visible; + + public event Action StateChanged; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs index fe0a47aec8..0792d0f80e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs +++ b/osu.Game/Screens/Edit/Compose/Components/MoveSelectionEvent.cs @@ -7,7 +7,7 @@ using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { /// - /// An event which occurs when a is moved. + /// An event which occurs when a is moved. /// public class MoveSelectionEvent { @@ -16,11 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public readonly SelectionBlueprint Blueprint; - /// - /// The starting screen-space position of the hitobject. - /// - public readonly Vector2 ScreenSpaceStartPosition; - /// /// The expected screen-space position of the hitobject at the current cursor position. /// @@ -29,18 +24,14 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The distance between and the hitobject's current position, in the coordinate-space of the hitobject's parent. /// - /// - /// This does not use and does not represent the cumulative movement distance. - /// public readonly Vector2 InstantDelta; - public MoveSelectionEvent(SelectionBlueprint blueprint, Vector2 screenSpaceStartPosition, Vector2 screenSpacePosition) + public MoveSelectionEvent(SelectionBlueprint blueprint, Vector2 screenSpacePosition) { Blueprint = blueprint; - ScreenSpaceStartPosition = screenSpaceStartPosition; ScreenSpacePosition = screenSpacePosition; - InstantDelta = Blueprint.DrawableObject.Parent.ToLocalSpace(ScreenSpacePosition) - Blueprint.DrawableObject.Position; + InstantDelta = Blueprint.GetInstantDelta(ScreenSpacePosition); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index c00892573e..fc46bf3fed 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -33,12 +33,12 @@ namespace osu.Game.Screens.Edit.Compose.Components public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; - public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.DrawableObject.HitObject); + public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject); private Drawable outline; [Resolved(CanBeNull = true)] - private IPlacementHandler placementHandler { get; set; } + private EditorBeatmap editorBeatmap { get; set; } public SelectionHandler() { @@ -104,7 +104,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Handle a blueprint becoming selected. /// /// The blueprint. - internal void HandleSelected(SelectionBlueprint blueprint) => selectedBlueprints.Add(blueprint); + internal void HandleSelected(SelectionBlueprint blueprint) + { + selectedBlueprints.Add(blueprint); + editorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); + + UpdateVisibility(); + } /// /// Handle a blueprint becoming deselected. @@ -113,6 +119,7 @@ namespace osu.Game.Screens.Edit.Compose.Components internal void HandleDeselected(SelectionBlueprint blueprint) { selectedBlueprints.Remove(blueprint); + editorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); // We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection if (selectedBlueprints.Count == 0) @@ -141,14 +148,12 @@ namespace osu.Game.Screens.Edit.Compose.Components DeselectAll?.Invoke(); blueprint.Select(); } - - UpdateVisibility(); } private void deleteSelected() { foreach (var h in selectedBlueprints.ToList()) - placementHandler.Delete(h.DrawableObject.HitObject); + editorBeatmap.Remove(h.HitObject); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index b8acc1ab44..96395696c3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.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 osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -11,10 +12,14 @@ using osu.Framework.Input.Events; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class Timeline : ZoomableScrollContainer + [Cached(typeof(IDistanceSnapProvider))] + [Cached] + public class Timeline : ZoomableScrollContainer, IDistanceSnapProvider { public readonly Bindable WaveformVisible = new Bindable(); public readonly IBindable Beatmap = new Bindable(); @@ -162,5 +167,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (trackWasPlaying) adjustableClock.Start(); } + + [Resolved] + private EditorBeatmap beatmap { get; set; } + + [Resolved] + private IBeatSnapProvider beatSnapProvider { get; set; } + + public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) + { + var targetTime = (position.X / Content.DrawWidth) * track.Length; + return (position, beatSnapProvider.SnapTime(targetTime, targetTime)); + } + + public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException(); + + public float DurationToDistance(double referenceTime, double duration) => throw new NotImplementedException(); + + public double DistanceToDuration(double referenceTime, float distance) => throw new NotImplementedException(); + + public double GetSnappedDurationFromDistance(double referenceTime, float distance) => throw new NotImplementedException(); + + public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => throw new NotImplementedException(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs new file mode 100644 index 0000000000..3b9cb1df24 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -0,0 +1,141 @@ +// 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 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.Events; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + internal class TimelineBlueprintContainer : BlueprintContainer + { + [Resolved(CanBeNull = true)] + private Timeline timeline { get; set; } + + private DragEvent lastDragEvent; + + public TimelineBlueprintContainer() + { + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Height = 0.4f; + + AddInternal(new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + DragBox.Alpha = 0; + } + + protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + + protected override void OnDrag(DragEvent e) + { + if (timeline != null) + { + var timelineQuad = timeline.ScreenSpaceDrawQuad; + var mouseX = e.ScreenSpaceMousePosition.X; + + // scroll if in a drag and dragging outside visible extents + if (mouseX > timelineQuad.TopRight.X) + timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime)); + else if (mouseX < timelineQuad.TopLeft.X) + timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime)); + } + + base.OnDrag(e); + lastDragEvent = e; + } + + protected override void OnDragEnd(DragEndEvent e) + { + base.OnDragEnd(e); + lastDragEvent = null; + } + + protected override void Update() + { + // trigger every frame so drags continue to update selection while playback is scrolling the timeline. + if (IsDragged) + OnDrag(lastDragEvent); + + base.Update(); + } + + protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler(); + + protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject); + + protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); + + internal class TimelineSelectionHandler : SelectionHandler + { + // for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation + public override bool HandleMovement(MoveSelectionEvent moveEvent) => true; + } + + private class TimelineDragBox : DragBox + { + private Vector2 lastMouseDown; + private float localMouseDown; + + public TimelineDragBox(Action performSelect) + : base(performSelect) + { + } + + protected override Drawable CreateBox() => new Box + { + RelativeSizeAxes = Axes.Y, + Alpha = 0.3f + }; + + public override bool HandleDrag(MouseButtonEvent e) + { + // store the original position of the mouse down, as we may be scrolled during selection. + if (lastMouseDown != e.ScreenSpaceMouseDownPosition) + { + lastMouseDown = e.ScreenSpaceMouseDownPosition; + localMouseDown = e.MouseDownPosition.X; + } + + float selection1 = localMouseDown; + float selection2 = e.MousePosition.X; + + Box.X = Math.Min(selection1, selection2); + Box.Width = Math.Abs(selection1 - selection2); + + PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat); + return true; + } + } + + protected class TimelineSelectionBlueprintContainer : Container + { + protected override Container Content { get; } + + public TimelineSelectionBlueprintContainer() + { + AddInternal(new TimelinePart(Content = new Container { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs new file mode 100644 index 0000000000..2ed5471444 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -0,0 +1,116 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class TimelineHitObjectBlueprint : SelectionBlueprint + { + private readonly Circle circle; + + private readonly Container extensionBar; + + [UsedImplicitly] + private readonly Bindable startTime; + + public const float THICKNESS = 3; + + private const float circle_size = 16; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || circle.ReceivePositionalInputAt(screenSpacePos); + + public TimelineHitObjectBlueprint(HitObject hitObject) + : base(hitObject) + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + startTime = hitObject.StartTimeBindable.GetBoundCopy(); + startTime.BindValueChanged(time => X = (float)time.NewValue, true); + + RelativePositionAxes = Axes.X; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + if (hitObject is IHasEndTime) + { + AddInternal(extensionBar = new Container + { + CornerRadius = 2, + Masking = true, + Size = new Vector2(1, THICKNESS), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.X, + Colour = Color4.Black, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + }); + } + + AddInternal(circle = new Circle + { + Size = new Vector2(circle_size), + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + AlwaysPresent = true, + Colour = Color4.White, + BorderColour = Color4.Black, + BorderThickness = THICKNESS, + }); + } + + protected override void Update() + { + base.Update(); + + // no bindable so we perform this every update + Width = (float)(HitObject.GetEndTime() - HitObject.StartTime); + } + + protected override void OnSelected() + { + circle.BorderColour = Color4.Orange; + if (extensionBar != null) + extensionBar.Colour = Color4.Orange; + } + + protected override void OnDeselected() + { + circle.BorderColour = Color4.Black; + if (extensionBar != null) + extensionBar.Colour = Color4.Black; + } + + public override Quad SelectionQuad + { + get + { + // correctly include the circle in the selection quad region, as it is usually outside the blueprint itself. + var circleQuad = circle.ScreenSpaceDrawQuad; + var actualQuad = ScreenSpaceDrawQuad; + + return new Quad(circleQuad.TopLeft, Vector2.ComponentMax(actualQuad.TopRight, circleQuad.TopRight), + circleQuad.BottomLeft, Vector2.ComponentMax(actualQuad.BottomRight, circleQuad.BottomRight)); + } + } + + public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft; + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs deleted file mode 100644 index f521d08ada..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs +++ /dev/null @@ -1,140 +0,0 @@ -// 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 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.Events; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Edit.Compose.Components.Timeline -{ - internal class TimelineHitObjectDisplay : BlueprintContainer - { - private EditorBeatmap beatmap { get; } - - private readonly TimelinePart content; - - public TimelineHitObjectDisplay(EditorBeatmap beatmap) - { - RelativeSizeAxes = Axes.Both; - - this.beatmap = beatmap; - - AddInternal(content = new TimelinePart { RelativeSizeAxes = Axes.Both }); - } - - [BackgroundDependencyLoader] - private void load() - { - foreach (var h in beatmap.HitObjects) - add(h); - - beatmap.HitObjectAdded += add; - beatmap.HitObjectRemoved += remove; - beatmap.StartTimeChanged += h => - { - remove(h); - add(h); - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - DragBox.Alpha = 0; - } - - private void remove(HitObject h) - { - foreach (var d in content.OfType().Where(c => c.HitObject == h)) - d.Expire(); - } - - private void add(HitObject h) - { - var yOffset = content.Count(d => d.X == h.StartTime); - - content.Add(new TimelineHitObjectRepresentation(h) { Y = -yOffset * TimelineHitObjectRepresentation.THICKNESS }); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - base.OnMouseDown(e); - - return false; // tempoerary until we correctly handle selections. - } - - protected override DragBox CreateDragBox(Action performSelect) => new NoDragDragBox(performSelect); - - internal class NoDragDragBox : DragBox - { - public NoDragDragBox(Action performSelect) - : base(performSelect) - { - } - - public override bool UpdateDrag(MouseButtonEvent e) => false; - } - - private class TimelineHitObjectRepresentation : CompositeDrawable - { - public const float THICKNESS = 3; - - public readonly HitObject HitObject; - - public TimelineHitObjectRepresentation(HitObject hitObject) - { - HitObject = hitObject; - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - - Width = (float)(hitObject.GetEndTime() - hitObject.StartTime); - - X = (float)hitObject.StartTime; - - RelativePositionAxes = Axes.X; - RelativeSizeAxes = Axes.X; - - if (hitObject is IHasEndTime) - { - AddInternal(new Container - { - CornerRadius = 2, - Masking = true, - Size = new Vector2(1, THICKNESS), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativePositionAxes = Axes.X, - RelativeSizeAxes = Axes.X, - Colour = Color4.Black, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - } - }); - } - - AddInternal(new Circle - { - Size = new Vector2(16), - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - AlwaysPresent = true, - Colour = Color4.White, - BorderColour = Color4.Black, - BorderThickness = THICKNESS, - }); - } - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 1a6aae294a..cdea200e10 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -32,6 +32,6 @@ namespace osu.Game.Screens.Edit.Compose return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(EditorBeatmap); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(); } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a67a0ff0f1..9c75d40bec 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Edit /// public event Action StartTimeChanged; + public BindableList SelectedHitObjects { get; } = new BindableList(); + public readonly IBeatmap PlayableBeatmap; private readonly BindableBeatDivisor beatDivisor; diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index aa8d99b517..8967f24185 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private TimelineArea timelineArea; + private Container timelineContainer; [BackgroundDependencyLoader(true)] private void load([CanBeNull] BindableBeatDivisor beatDivisor) @@ -62,11 +62,10 @@ namespace osu.Game.Screens.Edit { new Drawable[] { - new Container + timelineContainer = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = timelineArea = CreateTimelineArea() }, new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } }, @@ -100,14 +99,16 @@ namespace osu.Game.Screens.Edit mainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); - LoadComponentAsync(CreateTimelineContent(), timelineArea.Add); + LoadComponentAsync(new TimelineArea + { + RelativeSizeAxes = Axes.Both, + Child = CreateTimelineContent() + }, timelineContainer.Add); }); } protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); - - protected TimelineArea CreateTimelineArea() => new TimelineArea { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index b28d572b5c..cb5ceefb0f 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -69,6 +69,9 @@ namespace osu.Game.Screens.Menu private ExitConfirmOverlay exitConfirmOverlay; + private ParallaxContainer buttonsContainer; + private SongTicker songTicker; + [BackgroundDependencyLoader(true)] private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) { @@ -89,9 +92,9 @@ namespace osu.Game.Screens.Menu }); } - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { - new ParallaxContainer + buttonsContainer = new ParallaxContainer { ParallaxAmount = 0.01f, Children = new Drawable[] @@ -107,6 +110,13 @@ namespace osu.Game.Screens.Menu } }, sideFlashes = new MenuSideFlashes(), + songTicker = new SongTicker + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Margin = new MarginPadding { Right = 15, Top = 5 } + }, + exitConfirmOverlay?.CreateProxy() ?? Drawable.Empty() }); buttons.StateChanged += state => @@ -190,7 +200,7 @@ namespace osu.Game.Screens.Menu buttons.State = ButtonSystemState.TopLevel; this.FadeIn(FADE_IN_DURATION, Easing.OutQuint); - this.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint); + buttonsContainer.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint); sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint); } @@ -227,7 +237,7 @@ namespace osu.Game.Screens.Menu buttons.State = ButtonSystemState.EnteringMode; this.FadeOut(FADE_OUT_DURATION, Easing.InSine); - this.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine); + buttonsContainer.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine); sideFlashes.FadeOut(64, Easing.OutQuint); } @@ -262,6 +272,9 @@ namespace osu.Game.Screens.Menu } buttons.State = ButtonSystemState.Exit; + + songTicker.Hide(); + this.FadeOut(3000); return base.OnExiting(next); } diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs new file mode 100644 index 0000000000..c4943e77d5 --- /dev/null +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -0,0 +1,72 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK; +using osu.Game.Graphics; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Framework.Localisation; + +namespace osu.Game.Screens.Menu +{ + public class SongTicker : Container + { + private const int fade_duration = 800; + + [Resolved] + private Bindable beatmap { get; set; } + + private readonly OsuSpriteText title, artist; + + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + + public SongTicker() + { + AutoSizeAxes = Axes.Both; + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 3), + Children = new Drawable[] + { + title = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light, italics: true) + }, + artist = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(size: 16) + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatmap.BindValueChanged(_ => Scheduler.AddOnce(show), true); + } + + private void show() + { + var metadata = beatmap.Value.Metadata; + + title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); + artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); + + this.FadeInFromZero(fade_duration / 2f) + .Delay(4000) + .Then().FadeOut(fade_duration); + } + } +} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index b0d5be8140..cf49cf0228 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Select { private const float shear_width = 36.75f; - private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGED_CONTAINER_SIZE.Y, 0); + private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); private readonly IBindable ruleset = new Bindable(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 68a6ad8845..2ffb73f226 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(Beatmap.Metadata.Artist) || criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode); + match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(Beatmap.StarDifficulty); + if (match) { var terms = new List(); diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 5b81303788..c851b201d7 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -42,9 +42,15 @@ namespace osu.Game.Screens.Select Group = groupMode.Value, Sort = sortMode.Value, AllowConvertedBeatmaps = showConverted.Value, - Ruleset = ruleset.Value + Ruleset = ruleset.Value, }; + if (!minimumStars.IsDefault) + criteria.UserStarDifficulty.Min = minimumStars.Value; + + if (!maximumStars.IsDefault) + criteria.UserStarDifficulty.Max = maximumStars.Value; + FilterQueryParser.ApplyQueries(criteria, query); return criteria; } @@ -142,7 +148,9 @@ namespace osu.Game.Screens.Select private readonly IBindable ruleset = new Bindable(); - private Bindable showConverted; + private readonly Bindable showConverted = new Bindable(); + private readonly Bindable minimumStars = new Bindable(); + private readonly Bindable maximumStars = new Bindable(); public readonly Box Background; @@ -151,9 +159,15 @@ namespace osu.Game.Screens.Select { sortTabs.AccentColour = colours.GreenLight; - showConverted = config.GetBindable(OsuSetting.ShowConvertedBeatmaps); + config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted); showConverted.ValueChanged += _ => updateCriteria(); + config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars); + minimumStars.ValueChanged += _ => updateCriteria(); + + config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars); + maximumStars.ValueChanged += _ => updateCriteria(); + ruleset.BindTo(parentRuleset); ruleset.BindValueChanged(_ => updateCriteria()); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index c4d9996377..9fa57af01d 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -26,6 +26,12 @@ namespace osu.Game.Screens.Select public OptionalTextFilter Creator; public OptionalTextFilter Artist; + public OptionalRange UserStarDifficulty = new OptionalRange + { + IsLowerInclusive = true, + IsUpperInclusive = true + }; + public string[] SearchTerms = Array.Empty(); public RulesetInfo Ruleset; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index bf57def700..1a29f336ea 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select { public abstract class SongSelect : OsuScreen, IKeyBindingHandler { - public static readonly Vector2 WEDGED_CONTAINER_SIZE = new Vector2(0.5f, 245); + public static readonly float WEDGE_HEIGHT = 245; protected const float BACKGROUND_BLUR = 20; private const float left_area_padding = 20; @@ -96,98 +96,116 @@ namespace osu.Game.Screens.Select AddRangeInternal(new Drawable[] { - new ParallaxContainer - { - Masking = true, - ParallaxAmount = 0.005f, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new WedgeBackground - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = -150 }, - Size = new Vector2(WEDGED_CONTAINER_SIZE.X, 1), - } - } - }, - new Container - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(WEDGED_CONTAINER_SIZE.X, 1), - Padding = new MarginPadding - { - Bottom = Footer.HEIGHT, - Top = WEDGED_CONTAINER_SIZE.Y + left_area_padding, - Left = left_area_padding, - Right = left_area_padding * 2, - }, - Child = BeatmapDetails = new BeatmapDetailArea - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 10, Right = 5 }, - } - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 2, //avoid horizontal masking so the panels don't clip when screen stack is pushed. - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Top = FilterControl.HEIGHT, - Bottom = Footer.HEIGHT - }, - Child = Carousel = new BeatmapCarousel - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(1 - WEDGED_CONTAINER_SIZE.X, 1), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - SelectionChanged = updateSelectedBeatmap, - BeatmapSetsChanged = carouselBeatmapsLoaded, - }, - }, - FilterControl = new FilterControl - { - RelativeSizeAxes = Axes.X, - Height = FilterControl.HEIGHT, - FilterChanged = ApplyFilterToCarousel, - Background = { Width = 2 }, - }, - } - }, - }, - beatmapInfoWedge = new BeatmapInfoWedge - { - Size = WEDGED_CONTAINER_SIZE, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding - { - Top = left_area_padding, - Right = left_area_padding, - }, - }, new ResetScrollContainer(() => Carousel.ScrollToSelected()) { RelativeSizeAxes = Axes.Y, Width = 250, - } + }, + new VerticalMaskingContainer + { + Children = new Drawable[] + { + new GridContainer // used for max width implementation + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 850), + }, + Content = new[] + { + new Drawable[] + { + new ParallaxContainer + { + ParallaxAmount = 0.005f, + RelativeSizeAxes = Axes.Both, + Child = new WedgeBackground + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = -150 }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Top = FilterControl.HEIGHT, + Bottom = Footer.HEIGHT + }, + Child = Carousel = new BeatmapCarousel + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + SelectionChanged = updateSelectedBeatmap, + BeatmapSetsChanged = carouselBeatmapsLoaded, + }, + } + }, + } + }, + FilterControl = new FilterControl + { + RelativeSizeAxes = Axes.X, + Height = FilterControl.HEIGHT, + FilterChanged = ApplyFilterToCarousel, + Background = { Width = 2 }, + }, + new GridContainer // used for max width implementation + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 650), + }, + Content = new[] + { + new Drawable[] + { + new Container + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + + Children = new Drawable[] + { + beatmapInfoWedge = new BeatmapInfoWedge + { + Height = WEDGE_HEIGHT, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding + { + Top = left_area_padding, + Right = left_area_padding, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Bottom = Footer.HEIGHT, + Top = WEDGE_HEIGHT + left_area_padding, + Left = left_area_padding, + Right = left_area_padding * 2, + }, + Child = BeatmapDetails = new BeatmapDetailArea + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 10, Right = 5 }, + }, + }, + } + }, + }, + } + } + } + }, }); if (ShowFooter) @@ -705,6 +723,29 @@ namespace osu.Game.Screens.Select return base.OnKeyDown(e); } + private class VerticalMaskingContainer : Container + { + private const float panel_overflow = 1.2f; + + protected override Container Content { get; } + + public VerticalMaskingContainer() + { + RelativeSizeAxes = Axes.Both; + Masking = true; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Width = panel_overflow; //avoid horizontal masking so the panels don't clip when screen stack is pushed. + InternalChild = Content = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 1 / panel_overflow, + }; + } + } + private class ResetScrollContainer : Container { private readonly Action onHoverAction; diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs index 3233ee160d..6565f98666 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestScene.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual }); } - protected void AddBlueprint(SelectionBlueprint blueprint) + protected void AddBlueprint(OverlaySelectionBlueprint blueprint) { Add(blueprint.With(d => { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9b431e6425..0ea558bbc7 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 4dc7403553..a215bc65e8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -74,7 +74,7 @@ - + @@ -82,7 +82,7 @@ - +