Merge branch 'master' into fix-carousel-scroll-while-import

This commit is contained in:
Dan Balasescu 2020-03-16 12:06:50 +09:00 committed by GitHub
commit 3e65f13c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 281 additions and 116 deletions

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -55,14 +56,14 @@ namespace osu.Game.Rulesets.Catch.UI
} }
/// <summary> /// <summary>
/// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. /// Activate or deactivate the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met.
/// </summary> /// </summary>
protected bool Trail protected bool Trail
{ {
get => trail; get => trail;
set set
{ {
if (value == trail) return; if (value == trail || AdditiveTarget == null) return;
trail = value; trail = value;
@ -77,6 +78,8 @@ namespace osu.Game.Rulesets.Catch.UI
private CatcherSprite catcherKiai; private CatcherSprite catcherKiai;
private CatcherSprite catcherFail; private CatcherSprite catcherFail;
private CatcherSprite currentCatcher;
private int currentDirection; private int currentDirection;
private bool dashing; private bool dashing;
@ -236,10 +239,10 @@ namespace osu.Game.Rulesets.Catch.UI
this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint); this.FadeTo(0.2f, hyper_dash_transition_length, Easing.OutQuint);
Trail = true; Trail = true;
var hyperDashEndGlow = createAdditiveSprite(true); var hyperDashEndGlow = createAdditiveSprite();
hyperDashEndGlow.MoveToOffset(new Vector2(0, -20), 1200, Easing.In); hyperDashEndGlow.MoveToOffset(new Vector2(0, -10), 1200, Easing.In);
hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.9f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In); hyperDashEndGlow.ScaleTo(hyperDashEndGlow.Scale * 0.95f).ScaleTo(hyperDashEndGlow.Scale * 1.2f, 1200, Easing.In);
hyperDashEndGlow.FadeOut(1200); hyperDashEndGlow.FadeOut(1200);
hyperDashEndGlow.Expire(true); hyperDashEndGlow.Expire(true);
} }
@ -358,39 +361,36 @@ namespace osu.Game.Rulesets.Catch.UI
private void updateCatcher() private void updateCatcher()
{ {
catcherIdle.Hide(); currentCatcher?.Hide();
catcherKiai.Hide();
catcherFail.Hide();
CatcherSprite current;
switch (CurrentState) switch (CurrentState)
{ {
default: default:
current = catcherIdle; currentCatcher = catcherIdle;
break; break;
case CatcherAnimationState.Fail: case CatcherAnimationState.Fail:
current = catcherFail; currentCatcher = catcherFail;
break; break;
case CatcherAnimationState.Kiai: case CatcherAnimationState.Kiai:
current = catcherKiai; currentCatcher = catcherKiai;
break; break;
} }
current.Show(); currentCatcher.Show();
(current.Drawable as IAnimation)?.GotoFrame(0); (currentCatcher.Drawable as IAnimation)?.GotoFrame(0);
} }
private void beginTrail() private void beginTrail()
{ {
Trail &= dashing || HyperDashing; if (!dashing && !HyperDashing)
Trail &= AdditiveTarget != null; {
Trail = false;
return;
}
if (!Trail) return; var additive = createAdditiveSprite();
var additive = createAdditiveSprite(HyperDashing);
additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint);
additive.Expire(true); additive.Expire(true);
@ -398,27 +398,6 @@ namespace osu.Game.Rulesets.Catch.UI
Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
} }
private Drawable createAdditiveSprite(bool hyperDash)
{
var additive = createCatcherSprite();
additive.Anchor = Anchor;
additive.Scale = Scale;
additive.Colour = hyperDash ? Color4.Red : Color4.White;
additive.Blending = BlendingParameters.Additive;
additive.RelativePositionAxes = RelativePositionAxes;
additive.Position = Position;
AdditiveTarget.Add(additive);
return additive;
}
private Drawable createCatcherSprite()
{
return new CatcherSprite(CurrentState);
}
private void updateState(CatcherAnimationState state) private void updateState(CatcherAnimationState state)
{ {
if (CurrentState == state) if (CurrentState == state)
@ -428,6 +407,25 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher(); updateCatcher();
} }
private CatcherTrailSprite createAdditiveSprite()
{
var tex = (currentCatcher.Drawable as TextureAnimation)?.CurrentFrame ?? ((Sprite)currentCatcher.Drawable).Texture;
var sprite = new CatcherTrailSprite(tex)
{
Anchor = Anchor,
Scale = Scale,
Colour = HyperDashing ? Color4.Red : Color4.White,
Blending = BlendingParameters.Additive,
RelativePositionAxes = RelativePositionAxes,
Position = Position
};
AdditiveTarget?.Add(sprite);
return sprite;
}
private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action) private void removeFromPlateWithTransform(DrawableHitObject fruit, Action<DrawableHitObject> action)
{ {
if (ExplodingFruitTarget != null) if (ExplodingFruitTarget != null)

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherTrailSprite : Sprite
{
public CatcherTrailSprite(Texture texture)
{
Texture = texture;
Size = new Vector2(CatcherArea.CATCHER_SIZE);
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(0.5f, 0.06f) * CatcherArea.CATCHER_SIZE;
}
}
}

View File

@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.ApplySkin(skin, allowFallback); base.ApplySkin(skin, allowFallback);
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; Ball.Colour = allowBallTint ? AccentColour.Value : Color4.White;
} }
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)

View File

@ -38,8 +38,13 @@ namespace osu.Game.Tests.Visual.Online
private TestChatOverlay chatOverlay; private TestChatOverlay chatOverlay;
private ChannelManager channelManager; private ChannelManager channelManager;
private IEnumerable<Channel> visibleChannels => chatOverlay.ChannelTabControl.VisibleItems.Where(channel => channel.Name != "+");
private IEnumerable<Channel> joinedChannels => chatOverlay.ChannelTabControl.Items.Where(channel => channel.Name != "+");
private readonly List<Channel> channels; private readonly List<Channel> channels;
private Channel currentChannel => channelManager.CurrentChannel.Value;
private Channel nextChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) + 1);
private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1);
private Channel channel1 => channels[0]; private Channel channel1 => channels[0];
private Channel channel2 => channels[1]; private Channel channel2 => channels[1];
@ -91,7 +96,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); AddAssert("Current channel is channel 1", () => currentChannel == channel1);
AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
} }
@ -102,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Join channel 2", () => channelManager.JoinChannel(channel2)); AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2])); AddStep("Switch to channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
AddStep("Close channel 2", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child)); AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child));
AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); AddAssert("Selector remained closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
AddAssert("Current channel is channel 1", () => channelManager.CurrentChannel.Value == channel1); AddAssert("Current channel is channel 1", () => currentChannel == channel1);
AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child)); AddStep("Close channel 1", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
} }
@ -140,10 +145,67 @@ namespace osu.Game.Tests.Visual.Online
var targetNumberKey = oneBasedIndex % 10; var targetNumberKey = oneBasedIndex % 10;
var targetChannel = channels[zeroBasedIndex]; var targetChannel = channels[zeroBasedIndex];
AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey)); AddStep($"press Alt+{targetNumberKey}", () => pressChannelHotkey(targetNumberKey));
AddAssert($"channel #{oneBasedIndex} is selected", () => channelManager.CurrentChannel.Value == targetChannel); AddAssert($"channel #{oneBasedIndex} is selected", () => currentChannel == targetChannel);
} }
} }
private Channel expectedChannel;
[Test]
public void TestCloseChannelWhileActive()
{
AddUntilStep("Join until dropdown has channels", () =>
{
if (visibleChannels.Count() < joinedChannels.Count())
return true;
// Using temporary channels because they don't hide their names when not active
Channel toAdd = new Channel { Name = $"test channel {joinedChannels.Count()}", Type = ChannelType.Temporary };
channelManager.JoinChannel(toAdd);
return false;
});
AddStep("Switch to last tab", () => clickDrawable(chatOverlay.TabMap[visibleChannels.Last()]));
AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last());
// Closing the last channel before dropdown
AddStep("Close current channel", () =>
{
expectedChannel = nextChannel;
chatOverlay.ChannelTabControl.RemoveChannel(currentChannel);
});
AddAssert("Next channel selected", () => currentChannel == expectedChannel);
// Depending on the window size, one more channel might need to be closed for the selectorTab to appear
AddUntilStep("Close channels until selector visible", () =>
{
if (chatOverlay.ChannelTabControl.VisibleItems.Last().Name == "+")
return true;
chatOverlay.ChannelTabControl.RemoveChannel(visibleChannels.Last());
return false;
});
AddAssert("Last visible selected", () => currentChannel == visibleChannels.Last());
// Closing the last channel with dropdown no longer present
AddStep("Close last when selector next", () =>
{
expectedChannel = previousChannel;
chatOverlay.ChannelTabControl.RemoveChannel(currentChannel);
});
AddAssert("Channel changed to previous", () => currentChannel == expectedChannel);
// Standard channel closing
AddStep("Switch to previous channel", () => chatOverlay.ChannelTabControl.SwitchTab(-1));
AddStep("Close current channel", () =>
{
expectedChannel = nextChannel;
chatOverlay.ChannelTabControl.RemoveChannel(currentChannel);
});
AddAssert("Channel changed to next", () => currentChannel == expectedChannel);
}
private void pressChannelHotkey(int number) private void pressChannelHotkey(int number)
{ {
var channelKey = Key.Number0 + number; var channelKey = Key.Number0 + number;
@ -187,6 +249,8 @@ namespace osu.Game.Tests.Visual.Online
{ {
public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value; public Visibility SelectionOverlayState => ChannelSelectionOverlay.State.Value;
public new ChannelTabControl ChannelTabControl => base.ChannelTabControl;
public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay; public new ChannelSelectionOverlay ChannelSelectionOverlay => base.ChannelSelectionOverlay;
protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl(); protected override ChannelTabControl CreateChannelTabControl() => new TestTabControl();
@ -196,12 +260,22 @@ namespace osu.Game.Tests.Visual.Online
private class TestTabControl : ChannelTabControl private class TestTabControl : ChannelTabControl
{ {
protected override TabItem<Channel> CreateTabItem(Channel value) => new TestChannelTabItem(value); protected override TabItem<Channel> CreateTabItem(Channel value)
{
switch (value.Type)
{
case ChannelType.PM:
return new TestPrivateChannelTabItem(value);
default:
return new TestChannelTabItem(value);
}
}
public new IReadOnlyDictionary<Channel, TabItem<Channel>> TabMap => base.TabMap; public new IReadOnlyDictionary<Channel, TabItem<Channel>> TabMap => base.TabMap;
} }
private class TestChannelTabItem : PrivateChannelTabItem private class TestChannelTabItem : ChannelTabItem
{ {
public TestChannelTabItem(Channel channel) public TestChannelTabItem(Channel channel)
: base(channel) : base(channel)
@ -210,5 +284,15 @@ namespace osu.Game.Tests.Visual.Online
public new ClickableContainer CloseButton => base.CloseButton; public new ClickableContainer CloseButton => base.CloseButton;
} }
private class TestPrivateChannelTabItem : PrivateChannelTabItem
{
public TestPrivateChannelTabItem(Channel channel)
: base(channel)
{
}
public new ClickableContainer CloseButton => base.CloseButton;
}
} }
} }

View File

@ -399,7 +399,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("filter to ruleset 0", () => AddStep("filter to ruleset 0", () =>
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap == null);
AddStep("remove mixed set", () => AddStep("remove mixed set", () =>
{ {

View File

@ -436,6 +436,9 @@ namespace osu.Game.Tests.Visual.SongSelect
changeRuleset(0); changeRuleset(0);
// used for filter check below
AddStep("allow convert display", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null);
AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono"); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
@ -446,16 +449,28 @@ namespace osu.Game.Tests.Visual.SongSelect
BeatmapInfo target = null; BeatmapInfo target = null;
int targetRuleset = differentRuleset ? 1 : 0;
AddStep("select beatmap externally", () => AddStep("select beatmap externally", () =>
{ {
target = manager.GetAllUsableBeatmapSets().Where(b => b.Beatmaps.Any(bi => bi.RulesetID == (differentRuleset ? 1 : 0))) target = manager.GetAllUsableBeatmapSets()
.ElementAt(5).Beatmaps.First(); .Where(b => b.Beatmaps.Any(bi => bi.RulesetID == targetRuleset))
.ElementAt(5).Beatmaps.First(bi => bi.RulesetID == targetRuleset);
Beatmap.Value = manager.GetWorkingBeatmap(target); Beatmap.Value = manager.GetWorkingBeatmap(target);
}); });
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null);
AddAssert("selected only shows expected ruleset (plus converts)", () =>
{
var selectedPanel = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected);
// special case for converts checked here.
return selectedPanel.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().All(i =>
i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0);
});
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID);
@ -557,6 +572,7 @@ namespace osu.Game.Tests.Visual.SongSelect
difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>() difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>()
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
}); });
AddStep("Click on a difficulty", () => AddStep("Click on a difficulty", () =>
{ {
InputManager.MoveMouseTo(difficultyIcon); InputManager.MoveMouseTo(difficultyIcon);
@ -564,6 +580,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.PressButton(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon)); AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon));
double? maxBPM = null; double? maxBPM = null;
@ -576,16 +593,16 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
})); }));
BeatmapInfo filteredBeatmap = null;
DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null;
AddStep("Get filtered icon", () => AddStep("Get filtered icon", () =>
{ {
var filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.Find(b => b.BPM < maxBPM); filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM);
int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap);
filteredIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex); filteredIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>().ElementAt(filteredBeatmapIndex);
}); });
int? previousID = null;
AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID);
AddStep("Click on a filtered difficulty", () => AddStep("Click on a filtered difficulty", () =>
{ {
InputManager.MoveMouseTo(filteredIcon); InputManager.MoveMouseTo(filteredIcon);
@ -593,7 +610,55 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.PressButton(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID);
AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap);
}
[Test]
public void TestDifficultyIconSelectingForDifferentRuleset()
{
changeRuleset(0);
createSongSelect();
AddStep("import multi-ruleset map", () =>
{
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray();
manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait();
});
DrawableCarouselBeatmapSet set = null;
AddUntilStep("Find the DrawableCarouselBeatmapSet", () =>
{
set = songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().FirstOrDefault();
return set != null;
});
DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon for different ruleset", () =>
{
difficultyIcon = set.ChildrenOfType<DrawableCarouselBeatmapSet.FilterableDifficultyIcon>()
.First(icon => icon.Item.Beatmap.Ruleset.ID == 3);
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
int previousSetID = 0;
AddStep("record set ID", () => previousSetID = Beatmap.Value.BeatmapSetInfo.ID);
AddStep("Click on a difficulty", () =>
{
InputManager.MoveMouseTo(difficultyIcon);
InputManager.PressButton(MouseButton.Left);
InputManager.ReleaseButton(MouseButton.Left);
});
AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3);
AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmap.BeatmapSet.ID == previousSetID);
AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3);
} }
private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info);

View File

@ -12,5 +12,6 @@ namespace osu.Game.Online.Chat
Temporary, Temporary,
PM, PM,
Group, Group,
System,
} }
} }

View File

@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat.Tabs
public ChannelSelectorTabChannel() public ChannelSelectorTabChannel()
{ {
Name = "+"; Name = "+";
Type = ChannelType.System;
} }
} }
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Chat.Tabs
// performTabSort might've made selectorTab's position wonky, fix it // performTabSort might've made selectorTab's position wonky, fix it
TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); TabContainer.SetLayoutPosition(selectorTab, float.MaxValue);
((ChannelTabItem)item).OnRequestClose += tabCloseRequested; ((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value);
base.AddTabItem(item, addToDropdown); base.AddTabItem(item, addToDropdown);
} }
@ -74,18 +74,24 @@ namespace osu.Game.Overlays.Chat.Tabs
/// <summary> /// <summary>
/// Removes a channel from the ChannelTabControl. /// Removes a channel from the ChannelTabControl.
/// If the selected channel is the one that is beeing removed, the next available channel will be selected. /// If the selected channel is the one that is being removed, the next available channel will be selected.
/// </summary> /// </summary>
/// <param name="channel">The channel that is going to be removed.</param> /// <param name="channel">The channel that is going to be removed.</param>
public void RemoveChannel(Channel channel) public void RemoveChannel(Channel channel)
{ {
RemoveItem(channel);
if (Current.Value == channel) if (Current.Value == channel)
{ {
// Prefer non-selector channels first var allChannels = TabContainer.AllTabItems.Select(tab => tab.Value).ToList();
Current.Value = Items.FirstOrDefault(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)) ?? Items.FirstOrDefault(); var isNextTabSelector = allChannels[allChannels.IndexOf(channel) + 1] == selectorTab.Value;
// selectorTab is not switchable, so we have to explicitly select it if it's the only tab left
if (isNextTabSelector && allChannels.Count == 2)
SelectTab(selectorTab);
else
SwitchTab(isNextTabSelector ? -1 : 1);
} }
RemoveItem(channel);
} }
protected override void SelectTab(TabItem<Channel> tab) protected override void SelectTab(TabItem<Channel> tab)
@ -100,21 +106,6 @@ namespace osu.Game.Overlays.Chat.Tabs
selectorTab.Active.Value = false; selectorTab.Active.Value = false;
} }
private void tabCloseRequested(TabItem<Channel> tab)
{
int totalTabs = TabContainer.Count - 1; // account for selectorTab
int currentIndex = Math.Clamp(TabContainer.IndexOf(tab), 1, totalTabs);
if (tab == SelectedTab && totalTabs > 1)
// Select the tab after tab-to-be-removed's index, or the tab before if current == last
SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]);
else if (totalTabs == 1 && !selectorTab.Active.Value)
// Open channel selection overlay if all channel tabs will be closed after removing this tab
SelectTab(selectorTab);
OnRequestLeave?.Invoke(tab.Value);
}
protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer
{ {
Direction = FillDirection.Full, Direction = FillDirection.Full,

View File

@ -205,9 +205,6 @@ namespace osu.Game.Screens.Select
/// <summary> /// <summary>
/// Selects a given beatmap on the carousel. /// Selects a given beatmap on the carousel.
///
/// If bypassFilters is false, we will try to select another unfiltered beatmap in the same set. If the
/// entire set is filtered, no selection is made.
/// </summary> /// </summary>
/// <param name="beatmap">The beatmap to select.</param> /// <param name="beatmap">The beatmap to select.</param>
/// <param name="bypassFilters">Whether to select the beatmap even if it is filtered (i.e., not visible on carousel).</param> /// <param name="bypassFilters">Whether to select the beatmap even if it is filtered (i.e., not visible on carousel).</param>
@ -229,25 +226,21 @@ namespace osu.Game.Screens.Select
continue; continue;
if (!bypassFilters && item.Filtered.Value) if (!bypassFilters && item.Filtered.Value)
// The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set return false;
item = set.Beatmaps.FirstOrDefault(b => !b.Filtered.Value);
if (item != null) select(item);
// if we got here and the set is filtered, it means we were bypassing filters.
// in this case, reapplying the filter is necessary to ensure the panel is in the correct place
// (since it is forcefully being included in the carousel).
if (set.Filtered.Value)
{ {
select(item); Debug.Assert(bypassFilters);
// if we got here and the set is filtered, it means we were bypassing filters. applyActiveCriteria(false);
// in this case, reapplying the filter is necessary to ensure the panel is in the correct place
// (since it is forcefully being included in the carousel).
if (set.Filtered.Value)
{
Debug.Assert(bypassFilters);
applyActiveCriteria(false);
}
return true;
} }
return true;
} }
return false; return false;

View File

@ -25,18 +25,18 @@ namespace osu.Game.Screens.Select.Carousel
{ {
base.Filter(criteria); base.Filter(criteria);
if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
{
// bypass filtering for selected beatmap
Filtered.Value = false;
return;
}
bool match = bool match =
criteria.Ruleset == null || criteria.Ruleset == null ||
Beatmap.RulesetID == criteria.Ruleset.ID || Beatmap.RulesetID == criteria.Ruleset.ID ||
(Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps);
if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
{
// only check ruleset equality or convertability for selected beatmap
Filtered.Value = !match;
return;
}
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty); match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty);
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);

View File

@ -205,7 +205,9 @@ namespace osu.Game.Screens.Select.Carousel
{ {
private readonly BindableBool filtered = new BindableBool(); private readonly BindableBool filtered = new BindableBool();
private readonly CarouselBeatmap item; public bool IsFiltered => filtered.Value;
public readonly CarouselBeatmap Item;
public FilterableDifficultyIcon(CarouselBeatmap item) public FilterableDifficultyIcon(CarouselBeatmap item)
: base(item.Beatmap) : base(item.Beatmap)
@ -214,14 +216,12 @@ namespace osu.Game.Screens.Select.Carousel
filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100));
filtered.TriggerChange(); filtered.TriggerChange();
this.item = item; Item = item;
} }
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (!filtered.Value) Item.State.Value = CarouselItemState.Selected;
item.State.Value = CarouselItemState.Selected;
return true; return true;
} }
} }

View File

@ -380,6 +380,8 @@ namespace osu.Game.Screens.Select
{ {
if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return;
Logger.Log($"working beatmap updated to {e.NewValue}");
if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false)) if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false))
{ {
// A selection may not have been possible with filters applied. // A selection may not have been possible with filters applied.
@ -446,8 +448,10 @@ namespace osu.Game.Screens.Select
if (transferRulesetValue()) if (transferRulesetValue())
{ {
// if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset.
Mods.Value = Array.Empty<Mod>(); Mods.Value = Array.Empty<Mod>();
// required to return once in order to have the carousel in a good state.
// if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset.
return; return;
} }
@ -472,7 +476,7 @@ namespace osu.Game.Screens.Select
if (this.IsCurrentScreen()) if (this.IsCurrentScreen())
ensurePlayingSelected(); ensurePlayingSelected();
UpdateBeatmap(Beatmap.Value); updateComponentFromBeatmap(Beatmap.Value);
} }
} }
@ -547,7 +551,7 @@ namespace osu.Game.Screens.Select
if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending)
{ {
UpdateBeatmap(Beatmap.Value); updateComponentFromBeatmap(Beatmap.Value);
// restart playback on returning to song select, regardless. // restart playback on returning to song select, regardless.
music?.Play(); music?.Play();
@ -610,10 +614,8 @@ namespace osu.Game.Screens.Select
/// This is a debounced call (unlike directly binding to WorkingBeatmap.ValueChanged). /// This is a debounced call (unlike directly binding to WorkingBeatmap.ValueChanged).
/// </summary> /// </summary>
/// <param name="beatmap">The working beatmap.</param> /// <param name="beatmap">The working beatmap.</param>
protected virtual void UpdateBeatmap(WorkingBeatmap beatmap) private void updateComponentFromBeatmap(WorkingBeatmap beatmap)
{ {
Logger.Log($"working beatmap updated to {beatmap}");
if (Background is BackgroundScreenBeatmap backgroundModeBeatmap) if (Background is BackgroundScreenBeatmap backgroundModeBeatmap)
{ {
backgroundModeBeatmap.Beatmap = beatmap; backgroundModeBeatmap.Beatmap = beatmap;
@ -658,9 +660,17 @@ namespace osu.Game.Screens.Select
return; return;
// Attempt to select the current beatmap on the carousel, if it is valid to be selected. // Attempt to select the current beatmap on the carousel, if it is valid to be selected.
if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false)
&& Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) {
return; if (Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false))
return;
// prefer not changing ruleset at this point, so look for another difficulty in the currently playing beatmap
var found = Beatmap.Value.BeatmapSetInfo.Beatmaps.FirstOrDefault(b => b.Ruleset.Equals(decoupledRuleset.Value));
if (found != null && Carousel.SelectBeatmap(found, false))
return;
}
// If the current active beatmap could not be selected, select a new random beatmap. // If the current active beatmap could not be selected, select a new random beatmap.
if (!Carousel.SelectNextRandom()) if (!Carousel.SelectNextRandom())