mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 17:43:05 +08:00
Merge branch 'master' into mania-pooling
This commit is contained in:
commit
bddc3121dc
@ -15,7 +15,6 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
@ -35,7 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestPlaceBeforeCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
|
||||
AddStep("move mouse before current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
@ -45,7 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestPlaceAfterCurrentTimeDownwards()
|
||||
{
|
||||
AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single()));
|
||||
AddStep("move mouse after current time", () =>
|
||||
{
|
||||
var column = this.ChildrenOfType<Column>().Single();
|
||||
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
|
||||
});
|
||||
|
||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
|
@ -0,0 +1,67 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public class TestSceneDrawableManiaHitObject : OsuTestScene
|
||||
{
|
||||
private readonly ManualClock clock = new ManualClock();
|
||||
|
||||
private Column column;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = new ScrollingTestContainer(ScrollingDirection.Down)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
TimeRange = 2000,
|
||||
Clock = new FramedClock(clock),
|
||||
Child = column = new Column(0)
|
||||
{
|
||||
Action = { Value = ManiaAction.Key1 },
|
||||
Height = 0.85f,
|
||||
AccentColour = Color4.Gray
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestHoldNoteHeadVisibility()
|
||||
{
|
||||
DrawableHoldNote note = null;
|
||||
AddStep("Add hold note", () =>
|
||||
{
|
||||
var h = new HoldNote
|
||||
{
|
||||
StartTime = 0,
|
||||
Duration = 1000
|
||||
};
|
||||
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
column.Add(note = new DrawableHoldNote(h));
|
||||
});
|
||||
AddStep("Hold key", () =>
|
||||
{
|
||||
clock.CurrentTime = 0;
|
||||
note.OnPressed(ManiaAction.Key1);
|
||||
});
|
||||
AddStep("progress time", () => clock.CurrentTime = 500);
|
||||
AddAssert("head is visible", () => note.Head.Alpha == 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
|
||||
}
|
||||
|
||||
private class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
|
||||
availableLines.Push(line);
|
||||
|
||||
grid.Clear(false);
|
||||
grid.Clear();
|
||||
}
|
||||
|
||||
if (selectionTimeRange == null)
|
||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
=> AddStep("clear SHOC", () => hitObjectContainer.Clear(false));
|
||||
=> AddStep("clear SHOC", () => hitObjectContainer.Clear());
|
||||
|
||||
protected void AddHitObject(DrawableHitObject hitObject)
|
||||
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -12,6 +13,7 @@ using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Editing
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class TestSceneHitObjectContainerEventBuffer : OsuTestScene
|
||||
{
|
||||
private readonly TestHitObject testObj = new TestHitObject();
|
||||
|
@ -45,15 +45,16 @@ namespace osu.Game.Tests.Gameplay
|
||||
AddStep("Create DHO", () =>
|
||||
{
|
||||
dho = new TestDrawableHitObject(null);
|
||||
dho.Apply(entry = new TestLifetimeEntry(new HitObject())
|
||||
{
|
||||
LifetimeStart = 0,
|
||||
LifetimeEnd = 1000,
|
||||
});
|
||||
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
|
||||
Child = dho;
|
||||
});
|
||||
|
||||
AddStep("KeepAlive = true", () => entry.KeepAlive = true);
|
||||
AddStep("KeepAlive = true", () =>
|
||||
{
|
||||
entry.LifetimeStart = 0;
|
||||
entry.LifetimeEnd = 1000;
|
||||
entry.KeepAlive = true;
|
||||
});
|
||||
AddAssert("Lifetime is overriden", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == double.MaxValue);
|
||||
|
||||
AddStep("Set LifetimeStart", () => dho.LifetimeStart = 500);
|
||||
|
152
osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs
Normal file
152
osu.Game.Tests/Visual/Online/TestSceneNewsSidebar.cs
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.News.Sidebar;
|
||||
using static osu.Game.Overlays.News.Sidebar.YearsPanel;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public class TestSceneNewsSidebar : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
private TestNewsSidebar sidebar;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => Child = sidebar = new TestNewsSidebar { YearChanged = onYearChanged });
|
||||
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
AddStep("Add metadata", () => sidebar.Metadata.Value = getMetadata(2021));
|
||||
AddUntilStep("Month sections exist", () => sidebar.ChildrenOfType<MonthSection>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMetadataWithNoPosts()
|
||||
{
|
||||
AddStep("Add data with no posts", () => sidebar.Metadata.Value = metadata_with_no_posts);
|
||||
AddUntilStep("No month sections were created", () => !sidebar.ChildrenOfType<MonthSection>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestYearsPanelVisibility()
|
||||
{
|
||||
AddUntilStep("Years panel is hidden", () => yearsPanel?.Alpha == 0);
|
||||
AddStep("Add data", () => sidebar.Metadata.Value = getMetadata(2021));
|
||||
AddUntilStep("Years panel is visible", () => yearsPanel?.Alpha == 1);
|
||||
}
|
||||
|
||||
private void onYearChanged(int year) => sidebar.Metadata.Value = getMetadata(year);
|
||||
|
||||
private YearsPanel yearsPanel => sidebar.ChildrenOfType<YearsPanel>().FirstOrDefault();
|
||||
|
||||
private APINewsSidebar getMetadata(int year) => new APINewsSidebar
|
||||
{
|
||||
CurrentYear = year,
|
||||
Years = new[]
|
||||
{
|
||||
2021,
|
||||
2020,
|
||||
2019,
|
||||
2018,
|
||||
2017,
|
||||
2016,
|
||||
2015,
|
||||
2014,
|
||||
2013
|
||||
},
|
||||
NewsPosts = new List<APINewsPost>
|
||||
{
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "(Mar) Short title",
|
||||
PublishedAt = new DateTime(year, 3, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "(Mar) Oh boy that's a long post title I wonder if it will break anything",
|
||||
PublishedAt = new DateTime(year, 3, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "(Mar) Medium title, nothing to see here",
|
||||
PublishedAt = new DateTime(year, 3, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "(Feb) Short title",
|
||||
PublishedAt = new DateTime(year, 2, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "(Feb) Oh boy that's a long post title I wonder if it will break anything",
|
||||
PublishedAt = new DateTime(year, 2, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "(Feb) Medium title, nothing to see here",
|
||||
PublishedAt = new DateTime(year, 2, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "Short title",
|
||||
PublishedAt = new DateTime(year, 1, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "Oh boy that's a long post title I wonder if it will break anything",
|
||||
PublishedAt = new DateTime(year, 1, 1)
|
||||
},
|
||||
new APINewsPost
|
||||
{
|
||||
Title = "Medium title, nothing to see here",
|
||||
PublishedAt = new DateTime(year, 1, 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly APINewsSidebar metadata_with_no_posts = new APINewsSidebar
|
||||
{
|
||||
CurrentYear = 2021,
|
||||
Years = new[]
|
||||
{
|
||||
2021,
|
||||
2020,
|
||||
2019,
|
||||
2018,
|
||||
2017,
|
||||
2016,
|
||||
2015,
|
||||
2014,
|
||||
2013
|
||||
},
|
||||
NewsPosts = Array.Empty<APINewsPost>()
|
||||
};
|
||||
|
||||
private class TestNewsSidebar : NewsSidebar
|
||||
{
|
||||
public Action<int> YearChanged;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Metadata.BindValueChanged(metadata =>
|
||||
{
|
||||
foreach (var b in this.ChildrenOfType<YearButton>())
|
||||
b.Action = () => YearChanged?.Invoke(b.Year);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,5 +11,8 @@ namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
[JsonProperty("news_posts")]
|
||||
public IEnumerable<APINewsPost> NewsPosts;
|
||||
|
||||
[JsonProperty("news_sidebar")]
|
||||
public APINewsSidebar SidebarMetadata;
|
||||
}
|
||||
}
|
||||
|
20
osu.Game/Online/API/Requests/Responses/APINewsSidebar.cs
Normal file
20
osu.Game/Online/API/Requests/Responses/APINewsSidebar.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
public class APINewsSidebar
|
||||
{
|
||||
[JsonProperty("current_year")]
|
||||
public int CurrentYear { get; set; }
|
||||
|
||||
[JsonProperty("news_posts")]
|
||||
public IEnumerable<APINewsPost> NewsPosts { get; set; }
|
||||
|
||||
[JsonProperty("years")]
|
||||
public int[] Years { get; set; }
|
||||
}
|
||||
}
|
179
osu.Game/Overlays/News/Sidebar/MonthSection.cs
Normal file
179
osu.Game/Overlays/News/Sidebar/MonthSection.cs
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
{
|
||||
public class MonthSection : CompositeDrawable
|
||||
{
|
||||
private const int animation_duration = 250;
|
||||
|
||||
public readonly BindableBool Expanded = new BindableBool();
|
||||
|
||||
public MonthSection(int month, int year, IEnumerable<APINewsPost> posts)
|
||||
{
|
||||
Debug.Assert(posts.All(p => p.PublishedAt.Month == month && p.PublishedAt.Year == year));
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Masking = true;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DropdownHeader(month, year)
|
||||
{
|
||||
Expanded = { BindTarget = Expanded }
|
||||
},
|
||||
new PostsContainer
|
||||
{
|
||||
Expanded = { BindTarget = Expanded },
|
||||
Children = posts.Select(p => new PostButton(p)).ToArray()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class DropdownHeader : OsuClickableContainer
|
||||
{
|
||||
public readonly BindableBool Expanded = new BindableBool();
|
||||
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
public DropdownHeader(int month, int year)
|
||||
{
|
||||
var date = new DateTime(year, month, 1);
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 15;
|
||||
Action = Expanded.Toggle;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
|
||||
Text = date.ToString("MMM yyyy")
|
||||
},
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Size = new Vector2(10),
|
||||
Icon = FontAwesome.Solid.ChevronDown
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Expanded.BindValueChanged(open =>
|
||||
{
|
||||
icon.Scale = new Vector2(1, open.NewValue ? -1 : 1);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
private class PostButton : OsuHoverContainer
|
||||
{
|
||||
protected override IEnumerable<Drawable> EffectTargets => new[] { text };
|
||||
|
||||
private readonly TextFlowContainer text;
|
||||
private readonly APINewsPost post;
|
||||
|
||||
public PostButton(APINewsPost post)
|
||||
{
|
||||
this.post = post;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Child = text = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12))
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = post.Title
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColours, GameHost host)
|
||||
{
|
||||
IdleColour = overlayColours.Light2;
|
||||
HoverColour = overlayColours.Light1;
|
||||
|
||||
TooltipText = "view in browser";
|
||||
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
|
||||
}
|
||||
}
|
||||
|
||||
private class PostsContainer : Container
|
||||
{
|
||||
public readonly BindableBool Expanded = new BindableBool();
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
public PostsContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
AutoSizeDuration = animation_duration;
|
||||
AutoSizeEasing = Easing.Out;
|
||||
InternalChild = Content = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Alpha = 0
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
Expanded.BindValueChanged(updateState, true);
|
||||
}
|
||||
|
||||
private void updateState(ValueChangedEvent<bool> expanded)
|
||||
{
|
||||
ClearTransforms(true);
|
||||
|
||||
if (expanded.NewValue)
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Content.FadeIn(animation_duration, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
AutoSizeAxes = Axes.None;
|
||||
this.ResizeHeightTo(0, animation_duration, Easing.OutQuint);
|
||||
|
||||
Content.FadeOut(animation_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
103
osu.Game/Overlays/News/Sidebar/NewsSidebar.cs
Normal file
103
osu.Game/Overlays/News/Sidebar/NewsSidebar.cs
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
{
|
||||
public class NewsSidebar : CompositeDrawable
|
||||
{
|
||||
[Cached]
|
||||
public readonly Bindable<APINewsSidebar> Metadata = new Bindable<APINewsSidebar>();
|
||||
|
||||
private FillFlowContainer<MonthSection> monthsFlow;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Width = 250;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Vertical = 20,
|
||||
Left = 50,
|
||||
Right = 30
|
||||
},
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new YearsPanel(),
|
||||
monthsFlow = new FillFlowContainer<MonthSection>
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Metadata.BindValueChanged(onMetadataChanged, true);
|
||||
}
|
||||
|
||||
private void onMetadataChanged(ValueChangedEvent<APINewsSidebar> metadata)
|
||||
{
|
||||
monthsFlow.Clear();
|
||||
|
||||
if (metadata.NewValue == null)
|
||||
return;
|
||||
|
||||
var allPosts = metadata.NewValue.NewsPosts;
|
||||
|
||||
if (allPosts?.Any() != true)
|
||||
return;
|
||||
|
||||
var lookup = metadata.NewValue.NewsPosts.ToLookup(post => post.PublishedAt.Month);
|
||||
|
||||
var keys = lookup.Select(kvp => kvp.Key);
|
||||
var sortedKeys = keys.OrderByDescending(k => k).ToList();
|
||||
|
||||
var year = metadata.NewValue.CurrentYear;
|
||||
|
||||
for (int i = 0; i < sortedKeys.Count; i++)
|
||||
{
|
||||
var month = sortedKeys[i];
|
||||
var posts = lookup[month];
|
||||
|
||||
monthsFlow.Add(new MonthSection(month, year, posts)
|
||||
{
|
||||
Expanded = { Value = i == 0 }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
113
osu.Game/Overlays/News/Sidebar/YearsPanel.cs
Normal file
113
osu.Game/Overlays/News/Sidebar/YearsPanel.cs
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
{
|
||||
public class YearsPanel : CompositeDrawable
|
||||
{
|
||||
private readonly Bindable<APINewsSidebar> metadata = new Bindable<APINewsSidebar>();
|
||||
|
||||
private FillFlowContainer yearsFlow;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColours, Bindable<APINewsSidebar> metadata)
|
||||
{
|
||||
this.metadata.BindTo(metadata);
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Masking = true;
|
||||
CornerRadius = 6;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = overlayColours.Background3
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(5),
|
||||
Child = yearsFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 5)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
metadata.BindValueChanged(_ => recreateDrawables(), true);
|
||||
}
|
||||
|
||||
private void recreateDrawables()
|
||||
{
|
||||
yearsFlow.Clear();
|
||||
|
||||
if (metadata.Value == null)
|
||||
{
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
var currentYear = metadata.Value.CurrentYear;
|
||||
|
||||
foreach (var y in metadata.Value.Years)
|
||||
yearsFlow.Add(new YearButton(y, y == currentYear));
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
public class YearButton : OsuHoverContainer
|
||||
{
|
||||
public int Year { get; }
|
||||
|
||||
private readonly bool isCurrent;
|
||||
|
||||
public YearButton(int year, bool isCurrent)
|
||||
{
|
||||
Year = year;
|
||||
this.isCurrent = isCurrent;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Width = 0.25f;
|
||||
Height = 15;
|
||||
|
||||
Child = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.GetFont(size: 12, weight: isCurrent ? FontWeight.SemiBold : FontWeight.Medium),
|
||||
Text = year.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
IdleColour = isCurrent ? Color4.White : colourProvider.Light2;
|
||||
HoverColour = isCurrent ? Color4.White : colourProvider.Light1;
|
||||
Action = () => { }; // Avoid button being disabled since there's no proper action assigned.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// <summary>
|
||||
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// </summary>
|
||||
protected TEntry? Entry { get; private set; }
|
||||
public TEntry? Entry { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
@ -28,14 +29,28 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set => setLifetime(value, LifetimeEnd);
|
||||
get => Entry?.LifetimeStart ?? double.MinValue;
|
||||
set
|
||||
{
|
||||
if (Entry == null && LifetimeStart != value)
|
||||
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
|
||||
|
||||
if (Entry != null)
|
||||
Entry.LifetimeStart = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, value);
|
||||
get => Entry?.LifetimeEnd ?? double.MaxValue;
|
||||
set
|
||||
{
|
||||
if (Entry == null && LifetimeEnd != value)
|
||||
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
|
||||
|
||||
if (Entry != null)
|
||||
Entry.LifetimeEnd = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
@ -64,11 +79,8 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
|
||||
setLifetime(entry.LifetimeStart, entry.LifetimeEnd);
|
||||
Entry = entry;
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
}
|
||||
|
||||
@ -95,27 +107,12 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
}
|
||||
|
||||
private void setLifetime(double start, double end)
|
||||
{
|
||||
base.LifetimeStart = start;
|
||||
base.LifetimeEnd = end;
|
||||
|
||||
if (Entry != null)
|
||||
{
|
||||
Entry.LifetimeStart = start;
|
||||
Entry.LifetimeEnd = end;
|
||||
}
|
||||
}
|
||||
|
||||
private void free()
|
||||
{
|
||||
Debug.Assert(Entry != null && HasEntryApplied);
|
||||
|
||||
OnFree(Entry);
|
||||
|
||||
Entry = null;
|
||||
setLifetime(double.MaxValue, double.MaxValue);
|
||||
|
||||
HasEntryApplied = false;
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,18 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public class HitObjectContainer : LifetimeManagementContainer, IHitObjectContainer
|
||||
public class HitObjectContainer : CompositeDrawable, IHitObjectContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// All entries in this <see cref="HitObjectContainer"/> including dead entries.
|
||||
/// </summary>
|
||||
public IEnumerable<HitObjectLifetimeEntry> Entries => allEntries;
|
||||
|
||||
/// <summary>
|
||||
/// All alive entries and <see cref="DrawableHitObject"/>s used by the entries.
|
||||
/// </summary>
|
||||
public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
|
||||
|
||||
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
||||
|
||||
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
||||
@ -60,8 +70,12 @@ namespace osu.Game.Rulesets.UI
|
||||
internal double FutureLifetimeExtension { get; set; }
|
||||
|
||||
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
|
||||
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> drawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
|
||||
|
||||
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> aliveDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
|
||||
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
|
||||
|
||||
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||
private readonly HashSet<HitObjectLifetimeEntry> allEntries = new HashSet<HitObjectLifetimeEntry>();
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
|
||||
@ -72,6 +86,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
lifetimeManager.EntryBecameAlive += entryBecameAlive;
|
||||
lifetimeManager.EntryBecameDead += entryBecameDead;
|
||||
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
@ -84,93 +99,113 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#region Pooling support
|
||||
|
||||
public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry);
|
||||
|
||||
public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry);
|
||||
|
||||
private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry);
|
||||
|
||||
private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry);
|
||||
|
||||
private void addDrawable(HitObjectLifetimeEntry entry)
|
||||
public void Add(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
Debug.Assert(!drawableMap.ContainsKey(entry));
|
||||
allEntries.Add(entry);
|
||||
lifetimeManager.AddEntry(entry);
|
||||
}
|
||||
|
||||
var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null);
|
||||
public bool Remove(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
if (!lifetimeManager.RemoveEntry(entry)) return false;
|
||||
|
||||
// This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry.
|
||||
if (nonPooledDrawableMap.Remove(entry, out var drawable))
|
||||
removeDrawable(drawable);
|
||||
|
||||
allEntries.Remove(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void entryBecameAlive(LifetimeEntry lifetimeEntry)
|
||||
{
|
||||
var entry = (HitObjectLifetimeEntry)lifetimeEntry;
|
||||
Debug.Assert(!aliveDrawableMap.ContainsKey(entry));
|
||||
|
||||
bool isNonPooled = nonPooledDrawableMap.TryGetValue(entry, out var drawable);
|
||||
drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null);
|
||||
if (drawable == null)
|
||||
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
|
||||
|
||||
aliveDrawableMap[entry] = drawable;
|
||||
OnAdd(drawable);
|
||||
|
||||
if (isNonPooled) return;
|
||||
|
||||
addDrawable(drawable);
|
||||
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
||||
}
|
||||
|
||||
private void entryBecameDead(LifetimeEntry lifetimeEntry)
|
||||
{
|
||||
var entry = (HitObjectLifetimeEntry)lifetimeEntry;
|
||||
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
|
||||
|
||||
var drawable = aliveDrawableMap[entry];
|
||||
bool isNonPooled = nonPooledDrawableMap.ContainsKey(entry);
|
||||
|
||||
drawable.OnKilled();
|
||||
aliveDrawableMap.Remove(entry);
|
||||
OnRemove(drawable);
|
||||
|
||||
if (isNonPooled) return;
|
||||
|
||||
removeDrawable(drawable);
|
||||
// The hit object is not freed when the DHO was not pooled.
|
||||
HitObjectUsageFinished?.Invoke(entry.HitObject);
|
||||
}
|
||||
|
||||
private void addDrawable(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.OnNewResult += onNewResult;
|
||||
drawable.OnRevertResult += onRevertResult;
|
||||
|
||||
bindStartTime(drawable);
|
||||
AddInternal(drawableMap[entry] = drawable, false);
|
||||
OnAdd(drawable);
|
||||
|
||||
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
||||
AddInternal(drawable);
|
||||
}
|
||||
|
||||
private void removeDrawable(HitObjectLifetimeEntry entry)
|
||||
private void removeDrawable(DrawableHitObject drawable)
|
||||
{
|
||||
Debug.Assert(drawableMap.ContainsKey(entry));
|
||||
|
||||
var drawable = drawableMap[entry];
|
||||
|
||||
// OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding.
|
||||
drawable.OnKilled();
|
||||
drawable.OnNewResult -= onNewResult;
|
||||
drawable.OnRevertResult -= onRevertResult;
|
||||
|
||||
drawableMap.Remove(entry);
|
||||
|
||||
OnRemove(drawable);
|
||||
unbindStartTime(drawable);
|
||||
RemoveInternal(drawable);
|
||||
|
||||
HitObjectUsageFinished?.Invoke(entry.HitObject);
|
||||
RemoveInternal(drawable);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Non-pooling support
|
||||
|
||||
public virtual void Add(DrawableHitObject hitObject)
|
||||
public virtual void Add(DrawableHitObject drawable)
|
||||
{
|
||||
bindStartTime(hitObject);
|
||||
if (drawable.Entry == null)
|
||||
throw new InvalidOperationException($"May not add a {nameof(DrawableHitObject)} without {nameof(HitObject)} associated");
|
||||
|
||||
hitObject.OnNewResult += onNewResult;
|
||||
hitObject.OnRevertResult += onRevertResult;
|
||||
|
||||
AddInternal(hitObject);
|
||||
OnAdd(hitObject);
|
||||
nonPooledDrawableMap.Add(drawable.Entry, drawable);
|
||||
addDrawable(drawable);
|
||||
Add(drawable.Entry);
|
||||
}
|
||||
|
||||
public virtual bool Remove(DrawableHitObject hitObject)
|
||||
public virtual bool Remove(DrawableHitObject drawable)
|
||||
{
|
||||
OnRemove(hitObject);
|
||||
if (!RemoveInternal(hitObject))
|
||||
if (drawable.Entry == null)
|
||||
return false;
|
||||
|
||||
hitObject.OnNewResult -= onNewResult;
|
||||
hitObject.OnRevertResult -= onRevertResult;
|
||||
|
||||
unbindStartTime(hitObject);
|
||||
|
||||
return true;
|
||||
return Remove(drawable.Entry);
|
||||
}
|
||||
|
||||
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
|
||||
|
||||
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
|
||||
private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
|
||||
{
|
||||
if (!(e.Child is DrawableHitObject hitObject))
|
||||
return;
|
||||
if (nonPooledDrawableMap.TryGetValue((HitObjectLifetimeEntry)entry, out var drawable))
|
||||
OnChildLifetimeBoundaryCrossed(new LifetimeBoundaryCrossedEvent(drawable, kind, direction));
|
||||
}
|
||||
|
||||
if ((e.Kind == LifetimeBoundaryKind.End && e.Direction == LifetimeBoundaryCrossingDirection.Forward)
|
||||
|| (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward))
|
||||
{
|
||||
hitObject.OnKilled();
|
||||
}
|
||||
protected virtual void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -195,12 +230,13 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Clear(bool disposeChildren = true)
|
||||
public virtual void Clear()
|
||||
{
|
||||
lifetimeManager.ClearEntries();
|
||||
|
||||
ClearInternal(disposeChildren);
|
||||
unbindAllStartTimes();
|
||||
foreach (var drawable in nonPooledDrawableMap.Values)
|
||||
removeDrawable(drawable);
|
||||
nonPooledDrawableMap.Clear();
|
||||
Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed");
|
||||
}
|
||||
|
||||
protected override bool CheckChildrenLife()
|
||||
|
@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
timeRange.ValueChanged += _ => layoutCache.Invalidate();
|
||||
}
|
||||
|
||||
public override void Clear(bool disposeChildren = true)
|
||||
public override void Clear()
|
||||
{
|
||||
base.Clear(disposeChildren);
|
||||
base.Clear();
|
||||
|
||||
toComputeLifetime.Clear();
|
||||
layoutComputed.Clear();
|
||||
|
Loading…
Reference in New Issue
Block a user