diff --git a/.github/ISSUE_TEMPLATE/02-feature-request-issues.md b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md deleted file mode 100644 index c3357dd780..0000000000 --- a/.github/ISSUE_TEMPLATE/02-feature-request-issues.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Feature Request -about: Propose a feature you would like to see in the game! ---- -**Describe the new feature:** - -**Proposal designs of the feature:** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 69baeee60c..c62231e8e0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,12 @@ blank_issues_enabled: false contact_links: + - name: Suggestions or feature request + url: https://github.com/ppy/osu/discussions/categories/ideas + about: Got something you think should change or be added? Search for or start a new discussion! + - name: Help + url: https://github.com/ppy/osu/discussions/categories/q-a + about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section! - name: osu!stable issues url: https://github.com/ppy/osu-stable-issues - about: For issues regarding osu!stable (not osu!lazer), open them here. + about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports. + diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index d81af053d1..cd6bf1d8d2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -7,13 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking, IHasMainCirclePiece + public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, IHasMainCirclePiece { public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; @@ -111,7 +112,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public void UpdateSnakingPosition(Vector2 start, Vector2 end) => - Position = HitObject.RepeatIndex % 2 == 0 ? end : start; + protected override void OnApply() + { + base.OnApply(); + + if (Slider != null) + Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0); + } } } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index b51f096d7d..90c99316b1 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -79,8 +79,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // Old osu! used hit sounding to determine various hit type information IList samples = obj.Samples; - bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); - switch (obj) { case IHasDistance distanceData: @@ -94,15 +92,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing) { IList currentSamples = allSamples[i]; - bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE); - strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH); yield return new Hit { StartTime = j, - Type = isRim ? HitType.Rim : HitType.Centre, Samples = currentSamples, - IsStrong = strong }; i = (i + 1) % allSamples.Count; @@ -117,7 +111,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, Duration = taikoDuration, TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 }; @@ -143,16 +136,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps default: { - bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE; - - bool isRim = samples.Any(isRimDefinition); - yield return new Hit { StartTime = obj.StartTime, - Type = isRim ? HitType.Rim : HitType.Centre, Samples = samples, - IsStrong = strong }; break; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index a24130d6ac..ab3b729307 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -69,7 +69,11 @@ namespace osu.Game.Rulesets.Taiko.Edit { EditorBeatmap.PerformOnSelection(h => { - if (h is Hit taikoHit) taikoHit.Type = state ? HitType.Rim : HitType.Centre; + if (h is Hit taikoHit) + { + taikoHit.Type = state ? HitType.Rim : HitType.Centre; + EditorBeatmap.Update(h); + } }); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index f4a66c39a8..b4ed242893 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -17,13 +17,25 @@ namespace osu.Game.Rulesets.Taiko.Objects public HitType Type { get => TypeBindable.Value; - set - { - TypeBindable.Value = value; - updateSamplesFromType(); - } + set => TypeBindable.Value = value; } + public Hit() + { + TypeBindable.BindValueChanged(_ => updateSamplesFromType()); + SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); + } + + private void updateTypeFromSamples() + { + Type = getRimSamples().Any() ? HitType.Rim : HitType.Centre; + } + + /// + /// Returns an array of any samples which would cause this object to be a "rim" type hit. + /// + private HitSampleInfo[] getRimSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); + private void updateSamplesFromType() { var rimSamples = getRimSamples(); @@ -42,11 +54,6 @@ namespace osu.Game.Rulesets.Taiko.Objects } } - /// - /// Returns an array of any samples which would cause this object to be a "rim" type hit. - /// - private HitSampleInfo[] getRimSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); - protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; public class StrongNestedHit : StrongNestedHitObject diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index cac56d1269..6c17573b50 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -33,14 +33,21 @@ namespace osu.Game.Rulesets.Taiko.Objects public bool IsStrong { get => IsStrongBindable.Value; - set - { - IsStrongBindable.Value = value; - updateSamplesFromStrong(); - } + set => IsStrongBindable.Value = value; } - private void updateSamplesFromStrong() + protected TaikoStrongableHitObject() + { + IsStrongBindable.BindValueChanged(_ => updateSamplesFromType()); + SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples()); + } + + private void updateTypeFromSamples() + { + IsStrong = getStrongSamples().Any(); + } + + private void updateSamplesFromType() { var strongSamples = getStrongSamples(); diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index a8ee1bcc2e..a47631a83b 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = LoadOsuIntoHost(host); - await osu.CollectionManager.Import(new MemoryStream()); + await importCollectionsFromStream(osu, new MemoryStream()); Assert.That(osu.CollectionManager.Collections.Count, Is.Zero); } @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = LoadOsuIntoHost(host); - await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db")); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = LoadOsuIntoHost(host, true); - await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db")); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Collections.IO ms.Seek(0, SeekOrigin.Begin); - await osu.CollectionManager.Import(ms); + await importCollectionsFromStream(osu, ms); } Assert.That(host.UpdateThread.Running, Is.True); @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = LoadOsuIntoHost(host, true); - await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db")); // Move first beatmap from second collection into the first. osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]); @@ -169,5 +169,12 @@ namespace osu.Game.Tests.Collections.IO } } } + + private static async Task importCollectionsFromStream(TestOsuGameBase osu, Stream stream) + { + // intentionally spin this up on a separate task to avoid disposal deadlocks. + // see https://github.com/EventStore/EventStore/issues/1179 + await Task.Run(() => osu.CollectionManager.Import(stream).Wait()); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index cfdea31a75..8160a62991 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -88,13 +88,18 @@ namespace osu.Game.Tests.Visual.Gameplay { beforeLoadAction?.Invoke(); + prepareBeatmap(); + + LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); + } + + private void prepareBeatmap() + { Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning; foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); - - LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); } [Test] @@ -178,10 +183,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("load slow dummy beatmap", () => { - LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); - Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); + prepareBeatmap(); + slowPlayer = new SlowLoadPlayer(false, false); + LoadScreen(loader = new TestPlayerLoader(() => slowPlayer)); }); + AddStep("schedule slow load", () => Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000)); + AddUntilStep("wait for player to be current", () => slowPlayer.IsCurrentScreen()); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 0ac8e01482..5ef3eff856 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Gameplay CreateTest(null); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space)); - AddAssert("score shown", () => Player.IsScoreShown); + AddUntilStep("wait for score shown", () => Player.IsScoreShown); + AddUntilStep("time less than storyboard duration", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < currentStoryboardDuration); } [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 80b9aa8228..af2f6fa5fe 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -73,8 +73,11 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < users; i++) spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - Client.CurrentMatchPlayingUserIds.Clear(); - Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers); + spectatorClient.Schedule(() => + { + Client.CurrentMatchPlayingUserIds.Clear(); + Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers); + }); Children = new Drawable[] { @@ -91,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for load", () => leaderboard.IsLoaded); + AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0); } [Test] diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs new file mode 100644 index 0000000000..1e19af933a --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiMarkdownContainer.cs @@ -0,0 +1,175 @@ +// 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 Markdig.Syntax.Inlines; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers.Markdown; +using osu.Game.Overlays; +using osu.Game.Overlays.Wiki.Markdown; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneWikiMarkdownContainer : OsuTestScene + { + private TestMarkdownContainer markdownContainer; + + [Cached] + private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange); + + [SetUp] + public void Setup() => Schedule(() => + { + Children = new Drawable[] + { + new Box + { + Colour = overlayColour.Background5, + RelativeSizeAxes = Axes.Both, + }, + new BasicScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = markdownContainer = new TestMarkdownContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + }; + }); + + [Test] + public void TestLink() + { + AddStep("set current path", () => markdownContainer.CurrentPath = "Article_styling_criteria/"); + + AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page"); + + AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ"); + + AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing"); + + AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)"); + AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting"); + } + + [Test] + public void TestOutdatedNoticeBox() + { + AddStep("Add outdated yaml header", () => + { + markdownContainer.Text = @"--- +outdated: true +---"; + }); + } + + [Test] + public void TestNeedsCleanupNoticeBox() + { + AddStep("Add needs cleanup yaml header", () => + { + markdownContainer.Text = @"--- +needs_cleanup: true +---"; + }); + } + + [Test] + public void TestOnlyShowOutdatedNoticeBox() + { + AddStep("Add outdated and needs cleanup yaml", () => + { + markdownContainer.Text = @"--- +outdated: true +needs_cleanup: true +---"; + }); + } + + [Test] + public void TestAbsoluteImage() + { + AddStep("Add absolute image", () => + { + markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)"; + }); + } + + [Test] + public void TestRelativeImage() + { + AddStep("Add relative image", () => + { + markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.CurrentPath = "Interface/"; + markdownContainer.Text = "![intro](img/intro-screen.jpg)"; + }); + } + + [Test] + public void TestBlockImage() + { + AddStep("Add paragraph with block image", () => + { + markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.CurrentPath = "Interface/"; + markdownContainer.Text = @"Line before image + +![play menu](img/play-menu.jpg ""Main Menu in osu!"") + +Line after image"; + }); + } + + [Test] + public void TestInlineImage() + { + AddStep("Add inline image", () => + { + markdownContainer.DocumentUrl = "https://dev.ppy.sh"; + markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!"; + }); + } + + private class TestMarkdownContainer : WikiMarkdownContainer + { + public LinkInline Link; + + public new string DocumentUrl + { + set => base.DocumentUrl = value; + } + + public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer + { + UrlAdded = link => Link = link, + }; + + private class TestMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer + { + public Action UrlAdded; + + protected override void AddLinkText(string text, LinkInline linkInline) + { + base.AddLinkText(text, linkInline); + + UrlAdded?.Invoke(linkInline); + } + + protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline)); + } + } + } +} diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 55c5b5b9c2..acf9deb3cb 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -70,6 +70,7 @@ namespace osu.Game.Tests.Visual.Settings AddStep("click first row", () => { firstRow = panel.ChildrenOfType().First(); + InputManager.MoveMouseTo(firstRow); InputManager.Click(MouseButton.Left); }); @@ -138,6 +139,64 @@ namespace osu.Game.Tests.Visual.Settings } } + [Test] + public void TestSingleBindingResetButton() + { + KeyBindingRow settingsKeyBindingRow = null; + + AddStep("click first row", () => + { + settingsKeyBindingRow = panel.ChildrenOfType().First(); + + InputManager.MoveMouseTo(settingsKeyBindingRow); + InputManager.Click(MouseButton.Left); + InputManager.PressKey(Key.P); + InputManager.ReleaseKey(Key.P); + }); + + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + + AddStep("click reset button for bindings", () => + { + var resetButton = settingsKeyBindingRow.ChildrenOfType>().First(); + + resetButton.Click(); + }); + + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + + AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + } + + [Test] + public void TestResetAllBindingsButton() + { + KeyBindingRow settingsKeyBindingRow = null; + + AddStep("click first row", () => + { + settingsKeyBindingRow = panel.ChildrenOfType().First(); + + InputManager.MoveMouseTo(settingsKeyBindingRow); + InputManager.Click(MouseButton.Left); + InputManager.PressKey(Key.P); + InputManager.ReleaseKey(Key.P); + }); + + AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha > 0); + + AddStep("click reset button for bindings", () => + { + var resetButton = panel.ChildrenOfType().First(); + + resetButton.Click(); + }); + + AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType>().First().Alpha == 0); + + AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0))); + } + [Test] public void TestClickRowSelectsFirstBinding() { diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs index 8f1c17ed29..f63145f534 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Overlays.Settings; +using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Settings { @@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Settings private class TestSettingsTextBox : SettingsTextBox { - public new Drawable RestoreDefaultValueButton => this.ChildrenOfType().Single(); + public Drawable RestoreDefaultValueButton => this.ChildrenOfType>().Single(); } } -} +} \ No newline at end of file diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 44c9361ff8..78ddfa9ed2 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -786,9 +786,12 @@ namespace osu.Game.Tests.Visual.SongSelect } } - private void checkVisibleItemCount(bool diff, int count) => - AddAssert($"{count} {(diff ? "diffs" : "sets")} visible", () => + private void checkVisibleItemCount(bool diff, int count) + { + // until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet. + AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () => carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); + } private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null); diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 3a63587b30..086cc573d5 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -58,8 +58,13 @@ namespace osu.Game.Collections if (storage.Exists(database_name)) { + List beatmapCollections; + using (var stream = storage.GetStream(database_name)) - importCollections(readCollections(stream)); + beatmapCollections = readCollections(stream); + + // intentionally fire-and-forget async. + importCollections(beatmapCollections); } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 9c09b6e7d0..0df3359c28 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -46,12 +47,19 @@ namespace osu.Game.Overlays.KeyBinding } } + private Container content; + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + content.ReceivePositionalInputAt(screenSpacePos); + public bool FilteringActive { get; set; } private OsuSpriteText text; private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; + private Bindable isDefault { get; } = new BindableBool(true); + public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString()); public KeyBindingRow(object action, IEnumerable bindings) @@ -61,9 +69,6 @@ namespace osu.Game.Overlays.KeyBinding RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - - Masking = true; - CornerRadius = padding; } [Resolved] @@ -72,51 +77,72 @@ namespace osu.Game.Overlays.KeyBinding [BackgroundDependencyLoader] private void load(OsuColour colours) { - EdgeEffect = new EdgeEffectParameters - { - Radius = 2, - Colour = colours.YellowDark.Opacity(0), - Type = EdgeEffectType.Shadow, - Hollow = true, - }; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }; - Children = new Drawable[] + InternalChildren = new Drawable[] { - new Box + new RestoreDefaultValueButton { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, - }, - text = new OsuSpriteText - { - Text = action.GetDescription(), - Margin = new MarginPadding(padding), - }, - buttons = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight - }, - cancelAndClearButtons = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(padding) { Top = height + padding * 2 }, - Anchor = Anchor.TopRight, + Current = isDefault, + Action = RestoreDefaults, Origin = Anchor.TopRight, - Alpha = 0, - Spacing = new Vector2(5), + }, + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + CornerRadius = padding, + EdgeEffect = new EdgeEffectParameters + { + Radius = 2, + Colour = colours.YellowDark.Opacity(0), + Type = EdgeEffectType.Shadow, + Hollow = true, + }, Children = new Drawable[] { - new CancelButton { Action = finalise }, - new ClearButton { Action = clear }, - }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.6f, + }, + text = new OsuSpriteText + { + Text = action.GetDescription(), + Margin = new MarginPadding(padding), + }, + buttons = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight + }, + cancelAndClearButtons = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(padding) { Top = height + padding * 2 }, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Alpha = 0, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new CancelButton { Action = finalise }, + new ClearButton { Action = clear }, + }, + } + } } }; foreach (var b in bindings) buttons.Add(new KeyButton(b)); + + updateIsDefaultValue(); } public void RestoreDefaults() @@ -129,18 +155,20 @@ namespace osu.Game.Overlays.KeyBinding button.UpdateKeyCombination(d); store.Update(button.KeyBinding); } + + isDefault.Value = true; } protected override bool OnHover(HoverEvent e) { - FadeEdgeEffectTo(1, transition_time, Easing.OutQuint); + content.FadeEdgeEffectTo(1, transition_time, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - FadeEdgeEffectTo(0, transition_time, Easing.OutQuint); + content.FadeEdgeEffectTo(0, transition_time, Easing.OutQuint); base.OnHoverLost(e); } @@ -288,6 +316,8 @@ namespace osu.Game.Overlays.KeyBinding { store.Update(bindTarget.KeyBinding); + updateIsDefaultValue(); + bindTarget.IsBinding = false; Schedule(() => { @@ -305,8 +335,8 @@ namespace osu.Game.Overlays.KeyBinding protected override void OnFocus(FocusEvent e) { - AutoSizeDuration = 500; - AutoSizeEasing = Easing.OutQuint; + content.AutoSizeDuration = 500; + content.AutoSizeEasing = Easing.OutQuint; cancelAndClearButtons.FadeIn(300, Easing.OutQuint); cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y; @@ -331,6 +361,11 @@ namespace osu.Game.Overlays.KeyBinding if (bindTarget != null) bindTarget.IsBinding = true; } + private void updateIsDefaultValue() + { + isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults); + } + private class CancelButton : TriangleButton { public CancelButton() @@ -379,9 +414,6 @@ namespace osu.Game.Overlays.KeyBinding Margin = new MarginPadding(padding); - // todo: use this in a meaningful way - // var isDefault = keyBinding.Action is Enum; - Masking = true; CornerRadius = padding; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index 707176e63e..5e1f9d8f75 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -61,8 +61,11 @@ namespace osu.Game.Overlays.KeyBinding { Text = "Reset all bindings in section"; RelativeSizeAxes = Axes.X; - Margin = new MarginPadding { Top = 5 }; - Height = 20; + Width = 0.5f; + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + Margin = new MarginPadding { Top = 15 }; + Height = 30; Content.CornerRadius = 5; } diff --git a/osu.Game/Overlays/News/Displays/ArticleListing.cs b/osu.Game/Overlays/News/Displays/ArticleListing.cs index b49326a1f1..dc3b17b323 100644 --- a/osu.Game/Overlays/News/Displays/ArticleListing.cs +++ b/osu.Game/Overlays/News/Displays/ArticleListing.cs @@ -2,14 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osuTK; @@ -20,26 +19,16 @@ namespace osu.Game.Overlays.News.Displays /// public class ArticleListing : CompositeDrawable { - public Action SidebarMetadataUpdated; - - [Resolved] - private IAPIProvider api { get; set; } + private readonly Action fetchMorePosts; private FillFlowContainer content; private ShowMoreButton showMore; - private GetNewsRequest request; - private Cursor lastCursor; + private CancellationTokenSource cancellationToken; - private readonly int? year; - - /// - /// Instantiate a listing for the specified year. - /// - /// The year to load articles from. If null, will show the most recent articles. - public ArticleListing(int? year = null) + public ArticleListing(Action fetchMorePosts) { - this.year = year; + this.fetchMorePosts = fetchMorePosts; } [BackgroundDependencyLoader] @@ -47,6 +36,7 @@ namespace osu.Game.Overlays.News.Displays { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Vertical = 20, @@ -75,53 +65,25 @@ namespace osu.Game.Overlays.News.Displays { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Margin = new MarginPadding - { - Top = 15 - }, - Action = performFetch, + Margin = new MarginPadding { Top = 15 }, + Action = fetchMorePosts, Alpha = 0 } } }; - - performFetch(); } - private void performFetch() - { - request?.Cancel(); - - request = new GetNewsRequest(year, lastCursor); - request.Success += response => Schedule(() => onSuccess(response)); - api.PerformAsync(request); - } - - private CancellationTokenSource cancellationToken; - - private void onSuccess(GetNewsResponse response) - { - cancellationToken?.Cancel(); - - // only needs to be updated on the initial load, as the content won't change during pagination. - if (lastCursor == null) - SidebarMetadataUpdated?.Invoke(response.SidebarMetadata); - - // store cursor for next pagination request. - lastCursor = response.Cursor; - - LoadComponentsAsync(response.NewsPosts.Select(p => new NewsCard(p)).ToList(), loaded => + public void AddPosts(IEnumerable posts, bool morePostsAvailable) => Schedule(() => + LoadComponentsAsync(posts.Select(p => new NewsCard(p)).ToList(), loaded => { content.AddRange(loaded); - showMore.IsLoading = false; - showMore.Alpha = response.Cursor != null ? 1 : 0; - }, (cancellationToken = new CancellationTokenSource()).Token); - } + showMore.Alpha = morePostsAvailable ? 1 : 0; + }, (cancellationToken = new CancellationTokenSource()).Token) + ); protected override void Dispose(bool isDisposing) { - request?.Cancel(); cancellationToken?.Cancel(); base.Dispose(isDisposing); } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index dd6de40ecb..12e3f81ca1 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -6,6 +6,7 @@ using System.Threading; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests; using osu.Game.Overlays.News; using osu.Game.Overlays.News.Displays; using osu.Game.Overlays.News.Sidebar; @@ -14,13 +15,21 @@ namespace osu.Game.Overlays { public class NewsOverlay : OnlineOverlay { - private readonly Bindable article = new Bindable(null); + private readonly Bindable article = new Bindable(); private readonly Container sidebarContainer; private readonly NewsSidebar sidebar; - private readonly Container content; + private GetNewsRequest request; + + private Cursor lastCursor; + + /// + /// The year currently being displayed. If null, the main listing is being displayed. + /// + private int? displayedYear; + private CancellationTokenSource cancellationToken; private bool displayUpdateRequired = true; @@ -65,7 +74,13 @@ namespace osu.Game.Overlays base.LoadComplete(); // should not be run until first pop-in to avoid requesting data before user views. - article.BindValueChanged(onArticleChanged); + article.BindValueChanged(a => + { + if (a.NewValue == null) + loadListing(); + else + loadArticle(a.NewValue); + }); } protected override NewsHeader CreateHeader() => new NewsHeader { ShowFrontPage = ShowFrontPage }; @@ -95,7 +110,7 @@ namespace osu.Game.Overlays public void ShowYear(int year) { - loadFrontPage(year); + loadListing(year); Show(); } @@ -108,7 +123,11 @@ namespace osu.Game.Overlays protected void LoadDisplay(Drawable display) { ScrollFlow.ScrollToStart(); - LoadComponentAsync(display, loaded => content.Child = loaded, (cancellationToken = new CancellationTokenSource()).Token); + LoadComponentAsync(display, loaded => + { + content.Child = loaded; + Loading.Hide(); + }, (cancellationToken = new CancellationTokenSource()).Token); } protected override void UpdateAfterChildren() @@ -118,48 +137,65 @@ namespace osu.Game.Overlays sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); } - private void onArticleChanged(ValueChangedEvent article) + private void loadListing(int? year = null) { - if (article.NewValue == null) - loadFrontPage(); - else - loadArticle(article.NewValue); - } - - private void loadFrontPage(int? year = null) - { - beginLoading(); - Header.SetFrontPage(); - var page = new ArticleListing(year); - page.SidebarMetadataUpdated += metadata => Schedule(() => + displayedYear = year; + lastCursor = null; + + beginLoading(true); + + request = new GetNewsRequest(displayedYear); + request.Success += response => Schedule(() => { - sidebar.Metadata.Value = metadata; - Loading.Hide(); + lastCursor = response.Cursor; + sidebar.Metadata.Value = response.SidebarMetadata; + + var listing = new ArticleListing(getMorePosts); + listing.AddPosts(response.NewsPosts, response.Cursor != null); + LoadDisplay(listing); }); - LoadDisplay(page); + + API.PerformAsync(request); + } + + private void getMorePosts() + { + beginLoading(false); + + request = new GetNewsRequest(displayedYear, lastCursor); + request.Success += response => Schedule(() => + { + lastCursor = response.Cursor; + if (content.Child is ArticleListing listing) + listing.AddPosts(response.NewsPosts, response.Cursor != null); + }); + + API.PerformAsync(request); } private void loadArticle(string article) { - beginLoading(); + // This is not yet implemented nor called from anywhere. + beginLoading(true); Header.SetArticle(article); - - // Temporary, should be handled by ArticleDisplay later LoadDisplay(Empty()); - Loading.Hide(); } - private void beginLoading() + private void beginLoading(bool showLoadingOverlay) { + request?.Cancel(); cancellationToken?.Cancel(); - Loading.Show(); + + if (showLoadingOverlay) + Loading.Show(); } protected override void Dispose(bool isDisposing) { + request?.Cancel(); cancellationToken?.Cancel(); base.Dispose(isDisposing); } diff --git a/osu.Game/Overlays/RestoreDefaultValueButton.cs b/osu.Game/Overlays/RestoreDefaultValueButton.cs new file mode 100644 index 0000000000..213ad2ba68 --- /dev/null +++ b/osu.Game/Overlays/RestoreDefaultValueButton.cs @@ -0,0 +1,106 @@ +// 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.Bindables; +using osuTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + public class RestoreDefaultValueButton : OsuButton, IHasTooltip, IHasCurrentValue + { + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + // this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button. + public override bool AcceptsFocus => true; + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private Color4 buttonColour; + + private bool hovering; + + public RestoreDefaultValueButton() + { + Height = 1; + + RelativeSizeAxes = Axes.Y; + Width = SettingsPanel.CONTENT_MARGINS; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + BackgroundColour = colour.Yellow; + buttonColour = colour.Yellow; + Content.Width = 0.33f; + Content.CornerRadius = 3; + Content.EdgeEffect = new EdgeEffectParameters + { + Colour = buttonColour.Opacity(0.1f), + Type = EdgeEffectType.Glow, + Radius = 2, + }; + + Padding = new MarginPadding { Vertical = 1.5f }; + Alpha = 0f; + + Action += () => + { + if (!current.Disabled) current.SetDefault(); + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.ValueChanged += _ => UpdateState(); + Current.DisabledChanged += _ => UpdateState(); + Current.DefaultChanged += _ => UpdateState(); + + UpdateState(); + } + + public string TooltipText => "revert to default"; + + protected override bool OnHover(HoverEvent e) + { + hovering = true; + UpdateState(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hovering = false; + UpdateState(); + } + + public void UpdateState() => Scheduler.AddOnce(updateState); + + private void updateState() + { + if (current == null) + return; + + this.FadeTo(current.IsDefault ? 0f : + hovering && !current.Disabled ? 1f : 0.65f, 200, Easing.OutQuint); + this.FadeColour(current.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 86a836d29b..807916e7f6 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -5,16 +5,11 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osuTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -108,7 +103,7 @@ namespace osu.Game.Overlays.Settings protected SettingsItem() { - RestoreDefaultValueButton restoreDefaultButton; + RestoreDefaultValueButton restoreDefaultButton; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -116,7 +111,7 @@ namespace osu.Game.Overlays.Settings InternalChildren = new Drawable[] { - restoreDefaultButton = new RestoreDefaultValueButton(), + restoreDefaultButton = new RestoreDefaultValueButton(), FlowContent = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -137,7 +132,7 @@ namespace osu.Game.Overlays.Settings controlWithCurrent.Current.DisabledChanged += _ => updateDisabled(); if (ShowsDefaultIndicator) - restoreDefaultButton.Bindable = controlWithCurrent.Current; + restoreDefaultButton.Current = controlWithCurrent.Current; } } @@ -146,101 +141,5 @@ namespace osu.Game.Overlays.Settings if (labelText != null) labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } - - protected internal class RestoreDefaultValueButton : Container, IHasTooltip - { - public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; - - private Bindable bindable; - - public Bindable Bindable - { - get => bindable; - set - { - bindable = value; - bindable.ValueChanged += _ => UpdateState(); - bindable.DisabledChanged += _ => UpdateState(); - bindable.DefaultChanged += _ => UpdateState(); - UpdateState(); - } - } - - private Color4 buttonColour; - - private bool hovering; - - public RestoreDefaultValueButton() - { - RelativeSizeAxes = Axes.Y; - Width = SettingsPanel.CONTENT_MARGINS; - Padding = new MarginPadding { Vertical = 1.5f }; - Alpha = 0f; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colour) - { - buttonColour = colour.Yellow; - - Child = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - CornerRadius = 3, - Masking = true, - Colour = buttonColour, - EdgeEffect = new EdgeEffectParameters - { - Colour = buttonColour.Opacity(0.1f), - Type = EdgeEffectType.Glow, - Radius = 2, - }, - Width = 0.33f, - Child = new Box { RelativeSizeAxes = Axes.Both }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - UpdateState(); - } - - public string TooltipText => "revert to default"; - - protected override bool OnClick(ClickEvent e) - { - if (bindable != null && !bindable.Disabled) - bindable.SetDefault(); - return true; - } - - protected override bool OnHover(HoverEvent e) - { - hovering = true; - UpdateState(); - return false; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hovering = false; - UpdateState(); - } - - public void UpdateState() => Scheduler.AddOnce(updateState); - - private void updateState() - { - if (bindable == null) - return; - - this.FadeTo(bindable.IsDefault ? 0f : - hovering && !bindable.Disabled ? 1f : 0.65f, 200, Easing.OutQuint); - this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); - } - } } -} +} \ No newline at end of file diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index f0a11d67b7..eae828c142 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; 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.Events; using osu.Game.Graphics; @@ -49,8 +50,6 @@ namespace osu.Game.Overlays private readonly bool showSidebar; - protected Box Background; - protected SettingsPanel(bool showSidebar) { this.showSidebar = showSidebar; @@ -63,13 +62,13 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - InternalChild = ContentContainer = new Container + InternalChild = ContentContainer = new NonMaskedContent { Width = WIDTH, RelativeSizeAxes = Axes.Y, Children = new Drawable[] { - Background = new Box + new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -165,7 +164,7 @@ namespace osu.Game.Overlays { base.PopOut(); - ContentContainer.MoveToX(-WIDTH, TRANSITION_LENGTH, Easing.OutQuint); + ContentContainer.MoveToX(-WIDTH + ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint); Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); @@ -191,6 +190,12 @@ namespace osu.Game.Overlays Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; } + private class NonMaskedContent : Container + { + // masking breaks the pan-out transform with nested sub-settings panels. + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + } + public class SettingsSectionsContainer : SectionsContainer { public SearchContainer SearchContainer; diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs new file mode 100644 index 0000000000..4e671cca6d --- /dev/null +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownContainer.cs @@ -0,0 +1,50 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Markdig.Extensions.Yaml; +using Markdig.Syntax; +using Markdig.Syntax.Inlines; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Containers.Markdown; +using osu.Game.Graphics.Containers.Markdown; + +namespace osu.Game.Overlays.Wiki.Markdown +{ + public class WikiMarkdownContainer : OsuMarkdownContainer + { + public string CurrentPath + { + set => DocumentUrl = $"{DocumentUrl}wiki/{value}"; + } + + protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level) + { + switch (markdownObject) + { + case YamlFrontMatterBlock yamlFrontMatterBlock: + container.Add(new WikiNoticeContainer(yamlFrontMatterBlock)); + break; + + case ParagraphBlock paragraphBlock: + // Check if paragraph only contains an image + if (paragraphBlock.Inline.Count() == 1 && paragraphBlock.Inline.FirstChild is LinkInline { IsImage: true } linkInline) + { + container.Add(new WikiMarkdownImageBlock(linkInline)); + return; + } + + break; + } + + base.AddMarkdownComponent(markdownObject, container, level); + } + + public override MarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer(); + + private class WikiMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer + { + protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline)); + } + } +} diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs new file mode 100644 index 0000000000..c2115efeb5 --- /dev/null +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImage.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Markdig.Syntax.Inlines; +using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Cursor; + +namespace osu.Game.Overlays.Wiki.Markdown +{ + public class WikiMarkdownImage : MarkdownImage, IHasTooltip + { + public string TooltipText { get; } + + public WikiMarkdownImage(LinkInline linkInline) + : base(linkInline.Url) + { + TooltipText = linkInline.Title; + } + + protected override ImageContainer CreateImageContainer(string url) + { + // The idea is replace "https://website.url/wiki/{path-to-image}" to "https://website.url/wiki/images/{path-to-image}" + // "/wiki/images/*" is route to fetch wiki image from osu!web server (see: https://github.com/ppy/osu-web/blob/4205eb66a4da86bdee7835045e4bf28c35456e04/routes/web.php#L289) + url = url.Replace("/wiki/", "/wiki/images/"); + + return base.CreateImageContainer(url); + } + } +} diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs new file mode 100644 index 0000000000..179762103a --- /dev/null +++ b/osu.Game/Overlays/Wiki/Markdown/WikiMarkdownImageBlock.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Markdig.Syntax.Inlines; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Containers.Markdown; +using osuTK; + +namespace osu.Game.Overlays.Wiki.Markdown +{ + public class WikiMarkdownImageBlock : FillFlowContainer + { + [Resolved] + private IMarkdownTextComponent parentTextComponent { get; set; } + + private readonly LinkInline linkInline; + + public WikiMarkdownImageBlock(LinkInline linkInline) + { + this.linkInline = linkInline; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(0, 3); + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new WikiMarkdownImage(linkInline) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + parentTextComponent.CreateSpriteText().With(t => + { + t.Text = linkInline.Title; + t.Anchor = Anchor.TopCentre; + t.Origin = Anchor.TopCentre; + }), + }; + } + } +} diff --git a/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs new file mode 100644 index 0000000000..421806eea8 --- /dev/null +++ b/osu.Game/Overlays/Wiki/Markdown/WikiNoticeContainer.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Markdig.Extensions.Yaml; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Containers.Markdown; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Wiki.Markdown +{ + public class WikiNoticeContainer : FillFlowContainer + { + private readonly bool isOutdated; + private readonly bool needsCleanup; + + public WikiNoticeContainer(YamlFrontMatterBlock yamlFrontMatterBlock) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + + foreach (var line in yamlFrontMatterBlock.Lines) + { + switch (line.ToString()) + { + case "outdated: true": + isOutdated = true; + break; + + case "needs_cleanup: true": + needsCleanup = true; + break; + } + } + } + + [BackgroundDependencyLoader] + private void load() + { + // Reference : https://github.com/ppy/osu-web/blob/master/resources/views/wiki/_notice.blade.php and https://github.com/ppy/osu-web/blob/master/resources/lang/en/wiki.php + // TODO : add notice box for fallback translation, legal translation and outdated translation after implement wiki locale in the future. + if (isOutdated) + { + Add(new NoticeBox + { + Text = "The content on this page is incomplete or outdated. If you are able to help out, please consider updating the article!", + }); + } + else if (needsCleanup) + { + Add(new NoticeBox + { + Text = "This page does not meet the standards of the osu! wiki and needs to be cleaned up or rewritten. If you are able to help out, please consider updating the article!", + }); + } + } + + private class NoticeBox : Container + { + [Resolved] + private IMarkdownTextFlowComponent parentFlowComponent { get; set; } + + public string Text { get; set; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colour) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + MarkdownTextFlowContainer textFlow; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + textFlow = parentFlowComponent.CreateTextFlow().With(t => + { + t.Colour = colour.Orange1; + t.Padding = new MarginPadding + { + Vertical = 10, + Horizontal = 15, + }; + }) + }; + + textFlow.AddText(Text); + } + } + } +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index cc663c37af..cca55819c5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -311,6 +311,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Invoked for this to take on any values from a newly-applied . + /// This is also fired after any changes which occurred via an call. /// protected virtual void OnApply() { @@ -318,6 +319,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Invoked for this to revert any values previously taken on from the currently-applied . + /// This is also fired after any changes which occurred via an call. /// protected virtual void OnFree() { diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs index 5a6f98f504..22b211f257 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorBlueprintContainer.cs @@ -77,7 +77,13 @@ namespace osu.Game.Screens.Edit.Compose.Components double offset = result.Time.Value - blueprints.First().Item.StartTime; if (offset != 0) - Beatmap.PerformOnSelection(obj => obj.StartTime += offset); + { + Beatmap.PerformOnSelection(obj => + { + obj.StartTime += offset; + Beatmap.Update(obj); + }); + } } return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs index 2141c490df..246d4aa8d7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/EditorSelectionHandler.cs @@ -125,6 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return; h.Samples.Add(new HitSampleInfo(sampleName)); + EditorBeatmap.Update(h); }); } @@ -134,7 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName)); + EditorBeatmap.PerformOnSelection(h => + { + h.SamplesBindable.RemoveAll(s => s.Name == sampleName); + EditorBeatmap.Update(h); + }); } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 7c1bbd65f9..6f04f36b83 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -276,7 +276,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime); double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount; - EditorBeatmap.PerformOnSelection(h => h.StartTime += adjustment); + EditorBeatmap.PerformOnSelection(h => + { + h.StartTime += adjustment; + EditorBeatmap.Update(h); + }); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 39f9e2d388..a9f3edf049 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -522,7 +522,10 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) { ValidForResume = false; - this.MakeCurrent(); + + // in the potential case that this instance has already been exited, this is required to avoid a crash. + if (this.GetChildScreen() != null) + this.MakeCurrent(); return; } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index a3fca3d4e1..9773bd5ce9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - public IEnumerable DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty(); + public IEnumerable DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty() : beatmapContainer.AliveChildren; [CanBeNull] private Container beatmapContainer; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 4c42823779..ca041da801 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -57,7 +57,13 @@ namespace osu.Game.Storyboards.Drawables public DrawableStoryboard(Storyboard storyboard) { Storyboard = storyboard; + Size = new Vector2(640, 480); + + bool onlyHasVideoElements = Storyboard.Layers.SelectMany(l => l.Elements).Any(e => !(e is StoryboardVideo)); + + Width = Height * (storyboard.BeatmapInfo.WidescreenStoryboard || onlyHasVideoElements ? 16 / 9f : 4 / 3f); + Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs index 2ada83c3b4..1085b52d65 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -5,6 +5,7 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -15,6 +16,8 @@ namespace osu.Game.Storyboards.Drawables public override bool IsPresent => Enabled && base.IsPresent; + protected LayerElementContainer ElementContainer { get; } + public DrawableStoryboardLayer(StoryboardLayer layer) { Layer = layer; @@ -24,10 +27,10 @@ namespace osu.Game.Storyboards.Drawables Enabled = layer.VisibleWhenPassing; Masking = layer.Masking; - InternalChild = new LayerElementContainer(layer); + InternalChild = ElementContainer = new LayerElementContainer(layer); } - private class LayerElementContainer : LifetimeManagementContainer + protected class LayerElementContainer : LifetimeManagementContainer { private readonly StoryboardLayer storyboardLayer; @@ -35,8 +38,8 @@ namespace osu.Game.Storyboards.Drawables { storyboardLayer = layer; - Width = 640; - Height = 480; + Size = new Vector2(640, 480); + Anchor = Anchor.Centre; Origin = Anchor.Centre; } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index bc61f704dd..3486c1d66a 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -53,7 +53,7 @@ namespace osu.Game.Storyboards public Storyboard() { - layers.Add("Video", new StoryboardLayer("Video", 4, false)); + layers.Add("Video", new StoryboardVideoLayer("Video", 4, false)); layers.Add("Background", new StoryboardLayer("Background", 3)); layers.Add("Fail", new StoryboardLayer("Fail", 2) { VisibleWhenPassing = false, }); layers.Add("Pass", new StoryboardLayer("Pass", 1) { VisibleWhenFailing = false, }); @@ -85,12 +85,8 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) - { - var drawable = new DrawableStoryboard(this); - drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f); - return drawable; - } + public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) => + new DrawableStoryboard(this); public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) { diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs index 1cde7cf67a..fa9d4ebfea 100644 --- a/osu.Game/Storyboards/StoryboardLayer.cs +++ b/osu.Game/Storyboards/StoryboardLayer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Storyboards Elements.Add(element); } - public DrawableStoryboardLayer CreateDrawable() + public virtual DrawableStoryboardLayer CreateDrawable() => new DrawableStoryboardLayer(this) { Depth = Depth, Name = Name }; } } diff --git a/osu.Game/Storyboards/StoryboardVideoLayer.cs b/osu.Game/Storyboards/StoryboardVideoLayer.cs new file mode 100644 index 0000000000..2a01c2274a --- /dev/null +++ b/osu.Game/Storyboards/StoryboardVideoLayer.cs @@ -0,0 +1,32 @@ +// 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; +using osu.Game.Storyboards.Drawables; +using osuTK; + +namespace osu.Game.Storyboards +{ + public class StoryboardVideoLayer : StoryboardLayer + { + public StoryboardVideoLayer(string name, int depth, bool masking) + : base(name, depth, masking) + { + } + + public override DrawableStoryboardLayer CreateDrawable() + => new DrawableStoryboardVideoLayer(this) { Depth = Depth, Name = Name }; + + public class DrawableStoryboardVideoLayer : DrawableStoryboardLayer + { + public DrawableStoryboardVideoLayer(StoryboardVideoLayer layer) + : base(layer) + { + // for videos we want to take on the full size of the storyboard container hierarchy + // to allow the video to fill the full available region. + ElementContainer.RelativeSizeAxes = Axes.Both; + ElementContainer.Size = Vector2.One; + } + } + } +} diff --git a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs index 3a5ffa8770..c7aa43b377 100644 --- a/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs +++ b/osu.Game/Tests/Visual/Spectator/TestSpectatorClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -53,6 +54,8 @@ namespace osu.Game.Tests.Visual.Spectator }); } + public new void Schedule(Action action) => base.Schedule(action); + /// /// Sends frames for an arbitrary user. ///