From de393f735fdb3d2f4b93939af1a47f8a17217a17 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Fri, 29 Apr 2022 21:33:32 +0100 Subject: [PATCH 01/54] Implement basic layout and behaviour of new chat overlay Provides initial implementation of new chat overlay in component `ChatOverlayV2`. Contains only the basic functionality required for a functioning chat overlay according to the new design with the intent of added the rest of the functionality in subsequent PRs. Backports existing tests for the current chat overlay except for ones testing keyboard shortcuts (since they haven't been added) and tab closing behaviour (since no tabs). --- .../Visual/Online/TestSceneChatOverlayV2.cs | 365 ++++++++++++++++++ .../Chat/ChannelList/ChannelListItem.cs | 18 +- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 67 ++++ .../Chat/Listing/ChannelListingItem.cs | 18 +- osu.Game/Overlays/ChatOverlayV2.cs | 293 ++++++++++++++ 5 files changed, 743 insertions(+), 18 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs create mode 100644 osu.Game/Overlays/Chat/ChatOverlayTopBar.cs create mode 100644 osu.Game/Overlays/ChatOverlayV2.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs new file mode 100644 index 0000000000..312bda6460 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -0,0 +1,365 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Collections.Generic; +using System.Net; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Chat; +using osu.Game.Overlays; +using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Listing; +using osu.Game.Overlays.Chat.ChannelList; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Online +{ + [TestFixture] + public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene + { + private ChatOverlayV2 chatOverlay; + private ChannelManager channelManager; + + private readonly APIUser testUser; + private readonly Channel testPMChannel; + private readonly Channel[] testChannels; + private Channel testChannel1 => testChannels[0]; + private Channel testChannel2 => testChannels[1]; + + public TestSceneChatOverlayV2() + { + testUser = new APIUser { Username = "test user", Id = 5071479 }; + testPMChannel = new Channel(testUser); + testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new DependencyProvidingContainer + { + RelativeSizeAxes = Axes.Both, + CachedDependencies = new (Type, object)[] + { + (typeof(ChannelManager), channelManager = new ChannelManager()), + }, + Children = new Drawable[] + { + channelManager, + chatOverlay = new ChatOverlayV2 { RelativeSizeAxes = Axes.Both }, + }, + }; + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Setup request handler", () => + { + ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetUpdatesRequest getUpdates: + getUpdates.TriggerFailure(new WebException()); + return true; + + case JoinChannelRequest joinChannel: + joinChannel.TriggerSuccess(); + return true; + + case LeaveChannelRequest leaveChannel: + leaveChannel.TriggerSuccess(); + return true; + + case GetMessagesRequest getMessages: + getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel)); + return true; + + case GetUserRequest getUser: + if (getUser.Lookup == testUser.Username) + getUser.TriggerSuccess(testUser); + else + getUser.TriggerFailure(new WebException()); + return true; + + case PostMessageRequest postMessage: + postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000)) + { + Content = postMessage.Message.Content, + ChannelId = postMessage.Message.ChannelId, + Sender = postMessage.Message.Sender, + Timestamp = new DateTimeOffset(DateTime.Now), + }); + return true; + + default: + Logger.Log($"Unhandled Request Type: {req.GetType()}"); + return false; + } + }; + }); + + AddStep("Add test channels", () => + { + (channelManager.AvailableChannels as BindableList)?.AddRange(testChannels); + }); + } + + [Test] + public void TestShowHide() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden); + } + + [Test] + public void TestChannelSelection() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddAssert("Listing is hidden", () => listingVisibility == Visibility.Hidden); + AddAssert("Loading is hidden", () => loadingVisibility == Visibility.Hidden); + AddAssert("Current channel is correct", () => channelManager.CurrentChannel.Value == testChannel1); + AddAssert("DrawableChannel is correct", () => currentDrawableChannel.Channel == testChannel1); + } + + [Test] + public void TestSearchInListing() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); + AddStep("Search for 'number 2'", () => chatOverlay.ChildrenOfType().Single().Text = "number 2"); + AddUntilStep("Only channel 2 visibile", () => + { + IEnumerable listingItems = chatOverlay.ChildrenOfType() + .Where(item => item.IsPresent); + return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2; + }); + } + + [Test] + public void TestChannelCloseButton() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join PM and public channels", () => + { + channelManager.JoinChannel(testChannel1); + channelManager.JoinChannel(testPMChannel); + }); + AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel))); + AddStep("Click close button", () => + { + ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType().Single(); + clickDrawable(closeButton); + }); + AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel)); + AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Click close button", () => + { + ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType().Single(); + clickDrawable(closeButton); + }); + AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1)); + } + + [Test] + public void TestChatCommand() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); + AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); + AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist")); + AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage); + // Make sure no unnecessary requests are made when the PM channel is already open. + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null); + AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); + AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); + AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + } + + [Test] + public void TestMultiplayerChannelIsNotShown() + { + Channel multiplayerChannel = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser()) + { + Name = "#mp_1", + Type = ChannelType.Multiplayer, + })); + AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel)); + AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType() + .Where(item => item.IsPresent) + .Select(item => item.Channel) + .Contains(multiplayerChannel)); + } + + [Test] + public void TestHighlightOnCurrentChannel() + { + Message message = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Send message in channel 1", () => + { + testChannel1.AddNewMessages(message = new Message + { + ChannelId = testChannel1.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + } + + [Test] + public void TestHighlightOnAnotherChannel() + { + Message message = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Send message in channel 2", () => + { + testChannel2.AddNewMessages(message = new Message + { + ChannelId = testChannel2.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); + AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2); + AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2); + } + + [Test] + public void TestHighlightOnLeftChannel() + { + Message message = null; + + AddStep("Show overlay", () => chatOverlay.Show()); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddStep("Send message in channel 2", () => + { + testChannel2.AddNewMessages(message = new Message + { + ChannelId = testChannel2.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2)); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2)); + AddAssert("Channel 2 is selected", () => channelManager.CurrentChannel.Value == testChannel2); + AddAssert("Channel 2 is visible", () => currentDrawableChannel.Channel == testChannel2); + } + + [Test] + public void TestHighlightWhileChatNeverOpen() + { + Message message = null; + + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Send message in channel 1", () => + { + testChannel1.AddNewMessages(message = new Message + { + ChannelId = testChannel1.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + } + + [Test] + public void TestHighlightWithNullChannel() + { + Message message = null; + + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Send message in channel 1", () => + { + testChannel1.AddNewMessages(message = new Message + { + ChannelId = testChannel1.Id, + Content = "Message to highlight!", + Timestamp = DateTimeOffset.Now, + Sender = testUser, + }); + }); + AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null); + AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); + } + + private Visibility listingVisibility => + chatOverlay.ChildrenOfType().Single().State.Value; + + private Visibility loadingVisibility => + chatOverlay.ChildrenOfType().Single().State.Value; + + private DrawableChannel currentDrawableChannel => + chatOverlay.ChildrenOfType>().Single().Child; + + private ChannelListItem getChannelListItem(Channel channel) => + chatOverlay.ChildrenOfType().Single(item => item.Channel == channel); + + private void clickDrawable(Drawable d) + { + InputManager.MoveMouseTo(d); + InputManager.Click(MouseButton.Left); + } + + private List createChannelMessages(Channel channel) + { + var message = new Message + { + ChannelId = channel.Id, + Content = $"Hello, this is a message in {channel.Name}", + Sender = testUser, + Timestamp = new DateTimeOffset(DateTime.Now), + }; + return new List { message }; + } + + private Channel createPublicChannel(int id) => new Channel + { + Id = id, + Name = $"#channel-{id}", + Topic = $"We talk about the number {id} here", + Type = ChannelType.Public, + }; + } +} diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index 7c4a72559b..fa8fae29e5 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -25,14 +25,14 @@ namespace osu.Game.Overlays.Chat.ChannelList public event Action? OnRequestSelect; public event Action? OnRequestLeave; + public readonly Channel Channel; + public readonly BindableInt Mentions = new BindableInt(); public readonly BindableBool Unread = new BindableBool(); public readonly BindableBool SelectorActive = new BindableBool(); - private readonly Channel channel; - private Box hoverBox = null!; private Box selectBox = null!; private OsuSpriteText text = null!; @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat.ChannelList public ChannelListItem(Channel channel) { - this.channel = channel; + Channel = channel; } [BackgroundDependencyLoader] @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat.ChannelList { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = channel.Name, + Text = Channel.Name, Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Chat.ChannelList Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Right = 3 }, - Action = () => OnRequestLeave?.Invoke(channel), + Action = () => OnRequestLeave?.Invoke(Channel), } } }, @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.Chat.ChannelList }, }; - Action = () => OnRequestSelect?.Invoke(channel); + Action = () => OnRequestSelect?.Invoke(Channel); } protected override void LoadComplete() @@ -151,10 +151,10 @@ namespace osu.Game.Overlays.Chat.ChannelList private Drawable createIcon() { - if (channel.Type != ChannelType.PM) + if (Channel.Type != ChannelType.PM) return Drawable.Empty(); - return new UpdateableAvatar(channel.Users.First(), isInteractive: false) + return new UpdateableAvatar(Channel.Users.First(), isInteractive: false) { Size = new Vector2(20), Margin = new MarginPadding { Right = 5 }, @@ -167,7 +167,7 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateSelectState() { - if (selectedChannel.Value == channel && !SelectorActive.Value) + if (selectedChannel.Value == Channel && !SelectorActive.Value) selectBox.FadeIn(300, Easing.OutQuint); else selectBox.FadeOut(200, Easing.OutQuint); diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs new file mode 100644 index 0000000000..9ba7608d89 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -0,0 +1,67 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Chat +{ + public class ChatOverlayTopBar : Container + { + // IsHovered is used by overlay + public override bool HandlePositionalInput => true; + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, TextureStore textures) + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get("Icons/Hexacons/messaging"), + Size = new Vector2(18), + }, + // Placeholder text + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "osu!chat", + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + Margin = new MarginPadding { Bottom = 2f }, + }, + }, + }, + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs index 526cbcda87..1f0cbae7e2 100644 --- a/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs +++ b/osu.Game/Overlays/Chat/Listing/ChannelListingItem.cs @@ -25,11 +25,11 @@ namespace osu.Game.Overlays.Chat.Listing public event Action? OnRequestJoin; public event Action? OnRequestLeave; - public bool FilteringActive { get; set; } - public IEnumerable FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty }; - public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } + public readonly Channel Channel; - private readonly Channel channel; + public bool FilteringActive { get; set; } + public IEnumerable FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty }; + public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); } private Box hoverBox = null!; private SpriteIcon checkbox = null!; @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat.Listing public ChannelListingItem(Channel channel) { - this.channel = channel; + Channel = channel; } [BackgroundDependencyLoader] @@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Chat.Listing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = channel.Name, + Text = Channel.Name, Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold), Margin = new MarginPadding { Bottom = 2 }, }, @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Chat.Listing { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = channel.Topic, + Text = Channel.Topic, Font = OsuFont.Torus.With(size: text_size), Margin = new MarginPadding { Bottom = 2 }, }, @@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Chat.Listing { base.LoadComplete(); - channelJoined = channel.Joined.GetBoundCopy(); + channelJoined = Channel.Joined.GetBoundCopy(); channelJoined.BindValueChanged(change => { const double duration = 500; @@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Chat.Listing } }, true); - Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); + Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel); } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs new file mode 100644 index 0000000000..01b8ed2a45 --- /dev/null +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -0,0 +1,293 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.ChannelList; +using osu.Game.Overlays.Chat.Listing; + +namespace osu.Game.Overlays +{ + public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent + { + public string IconTexture => "Icons/Hexacons/messaging"; + public LocalisableString Title => ChatStrings.HeaderTitle; + public LocalisableString Description => ChatStrings.HeaderDescription; + + private ChatOverlayTopBar topBar = null!; + private ChannelList channelList = null!; + private LoadingLayer loading = null!; + private ChannelListing channelListing = null!; + private ChatTextBar textBar = null!; + private Container currentChannelContainer = null!; + + private Bindable? chatHeight; + private bool isDraggingTopBar; + private float dragStartChatHeight; + + private const int transition_length = 500; + private const float default_chat_height = 0.4f; + private const float top_bar_height = 40; + private const float side_bar_width = 190; + private const float chat_bar_height = 60; + + private readonly BindableBool selectorActive = new BindableBool(); + + [Resolved] + private OsuConfigManager config { get; set; } = null!; + + [Resolved] + private ChannelManager channelManager { get; set; } = null!; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + [Cached] + private readonly Bindable currentChannel = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + // Width = 0.85f; // Matches OnlineOverlay + Height = default_chat_height; + RelativeSizeAxes = Axes.Both; + RelativePositionAxes = Axes.Both; + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + Masking = true; + CornerRadius = 7f; + Margin = new MarginPadding { Bottom = -10 }; + Padding = new MarginPadding { Bottom = 10 }; + + Children = new Drawable[] + { + topBar = new ChatOverlayTopBar + { + RelativeSizeAxes = Axes.X, + Height = top_bar_height, + }, + channelList = new ChannelList + { + RelativeSizeAxes = Axes.Y, + Width = side_bar_width, + Padding = new MarginPadding { Top = top_bar_height }, + SelectorActive = { BindTarget = selectorActive }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding + { + Top = top_bar_height, + Left = side_bar_width, + Bottom = chat_bar_height, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4, + }, + currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + loading = new LoadingLayer(true), + channelListing = new ChannelListing + { + RelativeSizeAxes = Axes.Both, + }, + }, + }, + textBar = new ChatTextBar + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Padding = new MarginPadding { Left = side_bar_width }, + ShowSearch = { BindTarget = selectorActive }, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + loading.Show(); + + chatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy(); + chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); + + currentChannel.BindTo(channelManager.CurrentChannel); + channelManager.CurrentChannel.BindValueChanged(currentChannelChanged, true); + channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true); + channelManager.AvailableChannels.BindCollectionChanged(availableChannelsChanged, true); + + channelList.OnRequestSelect += channel => + { + // Manually selecting a channel should dismiss the selector + selectorActive.Value = false; + channelManager.CurrentChannel.Value = channel; + }; + channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel); + + channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel); + channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel); + + textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms; + textBar.OnChatMessageCommitted += handleChatMessage; + + selectorActive.BindValueChanged(v => channelListing.State.Value = v.NewValue ? Visibility.Visible : Visibility.Hidden, true); + } + + /// + /// Highlights a certain message in the specified channel. + /// + /// The message to highlight. + /// The channel containing the message. + public void HighlightMessage(Message message, Channel channel) + { + Debug.Assert(channel.Id == message.ChannelId); + + if (currentChannel.Value?.Id != channel.Id) + { + if (!channel.Joined.Value) + channel = channelManager.JoinChannel(channel); + + channelManager.CurrentChannel.Value = channel; + } + + selectorActive.Value = false; + + channel.HighlightedMessage.Value = message; + + Show(); + } + + protected override bool OnDragStart(DragStartEvent e) + { + isDraggingTopBar = topBar.IsHovered; + + if (!isDraggingTopBar) + return base.OnDragStart(e); + + dragStartChatHeight = chatHeight!.Value; + return true; + } + + protected override void OnDrag(DragEvent e) + { + if (!isDraggingTopBar) + return; + + float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; + chatHeight!.Value = targetChatHeight; + } + + protected override void OnDragEnd(DragEndEvent e) + { + isDraggingTopBar = false; + base.OnDragEnd(e); + } + + protected override void PopIn() + { + this.MoveToY(0, transition_length, Easing.OutQuint); + this.FadeIn(transition_length, Easing.OutQuint); + base.PopIn(); + } + + protected override void PopOut() + { + this.MoveToY(Height, transition_length, Easing.InSine); + this.FadeOut(transition_length, Easing.InSine); + base.PopOut(); + } + + private void currentChannelChanged(ValueChangedEvent e) + { + Channel? newChannel = e.NewValue; + + loading.Show(); + + // Channel is null when leaving the currently selected channel + if (newChannel == null) + { + // Find another channel to switch to + newChannel = channelManager.JoinedChannels.FirstOrDefault(chan => chan != e.OldValue); + + if (newChannel == null) + selectorActive.Value = true; + else + currentChannel.Value = newChannel; + + return; + } + + LoadComponentAsync(new DrawableChannel(newChannel), loaded => + { + currentChannelContainer.Clear(); + currentChannelContainer.Add(loaded); + loading.Hide(); + }); + } + + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + IEnumerable joinedChannels = filterChannels(args.NewItems); + foreach (var channel in joinedChannels) + channelList.AddChannel(channel); + break; + + case NotifyCollectionChangedAction.Remove: + IEnumerable leftChannels = filterChannels(args.OldItems); + foreach (var channel in leftChannels) + channelList.RemoveChannel(channel); + break; + } + } + + private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + => channelListing.UpdateAvailableChannels(channelManager.AvailableChannels); + + private IEnumerable filterChannels(IList channels) + => channels.Cast().Where(c => c.Type == ChannelType.Public || c.Type == ChannelType.PM); + + private void handleChatMessage(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return; + + if (message[0] == '/') + channelManager.PostCommand(message.Substring(1)); + else + channelManager.PostMessage(message); + } + } +} + From 4bd1d091486feba7933432359758c531c40ec29b Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sun, 1 May 2022 12:20:11 +0100 Subject: [PATCH 02/54] Remove blank line --- osu.Game/Overlays/ChatOverlayV2.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 01b8ed2a45..d0b49d9ef3 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -290,4 +290,3 @@ namespace osu.Game.Overlays } } } - From bcce807311ae0a455ebf0771322c63ba9ed8c6d7 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Sun, 1 May 2022 12:20:54 +0100 Subject: [PATCH 03/54] Fix chat command test as reference equality checks on PM channels doesn't seem to to work --- osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 312bda6460..c7bb6760c7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -185,16 +185,17 @@ namespace osu.Game.Tests.Visual.Online AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); - AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); - AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + AddAssert("PM channel is selected", () => + channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist")); AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage); + // Make sure no unnecessary requests are made when the PM channel is already open. AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null); AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}")); - AddAssert("PM channel is selected", () => channelManager.CurrentChannel.Value == testPMChannel); - AddAssert("PM channel is visibile", () => currentDrawableChannel.Channel == testPMChannel); + AddAssert("PM channel is selected", () => + channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser); } [Test] From e6f1ac6bec24e483f42e985a4ca43cf0454d99f5 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 20:08:33 +0100 Subject: [PATCH 04/54] Ensure "chatting in..." text is aligned with chat message --- osu.Game/Overlays/Chat/ChatTextBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index ef20149dac..15fa2d87db 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Chat private Container searchIconContainer = null!; private ChatTextBox chatTextBox = null!; - private const float chatting_text_width = 180; + private const float chatting_text_width = 240; private const float search_icon_width = 40; [BackgroundDependencyLoader] From 1473762e2567884d8e238e1ce3c713e76c0be42b Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 20:57:39 +0100 Subject: [PATCH 05/54] Don't wrap "chatting in.." text in `ChatTextBar` --- osu.Game/Overlays/Chat/ChatTextBar.cs | 31 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 15fa2d87db..316511d9a6 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -11,7 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osuTK; @@ -28,7 +29,8 @@ namespace osu.Game.Overlays.Chat [Resolved] private Bindable currentChannel { get; set; } = null!; - private OsuTextFlowContainer chattingTextContainer = null!; + private Container chattingTextContainer = null!; + private OsuSpriteText chattingText = null!; private Container searchIconContainer = null!; private ChatTextBox chatTextBox = null!; @@ -61,16 +63,19 @@ namespace osu.Game.Overlays.Chat { new Drawable[] { - chattingTextContainer = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 20)) + chattingTextContainer = new Container { - Masking = true, - Width = chatting_text_width, - Padding = new MarginPadding { Left = 10 }, RelativeSizeAxes = Axes.Y, - TextAnchor = Anchor.CentreRight, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = colourProvider.Background1, + Width = chatting_text_width, + Masking = true, + Child = chattingText = new OsuSpriteText + { + Font = OsuFont.Torus.With(size: 20), + Colour = colourProvider.Background1, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Truncate = true, + }, }, searchIconContainer = new Container { @@ -131,15 +136,15 @@ namespace osu.Game.Overlays.Chat switch (newChannel?.Type) { case ChannelType.Public: - chattingTextContainer.Text = $"chatting in {newChannel.Name}"; + chattingText.Text = $"chatting in {newChannel.Name}"; break; case ChannelType.PM: - chattingTextContainer.Text = $"chatting with {newChannel.Name}"; + chattingText.Text = $"chatting with {newChannel.Name}"; break; default: - chattingTextContainer.Text = string.Empty; + chattingText.Text = string.Empty; break; } }, true); From 7f8e00c1e3e8e6d66d4f9de132c0ffdddf9b37bf Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 21:22:47 +0100 Subject: [PATCH 06/54] Change "Add more channels" to sentence case in "ChannelList" --- osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs index 57ab7584b5..f9dab74eb1 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Chat.ChannelList { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = "Add More Channels", + Text = "Add more channels", Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Colour = colourProvider.Light3, Margin = new MarginPadding { Bottom = 2 }, From a931b1ecc37a089edc4fcf55f93c94543cf26d08 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Mon, 2 May 2022 22:32:25 +0100 Subject: [PATCH 07/54] Show selected channel text as white in `ChannelListItem` --- .../Chat/ChannelList/ChannelListItem.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index fa8fae29e5..e6a126f4d8 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -126,13 +126,9 @@ namespace osu.Game.Overlays.Chat.ChannelList { base.LoadComplete(); - selectedChannel.BindValueChanged(_ => updateSelectState(), true); - SelectorActive.BindValueChanged(_ => updateSelectState(), true); - - Unread.BindValueChanged(change => - { - text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint); - }, true); + selectedChannel.BindValueChanged(_ => updateState(), true); + SelectorActive.BindValueChanged(_ => updateState(), true); + Unread.BindValueChanged(_ => updateState(), true); } protected override bool OnHover(HoverEvent e) @@ -165,12 +161,21 @@ namespace osu.Game.Overlays.Chat.ChannelList }; } - private void updateSelectState() + private void updateState() { - if (selectedChannel.Value == Channel && !SelectorActive.Value) + if (showSelected) selectBox.FadeIn(300, Easing.OutQuint); else selectBox.FadeOut(200, Easing.OutQuint); + + if (showUnread || showSelected) + text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); + else + text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint); } + + private bool showUnread => Unread.Value; + + private bool showSelected => selectedChannel.Value == Channel && !SelectorActive.Value; } } From 2896612c5cd09cc4c6436f2f307d60218b13b8f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 May 2022 16:06:04 +0900 Subject: [PATCH 08/54] Make exiting multiplayer a dangerous operation, requiring hold --- .../Containers/HoldToConfirmContainer.cs | 8 ++++-- .../Dialog/PopupDialogDangerousButton.cs | 4 ++- .../Screens/Play/HUD/HoldForMenuButton.cs | 27 ++++++++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index 1b802a0a14..c74245461d 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -40,9 +40,13 @@ namespace osu.Game.Graphics.Containers private Bindable holdActivationDelay; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + [Resolved] + private OsuConfigManager config { get; set; } + + protected override void LoadComplete() { + base.LoadComplete(); + holdActivationDelay = HoldActivationDelay != null ? new Bindable(HoldActivationDelay.Value) : config.GetBindable(OsuSetting.UIHoldActivationDelay); diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index adc627e15b..6c775f44f8 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -12,6 +12,8 @@ namespace osu.Game.Overlays.Dialog { public class PopupDialogDangerousButton : PopupDialogButton { + public const double DANGEROUS_HOLD_ACTIVATION_DELAY = 500; + private Box progressBox; private DangerousConfirmContainer confirmContainer; @@ -42,7 +44,7 @@ namespace osu.Game.Overlays.Dialog private class DangerousConfirmContainer : HoldToConfirmContainer { - protected override double? HoldActivationDelay => 500; + protected override double? HoldActivationDelay => DANGEROUS_HOLD_ACTIVATION_DELAY; protected override bool OnMouseDown(MouseDownEvent e) { diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 3da63ec2cc..c6aa3fbe08 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; +using osu.Game.Overlays.Dialog; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -37,6 +38,14 @@ namespace osu.Game.Screens.Play.HUD private readonly OsuSpriteText text; + [Resolved] + private OsuConfigManager config { get; set; } + + [Resolved(canBeNull: true)] + private Player player { get; set; } + + private readonly Bindable activationDelay = new Bindable(); + public HoldForMenuButton() { Direction = FillDirection.Horizontal; @@ -60,14 +69,15 @@ namespace osu.Game.Screens.Play.HUD AutoSizeAxes = Axes.Both; } - [Resolved] - private OsuConfigManager config { get; set; } - - private Bindable activationDelay; - protected override void LoadComplete() { - activationDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); + if (player?.Configuration.AllowRestart == false) + { + activationDelay.Value = PopupDialogDangerousButton.DANGEROUS_HOLD_ACTIVATION_DELAY; + } + else + config.BindWith(OsuSetting.UIHoldActivationDelay, activationDelay); + activationDelay.BindValueChanged(v => { text.Text = v.NewValue > 0 @@ -115,6 +125,11 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; + [Resolved(canBeNull: true)] + private Player player { get; set; } + + protected override double? HoldActivationDelay => player?.Configuration.AllowRestart == false ? PopupDialogDangerousButton.DANGEROUS_HOLD_ACTIVATION_DELAY : (double?)null; + [BackgroundDependencyLoader] private void load(OsuColour colours) { From 50aee8b665bcb9285fd6c8b099135f9aa56b32f4 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 3 May 2022 22:32:01 +0100 Subject: [PATCH 09/54] Ensure `ChannelListSelector` text also turns white when selected --- .../Overlays/Chat/ChannelList/ChannelListSelector.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs index f9dab74eb1..9cba93ffa5 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListSelector.cs @@ -21,6 +21,10 @@ namespace osu.Game.Overlays.Chat.ChannelList private Box hoverBox = null!; private Box selectBox = null!; + private OsuSpriteText text = null!; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } = null!; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) @@ -46,7 +50,7 @@ namespace osu.Game.Overlays.Chat.ChannelList { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = 18, Right = 10 }, - Child = new OsuSpriteText + Child = text = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -68,9 +72,15 @@ namespace osu.Game.Overlays.Chat.ChannelList SelectorActive.BindValueChanged(selector => { if (selector.NewValue) + { + text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); selectBox.FadeIn(300, Easing.OutQuint); + } else + { + text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint); selectBox.FadeOut(200, Easing.OutQuint); + } }, true); Action = () => SelectorActive.Value = true; From c17edb2848e32dd803e88b6b95efe4c46d74ab2a Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 3 May 2022 22:32:51 +0100 Subject: [PATCH 10/54] Add padding to text in `ChatTextBar` to separate it from the textbox --- osu.Game/Overlays/Chat/ChatTextBar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 316511d9a6..3de7b67b0f 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.Y, Width = chatting_text_width, Masking = true, + Padding = new MarginPadding { Right = 5 }, Child = chattingText = new OsuSpriteText { Font = OsuFont.Torus.With(size: 20), From 60999e83e03647f71460c65ba57aa3d038d994ae Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Tue, 3 May 2022 22:33:36 +0100 Subject: [PATCH 11/54] Ensure `ChatTextBox` takes/leaves focus on chat overlay pop in/out --- osu.Game/Overlays/Chat/ChatTextBar.cs | 4 ++++ osu.Game/Overlays/ChatOverlayV2.cs | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs index 3de7b67b0f..0fa3613d38 100644 --- a/osu.Game/Overlays/Chat/ChatTextBar.cs +++ b/osu.Game/Overlays/Chat/ChatTextBar.cs @@ -26,6 +26,10 @@ namespace osu.Game.Overlays.Chat public event Action? OnSearchTermsChanged; + public void TextBoxTakeFocus() => chatTextBox.TakeFocus(); + + public void TextBoxKillFocus() => chatTextBox.KillFocus(); + [Resolved] private Bindable currentChannel { get; set; } = null!; diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index d0b49d9ef3..f6190bbe20 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -214,16 +214,22 @@ namespace osu.Game.Overlays protected override void PopIn() { + base.PopIn(); + this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); - base.PopIn(); + + textBar.TextBoxTakeFocus(); } protected override void PopOut() { + base.PopOut(); + this.MoveToY(Height, transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine); - base.PopOut(); + + textBar.TextBoxKillFocus(); } private void currentChannelChanged(ValueChangedEvent e) From 2f12c7d9e1e9c1231c82c2bd4766f198dfa709a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 19:09:22 +0900 Subject: [PATCH 12/54] Change `ChatTextBox` to not handle up/down arrows --- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index 6a7da52416..df210fcaf8 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Chat; using osu.Game.Resources.Localisation.Web; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Online.Chat { @@ -119,6 +120,20 @@ namespace osu.Game.Online.Chat public class ChatTextBox : FocusedTextBox { + protected override bool OnKeyDown(KeyDownEvent e) + { + // Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other + // elements on the same screen. + switch (e.Key) + { + case Key.Up: + case Key.Down: + return false; + } + + return base.OnKeyDown(e); + } + protected override void LoadComplete() { base.LoadComplete(); From df1f4aecdceee273e3fbafbe7e7fa2c208e50419 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 19:09:40 +0900 Subject: [PATCH 13/54] Add support for traversing playlist items using next/previous bindings Addresses https://github.com/ppy/osu/discussions/18061. --- .../OnlinePlay/DrawableRoomPlaylist.cs | 53 ++++++++++++++++++- .../Lounge/Components/RoomsContainer.cs | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 57bb4253cb..d1b21ca1b0 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -6,7 +6,10 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; +using osu.Game.Input.Bindings; using osu.Game.Online.Rooms; using osuTK; @@ -15,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// A scrollable list which displays the s in a . /// - public class DrawableRoomPlaylist : OsuRearrangeableListContainer + public class DrawableRoomPlaylist : OsuRearrangeableListContainer, IKeyBindingHandler { /// /// The currently-selected item. Selection is visually represented with a border. @@ -169,5 +172,53 @@ namespace osu.Game.Screens.OnlinePlay }); protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item); + + #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.SelectNext: + selectNext(1); + return true; + + case GlobalAction.SelectPrevious: + selectNext(-1); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + + private void selectNext(int direction) + { + if (SelectedItem.Disabled) + return; + + var visibleRooms = ListContainer.AsEnumerable().Where(r => r.IsPresent); + + PlaylistItem item; + + if (SelectedItem.Value == null) + item = visibleRooms.FirstOrDefault()?.Model; + else + { + if (direction < 0) + visibleRooms = visibleRooms.Reverse(); + + item = visibleRooms.SkipWhile(r => r.Model != SelectedItem.Value).Skip(1).FirstOrDefault()?.Model; + } + + // we already have a valid selection only change selection if we still have a room to switch to. + if (item != null) + SelectedItem.Value = item; + } + + #endregion } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 175cd2c44e..9bcda90e6d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -139,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return base.OnClick(e); } - #region Key selection logic (shared with BeatmapCarousel) + #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) public bool OnPressed(KeyBindingPressEvent e) { From 3b04daddaa9ec3de743eb590003df4f4cc17592d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 May 2022 12:36:43 +0200 Subject: [PATCH 14/54] Fix self-reference in region name --- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 9bcda90e6d..74e4225f11 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -139,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components return base.OnClick(e); } - #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) + #region Key selection logic (shared with BeatmapCarousel and DrawableRoomPlaylist) public bool OnPressed(KeyBindingPressEvent e) { From ec27fa8e857a357e7e07f205dcdc6c3891666416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 May 2022 13:12:21 +0200 Subject: [PATCH 15/54] Add test coverage for keyboard selection --- .../TestSceneDrawableRoomPlaylist.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 76353323d6..566a8db608 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -147,6 +147,40 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]); } + [Test] + public void KeyboardSelection() + { + createPlaylist(p => p.AllowSelection = true); + + AddStep("press down", () => InputManager.Key(Key.Down)); + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + + AddStep("press down", () => InputManager.Key(Key.Down)); + AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]); + + AddStep("press up", () => InputManager.Key(Key.Up)); + AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]); + + AddUntilStep("navigate to last item via keyboard", () => + { + InputManager.Key(Key.Down); + return playlist.SelectedItem.Value == playlist.Items.Last(); + }); + AddAssert("last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Last()); + AddUntilStep("last item is scrolled into view", () => + { + var drawableItem = playlist.ItemMap[playlist.Items.Last()]; + return playlist.ScreenSpaceDrawQuad.Contains(drawableItem.ScreenSpaceDrawQuad.TopLeft) + && playlist.ScreenSpaceDrawQuad.Contains(drawableItem.ScreenSpaceDrawQuad.BottomRight); + }); + + AddStep("press down", () => InputManager.Key(Key.Down)); + AddAssert("last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Last()); + + AddStep("press up", () => InputManager.Key(Key.Up)); + AddAssert("second last item is selected", () => playlist.SelectedItem.Value == playlist.Items.Reverse().ElementAt(1)); + } + [Test] public void TestDownloadButtonHiddenWhenBeatmapExists() { From 0405c1c34a18ca1c06be95fa5426f7deeeaf0daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 May 2022 13:15:26 +0200 Subject: [PATCH 16/54] Ensure selected playlist item is always scrolled into view --- .../Screens/OnlinePlay/DrawableRoomPlaylist.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index d1b21ca1b0..fe654c4ab8 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -173,6 +174,21 @@ namespace osu.Game.Screens.OnlinePlay protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item); + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(_ => scrollToSelection(), true); + } + + private void scrollToSelection() + { + if (SelectedItem.Value == null) return; + + Debug.Assert(ItemMap.TryGetValue(SelectedItem.Value, out var drawableItem)); + ScrollContainer.ScrollIntoView(drawableItem); + } + #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) public bool OnPressed(KeyBindingPressEvent e) From e6fdef2d7afbb769583bc5a4cab62eba438e5773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 4 May 2022 13:51:47 +0200 Subject: [PATCH 17/54] Fix test failures due to selection/item collection desyncs --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index fe654c4ab8..18cf454be3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -178,14 +177,19 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); - SelectedItem.BindValueChanged(_ => scrollToSelection(), true); + // schedules added as the properties may change value while the drawable items haven't been created yet. + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(scrollToSelection)); + Items.BindCollectionChanged((_, __) => Scheduler.AddOnce(scrollToSelection), true); } private void scrollToSelection() { - if (SelectedItem.Value == null) return; + // SelectedItem and ItemMap/drawable items are managed separately, + // so if the item can't be unmapped to a drawable, don't try to scroll to it. + // best effort is made to not drop any updates, by subscribing to both sources. + if (SelectedItem.Value == null || !ItemMap.TryGetValue(SelectedItem.Value, out var drawableItem)) + return; - Debug.Assert(ItemMap.TryGetValue(SelectedItem.Value, out var drawableItem)); ScrollContainer.ScrollIntoView(drawableItem); } From ddab3c6d8084a0c19289c56f53bdb779481ec4b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 May 2022 21:00:11 +0900 Subject: [PATCH 18/54] Tidy up state variables --- osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs index e6a126f4d8..c5ac87f527 100644 --- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs @@ -163,19 +163,17 @@ namespace osu.Game.Overlays.Chat.ChannelList private void updateState() { - if (showSelected) + bool selected = selectedChannel.Value == Channel && !SelectorActive.Value; + + if (selected) selectBox.FadeIn(300, Easing.OutQuint); else selectBox.FadeOut(200, Easing.OutQuint); - if (showUnread || showSelected) + if (Unread.Value || selected) text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint); else text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint); } - - private bool showUnread => Unread.Value; - - private bool showSelected => selectedChannel.Value == Channel && !SelectorActive.Value; } } From 1a85e1267bc0ccf3700360f831b69f5e5cc26312 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Wed, 4 May 2022 14:43:40 +0100 Subject: [PATCH 19/54] Ensure focus is directed to `ChatTextBox` from `ChatOverlay` and add tests --- .../Visual/Online/TestSceneChatOverlayV2.cs | 27 ++++++++++++++++++- osu.Game/Overlays/ChatOverlayV2.cs | 8 ++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index c7bb6760c7..9d4de11c5a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Listing is visible", () => listingVisibility == Visibility.Visible); - AddStep("Search for 'number 2'", () => chatOverlay.ChildrenOfType().Single().Text = "number 2"); + AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2"); AddUntilStep("Only channel 2 visibile", () => { IEnumerable listingItems = chatOverlay.ChildrenOfType() @@ -325,6 +325,28 @@ namespace osu.Game.Tests.Visual.Online AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1)); } + [Test] + public void TextBoxRetainsFocus() + { + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1)); + AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1))); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click selector", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click drawable channel", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType().Single())); + AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null); + } + private Visibility listingVisibility => chatOverlay.ChildrenOfType().Single().State.Value; @@ -337,6 +359,9 @@ namespace osu.Game.Tests.Visual.Online private ChannelListItem getChannelListItem(Channel channel) => chatOverlay.ChildrenOfType().Single(item => item.Channel == channel); + private ChatTextBox chatOverlayTextBox => + chatOverlay.ChildrenOfType().Single(); + private void clickDrawable(Drawable d) { InputManager.MoveMouseTo(d); diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index f6190bbe20..658a28bfdb 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -218,8 +218,6 @@ namespace osu.Game.Overlays this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); - - textBar.TextBoxTakeFocus(); } protected override void PopOut() @@ -232,6 +230,12 @@ namespace osu.Game.Overlays textBar.TextBoxKillFocus(); } + protected override void OnFocus(FocusEvent e) + { + textBar.TextBoxTakeFocus(); + base.OnFocus(e); + } + private void currentChannelChanged(ValueChangedEvent e) { Channel? newChannel = e.NewValue; From 464b3af5f37b23204c3c440d4d0359b670527384 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 05:42:20 +0300 Subject: [PATCH 20/54] Rename local correctly --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 18cf454be3..d1b83c3749 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -220,18 +220,18 @@ namespace osu.Game.Screens.OnlinePlay if (SelectedItem.Disabled) return; - var visibleRooms = ListContainer.AsEnumerable().Where(r => r.IsPresent); + var visibleItems = ListContainer.AsEnumerable().Where(r => r.IsPresent); PlaylistItem item; if (SelectedItem.Value == null) - item = visibleRooms.FirstOrDefault()?.Model; + item = visibleItems.FirstOrDefault()?.Model; else { if (direction < 0) - visibleRooms = visibleRooms.Reverse(); + visibleItems = visibleItems.Reverse(); - item = visibleRooms.SkipWhile(r => r.Model != SelectedItem.Value).Skip(1).FirstOrDefault()?.Model; + item = visibleItems.SkipWhile(r => r.Model != SelectedItem.Value).Skip(1).FirstOrDefault()?.Model; } // we already have a valid selection only change selection if we still have a room to switch to. From f28978b85661ea1c878c27864788e0d65e68de57 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 05:42:49 +0300 Subject: [PATCH 21/54] Handle against playlists which disallow selection `SelectedItem.Disabled` is also not checked against in the select-via-click flow inside `DrawableRoomPlaylistItem` (only `AllowSelection` is checked). --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 9 +++++++++ osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 566a8db608..d520384218 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -56,6 +56,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click", () => InputManager.Click(MouseButton.Left)); AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + + AddStep("press down", () => InputManager.Key(Key.Down)); + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); } [Test] @@ -73,6 +76,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click", () => InputManager.Click(MouseButton.Left)); AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + + AddStep("press down", () => InputManager.Key(Key.Down)); + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); } [Test] @@ -91,6 +97,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click", () => InputManager.Click(MouseButton.Left)); AddAssert("no item selected", () => playlist.SelectedItem.Value == null); + + AddStep("press down", () => InputManager.Key(Key.Down)); + AddAssert("no item selected", () => playlist.SelectedItem.Value == null); } [Test] diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index d1b83c3749..840fc48613 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -217,7 +217,7 @@ namespace osu.Game.Screens.OnlinePlay private void selectNext(int direction) { - if (SelectedItem.Disabled) + if (!AllowSelection) return; var visibleItems = ListContainer.AsEnumerable().Where(r => r.IsPresent); From a26793cd654a29ada9d9d0234e796f531cd404c8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 06:12:24 +0300 Subject: [PATCH 22/54] Add missing `Test` prefix --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index d520384218..e2b4b2870f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void KeyboardSelection() + public void TestKeyboardSelection() { createPlaylist(p => p.AllowSelection = true); From ac6342ff8d22213ac41a2df547ab69c816641a21 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 5 May 2022 10:16:16 +0300 Subject: [PATCH 23/54] Add workaround for item scrolling issue --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 840fc48613..2a72fc6eb1 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -190,7 +190,12 @@ namespace osu.Game.Screens.OnlinePlay if (SelectedItem.Value == null || !ItemMap.TryGetValue(SelectedItem.Value, out var drawableItem)) return; - ScrollContainer.ScrollIntoView(drawableItem); + // ScrollIntoView does not handle non-loaded items appropriately, delay scroll until the item finishes loading. + // see: https://github.com/ppy/osu-framework/issues/5158 + if (!drawableItem.IsLoaded) + drawableItem.OnLoadComplete += _ => ScrollContainer.ScrollIntoView(drawableItem); + else + ScrollContainer.ScrollIntoView(drawableItem); } #region Key selection logic (shared with BeatmapCarousel and RoomsContainer) From e7205d8593ec1ab27528137eed48b2002d61cd85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:09:56 +0900 Subject: [PATCH 24/54] Reset all test data before each test method to avoid channels stuck in joined state --- .../Visual/Online/TestSceneChatOverlayV2.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 9d4de11c5a..b15a7afbf9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -31,22 +31,20 @@ namespace osu.Game.Tests.Visual.Online private ChatOverlayV2 chatOverlay; private ChannelManager channelManager; - private readonly APIUser testUser; - private readonly Channel testPMChannel; - private readonly Channel[] testChannels; + private APIUser testUser; + private Channel testPMChannel; + private Channel[] testChannels; + private Channel testChannel1 => testChannels[0]; private Channel testChannel2 => testChannels[1]; - public TestSceneChatOverlayV2() - { - testUser = new APIUser { Username = "test user", Id = 5071479 }; - testPMChannel = new Channel(testUser); - testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); - } - [SetUp] public void SetUp() => Schedule(() => { + testUser = new APIUser { Username = "test user", Id = 5071479 }; + testPMChannel = new Channel(testUser); + testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); + Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, From 25ea660b0b7f449bb74280a09b3f31db5b471d67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:13:48 +0900 Subject: [PATCH 25/54] Replace `HandlePositionalInput` override with simple hover effect --- osu.Game/Overlays/Chat/ChatOverlayTopBar.cs | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs index 9ba7608d89..3a8cd1fb91 100644 --- a/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs +++ b/osu.Game/Overlays/Chat/ChatOverlayTopBar.cs @@ -4,31 +4,35 @@ #nullable enable using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Chat { public class ChatOverlayTopBar : Container { - // IsHovered is used by overlay - public override bool HandlePositionalInput => true; + private Box background = null!; + + private Color4 backgroundColour; [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider, TextureStore textures) { Children = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, + Colour = backgroundColour = colourProvider.Background3, }, new GridContainer { @@ -63,5 +67,17 @@ namespace osu.Game.Overlays.Chat }, }; } + + protected override bool OnHover(HoverEvent e) + { + background.FadeColour(backgroundColour.Lighten(0.1f), 300, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + background.FadeColour(backgroundColour, 300, Easing.OutQuint); + base.OnHoverLost(e); + } } } From 74505ba1666e821c905d27325bb86f7e0236aa2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:16:19 +0900 Subject: [PATCH 26/54] Remove `!` usage (also seems to fix height saving/loading) --- osu.Game/Overlays/ChatOverlayV2.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 658a28bfdb..ae4e160de5 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -39,7 +39,8 @@ namespace osu.Game.Overlays private ChatTextBar textBar = null!; private Container currentChannelContainer = null!; - private Bindable? chatHeight; + private readonly Bindable chatHeight = new Bindable(); + private bool isDraggingTopBar; private float dragStartChatHeight; @@ -137,7 +138,8 @@ namespace osu.Game.Overlays loading.Show(); - chatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy(); + config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight); + chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true); currentChannel.BindTo(channelManager.CurrentChannel); @@ -193,7 +195,7 @@ namespace osu.Game.Overlays if (!isDraggingTopBar) return base.OnDragStart(e); - dragStartChatHeight = chatHeight!.Value; + dragStartChatHeight = chatHeight.Value; return true; } @@ -203,7 +205,7 @@ namespace osu.Game.Overlays return; float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; - chatHeight!.Value = targetChatHeight; + chatHeight.Value = targetChatHeight; } protected override void OnDragEnd(DragEndEvent e) From e54f5e2d9206f49b387beda75f70e9fcfddc5a4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:17:32 +0900 Subject: [PATCH 27/54] Adjust value change variables to avoid `e` usage --- osu.Game/Overlays/ChatOverlayV2.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index ae4e160de5..8a73eabed2 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -238,9 +238,9 @@ namespace osu.Game.Overlays base.OnFocus(e); } - private void currentChannelChanged(ValueChangedEvent e) + private void currentChannelChanged(ValueChangedEvent channel) { - Channel? newChannel = e.NewValue; + Channel? newChannel = channel.NewValue; loading.Show(); @@ -248,7 +248,7 @@ namespace osu.Game.Overlays if (newChannel == null) { // Find another channel to switch to - newChannel = channelManager.JoinedChannels.FirstOrDefault(chan => chan != e.OldValue); + newChannel = channelManager.JoinedChannels.FirstOrDefault(c => c != channel.OldValue); if (newChannel == null) selectorActive.Value = true; From 97221d2ef1ee5b88464fc2bdbbe7dc372d05c882 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 May 2022 19:22:30 +0900 Subject: [PATCH 28/54] Tidy up initialisation --- osu.Game/Overlays/ChatOverlayV2.cs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 8a73eabed2..16da1f7c10 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -64,20 +64,27 @@ namespace osu.Game.Overlays [Cached] private readonly Bindable currentChannel = new Bindable(); + public ChatOverlayV2() + { + Height = default_chat_height; + + Masking = true; + + const float corner_radius = 7f; + + CornerRadius = corner_radius; + + // Hack to hide the bottom edge corner radius off-screen. + Margin = new MarginPadding { Bottom = -corner_radius }; + Padding = new MarginPadding { Bottom = corner_radius }; + + Anchor = Anchor.BottomCentre; + Origin = Anchor.BottomCentre; + } + [BackgroundDependencyLoader] private void load() { - // Width = 0.85f; // Matches OnlineOverlay - Height = default_chat_height; - RelativeSizeAxes = Axes.Both; - RelativePositionAxes = Axes.Both; - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; - Masking = true; - CornerRadius = 7f; - Margin = new MarginPadding { Bottom = -10 }; - Padding = new MarginPadding { Bottom = 10 }; - Children = new Drawable[] { topBar = new ChatOverlayTopBar From 5ea6f62951b93a9e558c59142e0275ce886d2459 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 14:20:33 +0100 Subject: [PATCH 29/54] Ensure `RelativePositionAxes` is set in BDL for animations to work --- osu.Game/Overlays/ChatOverlayV2.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 16da1f7c10..cab88136fc 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -85,6 +85,9 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { + // Required for the pop in/out animation + RelativePositionAxes = Axes.Both; + Children = new Drawable[] { topBar = new ChatOverlayTopBar From 9cb52f8879f839eb7ba64cd1b38a35d4bc0c69cb Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 14:21:26 +0100 Subject: [PATCH 30/54] Add tests for chat height saving/loading --- .../Visual/Online/TestSceneChatOverlayV2.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index b15a7afbf9..31ced4e8f7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Collections.Generic; using System.Net; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -21,6 +23,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Listing; using osu.Game.Overlays.Chat.ChannelList; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Online @@ -38,6 +41,9 @@ namespace osu.Game.Tests.Visual.Online private Channel testChannel1 => testChannels[0]; private Channel testChannel2 => testChannels[1]; + [Resolved] + private OsuConfigManager config { get; set; } = null!; + [SetUp] public void SetUp() => Schedule(() => { @@ -124,6 +130,28 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden); } + [Test] + public void TestChatHeight() + { + Bindable configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); + float newHeight = 0; + + AddStep("Set config chat height", () => configChatHeight.Value = 0.4f); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f); + AddStep("Drag overlay to new height", () => { + InputManager.MoveMouseTo(chatOverlayTopBar); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)); + InputManager.ReleaseButton(MouseButton.Left); + }); + AddStep("Store new height", () => newHeight = chatOverlay.Height); + AddAssert("Config height changed", () => configChatHeight.Value != 0.4f && configChatHeight.Value == newHeight); + AddStep("Hide overlay", () => chatOverlay.Hide()); + AddStep("Show overlay", () => chatOverlay.Show()); + AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight); + } + [Test] public void TestChannelSelection() { @@ -360,6 +388,9 @@ namespace osu.Game.Tests.Visual.Online private ChatTextBox chatOverlayTextBox => chatOverlay.ChildrenOfType().Single(); + private ChatOverlayTopBar chatOverlayTopBar => + chatOverlay.ChildrenOfType().Single(); + private void clickDrawable(Drawable d) { InputManager.MoveMouseTo(d); From 5657e7f11ec5d0e8551380efeb3e4998e6e55f3c Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 14:52:03 +0100 Subject: [PATCH 31/54] Fix chat height saving/loading test --- osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 31ced4e8f7..98574e5d53 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -133,13 +133,15 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChatHeight() { - Bindable configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); + Bindable configChatHeight = null; float newHeight = 0; + AddStep("Bind config chat height", () => configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy()); AddStep("Set config chat height", () => configChatHeight.Value = 0.4f); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f); - AddStep("Drag overlay to new height", () => { + AddStep("Drag overlay to new height", () => + { InputManager.MoveMouseTo(chatOverlayTopBar); InputManager.PressButton(MouseButton.Left); InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)); From 2cc56a4b19348b2b80ca4e4521a37e56a4bc0011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 May 2022 19:07:44 +0200 Subject: [PATCH 32/54] Fix hidden issue with automatic customisation panel show logic Doesn't cause tests to fail headless, but they do fail in the test browser. --- osu.Game/Overlays/Mods/ModSelectScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 8060bca65f..1779d59e33 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Mods foreach (var mod in SelectedMods.Value) { anyCustomisableMod |= mod.GetSettingsSourceProperties().Any(); - anyModWithRequiredCustomisationAdded |= !valueChangedEvent.OldValue.Contains(mod) && mod.RequiresConfiguration; + anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration; } if (anyCustomisableMod) From 8002726c9c1a1e1bf23befa68bb45eaec1cb57fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 May 2022 19:08:38 +0200 Subject: [PATCH 33/54] Add test coverage for dismissing customisation area with Escape --- .../Visual/UserInterface/TestSceneModSelectScreen.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index bdb423a43c..a3ce4b734b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -164,11 +164,19 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); assertCustomisationToggleState(disabled: false, active: true); - AddStep("dismiss mod customisation", () => + AddStep("dismiss mod customisation via mouse", () => { InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); + assertCustomisationToggleState(disabled: false, active: false); + + AddStep("reset mods", () => SelectedMods.SetDefault()); + AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("dismiss mod customisation via keyboard", () => InputManager.Key(Key.Escape)); + assertCustomisationToggleState(disabled: false, active: false); AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray()); assertCustomisationToggleState(disabled: false, active: false); From 79e1b93b303195ef57cb9d2fc839aca0670fefd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 May 2022 17:08:02 +0200 Subject: [PATCH 34/54] Close customisation area on escape press before exiting --- osu.Game/Overlays/Mods/ModSelectScreen.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 1779d59e33..a7f8a167f9 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Input; @@ -315,6 +316,17 @@ namespace osu.Game.Overlays.Mods } } + public override bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == GlobalAction.Back && customisationVisible.Value) + { + customisationVisible.Value = false; + return true; + } + + return base.OnPressed(e); + } + internal class ColumnScrollContainer : OsuScrollContainer { public ColumnScrollContainer() From 16a6c11bc87a906b36e699fcd2101eac371dcb25 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 21:39:31 +0100 Subject: [PATCH 35/54] Clamp maximum and minimum height the chat overlay can be resized to --- osu.Game/Overlays/ChatOverlayV2.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index cab88136fc..8b6fe70f3c 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; @@ -44,8 +45,11 @@ namespace osu.Game.Overlays private bool isDraggingTopBar; private float dragStartChatHeight; - private const int transition_length = 500; + private const float min_chat_height = 0.2f; + private const float max_chat_height = 1f; private const float default_chat_height = 0.4f; + + private const int transition_length = 500; private const float top_bar_height = 40; private const float side_bar_width = 190; private const float chat_bar_height = 60; @@ -215,7 +219,7 @@ namespace osu.Game.Overlays return; float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; - chatHeight.Value = targetChatHeight; + chatHeight.Value = Math.Clamp(targetChatHeight, min_chat_height, max_chat_height); } protected override void OnDragEnd(DragEndEvent e) From 0974de8fb007afdca51520ac44204c408a6d3523 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 23:13:32 +0100 Subject: [PATCH 36/54] Use `BindableFloat` for chat height --- osu.Game/Overlays/ChatOverlayV2.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index 8b6fe70f3c..4c7fa0f802 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -3,7 +3,6 @@ #nullable enable -using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; @@ -40,16 +39,13 @@ namespace osu.Game.Overlays private ChatTextBar textBar = null!; private Container currentChannelContainer = null!; - private readonly Bindable chatHeight = new Bindable(); + private readonly BindableFloat chatHeight = new BindableFloat(); private bool isDraggingTopBar; private float dragStartChatHeight; - private const float min_chat_height = 0.2f; - private const float max_chat_height = 1f; - private const float default_chat_height = 0.4f; - private const int transition_length = 500; + private const float default_chat_height = 0.4f; private const float top_bar_height = 40; private const float side_bar_width = 190; private const float chat_bar_height = 60; @@ -219,7 +215,7 @@ namespace osu.Game.Overlays return; float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; - chatHeight.Value = Math.Clamp(targetChatHeight, min_chat_height, max_chat_height); + chatHeight.Value = targetChatHeight; } protected override void OnDragEnd(DragEndEvent e) From 7ffe3b132fbe12ff2f8689fbc330aecf90bd1249 Mon Sep 17 00:00:00 2001 From: Jai Sharma Date: Thu, 5 May 2022 23:13:43 +0100 Subject: [PATCH 37/54] Use `BindableFloat` in chat height tests --- .../Visual/Online/TestSceneChatOverlayV2.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 98574e5d53..7c77ac925e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -133,22 +133,22 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChatHeight() { - Bindable configChatHeight = null; + BindableFloat configChatHeight = new BindableFloat(); + config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight); float newHeight = 0; - AddStep("Bind config chat height", () => configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy()); - AddStep("Set config chat height", () => configChatHeight.Value = 0.4f); + AddStep("Reset config chat height", () => configChatHeight.SetDefault()); AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f); - AddStep("Drag overlay to new height", () => + AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); + AddStep("Click top bar", () => { InputManager.MoveMouseTo(chatOverlayTopBar); InputManager.PressButton(MouseButton.Left); - InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)); - InputManager.ReleaseButton(MouseButton.Left); }); + AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); + AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("Store new height", () => newHeight = chatOverlay.Height); - AddAssert("Config height changed", () => configChatHeight.Value != 0.4f && configChatHeight.Value == newHeight); + AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight); AddStep("Hide overlay", () => chatOverlay.Hide()); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight); From be960eb092aacf2f8651d5099b1eb2d1eb09a8a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 14:34:31 +0900 Subject: [PATCH 38/54] Move dangerous hold specification to base class --- .../Containers/HoldToConfirmContainer.cs | 32 ++++++++++---- .../Dialog/PopupDialogDangerousButton.cs | 7 ++-- .../Screens/Play/HUD/HoldForMenuButton.cs | 42 +++++++------------ 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index c74245461d..078721ec77 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -12,6 +12,13 @@ namespace osu.Game.Graphics.Containers { public abstract class HoldToConfirmContainer : Container { + public const double DANGEROUS_HOLD_ACTIVATION_DELAY = 500; + + /// + /// Whether the associated action is considered dangerous, warranting a longer hold. + /// + public bool IsDangerousAction { get; } + public Action Action; private const int fadeout_delay = 200; @@ -29,12 +36,9 @@ namespace osu.Game.Graphics.Containers protected virtual bool AllowMultipleFires => false; /// - /// Specify a custom activation delay, overriding the game-wide user setting. + /// The current activation delay for this control. /// - /// - /// This should be used in special cases where we want to be extra sure the user knows what they are doing. An example is when changes would be lost. - /// - protected virtual double? HoldActivationDelay => null; + protected IBindable HoldActivationDelay => holdActivationDelay; public Bindable Progress = new BindableDouble(); @@ -43,13 +47,25 @@ namespace osu.Game.Graphics.Containers [Resolved] private OsuConfigManager config { get; set; } + protected HoldToConfirmContainer(bool isDangerousAction = false) + { + IsDangerousAction = isDangerousAction; + } + protected override void LoadComplete() { base.LoadComplete(); - holdActivationDelay = HoldActivationDelay != null - ? new Bindable(HoldActivationDelay.Value) - : config.GetBindable(OsuSetting.UIHoldActivationDelay); + if (IsDangerousAction) + { + holdActivationDelay.Value = DANGEROUS_HOLD_ACTIVATION_DELAY; + } + else + { + holdActivationDelay = HoldActivationDelay != null + ? new Bindable(HoldActivationDelay.Value) + : config.GetBindable(OsuSetting.UIHoldActivationDelay); + } } protected void BeginConfirm() diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index 6c775f44f8..6239c5e409 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -12,8 +12,6 @@ namespace osu.Game.Overlays.Dialog { public class PopupDialogDangerousButton : PopupDialogButton { - public const double DANGEROUS_HOLD_ACTIVATION_DELAY = 500; - private Box progressBox; private DangerousConfirmContainer confirmContainer; @@ -44,7 +42,10 @@ namespace osu.Game.Overlays.Dialog private class DangerousConfirmContainer : HoldToConfirmContainer { - protected override double? HoldActivationDelay => DANGEROUS_HOLD_ACTIVATION_DELAY; + public DangerousConfirmContainer() + : base(isDangerousAction: true) + { + } protected override bool OnMouseDown(MouseDownEvent e) { diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index c6aa3fbe08..9d280a1737 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -13,12 +13,10 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; -using osu.Game.Overlays.Dialog; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -29,20 +27,11 @@ namespace osu.Game.Screens.Play.HUD public readonly Bindable IsPaused = new Bindable(); - private readonly Button button; + private Button button; - public Action Action - { - set => button.Action = value; - } + public Action Action { get; set; } - private readonly OsuSpriteText text; - - [Resolved] - private OsuConfigManager config { get; set; } - - [Resolved(canBeNull: true)] - private Player player { get; set; } + private OsuSpriteText text; private readonly Bindable activationDelay = new Bindable(); @@ -51,6 +40,11 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); Margin = new MarginPadding(10); + } + + [BackgroundDependencyLoader(true)] + private void load(Player player) + { Children = new Drawable[] { text = new OsuSpriteText @@ -59,11 +53,12 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - button = new Button + button = new Button(player?.Configuration.AllowRestart == false) { HoverGained = () => text.FadeIn(500, Easing.OutQuint), HoverLost = () => text.FadeOut(500, Easing.OutQuint), - IsPaused = { BindTarget = IsPaused } + IsPaused = { BindTarget = IsPaused }, + Action = () => Action(), } }; AutoSizeAxes = Axes.Both; @@ -71,13 +66,6 @@ namespace osu.Game.Screens.Play.HUD protected override void LoadComplete() { - if (player?.Configuration.AllowRestart == false) - { - activationDelay.Value = PopupDialogDangerousButton.DANGEROUS_HOLD_ACTIVATION_DELAY; - } - else - config.BindWith(OsuSetting.UIHoldActivationDelay, activationDelay); - activationDelay.BindValueChanged(v => { text.Text = v.NewValue > 0 @@ -125,10 +113,10 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; - [Resolved(canBeNull: true)] - private Player player { get; set; } - - protected override double? HoldActivationDelay => player?.Configuration.AllowRestart == false ? PopupDialogDangerousButton.DANGEROUS_HOLD_ACTIVATION_DELAY : (double?)null; + public Button(bool isDangerousAction) + : base(isDangerousAction) + { + } [BackgroundDependencyLoader] private void load(OsuColour colours) From a11771c11b82992c2b5dfdda3acfcc09c59121e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 14:37:50 +0900 Subject: [PATCH 39/54] Better encapsulate exposed bindables of `HoldToConfirmContainer` --- .../Containers/HoldToConfirmContainer.cs | 40 ++++++++++--------- .../Screens/Play/HUD/HoldForMenuButton.cs | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index 078721ec77..540f9b97a7 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -14,15 +14,18 @@ namespace osu.Game.Graphics.Containers { public const double DANGEROUS_HOLD_ACTIVATION_DELAY = 500; + private const int fadeout_delay = 200; + /// /// Whether the associated action is considered dangerous, warranting a longer hold. /// public bool IsDangerousAction { get; } + /// + /// The action to perform when a hold successfully completes. + /// public Action Action; - private const int fadeout_delay = 200; - /// /// Whether currently in a fired state (and the confirm has been sent). /// @@ -30,19 +33,24 @@ namespace osu.Game.Graphics.Containers private bool confirming; + /// + /// The current activation delay for this control. + /// + public IBindable HoldActivationDelay => holdActivationDelay; + + /// + /// The progress of any ongoing hold operation. 0 means no hold has started; 1 means a hold has been completed. + /// + public IBindable Progress => progress; + /// /// Whether the overlay should be allowed to return from a fired state. /// protected virtual bool AllowMultipleFires => false; - /// - /// The current activation delay for this control. - /// - protected IBindable HoldActivationDelay => holdActivationDelay; + private readonly Bindable progress = new BindableDouble(); - public Bindable Progress = new BindableDouble(); - - private Bindable holdActivationDelay; + private readonly Bindable holdActivationDelay = new Bindable(); [Resolved] private OsuConfigManager config { get; set; } @@ -57,15 +65,9 @@ namespace osu.Game.Graphics.Containers base.LoadComplete(); if (IsDangerousAction) - { holdActivationDelay.Value = DANGEROUS_HOLD_ACTIVATION_DELAY; - } else - { - holdActivationDelay = HoldActivationDelay != null - ? new Bindable(HoldActivationDelay.Value) - : config.GetBindable(OsuSetting.UIHoldActivationDelay); - } + config.BindWith(OsuSetting.UIHoldActivationDelay, holdActivationDelay); } protected void BeginConfirm() @@ -74,7 +76,7 @@ namespace osu.Game.Graphics.Containers confirming = true; - this.TransformBindableTo(Progress, 1, holdActivationDelay.Value * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); + this.TransformBindableTo(progress, 1, holdActivationDelay.Value * (1 - progress.Value), Easing.Out).OnComplete(_ => Confirm()); } protected virtual void Confirm() @@ -91,9 +93,9 @@ namespace osu.Game.Graphics.Containers Fired = false; this - .TransformBindableTo(Progress, Progress.Value) + .TransformBindableTo(progress, progress.Value) .Delay(200) - .TransformBindableTo(Progress, 0, fadeout_delay, Easing.InSine); + .TransformBindableTo(progress, 0, fadeout_delay, Easing.InSine); } } } diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 9d280a1737..678c4256b4 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -164,7 +164,7 @@ namespace osu.Game.Screens.Play.HUD private void bind() { - circularProgress.Current.BindTo(Progress); + ((IBindable)circularProgress.Current).BindTo(Progress); Progress.ValueChanged += progress => icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); } From c38e97c9ecb390496c576ab815911a87d17c1a04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 14:43:24 +0900 Subject: [PATCH 40/54] Add full xmldoc to `HoldToConfirmContainer` --- .../Containers/HoldToConfirmContainer.cs | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index 540f9b97a7..28afd082c3 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -10,6 +10,16 @@ using osu.Game.Configuration; namespace osu.Game.Graphics.Containers { + /// + /// A container which adds a common "hold-to-perform" pattern to a container. + /// + /// + /// This container does not handle triggering the hold/abort operations. + /// To use this class, please call and when necessary. + /// + /// The is exposed as a transforming bindable which smoothly tracks the progress of a hold operation. + /// It can be used for animating and displaying progress directly. + /// public abstract class HoldToConfirmContainer : Container { public const double DANGEROUS_HOLD_ACTIVATION_DELAY = 500; @@ -70,6 +80,12 @@ namespace osu.Game.Graphics.Containers config.BindWith(OsuSetting.UIHoldActivationDelay, holdActivationDelay); } + /// + /// Begin a new confirmation. Should be called when the container is interacted with (ie. the user presses a key). + /// + /// + /// Calling this method when already in the process of confirming has no effect. + /// protected void BeginConfirm() { if (confirming || (!AllowMultipleFires && Fired)) return; @@ -79,12 +95,9 @@ namespace osu.Game.Graphics.Containers this.TransformBindableTo(progress, 1, holdActivationDelay.Value * (1 - progress.Value), Easing.Out).OnComplete(_ => Confirm()); } - protected virtual void Confirm() - { - Action?.Invoke(); - Fired = true; - } - + /// + /// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key). + /// protected void AbortConfirm() { if (!AllowMultipleFires && Fired) return; @@ -97,5 +110,15 @@ namespace osu.Game.Graphics.Containers .Delay(200) .TransformBindableTo(progress, 0, fadeout_delay, Easing.InSine); } + + /// + /// A method which is invoked when the confirmation sequence completes successfully. + /// By default, will fire the associated . + /// + protected virtual void Confirm() + { + Action?.Invoke(); + Fired = true; + } } } From 1c4aa125859d2de65858661b38bc207706a65625 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 14:44:10 +0900 Subject: [PATCH 41/54] Rename non-descript `Button` nested class --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 678c4256b4..e3283062ef 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.HUD public readonly Bindable IsPaused = new Bindable(); - private Button button; + private HoldButton button; public Action Action { get; set; } @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - button = new Button(player?.Configuration.AllowRestart == false) + button = new HoldButton(player?.Configuration.AllowRestart == false) { HoverGained = () => text.FadeIn(500, Easing.OutQuint), HoverLost = () => text.FadeOut(500, Easing.OutQuint), @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD } } - private class Button : HoldToConfirmContainer, IKeyBindingHandler + private class HoldButton : HoldToConfirmContainer, IKeyBindingHandler { private SpriteIcon icon; private CircularProgress circularProgress; @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; - public Button(bool isDangerousAction) + public HoldButton(bool isDangerousAction) : base(isDangerousAction) { } From 78959a6e05a4cf753dfc59d480b2fdf041296c33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 15:01:24 +0900 Subject: [PATCH 42/54] Add animation to denote a dangerous player exit --- .../Screens/Play/HUD/HoldForMenuButton.cs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index e3283062ef..f892ea1049 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -12,12 +12,14 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -113,6 +115,11 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; + private const double shake_duration = 20; + + private bool pendingAnimation; + private ScheduledDelegate shakeOperation; + public HoldButton(bool isDangerousAction) : base(isDangerousAction) { @@ -165,10 +172,38 @@ namespace osu.Game.Screens.Play.HUD private void bind() { ((IBindable)circularProgress.Current).BindTo(Progress); - Progress.ValueChanged += progress => icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); + Progress.ValueChanged += progress => + { + icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); + + if (IsDangerousAction) + { + //Child.Scale = new Vector2(1 + (float)progress.NewValue); + Colour = Interpolation.ValueAt(progress.NewValue, Color4.White, Color4.Red, 0, 1, Easing.OutQuint); + + if (progress.NewValue > 0 && progress.NewValue < 1) + { + shakeOperation ??= Scheduler.AddDelayed(shake, shake_duration, true); + } + else + { + Child.MoveTo(Vector2.Zero, shake_duration * 2, Easing.OutQuint); + shakeOperation?.Cancel(); + shakeOperation = null; + } + } + }; } - private bool pendingAnimation; + private void shake() + { + const float shake_magnitude = 8; + + Child.MoveTo(new Vector2( + RNG.NextSingle(-1, 1) * (float)Progress.Value * shake_magnitude, + RNG.NextSingle(-1, 1) * (float)Progress.Value * shake_magnitude + ), shake_duration); + } protected override void Confirm() { From 060461a431a3fe99a46786b652cba234933fbc85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 15:22:22 +0900 Subject: [PATCH 43/54] Add test coverage of multiplayer gameplay exit flow --- .../Multiplayer/TestSceneMultiplayer.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 6a69917fb4..8e45d99eae 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; @@ -17,6 +18,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -56,6 +58,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; + [Resolved] + private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -668,6 +673,43 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); } + [Test] + public void TestGameplayExitFlow() + { + Bindable holdDelay = null; + + AddStep("Set hold delay to zero", () => + { + holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); + holdDelay.Value = 0; + }); + + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + } + } + }); + + enterGameplay(); + + AddUntilStep("wait for playing", () => this.ChildrenOfType().FirstOrDefault()?.LocalUserPlaying.Value == true); + + AddStep("attempt exit without hold", () => InputManager.Key(Key.Escape)); + AddAssert("still in gameplay", () => multiplayerComponents.CurrentScreen is Player); + + AddStep("attempt exit with hold", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait for lounge", () => multiplayerComponents.CurrentScreen is Screens.OnlinePlay.Multiplayer.Multiplayer); + + AddStep("stop holding", () => InputManager.ReleaseKey(Key.Escape)); + AddStep("set hold delay to default", () => holdDelay.SetDefault()); + } + [Test] public void TestGameplayDoesntStartWithNonLoadedUser() { From f39fcee41bae044337bca75e5619f88772b50a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 May 2022 10:04:00 +0200 Subject: [PATCH 44/54] Remove commented-out code --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index f892ea1049..8ed7260aac 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -178,7 +178,6 @@ namespace osu.Game.Screens.Play.HUD if (IsDangerousAction) { - //Child.Scale = new Vector2(1 + (float)progress.NewValue); Colour = Interpolation.ValueAt(progress.NewValue, Color4.White, Color4.Red, 0, 1, Easing.OutQuint); if (progress.NewValue > 0 && progress.NewValue < 1) From f5026bbbeb579dfe72325fbbab92647ceafcafd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 May 2022 10:04:36 +0200 Subject: [PATCH 45/54] Bind to button's activation delay directly --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 8ed7260aac..2ba76d0896 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -35,8 +35,6 @@ namespace osu.Game.Screens.Play.HUD private OsuSpriteText text; - private readonly Bindable activationDelay = new Bindable(); - public HoldForMenuButton() { Direction = FillDirection.Horizontal; @@ -68,7 +66,7 @@ namespace osu.Game.Screens.Play.HUD protected override void LoadComplete() { - activationDelay.BindValueChanged(v => + button.HoldActivationDelay.BindValueChanged(v => { text.Text = v.NewValue > 0 ? "hold for menu" From 319867f73c968d9238ac319c05ed03fe38fa3076 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 17:32:55 +0900 Subject: [PATCH 46/54] Mark `GuestUser` as system user via `Id` for now Should resolve https://github.com/ppy/osu/issues/18105. Checking through usages, it doesn't immediately look like this will regress any other scenarios. --- osu.Game/Online/API/APIAccess.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8c9741b98b..62ddd49881 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -458,7 +458,7 @@ namespace osu.Game.Online.API public GuestUser() { Username = @"Guest"; - Id = 1; + Id = SYSTEM_USER_ID; } } From 8f217d1e975db8ffdf3099e982e9edabb6bc8fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 May 2022 11:37:50 +0200 Subject: [PATCH 47/54] Add failing test case for broken hover behaviour --- .../UserInterface/TestSceneModSelectScreen.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index a3ce4b734b..c92a738b56 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -164,7 +165,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); assertCustomisationToggleState(disabled: false, active: true); - AddStep("dismiss mod customisation via mouse", () => + AddStep("dismiss mod customisation via toggle", () => { InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); @@ -191,6 +192,29 @@ namespace osu.Game.Tests.Visual.UserInterface assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action. } + [Test] + public void TestDismissCustomisationViaDimmedArea() + { + createScreen(); + assertCustomisationToggleState(disabled: true, active: false); + + AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("move mouse to dimmed area", () => + { + InputManager.MoveMouseTo(new Vector2( + modSelectScreen.ScreenSpaceDrawQuad.TopLeft.X, + (modSelectScreen.ScreenSpaceDrawQuad.TopLeft.Y + modSelectScreen.ScreenSpaceDrawQuad.BottomLeft.Y) / 2)); + }); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + assertCustomisationToggleState(disabled: false, active: false); + + AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().First())); + AddAssert("first mod panel is hovered", () => modSelectScreen.ChildrenOfType().First().IsHovered); + } + /// /// Ensure that two mod overlays are not cross polluting via central settings instances. /// From daed42513e6476abe183fd6173ce328765970903 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 18:44:25 +0900 Subject: [PATCH 48/54] Fix outdated test asserts --- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 2 +- osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 6d0ed15e45..f6b8f4edc3 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 3f))); - assertSnappedDistance(beat_length * 2); + assertSnappedDistance(beat_length); } private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () => diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 9148965987..3aa3481cbf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Editing }); AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); - AddAssert("check correct interval count", () => grid.MaxIntervals == (end_time / grid.DistanceBetweenTicks)); + AddStep("check correct interval count", () => Assert.That((end_time / grid.DistanceBetweenTicks) * multiplier, Is.EqualTo(grid.MaxIntervals))); } private class TestDistanceSnapGrid : DistanceSnapGrid From c6bc6be1280dfff8db52757ab51ab8aa9111b28b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 19:06:40 +0900 Subject: [PATCH 49/54] Fix toolbox expand being interrupted by gaps between groups --- osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index e807dbd482..c6cc09a16c 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -1,7 +1,6 @@ // 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 osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Edit public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos); - private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.Children.Any(d => d.ScreenSpaceDrawQuad.Contains(screenSpacePos)); + private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.ScreenSpaceDrawQuad.Contains(screenSpacePos); protected override bool OnMouseDown(MouseDownEvent e) => true; From 08fd0ea086d4e6b7ca0d9d07179493120ecdc47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 May 2022 12:26:09 +0200 Subject: [PATCH 50/54] Fix click-to-return container still handling hover when inactive --- osu.Game/Overlays/Mods/ModSelectScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index a7f8a167f9..a1b4d7d8a0 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -464,6 +464,8 @@ namespace osu.Game.Overlays.Mods public Action? OnClicked { get; set; } + public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value; + protected override bool Handle(UIEvent e) { if (!HandleMouse.Value) From fad1f727bb26954f75a8eeea25ff333107f3325a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 May 2022 20:34:44 +0900 Subject: [PATCH 51/54] Fix editor drag box visuals --- .../Edit/Compose/Components/DragBox.cs | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index eaee2cd1e2..a256adbe4a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -9,7 +9,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osuTK.Graphics; +using osu.Framework.Layout; +using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { @@ -41,17 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components InternalChild = Box = CreateBox(); } - protected virtual Drawable CreateBox() => new Container - { - Masking = true, - BorderColour = Color4.White, - BorderThickness = SelectionBox.BORDER_RADIUS, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f - } - }; + protected virtual Drawable CreateBox() => new BoxWithBorders(); private RectangleF? dragRectangle; @@ -111,5 +102,75 @@ namespace osu.Game.Screens.Edit.Compose.Components public override void Show() => State = Visibility.Visible; public event Action StateChanged; + + public class BoxWithBorders : CompositeDrawable + { + private readonly LayoutValue cache = new LayoutValue(Invalidation.RequiredParentSizeToFit); + + public BoxWithBorders() + { + AddLayout(cache); + } + + protected override void Update() + { + base.Update(); + + if (!cache.IsValid) + { + createContent(); + cache.Validate(); + } + } + + private void createContent() + { + if (DrawSize == Vector2.Zero) + { + ClearInternal(); + return; + } + + // Make lines the same width independent of display resolution. + float lineThickness = DrawWidth > 0 + ? DrawWidth / ScreenSpaceDrawQuad.Width * 2 + : DrawHeight / ScreenSpaceDrawQuad.Height * 2; + + Padding = new MarginPadding(-lineThickness); + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = lineThickness, + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = lineThickness, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = lineThickness, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = lineThickness, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + } + }; + } + } } } From b119726b289334df0666c15709a99245b2c0a28d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 6 May 2022 15:36:52 +0300 Subject: [PATCH 52/54] Reword test step --- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index f6b8f4edc3..3c3c5cb939 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [TestCase(0.5f)] public void TestDistanceSpacing(float multiplier) { - AddStep($"set beat divisor = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); } [Test] From 29a3ab7e7a018d895ddcc5f14d65d0f864812855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 May 2022 15:21:25 +0200 Subject: [PATCH 53/54] Halve drag box padding Allows the drag box borders to collapse in on themselves to a single line if the drag selection has zero width or height. --- osu.Game/Screens/Edit/Compose/Components/DragBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index a256adbe4a..ecbac82db0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit.Compose.Components ? DrawWidth / ScreenSpaceDrawQuad.Width * 2 : DrawHeight / ScreenSpaceDrawQuad.Height * 2; - Padding = new MarginPadding(-lineThickness); + Padding = new MarginPadding(-lineThickness / 2); InternalChildren = new Drawable[] { From c533c93ffd088e2a2240fdccc822608fd1460317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 6 May 2022 15:29:54 +0200 Subject: [PATCH 54/54] Remove leftover border thickness spec in mod settings area Was never supposed to be there, it was a vestige of a previous design iteration that went by unnoticed. --- osu.Game/Overlays/Mods/ModSettingsArea.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index 9c5f3b7f11..08f563f93e 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -44,7 +44,6 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = 2, Children = new Drawable[] { background = new Box