1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 19:12:54 +08:00

Merge branch 'master' into multiplayer-disable-adaptive-speed

This commit is contained in:
Dean Herbert 2022-05-06 14:00:44 +09:00 committed by GitHub
commit 641584a049
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1215 additions and 155 deletions

View File

@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Catch.Edit
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
}); });
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{ {
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); var result = base.FindSnappedPositionAndTime(screenSpacePosition);
result.ScreenSpacePosition.X = screenSpacePosition.X; result.ScreenSpacePosition.X = screenSpacePosition.X;
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&

View File

@ -97,12 +97,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
set => InternalChild = value; set => InternalChild = value;
} }
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
{ {
throw new System.NotImplementedException(); throw new System.NotImplementedException();
} }

View File

@ -56,9 +56,9 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
Playfield.GetColumnByPosition(screenSpacePosition); Playfield.GetColumnByPosition(screenSpacePosition);
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{ {
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); var result = base.FindSnappedPositionAndTime(screenSpacePosition);
switch (ScrollingInfo.Direction.Value) switch (ScrollingInfo.Direction.Value)
{ {
@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.Edit
} }
else else
{ {
var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
if (result.Time is double time) if (result.Time is double time)
beatSnapGrid.SelectionTimeRange = (time, time); beatSnapGrid.SelectionTimeRange = (time, time);
else else

View File

@ -182,10 +182,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private class SnapProvider : IDistanceSnapProvider private class SnapProvider : IDistanceSnapProvider
{ {
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null); new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1); public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
@ -195,9 +195,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public double DistanceToDuration(HitObject referenceObject, float distance) => distance; public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
} }
} }
} }

View File

@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account // Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = snapProvider?.SnapScreenSpacePositionToValidTime(newHeadPosition); var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position; Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;

View File

@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider() private void updateSlider()
{ {
HitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject); bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle); headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition)
{ {
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult; return snapResult;
@ -131,9 +131,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return new SnapResult(screenSpacePosition, null); return new SnapResult(screenSpacePosition, null);
} }
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{ {
var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition); var positionSnap = FindSnappedPosition(screenSpacePosition);
if (positionSnap.ScreenSpacePosition != screenSpacePosition) if (positionSnap.ScreenSpacePosition != screenSpacePosition)
return positionSnap; return positionSnap;
@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition)); return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
} }
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); return base.FindSnappedPositionAndTime(screenSpacePosition);
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Acronym => @"AL"; public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!"; public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard; public override IconUsage? Icon => FontAwesome.Solid.Keyboard;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray();
/// <summary> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// How early before a hitobject's start time to trigger a hit.

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -23,9 +22,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => @"Strict Tracking"; public override string Name => @"Strict Tracking";
public override string Acronym => @"ST"; public override string Acronym => @"ST";
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious..."; public override string Description => @"Once you start a slider, follow precisely or get a miss.";
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) }; public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };

View File

@ -213,10 +213,10 @@ namespace osu.Game.Tests.Editing
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration); => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
private void assertSnappedDuration(float distance, double expectedDuration) private void assertSnappedDuration(float distance, double expectedDuration)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration); => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(new HitObject(), distance) == expectedDuration);
private void assertSnappedDistance(float distance, float expectedDistance) private void assertSnappedDistance(float distance, float expectedDistance)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance); => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(new HitObject(), distance) == expectedDistance);
private class TestHitObjectComposer : OsuHitObjectComposer private class TestHitObjectComposer : OsuHitObjectComposer
{ {

View File

@ -162,10 +162,10 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IDistanceSnapProvider private class SnapProvider : IDistanceSnapProvider
{ {
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null); new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1); public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
@ -175,9 +175,9 @@ namespace osu.Game.Tests.Visual.Editing
public double DistanceToDuration(HitObject referenceObject, float distance) => distance; public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; public double FindSnappedDuration(HitObject referenceObject, float distance) => 0;
public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; public float FindSnappedDistance(HitObject referenceObject, float distance) => 0;
} }
} }
} }

View File

@ -56,6 +56,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null); AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
AddStep("press down", () => InputManager.Key(Key.Down));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
} }
[Test] [Test]
@ -73,6 +76,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null); AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
AddStep("press down", () => InputManager.Key(Key.Down));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
} }
[Test] [Test]
@ -91,6 +97,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("click", () => InputManager.Click(MouseButton.Left)); AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null); AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
AddStep("press down", () => InputManager.Key(Key.Down));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
} }
[Test] [Test]
@ -147,6 +156,40 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]); AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
} }
[Test]
public void TestKeyboardSelection()
{
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] [Test]
public void TestDownloadButtonHiddenWhenBeatmapExists() public void TestDownloadButtonHiddenWhenBeatmapExists()
{ {

View File

@ -0,0 +1,422 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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;
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;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
{
private ChatOverlayV2 chatOverlay;
private ChannelManager channelManager;
private APIUser testUser;
private Channel testPMChannel;
private Channel[] testChannels;
private Channel testChannel1 => testChannels[0];
private Channel testChannel2 => testChannels[1];
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[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,
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<Channel>)?.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 TestChatHeight()
{
BindableFloat configChatHeight = new BindableFloat();
config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight);
float newHeight = 0;
AddStep("Reset config chat height", () => configChatHeight.SetDefault());
AddStep("Show overlay", () => chatOverlay.Show());
AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default);
AddStep("Click top bar", () =>
{
InputManager.MoveMouseTo(chatOverlayTopBar);
InputManager.PressButton(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.IsDefault && 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()
{
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'", () => chatOverlayTextBox.Text = "number 2");
AddUntilStep("Only channel 2 visibile", () =>
{
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
.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<ChannelListItemCloseButton>().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<ChannelListItemCloseButton>().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.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.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
}
[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<ChannelListingItem>()
.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));
}
[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<ChannelListSelector>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click drawable channel", () => clickDrawable(chatOverlay.ChildrenOfType<DrawableChannel>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().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<ChannelListing>().Single().State.Value;
private Visibility loadingVisibility =>
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value;
private DrawableChannel currentDrawableChannel =>
chatOverlay.ChildrenOfType<Container<DrawableChannel>>().Single().Child;
private ChannelListItem getChannelListItem(Channel channel) =>
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel == channel);
private ChatTextBox chatOverlayTextBox =>
chatOverlay.ChildrenOfType<ChatTextBox>().Single();
private ChatOverlayTopBar chatOverlayTopBar =>
chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single();
private void clickDrawable(Drawable d)
{
InputManager.MoveMouseTo(d);
InputManager.Click(MouseButton.Left);
}
private List<Message> 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> { message };
}
private Channel createPublicChannel(int id) => new Channel
{
Id = id,
Name = $"#channel-{id}",
Topic = $"We talk about the number {id} here",
Type = ChannelType.Public,
};
}
}

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
@ -15,15 +17,11 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private DialogOverlay overlay; private DialogOverlay overlay;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
}
[Test] [Test]
public void TestBasic() public void TestBasic()
{ {
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
TestPopupDialog firstDialog = null; TestPopupDialog firstDialog = null;
TestPopupDialog secondDialog = null; TestPopupDialog secondDialog = null;
@ -37,12 +35,12 @@ namespace osu.Game.Tests.Visual.UserInterface
new PopupDialogOkButton new PopupDialogOkButton
{ {
Text = @"I never want to see this again.", Text = @"I never want to see this again.",
Action = () => System.Console.WriteLine(@"OK"), Action = () => Console.WriteLine(@"OK"),
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = @"Firetruck, I still want quick ranks!", Text = @"Firetruck, I still want quick ranks!",
Action = () => System.Console.WriteLine(@"Cancel"), Action = () => Console.WriteLine(@"Cancel"),
}, },
}, },
})); }));
@ -87,9 +85,49 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null); AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null);
} }
[Test]
public void TestPushBeforeLoad()
{
PopupDialog dialog = null;
AddStep("create dialog overlay", () => overlay = new SlowLoadingDialogOverlay());
AddStep("start loading overlay", () => LoadComponentAsync(overlay, Add));
AddStep("push dialog before loaded", () =>
{
overlay.Push(dialog = new TestPopupDialog
{
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton { Text = @"OK" },
},
});
});
AddStep("complete load", () => ((SlowLoadingDialogOverlay)overlay).LoadEvent.Set());
AddUntilStep("wait for load", () => overlay.IsLoaded);
AddAssert("dialog displayed", () => overlay.CurrentDialog == dialog);
}
public class SlowLoadingDialogOverlay : DialogOverlay
{
public ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
[BackgroundDependencyLoader]
private void load()
{
LoadEvent.Wait(10000);
}
}
[Test] [Test]
public void TestDismissBeforePush() public void TestDismissBeforePush()
{ {
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
TestPopupDialog testDialog = null; TestPopupDialog testDialog = null;
AddStep("dismissed dialog push", () => AddStep("dismissed dialog push", () =>
{ {
@ -106,6 +144,8 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestDismissBeforePushViaButtonPress() public void TestDismissBeforePushViaButtonPress()
{ {
AddStep("create dialog overlay", () => Child = overlay = new DialogOverlay());
TestPopupDialog testDialog = null; TestPopupDialog testDialog = null;
AddStep("dismissed dialog push", () => AddStep("dismissed dialog push", () =>
{ {

View File

@ -164,11 +164,19 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
assertCustomisationToggleState(disabled: false, active: true); assertCustomisationToggleState(disabled: false, active: true);
AddStep("dismiss mod customisation", () => AddStep("dismiss mod customisation via mouse", () =>
{ {
InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType<ShearedToggleButton>().Single()); InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType<ShearedToggleButton>().Single());
InputManager.Click(MouseButton.Left); 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()); AddStep("append another mod not requiring config", () => SelectedMods.Value = SelectedMods.Value.Append(new OsuModFlashlight()).ToArray());
assertCustomisationToggleState(disabled: false, active: false); assertCustomisationToggleState(disabled: false, active: false);

View File

@ -1,21 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Scoring; using osu.Game.Scoring;
using Realms; using Realms;
#nullable enable
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
/// <summary> /// <summary>
@ -109,6 +111,16 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true; public bool SamplesMatchPlaybackRate { get; set; } = true;
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
public double DistanceSpacing { get; set; } = 1.0; public double DistanceSpacing { get; set; } = 1.0;
public int BeatDivisor { get; set; } public int BeatDivisor { get; set; }

View File

@ -153,7 +153,7 @@ namespace osu.Game.Beatmaps
} }
}; };
cacheDownloadRequest.PerformAsync(); Task.Run(() => cacheDownloadRequest.PerformAsync());
} }
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)

View File

@ -59,6 +59,9 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
new KeyBinding(InputKey.Down, GlobalAction.SelectNext), new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
new KeyBinding(InputKey.Left, GlobalAction.SelectPreviousGroup),
new KeyBinding(InputKey.Right, GlobalAction.SelectNextGroup),
new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Space, GlobalAction.Select),
new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select),
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
@ -105,7 +108,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection),
new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom),
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions) new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions),
}; };
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[] public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
@ -309,5 +312,11 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDecreaseDistanceSpacing))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDecreaseDistanceSpacing))]
EditorDecreaseDistanceSpacing, EditorDecreaseDistanceSpacing,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectPreviousGroup))]
SelectPreviousGroup,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectNextGroup))]
SelectNextGroup,
} }
} }

View File

@ -129,6 +129,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString SelectNext => new TranslatableString(getKey(@"select_next"), @"Next selection"); public static LocalisableString SelectNext => new TranslatableString(getKey(@"select_next"), @"Next selection");
/// <summary>
/// "Select previous group"
/// </summary>
public static LocalisableString SelectPreviousGroup => new TranslatableString(getKey(@"select_previous_group"), @"Select previous group");
/// <summary>
/// "Select next group"
/// </summary>
public static LocalisableString SelectNextGroup => new TranslatableString(getKey(@"select_next_group"), @"Select next group");
/// <summary> /// <summary>
/// "Home" /// "Home"
/// </summary> /// </summary>

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
{ {
@ -119,6 +120,20 @@ namespace osu.Game.Online.Chat
public class ChatTextBox : FocusedTextBox 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() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();

View File

@ -25,14 +25,14 @@ namespace osu.Game.Overlays.Chat.ChannelList
public event Action<Channel>? OnRequestSelect; public event Action<Channel>? OnRequestSelect;
public event Action<Channel>? OnRequestLeave; public event Action<Channel>? OnRequestLeave;
public readonly Channel Channel;
public readonly BindableInt Mentions = new BindableInt(); public readonly BindableInt Mentions = new BindableInt();
public readonly BindableBool Unread = new BindableBool(); public readonly BindableBool Unread = new BindableBool();
public readonly BindableBool SelectorActive = new BindableBool(); public readonly BindableBool SelectorActive = new BindableBool();
private readonly Channel channel;
private Box hoverBox = null!; private Box hoverBox = null!;
private Box selectBox = null!; private Box selectBox = null!;
private OsuSpriteText text = null!; private OsuSpriteText text = null!;
@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
public ChannelListItem(Channel channel) public ChannelListItem(Channel channel)
{ {
this.channel = channel; Channel = channel;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Text = channel.Name, Text = Channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3, Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 }, Margin = new MarginPadding { Bottom = 2 },
@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 3 }, Margin = new MarginPadding { Right = 3 },
Action = () => OnRequestLeave?.Invoke(channel), Action = () => OnRequestLeave?.Invoke(Channel),
} }
} }
}, },
@ -119,20 +119,16 @@ namespace osu.Game.Overlays.Chat.ChannelList
}, },
}; };
Action = () => OnRequestSelect?.Invoke(channel); Action = () => OnRequestSelect?.Invoke(Channel);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
selectedChannel.BindValueChanged(_ => updateSelectState(), true); selectedChannel.BindValueChanged(_ => updateState(), true);
SelectorActive.BindValueChanged(_ => updateSelectState(), true); SelectorActive.BindValueChanged(_ => updateState(), true);
Unread.BindValueChanged(_ => updateState(), true);
Unread.BindValueChanged(change =>
{
text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint);
}, true);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
@ -151,10 +147,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
private Drawable createIcon() private Drawable createIcon()
{ {
if (channel.Type != ChannelType.PM) if (Channel.Type != ChannelType.PM)
return Drawable.Empty(); return Drawable.Empty();
return new UpdateableAvatar(channel.Users.First(), isInteractive: false) return new UpdateableAvatar(Channel.Users.First(), isInteractive: false)
{ {
Size = new Vector2(20), Size = new Vector2(20),
Margin = new MarginPadding { Right = 5 }, Margin = new MarginPadding { Right = 5 },
@ -165,12 +161,19 @@ namespace osu.Game.Overlays.Chat.ChannelList
}; };
} }
private void updateSelectState() private void updateState()
{ {
if (selectedChannel.Value == channel && !SelectorActive.Value) bool selected = selectedChannel.Value == Channel && !SelectorActive.Value;
if (selected)
selectBox.FadeIn(300, Easing.OutQuint); selectBox.FadeIn(300, Easing.OutQuint);
else else
selectBox.FadeOut(200, Easing.OutQuint); selectBox.FadeOut(200, Easing.OutQuint);
if (Unread.Value || selected)
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
else
text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint);
} }
} }
} }

View File

@ -21,6 +21,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
private Box hoverBox = null!; private Box hoverBox = null!;
private Box selectBox = null!; private Box selectBox = null!;
private OsuSpriteText text = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
@ -46,11 +50,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 18, Right = 10 }, Padding = new MarginPadding { Left = 18, Right = 10 },
Child = new OsuSpriteText Child = text = new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Text = "Add More Channels", Text = "Add more channels",
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3, Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 }, Margin = new MarginPadding { Bottom = 2 },
@ -68,9 +72,15 @@ namespace osu.Game.Overlays.Chat.ChannelList
SelectorActive.BindValueChanged(selector => SelectorActive.BindValueChanged(selector =>
{ {
if (selector.NewValue) if (selector.NewValue)
{
text.FadeColour(colourProvider.Content1, 300, Easing.OutQuint);
selectBox.FadeIn(300, Easing.OutQuint); selectBox.FadeIn(300, Easing.OutQuint);
}
else else
{
text.FadeColour(colourProvider.Light3, 200, Easing.OutQuint);
selectBox.FadeOut(200, Easing.OutQuint); selectBox.FadeOut(200, Easing.OutQuint);
}
}, true); }, true);
Action = () => SelectorActive.Value = true; Action = () => SelectorActive.Value = true;

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#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
{
private Box background = null!;
private Color4 backgroundColour;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, TextureStore textures)
{
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour = 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 },
},
},
},
},
};
}
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);
}
}
}

View File

@ -11,7 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; 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 osu.Game.Online.Chat;
using osuTK; using osuTK;
@ -25,14 +26,19 @@ namespace osu.Game.Overlays.Chat
public event Action<string>? OnSearchTermsChanged; public event Action<string>? OnSearchTermsChanged;
public void TextBoxTakeFocus() => chatTextBox.TakeFocus();
public void TextBoxKillFocus() => chatTextBox.KillFocus();
[Resolved] [Resolved]
private Bindable<Channel> currentChannel { get; set; } = null!; private Bindable<Channel> currentChannel { get; set; } = null!;
private OsuTextFlowContainer chattingTextContainer = null!; private Container chattingTextContainer = null!;
private OsuSpriteText chattingText = null!;
private Container searchIconContainer = null!; private Container searchIconContainer = null!;
private ChatTextBox chatTextBox = 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; private const float search_icon_width = 40;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -61,16 +67,20 @@ namespace osu.Game.Overlays.Chat
{ {
new Drawable[] 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, RelativeSizeAxes = Axes.Y,
TextAnchor = Anchor.CentreRight, Width = chatting_text_width,
Anchor = Anchor.CentreLeft, Masking = true,
Origin = Anchor.CentreLeft, Padding = new MarginPadding { Right = 5 },
Child = chattingText = new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1, Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Truncate = true,
},
}, },
searchIconContainer = new Container searchIconContainer = new Container
{ {
@ -131,15 +141,15 @@ namespace osu.Game.Overlays.Chat
switch (newChannel?.Type) switch (newChannel?.Type)
{ {
case ChannelType.Public: case ChannelType.Public:
chattingTextContainer.Text = $"chatting in {newChannel.Name}"; chattingText.Text = $"chatting in {newChannel.Name}";
break; break;
case ChannelType.PM: case ChannelType.PM:
chattingTextContainer.Text = $"chatting with {newChannel.Name}"; chattingText.Text = $"chatting with {newChannel.Name}";
break; break;
default: default:
chattingTextContainer.Text = string.Empty; chattingText.Text = string.Empty;
break; break;
} }
}, true); }, true);

View File

@ -25,11 +25,11 @@ namespace osu.Game.Overlays.Chat.Listing
public event Action<Channel>? OnRequestJoin; public event Action<Channel>? OnRequestJoin;
public event Action<Channel>? OnRequestLeave; public event Action<Channel>? OnRequestLeave;
public bool FilteringActive { get; set; } public readonly Channel Channel;
public IEnumerable<string> FilterTerms => new[] { channel.Name, channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
private readonly Channel channel; public bool FilteringActive { get; set; }
public IEnumerable<string> FilterTerms => new[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
private Box hoverBox = null!; private Box hoverBox = null!;
private SpriteIcon checkbox = null!; private SpriteIcon checkbox = null!;
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Chat.Listing
public ChannelListingItem(Channel channel) public ChannelListingItem(Channel channel)
{ {
this.channel = channel; Channel = channel;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -94,7 +94,7 @@ namespace osu.Game.Overlays.Chat.Listing
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Text = channel.Name, Text = Channel.Name,
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold), Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2 }, Margin = new MarginPadding { Bottom = 2 },
}, },
@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Chat.Listing
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Text = channel.Topic, Text = Channel.Topic,
Font = OsuFont.Torus.With(size: text_size), Font = OsuFont.Torus.With(size: text_size),
Margin = new MarginPadding { Bottom = 2 }, Margin = new MarginPadding { Bottom = 2 },
}, },
@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Chat.Listing
{ {
base.LoadComplete(); base.LoadComplete();
channelJoined = channel.Joined.GetBoundCopy(); channelJoined = Channel.Joined.GetBoundCopy();
channelJoined.BindValueChanged(change => channelJoined.BindValueChanged(change =>
{ {
const double duration = 500; const double duration = 500;
@ -155,7 +155,7 @@ namespace osu.Game.Overlays.Chat.Listing
} }
}, true); }, true);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -0,0 +1,314 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#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<DrawableChannel> currentChannelContainer = null!;
private readonly BindableFloat chatHeight = new BindableFloat();
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<Channel> currentChannel = new Bindable<Channel>();
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()
{
// Required for the pop in/out animation
RelativePositionAxes = Axes.Both;
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<DrawableChannel>
{
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();
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
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);
}
/// <summary>
/// Highlights a certain message in the specified channel.
/// </summary>
/// <param name="message">The message to highlight.</param>
/// <param name="channel">The channel containing the message.</param>
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()
{
base.PopIn();
this.MoveToY(0, transition_length, Easing.OutQuint);
this.FadeIn(transition_length, Easing.OutQuint);
}
protected override void PopOut()
{
base.PopOut();
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
textBar.TextBoxKillFocus();
}
protected override void OnFocus(FocusEvent e)
{
textBar.TextBoxTakeFocus();
base.OnFocus(e);
}
private void currentChannelChanged(ValueChangedEvent<Channel> channel)
{
Channel? newChannel = channel.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(c => c != channel.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<Channel> joinedChannels = filterChannels(args.NewItems);
foreach (var channel in joinedChannels)
channelList.AddChannel(channel);
break;
case NotifyCollectionChangedAction.Remove:
IEnumerable<Channel> 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<Channel> filterChannels(IList channels)
=> channels.Cast<Channel>().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);
}
}
}

View File

@ -49,18 +49,24 @@ namespace osu.Game.Overlays
{ {
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return; if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
// if any existing dialog is being displayed, dismiss it before showing a new one. var lastDialog = CurrentDialog;
CurrentDialog?.Hide();
// Immediately update the externally accessible property as this may be used for checks even before
// a DialogOverlay instance has finished loading.
CurrentDialog = dialog; CurrentDialog = dialog;
CurrentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
dialogContainer.Add(CurrentDialog); Scheduler.Add(() =>
{
// if any existing dialog is being displayed, dismiss it before showing a new one.
lastDialog?.Hide();
dialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
dialogContainer.Add(dialog);
Show(); Show();
}, false);
} }
public override bool IsPresent => dialogContainer.Children.Count > 0; public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
@ -81,23 +87,16 @@ namespace osu.Game.Overlays
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn(); base.PopIn();
this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint);
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);
} }
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut(); base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
if (CurrentDialog?.State.Value == Visibility.Visible) if (CurrentDialog?.State.Value == Visibility.Visible)
{
CurrentDialog.Hide(); CurrentDialog.Hide();
return;
}
this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine);
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)

View File

@ -18,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -217,7 +218,7 @@ namespace osu.Game.Overlays.Mods
foreach (var mod in SelectedMods.Value) foreach (var mod in SelectedMods.Value)
{ {
anyCustomisableMod |= mod.GetSettingsSourceProperties().Any(); anyCustomisableMod |= mod.GetSettingsSourceProperties().Any();
anyModWithRequiredCustomisationAdded |= !valueChangedEvent.OldValue.Contains(mod) && mod.RequiresConfiguration; anyModWithRequiredCustomisationAdded |= valueChangedEvent.OldValue.All(m => m.GetType() != mod.GetType()) && mod.RequiresConfiguration;
} }
if (anyCustomisableMod) if (anyCustomisableMod)
@ -315,6 +316,17 @@ namespace osu.Game.Overlays.Mods
} }
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Back && customisationVisible.Value)
{
customisationVisible.Value = false;
return true;
}
return base.OnPressed(e);
}
internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer> internal class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{ {
public ColumnScrollContainer() public ColumnScrollContainer()

View File

@ -372,12 +372,12 @@ namespace osu.Game.Overlays.Volume
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.SelectPrevious: case GlobalAction.SelectPreviousGroup:
State = SelectionState.Selected; State = SelectionState.Selected;
adjust(1, false); adjust(1, false);
return true; return true;
case GlobalAction.SelectNext: case GlobalAction.SelectNextGroup:
State = SelectionState.Selected; State = SelectionState.Selected;
adjust(-1, false); adjust(-1, false);
return true; return true;

View File

@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Edit
/// Represents a <see cref="HitObjectComposer{TObject}"/> for rulesets with the concept of distances between objects. /// Represents a <see cref="HitObjectComposer{TObject}"/> for rulesets with the concept of distances between objects.
/// </summary> /// </summary>
/// <typeparam name="TObject">The base type of supported objects.</typeparam> /// <typeparam name="TObject">The base type of supported objects.</typeparam>
[Cached(typeof(IDistanceSnapProvider))]
public abstract class DistancedHitObjectComposer<TObject> : HitObjectComposer<TObject>, IDistanceSnapProvider, IScrollBindingHandler<GlobalAction> public abstract class DistancedHitObjectComposer<TObject> : HitObjectComposer<TObject>, IDistanceSnapProvider, IScrollBindingHandler<GlobalAction>
where TObject : HitObject where TObject : HitObject
{ {
@ -146,10 +145,10 @@ namespace osu.Game.Rulesets.Edit
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
} }
public virtual double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) public virtual double FindSnappedDuration(HitObject referenceObject, float distance)
=> BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
public virtual float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) public virtual float FindSnappedDistance(HitObject referenceObject, float distance)
{ {
double startTime = referenceObject.StartTime; double startTime = referenceObject.StartTime;

View File

@ -35,7 +35,6 @@ namespace osu.Game.Rulesets.Edit
/// Responsible for providing snapping and generally gluing components together. /// Responsible for providing snapping and generally gluing components together.
/// </summary> /// </summary>
/// <typeparam name="TObject">The base type of supported objects.</typeparam> /// <typeparam name="TObject">The base type of supported objects.</typeparam>
[Cached(Type = typeof(IPlacementHandler))]
public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler
where TObject : HitObject where TObject : HitObject
{ {
@ -362,7 +361,7 @@ namespace osu.Game.Rulesets.Edit
/// <returns>The most relevant <see cref="Playfield"/>.</returns> /// <returns>The most relevant <see cref="Playfield"/>.</returns>
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
{ {
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null; double? targetTime = null;
@ -388,8 +387,7 @@ namespace osu.Game.Rulesets.Edit
/// A non-generic definition of a HitObject composer class. /// A non-generic definition of a HitObject composer class.
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />. /// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
/// </summary> /// </summary>
[Cached(typeof(HitObjectComposer))] [Cached]
[Cached(typeof(IPositionSnapProvider))]
public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{ {
protected HitObjectComposer() protected HitObjectComposer()
@ -416,9 +414,9 @@ namespace osu.Game.Rulesets.Edit
#region IPositionSnapProvider #region IPositionSnapProvider
public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null); new SnapResult(screenSpacePosition, null);
#endregion #endregion

View File

@ -1,16 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
/// <summary>
/// A snap provider which given a reference hit object and proposed distance from it, offers a more correct duration or distance value.
/// </summary>
[Cached]
public interface IDistanceSnapProvider : IPositionSnapProvider public interface IDistanceSnapProvider : IPositionSnapProvider
{ {
/// <summary> /// <summary>
/// The spacing multiplier applied to beat snap distances. /// A multiplier which changes the ratio of distance travelled per time unit.
/// </summary> /// </summary>
/// <seealso cref="BeatmapInfo.DistanceSpacing"/> /// <seealso cref="BeatmapInfo.DistanceSpacing"/>
IBindable<double> DistanceSpacingMultiplier { get; } IBindable<double> DistanceSpacingMultiplier { get; }
@ -23,7 +28,7 @@ namespace osu.Game.Rulesets.Edit
float GetBeatSnapDistanceAt(HitObject referenceObject); float GetBeatSnapDistanceAt(HitObject referenceObject);
/// <summary> /// <summary>
/// Converts a duration to a distance. /// Converts a duration to a distance without applying any snapping.
/// </summary> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> /// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="duration">The duration to convert.</param> /// <param name="duration">The duration to convert.</param>
@ -31,7 +36,7 @@ namespace osu.Game.Rulesets.Edit
float DurationToDistance(HitObject referenceObject, double duration); float DurationToDistance(HitObject referenceObject, double duration);
/// <summary> /// <summary>
/// Converts a distance to a duration. /// Converts a distance to a duration without applying any snapping.
/// </summary> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> /// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param> /// <param name="distance">The distance to convert.</param>
@ -39,20 +44,22 @@ namespace osu.Game.Rulesets.Edit
double DistanceToDuration(HitObject referenceObject, float distance); double DistanceToDuration(HitObject referenceObject, float distance);
/// <summary> /// <summary>
/// Converts a distance to a snapped duration. /// Given a distance from the provided hit object, find the valid snapped duration.
/// </summary> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> /// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param> /// <param name="distance">The distance to convert.</param>
/// <returns>A value that represents <paramref name="distance"/> as a duration snapped to the closest beat of the timing point.</returns> /// <returns>A value that represents <paramref name="distance"/> as a duration snapped to the closest beat of the timing point.</returns>
double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); double FindSnappedDuration(HitObject referenceObject, float distance);
/// <summary> /// <summary>
/// Converts an unsnapped distance to a snapped distance. /// Given a distance from the provided hit object, find the valid snapped distance.
/// The returned distance will always be floored (as to never exceed the provided <paramref name="distance"/>.
/// </summary> /// </summary>
/// <param name="referenceObject">An object to be used as a reference point for this operation.</param> /// <param name="referenceObject">An object to be used as a reference point for this operation.</param>
/// <param name="distance">The distance to convert.</param> /// <param name="distance">The distance to convert.</param>
/// <returns>A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.</returns> /// <returns>
float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); /// A value that represents <paramref name="distance"/> snapped to the closest beat of the timing point.
/// The distance will always be less than or equal to the provided <paramref name="distance"/>.
/// </returns>
float FindSnappedDistance(HitObject referenceObject, float distance);
} }
} }

View File

@ -1,27 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
/// <summary>
/// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap.
/// Provided values are inferred in an isolated context, without consideration of other nearby hit objects.
/// </summary>
[Cached]
public interface IPositionSnapProvider public interface IPositionSnapProvider
{ {
/// <summary> /// <summary>
/// Given a position, find a valid time and position snap. /// Given a position, find a valid time and position snap.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This call should be equivalent to running <see cref="SnapScreenSpacePositionToValidPosition"/> with any additional logic that can be performed without the time immutability restriction. /// This call should be equivalent to running <see cref="FindSnappedPosition"/> with any additional logic that can be performed without the time immutability restriction.
/// </remarks> /// </remarks>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param> /// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <returns>The time and position post-snapping.</returns> /// <returns>The time and position post-snapping.</returns>
SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition);
/// <summary> /// <summary>
/// Given a position, find a value position snap, restricting time to its input value. /// Given a position, find a valid position snap, without changing the time value.
/// </summary> /// </summary>
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param> /// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
/// <returns>The position post-snapping. Time will always be null.</returns> /// <returns>The position post-snapping. Time will always be null.</returns>
SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition); SnapResult FindSnappedPosition(Vector2 screenSpacePosition);
} }
} }

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Objects
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider) public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
where THitObject : HitObject, IHasPath where THitObject : HitObject, IHasPath
{ {
hitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance; hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance;
} }
/// <summary> /// <summary>

View File

@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 originalPosition = movementBlueprintOriginalPositions[i]; Vector2 originalPosition = movementBlueprintOriginalPositions[i];
var testPosition = originalPosition + distanceTravelled; var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition); var positionalResult = snapProvider.FindSnappedPosition(testPosition);
if (positionalResult.ScreenSpacePosition == testPosition) continue; if (positionalResult.ScreenSpacePosition == testPosition) continue;
@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
// Retrieve a snapped position. // Retrieve a snapped position.
var result = snapProvider?.SnapScreenSpacePositionToValidTime(movePosition); var result = snapProvider?.FindSnappedPositionAndTime(movePosition);
if (result == null) if (result == null)
{ {

View File

@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 normalisedDirection = direction * new Vector2(1f / distance); Vector2 normalisedDirection = direction * new Vector2(1f / distance);
Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius; Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius;
return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length)); return (snappedPosition, StartTime + SnapProvider.FindSnappedDuration(ReferenceObject, (snappedPosition - StartPosition).Length));
} }
} }
} }

View File

@ -214,7 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementPosition() private void updatePlacementPosition()
{ {
var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
// if no time was found from positional snapping, we should still quantize to the beat. // if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);

View File

@ -19,7 +19,6 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
[Cached(typeof(IPositionSnapProvider))]
[Cached] [Cached]
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{ {
@ -307,10 +306,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
public double VisibleRange => track.Length / Zoom; public double VisibleRange => track.Length / Zoom;
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null); new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition))));
private double getTimeFromPosition(Vector2 localPosition) => private double getTimeFromPosition(Vector2 localPosition) =>

View File

@ -382,7 +382,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
OnDragHandled?.Invoke(e); OnDragHandled?.Invoke(e);
if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) if (timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time)
{ {
switch (hitObject) switch (hitObject)
{ {

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Compose namespace osu.Game.Screens.Edit.Compose
{ {
[Cached]
public interface IPlacementHandler public interface IPlacementHandler
{ {
/// <summary> /// <summary>

View File

@ -6,7 +6,10 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osuTK; using osuTK;
@ -15,7 +18,7 @@ namespace osu.Game.Screens.OnlinePlay
/// <summary> /// <summary>
/// A scrollable list which displays the <see cref="PlaylistItem"/>s in a <see cref="Room"/>. /// A scrollable list which displays the <see cref="PlaylistItem"/>s in a <see cref="Room"/>.
/// </summary> /// </summary>
public class DrawableRoomPlaylist : OsuRearrangeableListContainer<PlaylistItem> public class DrawableRoomPlaylist : OsuRearrangeableListContainer<PlaylistItem>, IKeyBindingHandler<GlobalAction>
{ {
/// <summary> /// <summary>
/// The currently-selected item. Selection is visually represented with a border. /// The currently-selected item. Selection is visually represented with a border.
@ -169,5 +172,78 @@ namespace osu.Game.Screens.OnlinePlay
}); });
protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item); protected virtual DrawableRoomPlaylistItem CreateDrawablePlaylistItem(PlaylistItem item) => new DrawableRoomPlaylistItem(item);
protected override void LoadComplete()
{
base.LoadComplete();
// 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()
{
// 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;
// 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)
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.SelectNext:
selectNext(1);
return true;
case GlobalAction.SelectPrevious:
selectNext(-1);
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void selectNext(int direction)
{
if (!AllowSelection)
return;
var visibleItems = ListContainer.AsEnumerable().Where(r => r.IsPresent);
PlaylistItem item;
if (SelectedItem.Value == null)
item = visibleItems.FirstOrDefault()?.Model;
else
{
if (direction < 0)
visibleItems = visibleItems.Reverse();
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.
if (item != null)
SelectedItem.Value = item;
}
#endregion
} }
} }

View File

@ -139,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return base.OnClick(e); return base.OnClick(e);
} }
#region Key selection logic (shared with BeatmapCarousel) #region Key selection logic (shared with BeatmapCarousel and DrawableRoomPlaylist)
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {

View File

@ -604,34 +604,20 @@ namespace osu.Game.Screens.Select
public void ScrollToSelected(bool immediate = false) => public void ScrollToSelected(bool immediate = false) =>
pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard; pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard;
#region Key / button selection logic #region Button selection logic
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.Left:
SelectNext(-1);
return true;
case Key.Right:
SelectNext();
return true;
}
return false;
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.SelectNext: case GlobalAction.SelectNext:
SelectNext(1, false); case GlobalAction.SelectNextGroup:
SelectNext(1, e.Action == GlobalAction.SelectNextGroup);
return true; return true;
case GlobalAction.SelectPrevious: case GlobalAction.SelectPrevious:
SelectNext(-1, false); case GlobalAction.SelectPreviousGroup:
SelectNext(-1, e.Action == GlobalAction.SelectPreviousGroup);
return true; return true;
} }

View File

@ -14,7 +14,6 @@ using osu.Game.Screens.Edit.Compose;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
[Cached(Type = typeof(IPlacementHandler))]
public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
{ {
protected readonly Container HitObjectContainer; protected readonly Container HitObjectContainer;