diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e7f9628fc..f2066f27de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,60 @@ on: [push, pull_request] name: Continuous Integration jobs: + inspect-code: + name: Code Quality + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + # FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side. + # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e + - name: Install .NET 3.1.x LTS + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "3.1.x" + + - name: Install .NET 6.0.x + uses: actions/setup-dotnet@v1 + with: + dotnet-version: "6.0.x" + + - name: Restore Tools + run: dotnet tool restore + + - name: Restore Packages + run: dotnet restore + + - name: Restore inspectcode cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/inspectcode + key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }} + + - name: CodeFileSanity + run: | + # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. + # FIXME: Suppress warnings from templates project + exit_code=0 + while read -r line; do + if [[ ! -z "$line" ]]; then + echo "::error::$line" + exit_code=1 + fi + done <<< $(dotnet codefilesanity) + exit $exit_code + + # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. + # - name: .NET Format (Dry Run) + # run: dotnet format --dry-run --check + + - name: InspectCode + run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN + + - name: NVika + run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors + test: name: Test runs-on: ${{matrix.os.fullname}} @@ -93,58 +147,4 @@ jobs: # cannot accept .sln(f) files as arguments. # Build just the main game for now. - name: Build - run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug - - inspect-code: - name: Code Quality - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - # FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side. - # https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e - - name: Install .NET 3.1.x LTS - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "3.1.x" - - - name: Install .NET 6.0.x - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "6.0.x" - - - name: Restore Tools - run: dotnet tool restore - - - name: Restore Packages - run: dotnet restore - - - name: Restore inspectcode cache - uses: actions/cache@v3 - with: - path: ${{ github.workspace }}/inspectcode - key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }} - - - name: CodeFileSanity - run: | - # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. - # FIXME: Suppress warnings from templates project - exit_code=0 - while read -r line; do - if [[ ! -z "$line" ]]; then - echo "::error::$line" - exit_code=1 - fi - done <<< $(dotnet codefilesanity) - exit $exit_code - - # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. - # - name: .NET Format (Dry Run) - # run: dotnet format --dry-run --check - - - name: InspectCode - run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN - - - name: NVika - run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors + run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug \ No newline at end of file diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs new file mode 100644 index 0000000000..a3bfbd47a3 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChannelList.cs @@ -0,0 +1,188 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Chat.ChannelList; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChannelList : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [Cached] + private readonly Bindable selected = new Bindable(); + + private OsuSpriteText selectorText; + private OsuSpriteText selectedText; + private OsuSpriteText leaveText; + private ChannelList channelList; + + [SetUp] + public void SetUp() + { + Schedule(() => + { + Child = new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Height = 0.7f, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + selectorText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }, + new Drawable[] + { + selectedText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }, + new Drawable[] + { + leaveText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + }, + new Drawable[] + { + channelList = new ChannelList + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + Width = 190, + }, + }, + }, + }; + + channelList.OnRequestSelect += channel => + { + channelList.SelectorActive.Value = false; + selected.Value = channel; + }; + + channelList.OnRequestLeave += channel => + { + leaveText.Text = $"OnRequestLeave: {channel.Name}"; + leaveText.FadeOutFromOne(1000, Easing.InQuint); + selected.Value = null; + channelList.RemoveChannel(channel); + }; + + channelList.SelectorActive.BindValueChanged(change => + { + selectorText.Text = $"Channel Selector Active: {change.NewValue}"; + selected.Value = null; + }, true); + + selected.BindValueChanged(change => + { + selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}"; + }, true); + }); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Add Public Channels", () => + { + for (int i = 0; i < 10; i++) + channelList.AddChannel(createRandomPublicChannel()); + }); + + AddStep("Add Private Channels", () => + { + for (int i = 0; i < 10; i++) + channelList.AddChannel(createRandomPrivateChannel()); + }); + } + + [Test] + public void TestVisual() + { + AddStep("Unread Selected", () => + { + if (selected.Value != null) + channelList.GetItem(selected.Value).Unread.Value = true; + }); + + AddStep("Read Selected", () => + { + if (selected.Value != null) + channelList.GetItem(selected.Value).Unread.Value = false; + }); + + AddStep("Add Mention Selected", () => + { + if (selected.Value != null) + channelList.GetItem(selected.Value).Mentions.Value++; + }); + + AddStep("Add 98 Mentions Selected", () => + { + if (selected.Value != null) + channelList.GetItem(selected.Value).Mentions.Value += 98; + }); + + AddStep("Clear Mentions Selected", () => + { + if (selected.Value != null) + channelList.GetItem(selected.Value).Mentions.Value = 0; + }); + } + + private Channel createRandomPublicChannel() + { + int id = RNG.Next(0, 10000); + return new Channel + { + Name = $"#channel-{id}", + Type = ChannelType.Public, + Id = id, + }; + } + + private Channel createRandomPrivateChannel() + { + int id = RNG.Next(0, 10000); + return new Channel(new APIUser + { + Id = id, + Username = $"test user {id}", + }); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs b/osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs deleted file mode 100644 index af419c8b91..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using System.Collections.Generic; -using System.Linq; -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.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Chat; -using osu.Game.Overlays; -using osu.Game.Overlays.Chat.ChannelList; -using osuTK; - -namespace osu.Game.Tests.Visual.Online -{ - [TestFixture] - public class TestSceneChannelListItem : OsuTestScene - { - [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - - [Cached] - private readonly Bindable selected = new Bindable(); - - private static readonly List channels = new List - { - createPublicChannel("#public-channel"), - createPublicChannel("#public-channel-long-name"), - createPrivateChannel("test user", 2), - createPrivateChannel("test user long name", 3), - }; - - private readonly Dictionary channelMap = new Dictionary(); - - private FillFlowContainer flow; - private OsuSpriteText selectedText; - private OsuSpriteText leaveText; - - [SetUp] - public void SetUp() - { - Schedule(() => - { - foreach (var item in channelMap.Values) - item.Expire(); - - channelMap.Clear(); - - Child = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(10), - Children = new Drawable[] - { - selectedText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }, - leaveText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Height = 16, - AlwaysPresent = true, - }, - new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Y, - Width = 190, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background6, - }, - flow = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - }, - }, - }, - }; - - selected.BindValueChanged(change => - { - selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}"; - }, true); - - foreach (var channel in channels) - { - var item = new ChannelListItem(channel); - flow.Add(item); - channelMap.Add(channel, item); - item.OnRequestSelect += c => selected.Value = c; - item.OnRequestLeave += leaveChannel; - } - }); - } - - [Test] - public void TestVisual() - { - AddStep("Select second item", () => selected.Value = channels.Skip(1).First()); - - AddStep("Unread Selected", () => - { - if (selected.Value != null) - channelMap[selected.Value].Unread.Value = true; - }); - - AddStep("Read Selected", () => - { - if (selected.Value != null) - channelMap[selected.Value].Unread.Value = false; - }); - - AddStep("Add Mention Selected", () => - { - if (selected.Value != null) - channelMap[selected.Value].Mentions.Value++; - }); - - AddStep("Add 98 Mentions Selected", () => - { - if (selected.Value != null) - channelMap[selected.Value].Mentions.Value += 98; - }); - - AddStep("Clear Mentions Selected", () => - { - if (selected.Value != null) - channelMap[selected.Value].Mentions.Value = 0; - }); - } - - private void leaveChannel(Channel channel) - { - leaveText.Text = $"OnRequestLeave: {channel.Name}"; - leaveText.FadeOutFromOne(1000, Easing.InQuint); - } - - private static Channel createPublicChannel(string name) => - new Channel { Name = name, Type = ChannelType.Public, Id = 1234 }; - - private static Channel createPrivateChannel(string username, int id) - => new Channel(new APIUser { Id = id, Username = username }); - } -} diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs new file mode 100644 index 0000000000..076dc5719e --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelList.cs @@ -0,0 +1,135 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Collections.Generic; +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.Chat; + +namespace osu.Game.Overlays.Chat.ChannelList +{ + public class ChannelList : Container + { + public Action? OnRequestSelect; + public Action? OnRequestLeave; + + public readonly BindableBool SelectorActive = new BindableBool(); + + private readonly Dictionary channelMap = new Dictionary(); + + private ChannelListItemFlow publicChannelFlow = null!; + private ChannelListItemFlow privateChannelFlow = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background6, + }, + new OsuScrollContainer + { + Padding = new MarginPadding { Vertical = 7 }, + RelativeSizeAxes = Axes.Both, + ScrollbarAnchor = Anchor.TopRight, + ScrollDistance = 35f, + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + publicChannelFlow = new ChannelListItemFlow("CHANNELS"), + new ChannelListSelector + { + Margin = new MarginPadding { Bottom = 10 }, + SelectorActive = { BindTarget = SelectorActive }, + }, + privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"), + }, + }, + }, + }; + } + + public void AddChannel(Channel channel) + { + if (channelMap.ContainsKey(channel)) + return; + + ChannelListItem item = new ChannelListItem(channel); + item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan); + item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan); + item.SelectorActive.BindTarget = SelectorActive; + + ChannelListItemFlow flow = getFlowForChannel(channel); + channelMap.Add(channel, item); + flow.Add(item); + } + + public void RemoveChannel(Channel channel) + { + if (!channelMap.ContainsKey(channel)) + return; + + ChannelListItem item = channelMap[channel]; + ChannelListItemFlow flow = getFlowForChannel(channel); + + channelMap.Remove(channel); + flow.Remove(item); + } + + public ChannelListItem GetItem(Channel channel) + { + if (!channelMap.ContainsKey(channel)) + throw new ArgumentOutOfRangeException(); + + return channelMap[channel]; + } + + private ChannelListItemFlow getFlowForChannel(Channel channel) + { + switch (channel.Type) + { + case ChannelType.Public: + return publicChannelFlow; + + case ChannelType.PM: + return privateChannelFlow; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + private class ChannelListItemFlow : FillFlowContainer + { + public ChannelListItemFlow(string label) + { + Direction = FillDirection.Vertical; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Add(new OsuSpriteText + { + Text = label, + Margin = new MarginPadding { Left = 18, Bottom = 5 }, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + }); + } + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 43574351ed..7c4a72559b 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -29,12 +29,14 @@ namespace osu.Game.Overlays.Chat.ChannelList public readonly BindableBool Unread = new BindableBool(); + public readonly BindableBool SelectorActive = new BindableBool(); + private readonly Channel channel; - private Box? hoverBox; - private Box? selectBox; - private OsuSpriteText? text; - private ChannelListItemCloseButton? close; + private Box hoverBox = null!; + private Box selectBox = null!; + private OsuSpriteText text = null!; + private ChannelListItemCloseButton close = null!; [Resolved] private Bindable selectedChannel { get; set; } = null!; @@ -124,31 +126,26 @@ namespace osu.Game.Overlays.Chat.ChannelList { base.LoadComplete(); - selectedChannel.BindValueChanged(change => - { - if (change.NewValue == channel) - selectBox?.FadeIn(300, Easing.OutQuint); - else - selectBox?.FadeOut(200, Easing.OutQuint); - }, true); + selectedChannel.BindValueChanged(_ => updateSelectState(), true); + SelectorActive.BindValueChanged(_ => updateSelectState(), true); Unread.BindValueChanged(change => { - text!.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint); + text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint); }, true); } protected override bool OnHover(HoverEvent e) { - hoverBox?.FadeIn(300, Easing.OutQuint); - close?.FadeIn(300, Easing.OutQuint); + hoverBox.FadeIn(300, Easing.OutQuint); + close.FadeIn(300, Easing.OutQuint); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - hoverBox?.FadeOut(200, Easing.OutQuint); - close?.FadeOut(200, Easing.OutQuint); + hoverBox.FadeOut(200, Easing.OutQuint); + close.FadeOut(200, Easing.OutQuint); base.OnHoverLost(e); } @@ -167,5 +164,13 @@ namespace osu.Game.Overlays.Chat.ChannelList Masking = true, }; } + + private void updateSelectState() + { + if (selectedChannel.Value == channel && !SelectorActive.Value) + selectBox.FadeIn(300, Easing.OutQuint); + else + selectBox.FadeOut(200, Easing.OutQuint); + } } } diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs new file mode 100644 index 0000000000..57ab7584b5 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs @@ -0,0 +1,91 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Overlays.Chat.ChannelList +{ + public class ChannelListSelector : OsuClickableContainer + { + public readonly BindableBool SelectorActive = new BindableBool(); + + private Box hoverBox = null!; + private Box selectBox = null!; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Height = 30; + RelativeSizeAxes = Axes.X; + + Children = new Drawable[] + { + hoverBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + Alpha = 0f, + }, + selectBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + Alpha = 0f, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 18, Right = 10 }, + Child = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "Add More Channels", + Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), + Colour = colourProvider.Light3, + Margin = new MarginPadding { Bottom = 2 }, + RelativeSizeAxes = Axes.X, + Truncate = true, + }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectorActive.BindValueChanged(selector => + { + if (selector.NewValue) + selectBox.FadeIn(300, Easing.OutQuint); + else + selectBox.FadeOut(200, Easing.OutQuint); + }, true); + + Action = () => SelectorActive.Value = true; + } + + protected override bool OnHover(HoverEvent e) + { + hoverBox.FadeIn(300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverBox.FadeOut(200, Easing.OutQuint); + base.OnHoverLost(e); + } + } +} diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 9807a65250..a9312e9a3a 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -14,7 +14,6 @@ using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Spectator; -using osu.Game.Resources.Localisation.Web; using osu.Game.Screens; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; diff --git a/osu.Game/Skinning/Editor/SkinBlueprint.cs b/osu.Game/Skinning/Editor/SkinBlueprint.cs index 0a4bd1d75f..1860c6006c 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprint.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprint.cs @@ -146,8 +146,10 @@ namespace osu.Game.Skinning.Editor { anchorLine = new Box { - Colour = Color4.Yellow, Height = 2, + Origin = Anchor.CentreLeft, + Colour = Color4.Yellow, + EdgeSmoothness = Vector2.One }, originBox = new Box {