mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 16:02:58 +08:00
Merge branch 'master' into export
This commit is contained in:
commit
65b4b198a4
@ -191,6 +191,8 @@ csharp_style_prefer_index_operator = false:silent
|
||||
csharp_style_prefer_range_operator = false:silent
|
||||
csharp_style_prefer_switch_expression = false:none
|
||||
|
||||
csharp_style_namespace_declarations = block_scoped:warning
|
||||
|
||||
[*.{yaml,yml}]
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
|
@ -11,7 +11,7 @@
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.506.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
|
@ -15,6 +15,10 @@ using osuTK.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -22,6 +26,7 @@ using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
@ -30,6 +35,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
private readonly BindableBool snakingIn = new BindableBool();
|
||||
private readonly BindableBool snakingOut = new BindableBool();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddToggleStep("toggle snaking", v =>
|
||||
{
|
||||
snakingIn.Value = v;
|
||||
snakingOut.Value = v;
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var config = (OsuRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
|
||||
config.BindWith(OsuRulesetSetting.SnakingInSliders, snakingIn);
|
||||
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVariousSliders()
|
||||
{
|
||||
|
@ -87,12 +87,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
|
||||
|
||||
animDuration = Math.Min(300, HitObject.SpanDuration);
|
||||
|
||||
this.Animate(
|
||||
d => d.FadeIn(animDuration),
|
||||
d => d.ScaleTo(0.5f).ScaleTo(1f, animDuration * 2, Easing.OutElasticHalf)
|
||||
);
|
||||
this
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
|
||||
.FadeIn(HitObject.RepeatIndex == 0 ? HitObject.TimeFadeIn : animDuration);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
@ -91,7 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateInitialTransforms();
|
||||
|
||||
CirclePiece.FadeInFromZero(HitObject.TimeFadeIn);
|
||||
// When snaking in is enabled, the first end circle needs to be delayed until the snaking completes.
|
||||
bool delayFadeIn = DrawableSlider.SliderBody?.SnakingIn.Value == true && HitObject.RepeatIndex == 0;
|
||||
|
||||
CirclePiece
|
||||
.FadeOut()
|
||||
.Delay(delayFadeIn ? (Slider?.TimePreempt ?? 0) / 3 : 0)
|
||||
.FadeIn(HitObject.TimeFadeIn);
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
|
@ -39,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
}
|
||||
else
|
||||
{
|
||||
// taken from osu-stable
|
||||
const float first_end_circle_preempt_adjust = 2 / 3f;
|
||||
|
||||
// The first end circle should fade in with the slider.
|
||||
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
|
||||
TimePreempt += StartTime - slider.StartTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
localCursorContainer?.Expire();
|
||||
localCursorContainer = null;
|
||||
GameplayCursor?.ActiveCursor?.Show();
|
||||
GameplayCursor?.ActiveCursor.Show();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true;
|
||||
|
@ -49,5 +49,31 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(mod3, Is.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModWithMultipleSettings()
|
||||
{
|
||||
var ruleset = new OsuRuleset();
|
||||
|
||||
var mod1 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 0 } };
|
||||
var mod2 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } };
|
||||
var mod3 = new OsuModDifficultyAdjust { OverallDifficulty = { Value = 10 }, CircleSize = { Value = 6 } };
|
||||
|
||||
var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
|
||||
var doubleConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
|
||||
var doubleConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
|
||||
|
||||
Assert.That(mod1, Is.Not.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doubleConvertedMod2));
|
||||
|
||||
Assert.That(mod2, Is.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod2));
|
||||
|
||||
Assert.That(mod2, Is.EqualTo(mod3));
|
||||
Assert.That(doubleConvertedMod2, Is.EqualTo(doubleConvertedMod3));
|
||||
|
||||
Assert.That(mod3, Is.EqualTo(mod2));
|
||||
Assert.That(doubleConvertedMod3, Is.EqualTo(doubleConvertedMod2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
|
||||
@ -10,7 +13,7 @@ namespace osu.Game.Tests.Mods
|
||||
public class ModSettingsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestModSettingsUnboundWhenCopied()
|
||||
public void TestModSettingsUnboundWhenCloned()
|
||||
{
|
||||
var original = new OsuModDoubleTime();
|
||||
var copy = (OsuModDoubleTime)original.DeepClone();
|
||||
@ -22,7 +25,7 @@ namespace osu.Game.Tests.Mods
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiModSettingsUnboundWhenCopied()
|
||||
public void TestMultiModSettingsUnboundWhenCloned()
|
||||
{
|
||||
var original = new MultiMod(new OsuModDoubleTime());
|
||||
var copy = (MultiMod)original.DeepClone();
|
||||
@ -32,5 +35,67 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value, Is.EqualTo(2.0));
|
||||
Assert.That(((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value, Is.EqualTo(1.5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDifferentTypeSettingsKeptWhenCopied()
|
||||
{
|
||||
const double setting_change = 50.4;
|
||||
|
||||
var modDouble = new TestNonMatchingSettingTypeModDouble { TestSetting = { Value = setting_change } };
|
||||
var modBool = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } };
|
||||
var modInt = new TestNonMatchingSettingTypeModInt { TestSetting = { Value = (int)setting_change / 2 } };
|
||||
|
||||
modDouble.CopyCommonSettingsFrom(modBool);
|
||||
modDouble.CopyCommonSettingsFrom(modInt);
|
||||
modBool.CopyCommonSettingsFrom(modDouble);
|
||||
modBool.CopyCommonSettingsFrom(modInt);
|
||||
modInt.CopyCommonSettingsFrom(modDouble);
|
||||
modInt.CopyCommonSettingsFrom(modBool);
|
||||
|
||||
Assert.That(modDouble.TestSetting.Value, Is.EqualTo(setting_change));
|
||||
Assert.That(modBool.TestSetting.Value, Is.EqualTo(true));
|
||||
Assert.That(modInt.TestSetting.Value, Is.EqualTo((int)setting_change / 2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDefaultValueKeptWhenCopied()
|
||||
{
|
||||
var modBoolTrue = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = true, Value = false } };
|
||||
var modBoolFalse = new TestNonMatchingSettingTypeModBool { TestSetting = { Default = false, Value = true } };
|
||||
|
||||
modBoolFalse.CopyCommonSettingsFrom(modBoolTrue);
|
||||
|
||||
Assert.That(modBoolFalse.TestSetting.Default, Is.EqualTo(false));
|
||||
Assert.That(modBoolFalse.TestSetting.Value, Is.EqualTo(modBoolTrue.TestSetting.Value));
|
||||
}
|
||||
|
||||
private class TestNonMatchingSettingTypeModDouble : TestNonMatchingSettingTypeMod
|
||||
{
|
||||
public override string Acronym => "NMD";
|
||||
public override BindableNumber<double> TestSetting { get; } = new BindableDouble();
|
||||
}
|
||||
|
||||
private class TestNonMatchingSettingTypeModInt : TestNonMatchingSettingTypeMod
|
||||
{
|
||||
public override string Acronym => "NMI";
|
||||
public override BindableNumber<int> TestSetting { get; } = new BindableInt();
|
||||
}
|
||||
|
||||
private class TestNonMatchingSettingTypeModBool : TestNonMatchingSettingTypeMod
|
||||
{
|
||||
public override string Acronym => "NMB";
|
||||
public override Bindable<bool> TestSetting { get; } = new BindableBool();
|
||||
}
|
||||
|
||||
private abstract class TestNonMatchingSettingTypeMod : Mod
|
||||
{
|
||||
public override string Name => "Non-matching setting type mod";
|
||||
public override LocalisableString Description => "Description";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
[SettingSource("Test setting")]
|
||||
public abstract IBindable TestSetting { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,15 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
@ -182,6 +185,64 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("all boxes still selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUndoEditHistory()
|
||||
{
|
||||
SkinComponentsContainer firstTarget = null!;
|
||||
TestSkinEditorChangeHandler changeHandler = null!;
|
||||
byte[] defaultState = null!;
|
||||
IEnumerable<ISerialisableDrawable> testComponents = null!;
|
||||
|
||||
AddStep("Load necessary things", () =>
|
||||
{
|
||||
firstTarget = Player.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
changeHandler = new TestSkinEditorChangeHandler(firstTarget);
|
||||
|
||||
changeHandler.SaveState();
|
||||
defaultState = changeHandler.GetCurrentState();
|
||||
|
||||
testComponents = new[]
|
||||
{
|
||||
targetContainer.Components.First(),
|
||||
targetContainer.Components[targetContainer.Components.Count / 2],
|
||||
targetContainer.Components.Last()
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("Press undo", () => InputManager.Keys(PlatformAction.Undo));
|
||||
AddAssert("Nothing changed", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
|
||||
|
||||
AddStep("Add components", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
AddStep("Move components", () =>
|
||||
{
|
||||
changeHandler.BeginChange();
|
||||
testComponents.ForEach(c => ((Drawable)c).Position += Vector2.One);
|
||||
changeHandler.EndChange();
|
||||
});
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
AddStep("Select components", () => skinEditor.SelectedComponents.AddRange(testComponents));
|
||||
AddStep("Bring to front", () => skinEditor.BringSelectionToFront());
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
AddStep("Remove components", () => testComponents.ForEach(c => firstTarget.Remove(c, false)));
|
||||
revertAndCheckUnchanged();
|
||||
|
||||
void revertAndCheckUnchanged()
|
||||
{
|
||||
AddStep("Revert changes", () => changeHandler.RestoreState(int.MinValue));
|
||||
AddAssert("Current state is same as default", () => defaultState.SequenceEqual(changeHandler.GetCurrentState()));
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestBringToFront(bool alterSelectionOrder)
|
||||
@ -269,5 +330,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler
|
||||
{
|
||||
public TestSkinEditorChangeHandler(Drawable targetScreen)
|
||||
: base(targetScreen)
|
||||
{
|
||||
}
|
||||
|
||||
public byte[] GetCurrentState()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
|
||||
WriteCurrentStateToStream(stream);
|
||||
byte[] newState = stream.ToArray();
|
||||
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,12 @@ using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
@ -30,6 +32,7 @@ using osu.Game.Overlays.Chat.Listing;
|
||||
using osu.Game.Overlays.Chat.ChannelList;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@ -53,6 +56,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
private int currentMessageId;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@ -576,6 +582,75 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChatReport()
|
||||
{
|
||||
ChatReportRequest request = null;
|
||||
|
||||
AddStep("Show overlay with channel", () =>
|
||||
{
|
||||
chatOverlay.Show();
|
||||
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
|
||||
});
|
||||
|
||||
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
||||
waitForChannel1Visible();
|
||||
|
||||
AddStep("Setup request handling", () =>
|
||||
{
|
||||
requestLock.Reset();
|
||||
|
||||
dummyAPI.HandleRequest = r =>
|
||||
{
|
||||
if (!(r is ChatReportRequest req))
|
||||
return false;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
request = req;
|
||||
requestLock.Wait(10000);
|
||||
req.TriggerSuccess();
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("Show report popover", () => this.ChildrenOfType<ChatLine>().First().ShowPopover());
|
||||
|
||||
AddStep("Set report reason to other", () =>
|
||||
{
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<ChatReportReason>>().Single();
|
||||
reason.Current.Value = ChatReportReason.Other;
|
||||
});
|
||||
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
var btn = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<RoundedButton>().Single();
|
||||
InputManager.MoveMouseTo(btn);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportChatPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
|
||||
field.Current.Value = "test other";
|
||||
});
|
||||
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
var btn = this.ChildrenOfType<ReportChatPopover>().Single().ChildrenOfType<RoundedButton>().Single();
|
||||
InputManager.MoveMouseTo(btn);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Overlay closed", () => !this.ChildrenOfType<ReportChatPopover>().Any());
|
||||
AddStep("Complete request", () => requestLock.Set());
|
||||
AddUntilStep("Request sent", () => request != null);
|
||||
AddUntilStep("Info message displayed", () => channelManager.CurrentChannel.Value.Messages.Last(), () => Is.InstanceOf(typeof(InfoMessage)));
|
||||
}
|
||||
|
||||
private void joinTestChannel(int i)
|
||||
{
|
||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||
|
@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click delete", () =>
|
||||
{
|
||||
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().Single();
|
||||
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(1);
|
||||
InputManager.MoveMouseTo(deleteItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
@ -261,6 +261,137 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("preset soft-deleted", () => Realm.Run(r => r.All<ModPreset>().Count(preset => preset.DeletePending) == 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditPresetName()
|
||||
{
|
||||
ModPresetColumn modPresetColumn = null!;
|
||||
string presetName = null!;
|
||||
ModPresetPanel panel = null!;
|
||||
|
||||
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
|
||||
AddStep("right click first panel", () =>
|
||||
{
|
||||
panel = this.ChildrenOfType<ModPresetPanel>().First();
|
||||
presetName = panel.Preset.Value.Name;
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click edit", () =>
|
||||
{
|
||||
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
|
||||
InputManager.MoveMouseTo(editItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
OsuPopover? popover = null;
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
|
||||
AddStep("clear preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "");
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("preset is not changed", () => panel.Preset.Value.Name == presetName);
|
||||
AddUntilStep("popover is unchanged", () => this.ChildrenOfType<OsuPopover>().FirstOrDefault() == popover);
|
||||
AddStep("edit preset name", () => popover.ChildrenOfType<LabelledTextBox>().First().Current.Value = "something new");
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
|
||||
AddAssert("preset is changed", () => panel.Preset.Value.Name != presetName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditPresetMod()
|
||||
{
|
||||
ModPresetColumn modPresetColumn = null!;
|
||||
var mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() };
|
||||
List<Mod> previousMod = null!;
|
||||
|
||||
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
|
||||
|
||||
AddStep("right click first panel", () =>
|
||||
{
|
||||
var panel = this.ChildrenOfType<ModPresetPanel>().First();
|
||||
previousMod = panel.Preset.Value.Mods.ToList();
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click edit", () =>
|
||||
{
|
||||
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
|
||||
InputManager.MoveMouseTo(editItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
OsuPopover? popover = null;
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
|
||||
AddStep("click use current mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("preset mod not changed", () =>
|
||||
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(previousMod));
|
||||
|
||||
AddStep("select mods", () => SelectedMods.Value = mods);
|
||||
AddStep("right click first panel", () =>
|
||||
{
|
||||
var panel = this.ChildrenOfType<ModPresetPanel>().First();
|
||||
InputManager.MoveMouseTo(panel);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
|
||||
AddStep("click edit", () =>
|
||||
{
|
||||
var editItem = this.ChildrenOfType<DrawableOsuMenuItem>().ElementAt(0);
|
||||
InputManager.MoveMouseTo(editItem);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<OsuPopover>().FirstOrDefault()) != null);
|
||||
AddStep("click use current mods", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(0));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("attempt preset edit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(popover.ChildrenOfType<ShearedButton>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("preset mod is changed", () =>
|
||||
new HashSet<Mod>(this.ChildrenOfType<ModPresetPanel>().First().Preset.Value.Mods).SetEquals(mods));
|
||||
}
|
||||
|
||||
private ICollection<ModPreset> createTestPresets() => new[]
|
||||
{
|
||||
new ModPreset
|
||||
|
@ -22,6 +22,7 @@ using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Tests.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -385,6 +386,50 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("no mod selected", () => SelectedMods.Value.Count == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeepSharedSettingsFromSimilarMods()
|
||||
{
|
||||
const float setting_change = 1.2f;
|
||||
|
||||
createScreen();
|
||||
changeRuleset(0);
|
||||
|
||||
AddStep("select difficulty adjust mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod<ModDifficultyAdjust>()! });
|
||||
|
||||
changeRuleset(0);
|
||||
AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModDifficultyAdjust);
|
||||
|
||||
AddStep("change mod settings", () =>
|
||||
{
|
||||
var osuMod = getSelectedMod<OsuModDifficultyAdjust>();
|
||||
|
||||
osuMod.ExtendedLimits.Value = true;
|
||||
osuMod.CircleSize.Value = setting_change;
|
||||
osuMod.DrainRate.Value = setting_change;
|
||||
osuMod.OverallDifficulty.Value = setting_change;
|
||||
osuMod.ApproachRate.Value = setting_change;
|
||||
});
|
||||
|
||||
changeRuleset(1);
|
||||
AddAssert("taiko variant selected", () => SelectedMods.Value.SingleOrDefault() is TaikoModDifficultyAdjust);
|
||||
|
||||
AddAssert("shared settings preserved", () =>
|
||||
{
|
||||
var taikoMod = getSelectedMod<TaikoModDifficultyAdjust>();
|
||||
|
||||
return taikoMod.ExtendedLimits.Value &&
|
||||
taikoMod.DrainRate.Value == setting_change &&
|
||||
taikoMod.OverallDifficulty.Value == setting_change;
|
||||
});
|
||||
|
||||
AddAssert("non-shared settings remain default", () =>
|
||||
{
|
||||
var taikoMod = getSelectedMod<TaikoModDifficultyAdjust>();
|
||||
|
||||
return taikoMod.ScrollSpeed.IsDefault;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternallySetCustomizedMod()
|
||||
{
|
||||
@ -617,6 +662,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert($"customisation toggle is {(active ? "" : "not ")}active", () => modSelectOverlay.CustomisationButton.AsNonNull().Active.Value == active);
|
||||
}
|
||||
|
||||
private T getSelectedMod<T>() where T : Mod => SelectedMods.Value.OfType<T>().Single();
|
||||
|
||||
private ModPanel getPanelForMod(Type modType)
|
||||
=> modSelectOverlay.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
|
||||
|
||||
|
133
osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs
Normal file
133
osu.Game/Graphics/UserInterfaceV2/ReportPopover.cs
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
/// <summary>
|
||||
/// A generic popover for sending an online report about something.
|
||||
/// </summary>
|
||||
/// <typeparam name="TReportReason">An enumeration type with all valid reasons for the report.</typeparam>
|
||||
public abstract partial class ReportPopover<TReportReason> : OsuPopover
|
||||
where TReportReason : struct, Enum
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to run when the report is finalised.
|
||||
/// The arguments to this action are: the reason for the report, and an optional additional comment.
|
||||
/// </summary>
|
||||
public Action<TReportReason, string>? Action;
|
||||
|
||||
private OsuEnumDropdown<TReportReason> reasonDropdown = null!;
|
||||
private OsuTextBox commentsTextBox = null!;
|
||||
private RoundedButton submitButton = null!;
|
||||
|
||||
private readonly LocalisableString header;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ReportPopover{TReportReason}"/>.
|
||||
/// </summary>
|
||||
/// <param name="headerString">The text to display in the header of the popover.</param>
|
||||
protected ReportPopover(LocalisableString headerString)
|
||||
{
|
||||
header = headerString;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Child = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
Width = 500,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
Size = new Vector2(36),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = header,
|
||||
Font = OsuFont.Torus.With(size: 25),
|
||||
Margin = new MarginPadding { Bottom = 10 }
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportReason,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 40,
|
||||
Child = reasonDropdown = new OsuEnumDropdown<TReportReason>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportComments,
|
||||
},
|
||||
commentsTextBox = new OsuTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
PlaceholderText = UsersStrings.ReportPlaceholder,
|
||||
},
|
||||
submitButton = new RoundedButton
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Width = 200,
|
||||
BackgroundColour = colours.Red3,
|
||||
Text = UsersStrings.ReportActionsSend,
|
||||
Action = () =>
|
||||
{
|
||||
Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text);
|
||||
this.HidePopover();
|
||||
},
|
||||
Margin = new MarginPadding { Bottom = 5, Top = 10 },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
commentsTextBox.Current.BindValueChanged(_ => updateStatus());
|
||||
|
||||
reasonDropdown.Current.BindValueChanged(_ => updateStatus());
|
||||
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void updateStatus()
|
||||
{
|
||||
submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || !IsCommentRequired(reasonDropdown.Current.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an additional comment is required for submitting the report with the supplied <paramref name="reason"/>.
|
||||
/// </summary>
|
||||
protected virtual bool IsCommentRequired(TReportReason reason) => true;
|
||||
}
|
||||
}
|
@ -34,6 +34,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString AddPreset => new TranslatableString(getKey(@"add_preset"), @"Add preset");
|
||||
|
||||
/// <summary>
|
||||
/// "Use current mods"
|
||||
/// </summary>
|
||||
public static LocalisableString UseCurrentMods => new TranslatableString(getKey(@"use_current_mods"), @"Use current mods");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
38
osu.Game/Online/API/Requests/ChatReportRequest.cs
Normal file
38
osu.Game/Online/API/Requests/ChatReportRequest.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// 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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Overlays.Chat;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class ChatReportRequest : APIRequest
|
||||
{
|
||||
public readonly long? MessageId;
|
||||
public readonly ChatReportReason Reason;
|
||||
public readonly string Comment;
|
||||
|
||||
public ChatReportRequest(long? id, ChatReportReason reason, string comment)
|
||||
{
|
||||
MessageId = id;
|
||||
Reason = reason;
|
||||
Comment = comment;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
|
||||
req.AddParameter(@"reportable_type", @"message");
|
||||
req.AddParameter(@"reportable_id", $"{MessageId}");
|
||||
req.AddParameter(@"reason", Reason.ToString());
|
||||
req.AddParameter(@"comments", Comment);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => @"reports";
|
||||
}
|
||||
}
|
@ -58,7 +58,6 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
using File = System.IO.File;
|
||||
using RuntimeInfo = osu.Framework.RuntimeInfo;
|
||||
|
||||
namespace osu.Game
|
||||
@ -626,15 +625,22 @@ namespace osu.Game
|
||||
return;
|
||||
}
|
||||
|
||||
var previouslySelectedMods = SelectedMods.Value.ToArray();
|
||||
|
||||
if (!SelectedMods.Disabled)
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
|
||||
AvailableMods.Value = dict;
|
||||
|
||||
if (!SelectedMods.Disabled)
|
||||
SelectedMods.Value = previouslySelectedMods.Select(m => instance.CreateModFromAcronym(m.Acronym)).Where(m => m != null).ToArray();
|
||||
if (SelectedMods.Disabled)
|
||||
return;
|
||||
|
||||
var convertedMods = SelectedMods.Value.Select(mod =>
|
||||
{
|
||||
var newMod = instance.CreateModFromAcronym(mod.Acronym);
|
||||
newMod?.CopyCommonSettingsFrom(mod);
|
||||
return newMod;
|
||||
}).Where(newMod => newMod != null).ToList();
|
||||
|
||||
if (!ModUtils.CheckValidForGameplay(convertedMods, out var invalid))
|
||||
invalid.ForEach(newMod => convertedMods.Remove(newMod));
|
||||
|
||||
SelectedMods.Value = convertedMods;
|
||||
|
||||
void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
|
||||
}
|
||||
|
@ -1,25 +1,28 @@
|
||||
// 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.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public partial class ChatLine : CompositeDrawable
|
||||
public partial class ChatLine : CompositeDrawable, IHasPopover
|
||||
{
|
||||
private Message message = null!;
|
||||
|
||||
@ -55,7 +58,7 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
private readonly OsuSpriteText drawableTimestamp;
|
||||
|
||||
private readonly DrawableUsername drawableUsername;
|
||||
private readonly DrawableChatUsername drawableUsername;
|
||||
|
||||
private readonly LinkFlowContainer drawableContentFlow;
|
||||
|
||||
@ -92,7 +95,7 @@ namespace osu.Game.Overlays.Chat
|
||||
Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
drawableUsername = new DrawableUsername(message.Sender)
|
||||
drawableUsername = new DrawableChatUsername(message.Sender)
|
||||
{
|
||||
Width = UsernameWidth,
|
||||
FontSize = FontSize,
|
||||
@ -100,6 +103,7 @@ namespace osu.Game.Overlays.Chat
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
Margin = new MarginPadding { Horizontal = Spacing },
|
||||
ReportRequested = this.ShowPopover,
|
||||
},
|
||||
drawableContentFlow = new LinkFlowContainer(styleMessageContent)
|
||||
{
|
||||
@ -128,6 +132,8 @@ namespace osu.Game.Overlays.Chat
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new ReportChatPopover(message);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a highlight animation on this <see cref="ChatLine"/>.
|
||||
/// </summary>
|
||||
|
37
osu.Game/Overlays/Chat/ChatReportReason.cs
Normal file
37
osu.Game/Overlays/Chat/ChatReportReason.cs
Normal file
@ -0,0 +1,37 @@
|
||||
// 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.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
/// <remarks>
|
||||
/// References:
|
||||
/// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L50
|
||||
/// https://github.com/ppy/osu-web/blob/0a41b13acf5f47bb0d2b08bab42a9646b7ab5821/app/Models/UserReport.php#L39
|
||||
/// </remarks>
|
||||
public enum ChatReportReason
|
||||
{
|
||||
[Description("Insulting People")]
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsInsults))]
|
||||
Insults,
|
||||
|
||||
[Description("Spam")]
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsSpam))]
|
||||
Spam,
|
||||
|
||||
[Description("Unwanted Content")]
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))]
|
||||
UnwantedContent,
|
||||
|
||||
[Description("Nonsense")]
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsNonsense))]
|
||||
Nonsense,
|
||||
|
||||
[Description("Other")]
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsOther))]
|
||||
Other
|
||||
}
|
||||
}
|
@ -29,8 +29,10 @@ using ChatStrings = osu.Game.Localisation.ChatStrings;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public partial class DrawableUsername : OsuClickableContainer, IHasContextMenu
|
||||
public partial class DrawableChatUsername : OsuClickableContainer, IHasContextMenu
|
||||
{
|
||||
public Action? ReportRequested;
|
||||
|
||||
public Color4 AccentColour { get; }
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
@ -75,7 +77,7 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
private readonly Drawable colouredDrawable;
|
||||
|
||||
public DrawableUsername(APIUser user)
|
||||
public DrawableChatUsername(APIUser user)
|
||||
{
|
||||
this.user = user;
|
||||
|
||||
@ -169,6 +171,9 @@ namespace osu.Game.Overlays.Chat
|
||||
}));
|
||||
}
|
||||
|
||||
if (!user.Equals(api.LocalUser.Value))
|
||||
items.Add(new OsuMenuItem("Report", MenuItemType.Destructive, ReportRequested));
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
41
osu.Game/Overlays/Chat/ReportChatPopover.cs
Normal file
41
osu.Game/Overlays/Chat/ReportChatPopover.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public partial class ReportChatPopover : ReportPopover<ChatReportReason>
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ChannelManager channelManager { get; set; } = null!;
|
||||
|
||||
private readonly Message message;
|
||||
|
||||
public ReportChatPopover(Message message)
|
||||
: base(ReportStrings.UserTitle(message.Sender?.Username ?? @"Someone"))
|
||||
{
|
||||
this.message = message;
|
||||
Action = report;
|
||||
}
|
||||
|
||||
protected override bool IsCommentRequired(ChatReportReason reason) => reason == ChatReportReason.Other;
|
||||
|
||||
private void report(ChatReportReason reason, string comments)
|
||||
{
|
||||
var request = new ChatReportRequest(message.Id, reason, comments);
|
||||
|
||||
request.Success += () => channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(UsersStrings.ReportThanks.ToString()));
|
||||
|
||||
api.Queue(request);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@ -134,9 +135,13 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
currentChannelContainer = new Container<DrawableChannel>
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = currentChannelContainer = new Container<DrawableChannel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
loading = new LoadingLayer(true),
|
||||
channelListing = new ChannelListing
|
||||
|
@ -57,6 +57,11 @@ namespace osu.Game.Overlays.Comments
|
||||
link.AddLink(ReportStrings.CommentButton.ToLower(), this.ShowPopover);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new ReportCommentPopover(comment)
|
||||
{
|
||||
Action = report
|
||||
};
|
||||
|
||||
private void report(CommentReportReason reason, string comments)
|
||||
{
|
||||
var request = new CommentReportRequest(comment.Id, reason, comments);
|
||||
@ -83,10 +88,5 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
api.Queue(request);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new ReportCommentPopover(comment)
|
||||
{
|
||||
Action = report
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,111 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
public partial class ReportCommentPopover : OsuPopover
|
||||
public partial class ReportCommentPopover : ReportPopover<CommentReportReason>
|
||||
{
|
||||
public Action<CommentReportReason, string>? Action;
|
||||
|
||||
private readonly Comment? comment;
|
||||
|
||||
private OsuEnumDropdown<CommentReportReason> reasonDropdown = null!;
|
||||
private OsuTextBox commentsTextBox = null!;
|
||||
private RoundedButton submitButton = null!;
|
||||
|
||||
public ReportCommentPopover(Comment? comment)
|
||||
: base(ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"))
|
||||
{
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Child = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
Width = 500,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
Size = new Vector2(36),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"),
|
||||
Font = OsuFont.Torus.With(size: 25),
|
||||
Margin = new MarginPadding { Bottom = 10 }
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportReason,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 40,
|
||||
Child = reasonDropdown = new OsuEnumDropdown<CommentReportReason>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportComments,
|
||||
},
|
||||
commentsTextBox = new OsuTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
PlaceholderText = UsersStrings.ReportPlaceholder,
|
||||
},
|
||||
submitButton = new RoundedButton
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Width = 200,
|
||||
BackgroundColour = colours.Red3,
|
||||
Text = UsersStrings.ReportActionsSend,
|
||||
Action = () =>
|
||||
{
|
||||
Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text);
|
||||
this.HidePopover();
|
||||
},
|
||||
Margin = new MarginPadding { Bottom = 5, Top = 10 },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
commentsTextBox.Current.BindValueChanged(e =>
|
||||
{
|
||||
submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
@ -67,7 +66,7 @@ namespace osu.Game.Overlays.Mods
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = ModSelectOverlayStrings.AddPreset,
|
||||
Action = tryCreatePreset
|
||||
Action = createPreset
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -89,16 +88,15 @@ namespace osu.Game.Overlays.Mods
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
|
||||
|
||||
nameTextBox.Current.BindValueChanged(s =>
|
||||
{
|
||||
createButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue);
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void tryCreatePreset()
|
||||
private void createPreset()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(nameTextBox.Current.Value))
|
||||
{
|
||||
Body.Shake();
|
||||
return;
|
||||
}
|
||||
|
||||
realm.Write(r => r.Add(new ModPreset
|
||||
{
|
||||
Name = nameTextBox.Current.Value,
|
||||
|
172
osu.Game/Overlays/Mods/EditPresetPopover.cs
Normal file
172
osu.Game/Overlays/Mods/EditPresetPopover.cs
Normal file
@ -0,0 +1,172 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
internal partial class EditPresetPopover : OsuPopover
|
||||
{
|
||||
private LabelledTextBox nameTextBox = null!;
|
||||
private LabelledTextBox descriptionTextBox = null!;
|
||||
private ShearedButton useCurrentModsButton = null!;
|
||||
private ShearedButton saveButton = null!;
|
||||
private FillFlowContainer scrollContent = null!;
|
||||
|
||||
private readonly Live<ModPreset> preset;
|
||||
|
||||
private HashSet<Mod> saveableMods;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public EditPresetPopover(Live<ModPreset> preset)
|
||||
{
|
||||
this.preset = preset;
|
||||
saveableMods = preset.PerformRead(p => p.Mods).ToHashSet();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 300,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
nameTextBox = new LabelledTextBox
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Label = CommonStrings.Name,
|
||||
TabbableContentContainer = this,
|
||||
Current = { Value = preset.PerformRead(p => p.Name) },
|
||||
},
|
||||
descriptionTextBox = new LabelledTextBox
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Label = CommonStrings.Description,
|
||||
TabbableContentContainer = this,
|
||||
Current = { Value = preset.PerformRead(p => p.Description) },
|
||||
},
|
||||
new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 100,
|
||||
Padding = new MarginPadding(7),
|
||||
Child = scrollContent = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding(7),
|
||||
Spacing = new Vector2(7),
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
useCurrentModsButton = new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = ModSelectOverlayStrings.UseCurrentMods,
|
||||
DarkerColour = colours.Blue1,
|
||||
LighterColour = colours.Blue0,
|
||||
TextColour = colourProvider.Background6,
|
||||
Action = useCurrentMods,
|
||||
},
|
||||
saveButton = new ShearedButton
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = Resources.Localisation.Web.CommonStrings.ButtonsSave,
|
||||
DarkerColour = colours.Orange1,
|
||||
LighterColour = colours.Orange0,
|
||||
TextColour = colourProvider.Background6,
|
||||
Action = save,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Body.BorderThickness = 3;
|
||||
Body.BorderColour = colours.Orange1;
|
||||
|
||||
selectedMods.BindValueChanged(_ => updateState(), true);
|
||||
nameTextBox.Current.BindValueChanged(s =>
|
||||
{
|
||||
saveButton.Enabled.Value = !string.IsNullOrWhiteSpace(s.NewValue);
|
||||
}, true);
|
||||
}
|
||||
|
||||
private void useCurrentMods()
|
||||
{
|
||||
saveableMods = selectedMods.Value.ToHashSet();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
scrollContent.ChildrenEnumerable = saveableMods.Select(mod => new ModPresetRow(mod));
|
||||
useCurrentModsButton.Enabled.Value = checkSelectedModsDiffersFromSaved();
|
||||
}
|
||||
|
||||
private bool checkSelectedModsDiffersFromSaved()
|
||||
{
|
||||
if (!selectedMods.Value.Any())
|
||||
return false;
|
||||
|
||||
return !saveableMods.SetEquals(selectedMods.Value);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
|
||||
}
|
||||
|
||||
private void save()
|
||||
{
|
||||
preset.PerformWrite(s =>
|
||||
{
|
||||
s.Name = nameTextBox.Current.Value;
|
||||
s.Description = descriptionTextBox.Current.Value;
|
||||
s.Mods = saveableMods;
|
||||
});
|
||||
|
||||
this.HidePopover();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
@ -17,7 +18,7 @@ using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>, IHasContextMenu
|
||||
public partial class ModPresetPanel : ModSelectPanel, IHasCustomTooltip<ModPreset>, IHasContextMenu, IHasPopover
|
||||
{
|
||||
public readonly Live<ModPreset> Preset;
|
||||
|
||||
@ -91,7 +92,8 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
public MenuItem[] ContextMenuItems => new MenuItem[]
|
||||
{
|
||||
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset)))
|
||||
new OsuMenuItem(CommonStrings.ButtonsEdit, MenuItemType.Highlighted, this.ShowPopover),
|
||||
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new DeleteModPresetDialog(Preset))),
|
||||
};
|
||||
|
||||
#endregion
|
||||
@ -102,5 +104,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
settingChangeTracker?.Dispose();
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new EditPresetPopover(Preset);
|
||||
}
|
||||
}
|
||||
|
64
osu.Game/Overlays/Mods/ModPresetRow.cs
Normal file
64
osu.Game/Overlays/Mods/ModPresetRow.cs
Normal file
@ -0,0 +1,64 @@
|
||||
// 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;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class ModPresetRow : FillFlowContainer
|
||||
{
|
||||
public ModPresetRow(Mod mod)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Vertical;
|
||||
Spacing = new Vector2(4);
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ModSwitchTiny(mod)
|
||||
{
|
||||
Active = { Value = true },
|
||||
Scale = new Vector2(0.6f),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = mod.Name,
|
||||
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Bottom = 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(mod.SettingDescription))
|
||||
{
|
||||
AddInternal(new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Left = 14 },
|
||||
Text = mod.SettingDescription
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,11 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
@ -61,55 +57,5 @@ namespace osu.Game.Overlays.Mods
|
||||
protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint);
|
||||
|
||||
public void Move(Vector2 pos) => Position = pos;
|
||||
|
||||
private partial class ModPresetRow : FillFlowContainer
|
||||
{
|
||||
public ModPresetRow(Mod mod)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Vertical;
|
||||
Spacing = new Vector2(4);
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(7),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ModSwitchTiny(mod)
|
||||
{
|
||||
Active = { Value = true },
|
||||
Scale = new Vector2(0.6f),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = mod.Name,
|
||||
Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Bottom = 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(mod.SettingDescription))
|
||||
{
|
||||
AddInternal(new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Left = 14 },
|
||||
Text = mod.SettingDescription
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -56,20 +57,53 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (deserializedContent == null)
|
||||
return;
|
||||
|
||||
SerialisedDrawableInfo[] skinnableInfo = deserializedContent.ToArray();
|
||||
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray();
|
||||
SerialisedDrawableInfo[] skinnableInfos = deserializedContent.ToArray();
|
||||
ISerialisableDrawable[] targetComponents = firstTarget.Components.ToArray();
|
||||
|
||||
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
|
||||
// Store components based on type for later reuse
|
||||
var componentsPerTypeLookup = new Dictionary<Type, Queue<Drawable>>();
|
||||
|
||||
foreach (ISerialisableDrawable component in targetComponents)
|
||||
{
|
||||
// Perform a naive full reload for now.
|
||||
firstTarget.Reload(skinnableInfo);
|
||||
Type lookup = component.GetType();
|
||||
|
||||
if (!componentsPerTypeLookup.TryGetValue(lookup, out Queue<Drawable>? componentsOfSameType))
|
||||
componentsPerTypeLookup.Add(lookup, componentsOfSameType = new Queue<Drawable>());
|
||||
|
||||
componentsOfSameType.Enqueue((Drawable)component);
|
||||
}
|
||||
else
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
foreach (var drawable in targetComponents)
|
||||
drawable.ApplySerialisedInfo(skinnableInfo[i++]);
|
||||
for (int i = targetComponents.Length - 1; i >= 0; i--)
|
||||
firstTarget.Remove(targetComponents[i], false);
|
||||
|
||||
foreach (var skinnableInfo in skinnableInfos)
|
||||
{
|
||||
Type lookup = skinnableInfo.Type;
|
||||
|
||||
if (!componentsPerTypeLookup.TryGetValue(lookup, out Queue<Drawable>? componentsOfSameType))
|
||||
{
|
||||
firstTarget.Add((ISerialisableDrawable)skinnableInfo.CreateInstance());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wherever possible, attempt to reuse existing component instances.
|
||||
if (componentsOfSameType.TryDequeue(out Drawable? component))
|
||||
{
|
||||
component.ApplySerialisedInfo(skinnableInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
component = skinnableInfo.CreateInstance();
|
||||
}
|
||||
|
||||
firstTarget.Add((ISerialisableDrawable)component);
|
||||
}
|
||||
|
||||
// Dispose components which were not reused.
|
||||
foreach ((Type _, Queue<Drawable> typeComponents) in componentsPerTypeLookup)
|
||||
{
|
||||
foreach (var component in typeComponents)
|
||||
component.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,146 +0,0 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
internal partial class HitObjectInspector : CompositeDrawable
|
||||
{
|
||||
private OsuTextFlowContainer inspectorText = null!;
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
InternalChild = inspectorText = new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
|
||||
EditorBeatmap.TransactionBegan += updateInspectorText;
|
||||
EditorBeatmap.TransactionEnded += updateInspectorText;
|
||||
updateInspectorText();
|
||||
}
|
||||
|
||||
private ScheduledDelegate? rollingTextUpdate;
|
||||
|
||||
private void updateInspectorText()
|
||||
{
|
||||
inspectorText.Clear();
|
||||
rollingTextUpdate?.Cancel();
|
||||
rollingTextUpdate = null;
|
||||
|
||||
switch (EditorBeatmap.SelectedHitObjects.Count)
|
||||
{
|
||||
case 0:
|
||||
addValue("No selection");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
var selected = EditorBeatmap.SelectedHitObjects.Single();
|
||||
|
||||
addHeader("Type");
|
||||
addValue($"{selected.GetType().ReadableName()}");
|
||||
|
||||
addHeader("Time");
|
||||
addValue($"{selected.StartTime:#,0.##}ms");
|
||||
|
||||
switch (selected)
|
||||
{
|
||||
case IHasPosition pos:
|
||||
addHeader("Position");
|
||||
addValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
|
||||
break;
|
||||
|
||||
case IHasXPosition x:
|
||||
addHeader("Position");
|
||||
|
||||
addValue($"x:{x.X:#,0.##} ");
|
||||
break;
|
||||
|
||||
case IHasYPosition y:
|
||||
addHeader("Position");
|
||||
|
||||
addValue($"y:{y.Y:#,0.##}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (selected is IHasDistance distance)
|
||||
{
|
||||
addHeader("Distance");
|
||||
addValue($"{distance.Distance:#,0.##}px");
|
||||
}
|
||||
|
||||
if (selected is IHasRepeats repeats)
|
||||
{
|
||||
addHeader("Repeats");
|
||||
addValue($"{repeats.RepeatCount:#,0.##}");
|
||||
}
|
||||
|
||||
if (selected is IHasDuration duration)
|
||||
{
|
||||
addHeader("End Time");
|
||||
addValue($"{duration.EndTime:#,0.##}ms");
|
||||
addHeader("Duration");
|
||||
addValue($"{duration.Duration:#,0.##}ms");
|
||||
}
|
||||
|
||||
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
|
||||
// This is a good middle-ground for the time being.
|
||||
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
|
||||
break;
|
||||
|
||||
default:
|
||||
addHeader("Selected Objects");
|
||||
addValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
|
||||
|
||||
addHeader("Start Time");
|
||||
addValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
|
||||
|
||||
addHeader("End Time");
|
||||
addValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
|
||||
break;
|
||||
}
|
||||
|
||||
void addHeader(string header) => inspectorText.AddParagraph($"{header}: ", s =>
|
||||
{
|
||||
s.Padding = new MarginPadding { Top = 2 };
|
||||
s.Font = s.Font.With(size: 12);
|
||||
s.Colour = colourProvider.Content2;
|
||||
});
|
||||
|
||||
void addValue(string value) => inspectorText.AddParagraph(value, s =>
|
||||
{
|
||||
s.Font = s.Font.With(weight: FontWeight.SemiBold);
|
||||
s.Colour = colourProvider.Content1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@ -113,21 +114,29 @@ namespace osu.Game.Rulesets.Mods
|
||||
[JsonIgnore]
|
||||
public virtual Type[] IncompatibleMods => Array.Empty<Type>();
|
||||
|
||||
private IReadOnlyList<IBindable>? settingsBacking;
|
||||
private IReadOnlyDictionary<string, IBindable>? settingsBacking;
|
||||
|
||||
/// <summary>
|
||||
/// A list of the all <see cref="IBindable"/> settings within this mod.
|
||||
/// All <see cref="IBindable"/> settings within this mod.
|
||||
/// </summary>
|
||||
internal IReadOnlyList<IBindable> Settings =>
|
||||
/// <remarks>
|
||||
/// The settings are returned in ascending key order as per <see cref="SettingsMap"/>.
|
||||
/// The ordering is intentionally enforced manually, as ordering of <see cref="Dictionary{TKey,TValue}.Values"/> is unspecified.
|
||||
/// </remarks>
|
||||
internal IEnumerable<IBindable> SettingsBindables => SettingsMap.OrderBy(pair => pair.Key).Select(pair => pair.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Provides mapping of names to <see cref="IBindable"/>s of all settings within this mod.
|
||||
/// </summary>
|
||||
internal IReadOnlyDictionary<string, IBindable> SettingsMap =>
|
||||
settingsBacking ??= this.GetSettingsSourceProperties()
|
||||
.Select(p => p.Item2.GetValue(this))
|
||||
.Cast<IBindable>()
|
||||
.ToList();
|
||||
.Select(p => p.Item2)
|
||||
.ToDictionary(property => property.Name.ToSnakeCase(), property => (IBindable)property.GetValue(this)!);
|
||||
|
||||
/// <summary>
|
||||
/// Whether all settings in this mod are set to their default state.
|
||||
/// </summary>
|
||||
protected virtual bool UsesDefaultConfiguration => Settings.All(s => s.IsDefault);
|
||||
protected virtual bool UsesDefaultConfiguration => SettingsBindables.All(s => s.IsDefault);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of this <see cref="Mod"/> initialised to a default state.
|
||||
@ -148,15 +157,53 @@ namespace osu.Game.Rulesets.Mods
|
||||
if (source.GetType() != GetType())
|
||||
throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source));
|
||||
|
||||
foreach (var (_, prop) in this.GetSettingsSourceProperties())
|
||||
foreach (var (_, property) in this.GetSettingsSourceProperties())
|
||||
{
|
||||
var targetBindable = (IBindable)prop.GetValue(this)!;
|
||||
var sourceBindable = (IBindable)prop.GetValue(source)!;
|
||||
var targetBindable = (IBindable)property.GetValue(this)!;
|
||||
var sourceBindable = (IBindable)property.GetValue(source)!;
|
||||
|
||||
CopyAdjustedSetting(targetBindable, sourceBindable);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method copies the values of all settings from <paramref name="source"/> that share the same names with this mod instance.
|
||||
/// The most frequent use of this is when switching rulesets, in order to preserve values of common settings during the switch.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The values are copied directly, without adjusting for possibly different allowed ranges of values.
|
||||
/// If the value of a setting is not valid for this instance due to not falling inside of the allowed range, it will be clamped accordingly.
|
||||
/// </remarks>
|
||||
/// <param name="source">The mod to extract settings from.</param>
|
||||
public void CopyCommonSettingsFrom(Mod source)
|
||||
{
|
||||
if (source.UsesDefaultConfiguration)
|
||||
return;
|
||||
|
||||
foreach (var (name, targetSetting) in SettingsMap)
|
||||
{
|
||||
if (!source.SettingsMap.TryGetValue(name, out IBindable? sourceSetting))
|
||||
continue;
|
||||
|
||||
if (sourceSetting.IsDefault)
|
||||
continue;
|
||||
|
||||
var targetBindableType = targetSetting.GetType();
|
||||
var sourceBindableType = sourceSetting.GetType();
|
||||
|
||||
// if either the target is assignable to the source or the source is assignable to the target,
|
||||
// then we presume that the data types contained in both bindables are compatible and we can proceed with the copy.
|
||||
// this handles cases like `Bindable<int>` and `BindableInt`.
|
||||
if (!targetBindableType.IsAssignableFrom(sourceBindableType) && !sourceBindableType.IsAssignableFrom(targetBindableType))
|
||||
continue;
|
||||
|
||||
// TODO: special case for handling number types
|
||||
|
||||
PropertyInfo property = targetSetting.GetType().GetProperty(nameof(Bindable<bool>.Value))!;
|
||||
property.SetValue(targetSetting, property.GetValue(sourceSetting));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When creating copies or clones of a Mod, this method will be called
|
||||
/// to copy explicitly adjusted user settings from <paramref name="target"/>.
|
||||
@ -191,7 +238,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
return GetType() == other.GetType() &&
|
||||
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
|
||||
SettingsBindables.SequenceEqual(other.SettingsBindables, ModSettingsEqualityComparer.Default);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
@ -200,7 +247,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
hashCode.Add(GetType());
|
||||
|
||||
foreach (var setting in Settings)
|
||||
foreach (var setting in SettingsBindables)
|
||||
hashCode.Add(setting.GetUnderlyingSettingValue());
|
||||
|
||||
return hashCode.ToHashCode();
|
||||
|
@ -3,17 +3,18 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
/// <summary>
|
||||
/// A HitObject that has a slider velocity multiplier.
|
||||
/// </summary>
|
||||
public interface IHasSliderVelocity
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// The slider velocity multiplier.
|
||||
/// A HitObject that has a slider velocity multiplier.
|
||||
/// </summary>
|
||||
double SliderVelocity { get; set; }
|
||||
public interface IHasSliderVelocity
|
||||
{
|
||||
/// <summary>
|
||||
/// The slider velocity multiplier.
|
||||
/// </summary>
|
||||
double SliderVelocity { get; set; }
|
||||
|
||||
BindableNumber<double> SliderVelocityBindable { get; }
|
||||
BindableNumber<double> SliderVelocityBindable { get; }
|
||||
}
|
||||
}
|
||||
|
49
osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs
Normal file
49
osu.Game/Screens/Edit/Compose/Components/EditorInspector.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
internal partial class EditorInspector : CompositeDrawable
|
||||
{
|
||||
protected OsuTextFlowContainer InspectorText = null!;
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
InternalChild = InspectorText = new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
};
|
||||
}
|
||||
|
||||
protected void AddHeader(string header) => InspectorText.AddParagraph($"{header}: ", s =>
|
||||
{
|
||||
s.Padding = new MarginPadding { Top = 2 };
|
||||
s.Font = s.Font.With(size: 12);
|
||||
s.Colour = colourProvider.Content2;
|
||||
});
|
||||
|
||||
protected void AddValue(string value) => InspectorText.AddParagraph(value, s =>
|
||||
{
|
||||
s.Font = s.Font.With(weight: FontWeight.SemiBold);
|
||||
s.Colour = colourProvider.Content1;
|
||||
});
|
||||
}
|
||||
}
|
111
osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs
Normal file
111
osu.Game/Screens/Edit/Compose/Components/HitObjectInspector.cs
Normal file
@ -0,0 +1,111 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
internal partial class HitObjectInspector : EditorInspector
|
||||
{
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
|
||||
EditorBeatmap.TransactionBegan += updateInspectorText;
|
||||
EditorBeatmap.TransactionEnded += updateInspectorText;
|
||||
updateInspectorText();
|
||||
}
|
||||
|
||||
private ScheduledDelegate? rollingTextUpdate;
|
||||
|
||||
private void updateInspectorText()
|
||||
{
|
||||
InspectorText.Clear();
|
||||
rollingTextUpdate?.Cancel();
|
||||
rollingTextUpdate = null;
|
||||
|
||||
switch (EditorBeatmap.SelectedHitObjects.Count)
|
||||
{
|
||||
case 0:
|
||||
AddValue("No selection");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
var selected = EditorBeatmap.SelectedHitObjects.Single();
|
||||
|
||||
AddHeader("Type");
|
||||
AddValue($"{selected.GetType().ReadableName()}");
|
||||
|
||||
AddHeader("Time");
|
||||
AddValue($"{selected.StartTime:#,0.##}ms");
|
||||
|
||||
switch (selected)
|
||||
{
|
||||
case IHasPosition pos:
|
||||
AddHeader("Position");
|
||||
AddValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
|
||||
break;
|
||||
|
||||
case IHasXPosition x:
|
||||
AddHeader("Position");
|
||||
|
||||
AddValue($"x:{x.X:#,0.##} ");
|
||||
break;
|
||||
|
||||
case IHasYPosition y:
|
||||
AddHeader("Position");
|
||||
|
||||
AddValue($"y:{y.Y:#,0.##}");
|
||||
break;
|
||||
}
|
||||
|
||||
if (selected is IHasDistance distance)
|
||||
{
|
||||
AddHeader("Distance");
|
||||
AddValue($"{distance.Distance:#,0.##}px");
|
||||
}
|
||||
|
||||
if (selected is IHasSliderVelocity sliderVelocity)
|
||||
{
|
||||
AddHeader("Slider Velocity");
|
||||
AddValue($"{sliderVelocity.SliderVelocity:#,0.00}x");
|
||||
}
|
||||
|
||||
if (selected is IHasRepeats repeats)
|
||||
{
|
||||
AddHeader("Repeats");
|
||||
AddValue($"{repeats.RepeatCount:#,0.##}");
|
||||
}
|
||||
|
||||
if (selected is IHasDuration duration)
|
||||
{
|
||||
AddHeader("End Time");
|
||||
AddValue($"{duration.EndTime:#,0.##}ms");
|
||||
AddHeader("Duration");
|
||||
AddValue($"{duration.Duration:#,0.##}ms");
|
||||
}
|
||||
|
||||
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
|
||||
// This is a good middle-ground for the time being.
|
||||
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
|
||||
break;
|
||||
|
||||
default:
|
||||
AddHeader("Selected Objects");
|
||||
AddValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
|
||||
|
||||
AddHeader("Start Time");
|
||||
AddValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
|
||||
|
||||
AddHeader("End Time");
|
||||
AddValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -95,7 +95,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Hold shift while dragging the end of an object to adjust velocity while snapping."
|
||||
}
|
||||
},
|
||||
new SliderVelocityInspector(),
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -105,7 +106,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray();
|
||||
|
||||
// even if there are multiple objects selected, we can still display a value if they all have the same value.
|
||||
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable : null;
|
||||
var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1
|
||||
? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable
|
||||
: null;
|
||||
|
||||
if (selectedPointBindable != null)
|
||||
{
|
||||
@ -139,4 +142,45 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class SliderVelocityInspector : EditorInspector
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
EditorBeatmap.TransactionBegan += updateInspectorText;
|
||||
EditorBeatmap.TransactionEnded += updateInspectorText;
|
||||
updateInspectorText();
|
||||
}
|
||||
|
||||
private void updateInspectorText()
|
||||
{
|
||||
InspectorText.Clear();
|
||||
|
||||
double[] sliderVelocities = EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>().Select(sv => sv.SliderVelocity).OrderBy(v => v).ToArray();
|
||||
|
||||
if (sliderVelocities.Length < 2)
|
||||
return;
|
||||
|
||||
double? modeSliderVelocity = sliderVelocities.GroupBy(v => v).MaxBy(v => v.Count())?.Key;
|
||||
double? medianSliderVelocity = sliderVelocities[sliderVelocities.Length / 2];
|
||||
|
||||
AddHeader("Average velocity");
|
||||
AddValue($"{medianSliderVelocity:#,0.00}x");
|
||||
|
||||
AddHeader("Most used velocity");
|
||||
AddValue($"{modeSliderVelocity:#,0.00}x");
|
||||
|
||||
AddHeader("Velocity range");
|
||||
AddValue($"{sliderVelocities.First():#,0.00}x - {sliderVelocities.Last():#,0.00}x");
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
EditorBeatmap.TransactionBegan -= updateInspectorText;
|
||||
EditorBeatmap.TransactionEnded -= updateInspectorText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ namespace osu.Game.Screens.Menu
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Colour = Color4.DarkBlue,
|
||||
Size = new Vector2(0.96f)
|
||||
Size = OsuLogo.SCALE_ADJUST,
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
|
@ -35,6 +35,12 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
private const double transition_length = 300;
|
||||
|
||||
/// <summary>
|
||||
/// The osu! logo sprite has a shadow included in its texture.
|
||||
/// This adjustment vector is used to match the precise edge of the border of the logo.
|
||||
/// </summary>
|
||||
public static readonly Vector2 SCALE_ADJUST = new Vector2(0.96f);
|
||||
|
||||
private readonly Sprite logo;
|
||||
private readonly CircularContainer logoContainer;
|
||||
private readonly Container logoBounceContainer;
|
||||
@ -150,7 +156,7 @@ namespace osu.Game.Screens.Menu
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Alpha = visualizer_default_alpha,
|
||||
Size = new Vector2(0.96f)
|
||||
Size = SCALE_ADJUST
|
||||
},
|
||||
new Container
|
||||
{
|
||||
@ -162,7 +168,7 @@ namespace osu.Game.Screens.Menu
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(0.88f),
|
||||
Scale = SCALE_ADJUST,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -406,7 +412,7 @@ namespace osu.Game.Screens.Menu
|
||||
public void Impact()
|
||||
{
|
||||
impactContainer.FadeOutFromOne(250, Easing.In);
|
||||
impactContainer.ScaleTo(0.96f);
|
||||
impactContainer.ScaleTo(SCALE_ADJUST);
|
||||
impactContainer.ScaleTo(1.12f, 250);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Screens.Play.Break
|
||||
set
|
||||
{
|
||||
icon.Size = value;
|
||||
base.Size = value + BlurSigma * 2.5f;
|
||||
base.Size = value + BlurSigma * 5;
|
||||
ForceRedraw();
|
||||
}
|
||||
get => base.Size;
|
||||
|
@ -36,7 +36,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.20.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.506.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.417.0" />
|
||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
|
@ -16,6 +16,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.418.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.506.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -277,6 +277,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Explicit</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/LOCAL_FUNCTION_BODY/@EntryValue">ExpressionBody</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">BlockBody</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/NAMESPACE_BODY/@EntryValue">BlockScoped</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_EVIDENT/@EntryValue">ExplicitlyTyped</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/USE_HEURISTICS_FOR_BODY_STYLE/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>
|
||||
|
Loading…
Reference in New Issue
Block a user