1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 22:57:42 +08:00

Merge branch 'master' into fix-spinner-rpm-user-rate-adjust

This commit is contained in:
Dean Herbert 2020-09-25 20:21:35 +09:00 committed by GitHub
commit b2261f03e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1255 additions and 293 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.923.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.925.0" />
</ItemGroup>
</Project>

View File

@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{

View File

@ -20,7 +20,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
{

View File

@ -23,7 +23,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneManiaHitObjectComposer : EditorClockTestScene
{

View File

@ -18,7 +18,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene
{

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene
{

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
{

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene
{

View File

@ -0,0 +1,90 @@
// 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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSceneObjectObjectSnap : TestSceneOsuEditor
{
private OsuPlayfield playfield;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("get playfield", () => playfield = Editor.ChildrenOfType<OsuPlayfield>().First());
}
[TestCase(true)]
[TestCase(false)]
public void TestHitCircleSnapsToOtherHitCircle(bool distanceSnapEnabled)
{
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
if (!distanceSnapEnabled)
AddStep("disable distance snap", () => InputManager.Key(Key.Q));
AddStep("enter placement mode", () => InputManager.Key(Key.Number2));
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
AddAssert("both objects at same location", () =>
{
var objects = EditorBeatmap.HitObjects;
var first = (OsuHitObject)objects.First();
var second = (OsuHitObject)objects.Last();
return first.Position == second.Position;
});
}
[Test]
public void TestHitCircleSnapsToSliderEnd()
{
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre));
AddStep("disable distance snap", () => InputManager.Key(Key.Q));
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
AddStep("start slider placement", () => InputManager.Click(MouseButton.Left));
AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0)));
AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
AddAssert("circle is at slider's end", () =>
{
var objects = EditorBeatmap.HitObjects;
var first = (Slider)objects.First();
var second = (OsuHitObject)objects.Last();
return Precision.AlmostEquals(first.EndPosition, second.Position);
});
}
}
}

View File

@ -19,7 +19,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene
{

View File

@ -4,10 +4,10 @@
using NUnit.Framework;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene
public class TestSceneOsuEditor : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
}

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestScenePathControlPointVisualiser : OsuTestScene
{

View File

@ -14,7 +14,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
{

View File

@ -16,7 +16,7 @@ using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene
{

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
{

View File

@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene
{

View File

@ -9,7 +9,9 @@ using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@ -17,6 +19,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -39,12 +42,12 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool()
};
private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" };
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
protected override IEnumerable<BindableBool> Toggles => new[]
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
distanceSnapToggle
};
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
private BindableList<HitObject> selectedHitObjects;
@ -94,6 +97,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
return snapResult;
// will be null if distance snap is disabled or not feasible for the current time value.
if (distanceSnapGrid == null)
return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
@ -102,13 +109,57 @@ namespace osu.Game.Rulesets.Osu.Edit
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
}
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
{
// check other on-screen objects for snapping/stacking
var blueprints = BlueprintContainer.SelectionBlueprints.AliveChildren;
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
float snapRadius =
playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X -
playfield.GamefieldToScreenSpace(Vector2.Zero).X;
foreach (var b in blueprints)
{
if (b.IsSelected)
continue;
var hitObject = (OsuHitObject)b.HitObject;
Vector2? snap = checkSnap(hitObject.Position);
if (snap == null && hitObject.Position != hitObject.EndPosition)
snap = checkSnap(hitObject.EndPosition);
if (snap != null)
{
// only return distance portion, since time is not really valid
snapResult = new SnapResult(snap.Value, null, playfield);
return true;
}
Vector2? checkSnap(Vector2 checkPos)
{
Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos);
if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius)
return checkScreenPos;
return null;
}
}
snapResult = null;
return false;
}
private void updateDistanceSnapGrid()
{
distanceSnapGridContainer.Clear();
distanceSnapGridCache.Invalidate();
distanceSnapGrid = null;
if (!distanceSnapToggle.Value)
if (distanceSnapToggle.Value != TernaryState.True)
return;
switch (BlueprintContainer.CurrentTool)

View File

@ -4,7 +4,7 @@
using NUnit.Framework;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
[TestFixture]
public class TestSceneEditor : EditorTestScene

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneTaikoHitObjectComposer : EditorClockTestScene
{

View File

@ -1,9 +1,10 @@
// 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@ -14,75 +15,80 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
public class TaikoSelectionHandler : SelectionHandler
{
private readonly Bindable<TernaryState> selectionRimState = new Bindable<TernaryState>();
private readonly Bindable<TernaryState> selectionStrongState = new Bindable<TernaryState>();
[BackgroundDependencyLoader]
private void load()
{
selectionStrongState.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
SetStrongState(false);
break;
case TernaryState.True:
SetStrongState(true);
break;
}
};
selectionRimState.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
SetRimState(false);
break;
case TernaryState.True:
SetRimState(true);
break;
}
};
}
public void SetStrongState(bool state)
{
var hits = SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
foreach (var h in hits)
h.IsStrong = state;
ChangeHandler.EndChange();
}
public void SetRimState(bool state)
{
var hits = SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
foreach (var h in hits)
h.Type = state ? HitType.Rim : HitType.Centre;
ChangeHandler.EndChange();
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
{
if (selection.All(s => s.HitObject is Hit))
{
var hits = selection.Select(s => s.HitObject).OfType<Hit>();
yield return new TernaryStateMenuItem("Rim", action: state =>
{
ChangeHandler.BeginChange();
foreach (var h in hits)
{
switch (state)
{
case TernaryState.True:
h.Type = HitType.Rim;
break;
case TernaryState.False:
h.Type = HitType.Centre;
break;
}
}
ChangeHandler.EndChange();
})
{
State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) }
};
}
yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } };
if (selection.All(s => s.HitObject is TaikoHitObject))
{
var hits = selection.Select(s => s.HitObject).OfType<TaikoHitObject>();
yield return new TernaryStateMenuItem("Strong", action: state =>
{
ChangeHandler.BeginChange();
foreach (var h in hits)
{
switch (state)
{
case TernaryState.True:
h.IsStrong = true;
break;
case TernaryState.False:
h.IsStrong = false;
break;
}
EditorBeatmap?.UpdateHitObject(h);
}
ChangeHandler.EndChange();
})
{
State = { Value = getTernaryState(hits, h => h.IsStrong) }
};
}
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
}
private TernaryState getTernaryState<T>(IEnumerable<T> selection, Func<T, bool> func)
protected override void UpdateTernaryStates()
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
base.UpdateTernaryStates();
return TernaryState.False;
selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
}
}
}

View File

@ -36,35 +36,64 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private bool pressHandledThisFrame;
private Bindable<HitType> type;
private readonly Bindable<HitType> type;
public DrawableHit(Hit hit)
: base(hit)
{
type = HitObject.TypeBindable.GetBoundCopy();
FillMode = FillMode.Fit;
updateActionsFromType();
}
[BackgroundDependencyLoader]
private void load()
{
type = HitObject.TypeBindable.GetBoundCopy();
type.BindValueChanged(_ =>
{
updateType();
updateActionsFromType();
// will overwrite samples, should only be called on change.
updateSamplesFromTypeChange();
RecreatePieces();
});
updateType();
}
private void updateType()
private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre;
}
private void updateSamplesFromTypeChange()
{
var rimSamples = getRimSamples();
bool isRimType = HitObject.Type == HitType.Rim;
if (isRimType != rimSamples.Any())
{
if (isRimType)
HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP });
else
{
foreach (var sample in rimSamples)
HitObject.Samples.Remove(sample);
}
}
}
private void updateActionsFromType()
{
HitActions =
HitObject.Type == HitType.Centre
? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre }
: new[] { TaikoAction.LeftRim, TaikoAction.RightRim };
RecreatePieces();
}
protected override SkinnableDrawable CreateMainPiece() => HitObject.Type == HitType.Centre

View File

@ -1,19 +1,19 @@
// 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.Input.Bindings;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using System.Linq;
using osu.Game.Audio;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected Vector2 BaseSize;
protected SkinnableDrawable MainPiece;
private Bindable<bool> isStrong;
private readonly Bindable<bool> isStrong;
private readonly Container<DrawableStrongNestedHit> strongHitContainer;
@ -128,6 +128,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
: base(hitObject)
{
HitObject = hitObject;
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
Anchor = Anchor.CentreLeft;
Origin = Anchor.Custom;
@ -140,8 +141,40 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
isStrong = HitObject.IsStrongBindable.GetBoundCopy();
isStrong.BindValueChanged(_ => RecreatePieces(), true);
isStrong.BindValueChanged(_ =>
{
// will overwrite samples, should only be called on change.
updateSamplesFromStrong();
RecreatePieces();
});
RecreatePieces();
}
private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray();
protected override void LoadSamples()
{
base.LoadSamples();
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
{
var strongSamples = getStrongSamples();
if (isStrong.Value != strongSamples.Any())
{
if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH });
else
{
foreach (var sample in strongSamples)
HitObject.Samples.Remove(sample);
}
}
}
protected virtual void RecreatePieces()

View File

@ -16,12 +16,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Rulesets
{
[HeadlessTest]
public class TestSceneDrawableRulesetDependencies : OsuTestScene
{
[Test]

View File

@ -0,0 +1,69 @@
// 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.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorBeatmapCreation : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
public override void SetUpSteps()
{
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
base.SetUpSteps();
// if we save a beatmap with a hash collision, things fall over.
// probably needs a more solid resolution in the future but this will do for now.
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
}
[Test]
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
}
[Test]
public void TestAddAudioTrack()
{
AddAssert("switch track to real track", () =>
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"));
File.Delete(temp);
Directory.Delete(extractedFolder, true);
return success;
});
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
}
}
}

View File

@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.Settings
@ -11,7 +10,7 @@ namespace osu.Game.Tests.Visual.Settings
public class TestSceneDirectorySelector : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(GameHost host)
private void load()
{
Add(new DirectorySelector { RelativeSizeAxes = Axes.Both });
}

View File

@ -0,0 +1,24 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.Settings
{
public class TestSceneFileSelector : OsuTestScene
{
[Test]
public void TestAllFiles()
{
AddStep("create", () => Child = new FileSelector { RelativeSizeAxes = Axes.Both });
}
[Test]
public void TestJpgFilesOnly()
{
AddStep("create", () => Child = new FileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both });
}
}
}

View File

@ -129,7 +129,7 @@ namespace osu.Game.Tournament.Screens
protected virtual void ChangePath()
{
var target = directorySelector.CurrentDirectory.Value.FullName;
var target = directorySelector.CurrentPath.Value.FullName;
var fileBasedIpc = ipc as FileBasedIPC;
Logger.Log($"Changing Stable CE location to {target}");

View File

@ -17,6 +17,11 @@ namespace osu.Game.Audio
public const string HIT_NORMAL = @"hitnormal";
public const string HIT_CLAP = @"hitclap";
/// <summary>
/// All valid sample addition constants.
/// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
/// <summary>
/// The bank to load the sample from.
/// </summary>

View File

@ -260,7 +260,7 @@ namespace osu.Game.Beatmaps
fileInfo.Filename = beatmapInfo.Path;
stream.Seek(0, SeekOrigin.Begin);
UpdateFile(setInfo, fileInfo, stream);
ReplaceFile(setInfo, fileInfo, stream);
}
}

View File

@ -401,12 +401,27 @@ namespace osu.Game.Database
}
/// <summary>
/// Update an existing file, or create a new entry if not already part of the <paramref name="model"/>'s files.
/// Replace an existing file with a new version.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The file model to be updated or added.</param>
/// <param name="file">The existing file to be replaced.</param>
/// <param name="contents">The new file contents.</param>
public void UpdateFile(TModel model, TFileModel file, Stream contents)
/// <param name="filename">An optional filename for the new file. Will use the previous filename if not specified.</param>
public void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null)
{
using (ContextFactory.GetForWrite())
{
DeleteFile(model, file);
AddFile(model, contents, filename ?? file.Filename);
}
}
/// <summary>
/// Delete new file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be deleted.</param>
public void DeleteFile(TModel model, TFileModel file)
{
using (var usage = ContextFactory.GetForWrite())
{
@ -415,15 +430,28 @@ namespace osu.Game.Database
{
Files.Dereference(file.FileInfo);
// Remove the file model.
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
// Definitely can be removed once we rework the database backend.
usage.Context.Set<TFileModel>().Remove(file);
}
// Add the new file info and containing file model.
model.Files.Remove(file);
}
}
/// <summary>
/// Add a new file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="contents">The new file contents.</param>
/// <param name="filename">The filename for the new file.</param>
public void AddFile(TModel model, Stream contents, string filename)
{
using (ContextFactory.GetForWrite())
{
model.Files.Add(new TFileModel
{
Filename = file.Filename,
Filename = filename,
FileInfo = Files.Add(contents)
});

View File

@ -28,11 +28,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
private GameHost host { get; set; }
[Cached]
public readonly Bindable<DirectoryInfo> CurrentDirectory = new Bindable<DirectoryInfo>();
public readonly Bindable<DirectoryInfo> CurrentPath = new Bindable<DirectoryInfo>();
public DirectorySelector(string initialPath = null)
{
CurrentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
CurrentPath.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
}
[BackgroundDependencyLoader]
@ -74,7 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
};
CurrentDirectory.BindValueChanged(updateDisplay, true);
CurrentPath.BindValueChanged(updateDisplay, true);
}
private void updateDisplay(ValueChangedEvent<DirectoryInfo> directory)
@ -92,22 +92,27 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
else
{
directoryFlow.Add(new ParentDirectoryPiece(CurrentDirectory.Value.Parent));
directoryFlow.Add(new ParentDirectoryPiece(CurrentPath.Value.Parent));
foreach (var dir in CurrentDirectory.Value.GetDirectories().OrderBy(d => d.Name))
{
if ((dir.Attributes & FileAttributes.Hidden) == 0)
directoryFlow.Add(new DirectoryPiece(dir));
}
directoryFlow.AddRange(GetEntriesForPath(CurrentPath.Value));
}
}
catch (Exception)
{
CurrentDirectory.Value = directory.OldValue;
CurrentPath.Value = directory.OldValue;
this.FlashColour(Color4.Red, 300);
}
}
protected virtual IEnumerable<DisplayPiece> GetEntriesForPath(DirectoryInfo path)
{
foreach (var dir in path.GetDirectories().OrderBy(d => d.Name))
{
if ((dir.Attributes & FileAttributes.Hidden) == 0)
yield return new DirectoryPiece(dir);
}
}
private class CurrentDirectoryDisplay : CompositeDrawable
{
[Resolved]
@ -126,7 +131,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(5),
Height = DirectoryPiece.HEIGHT,
Height = DisplayPiece.HEIGHT,
Direction = FillDirection.Horizontal,
},
};
@ -150,7 +155,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
flow.ChildrenEnumerable = new Drawable[]
{
new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DirectoryPiece.HEIGHT), },
new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DisplayPiece.HEIGHT), },
new ComputerPiece(),
}.Concat(pathPieces);
}
@ -198,24 +203,44 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
private class DirectoryPiece : CompositeDrawable
protected class DirectoryPiece : DisplayPiece
{
public const float HEIGHT = 20;
protected const float FONT_SIZE = 16;
protected readonly DirectoryInfo Directory;
private readonly string displayName;
protected FillFlowContainer Flow;
[Resolved]
private Bindable<DirectoryInfo> currentDirectory { get; set; }
public DirectoryPiece(DirectoryInfo directory, string displayName = null)
: base(displayName)
{
Directory = directory;
}
protected override bool OnClick(ClickEvent e)
{
currentDirectory.Value = Directory;
return true;
}
protected override string FallbackName => Directory.Name;
protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
? FontAwesome.Solid.Database
: FontAwesome.Regular.Folder;
}
protected abstract class DisplayPiece : CompositeDrawable
{
public const float HEIGHT = 20;
protected const float FONT_SIZE = 16;
private readonly string displayName;
protected FillFlowContainer Flow;
protected DisplayPiece(string displayName = null)
{
this.displayName = displayName;
}
@ -259,20 +284,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = displayName ?? Directory.Name,
Text = displayName ?? FallbackName,
Font = OsuFont.Default.With(size: FONT_SIZE)
});
}
protected override bool OnClick(ClickEvent e)
{
currentDirectory.Value = Directory;
return true;
}
protected abstract string FallbackName { get; }
protected virtual IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar)
? FontAwesome.Solid.Database
: FontAwesome.Regular.Folder;
protected abstract IconUsage? Icon { get; }
}
}
}

View File

@ -0,0 +1,94 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class FileSelector : DirectorySelector
{
private readonly string[] validFileExtensions;
[Cached]
public readonly Bindable<FileInfo> CurrentFile = new Bindable<FileInfo>();
public FileSelector(string initialPath = null, string[] validFileExtensions = null)
: base(initialPath)
{
this.validFileExtensions = validFileExtensions ?? Array.Empty<string>();
}
protected override IEnumerable<DisplayPiece> GetEntriesForPath(DirectoryInfo path)
{
foreach (var dir in base.GetEntriesForPath(path))
yield return dir;
IEnumerable<FileInfo> files = path.GetFiles();
if (validFileExtensions.Length > 0)
files = files.Where(f => validFileExtensions.Contains(f.Extension));
foreach (var file in files.OrderBy(d => d.Name))
{
if ((file.Attributes & FileAttributes.Hidden) == 0)
yield return new FilePiece(file);
}
}
protected class FilePiece : DisplayPiece
{
private readonly FileInfo file;
[Resolved]
private Bindable<FileInfo> currentFile { get; set; }
public FilePiece(FileInfo file)
{
this.file = file;
}
protected override bool OnClick(ClickEvent e)
{
currentFile.Value = file;
return true;
}
protected override string FallbackName => file.Name;
protected override IconUsage? Icon
{
get
{
switch (file.Extension)
{
case ".ogg":
case ".mp3":
case ".wav":
return FontAwesome.Regular.FileAudio;
case ".jpg":
case ".jpeg":
case ".png":
return FontAwesome.Regular.FileImage;
case ".mp4":
case ".avi":
case ".mov":
case ".flv":
return FontAwesome.Regular.FileVideo;
default:
return FontAwesome.Regular.File;
}
}
}
}
}
}

View File

@ -44,12 +44,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
Component.BorderColour = colours.Blue;
}
protected override OsuTextBox CreateComponent() => new OsuTextBox
protected virtual OsuTextBox CreateTextBox() => new OsuTextBox
{
CommitOnFocusLost = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS,
}.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText));
};
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>
{
t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText);
});
}
}

View File

@ -81,6 +81,11 @@ namespace osu.Game.Overlays
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
}
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
public void ReloadCurrentTrack() => changeTrack();
/// <summary>
/// Change the position of a <see cref="BeatmapSetInfo"/> in the current playlist.
/// </summary>

View File

@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private void start()
{
var target = directorySelector.CurrentDirectory.Value;
var target = directorySelector.CurrentPath.Value;
try
{

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@ -14,7 +13,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@ -24,6 +22,7 @@ using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -31,6 +30,11 @@ using osuTK.Input;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// Top level container for editor compose mode.
/// Responsible for providing snapping and generally gluing components together.
/// </summary>
/// <typeparam name="TObject">The base type of supported objects.</typeparam>
[Cached(Type = typeof(IPlacementHandler))]
public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler
where TObject : HitObject
@ -58,7 +62,7 @@ namespace osu.Game.Rulesets.Edit
private RadioButtonCollection toolboxCollection;
private ToolboxGroup togglesCollection;
private FillFlowContainer togglesCollection;
protected HitObjectComposer(Ruleset ruleset)
{
@ -116,14 +120,19 @@ namespace osu.Game.Rulesets.Edit
Spacing = new Vector2(10),
Children = new Drawable[]
{
new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } },
togglesCollection = new ToolboxGroup("toggles")
new ToolboxGroup("toolbox (1-9)")
{
ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox
Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X }
},
new ToolboxGroup("toggles (Q~P)")
{
Child = togglesCollection = new FillFlowContainer
{
Bindable = b,
LabelText = b?.Description ?? "unknown"
})
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
}
},
@ -134,6 +143,9 @@ namespace osu.Game.Rulesets.Edit
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
.ToList();
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
setSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
@ -162,10 +174,14 @@ namespace osu.Game.Rulesets.Edit
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
/// <summary>
/// A collection of toggles which will be displayed to the user.
/// The display name will be decided by <see cref="Bindable{T}.Description"/>.
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
protected virtual IEnumerable<BindableBool> Toggles => Enumerable.Empty<BindableBool>();
public TernaryButton[] TernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.TernaryStates;
/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
@ -192,6 +208,9 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed)
return false;
if (checkLeftToggleFromKey(e.Key, out var leftIndex))
{
var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex);
@ -205,11 +224,11 @@ namespace osu.Game.Rulesets.Edit
if (checkRightToggleFromKey(e.Key, out var rightIndex))
{
var item = togglesCollection.Children[rightIndex];
var item = togglesCollection.ElementAtOrDefault(rightIndex);
if (item is SettingsCheckbox checkbox)
if (item is DrawableTernaryButton button)
{
checkbox.Bindable.Value = !checkbox.Bindable.Value;
button.Button.Toggle();
return true;
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
protected readonly HitObject HitObject;
public readonly HitObject HitObject;
[Resolved(canBeNull: true)]
protected EditorClock EditorClock { get; private set; }

View File

@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(ArmedState.Idle, true);
}
/// <summary>
/// Invoked by the base <see cref="DrawableHitObject"/> to populate samples, once on initial load and potentially again on any change to the samples collection.
/// </summary>
protected virtual void LoadSamples()
{
if (Samples != null)
@ -183,6 +186,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
private void onDefaultsApplied(HitObject hitObject)
{
apply(hitObject);
updateState(state.Value, true);
DefaultsApplied?.Invoke(this);
}

View File

@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Components
private const float contents_padding = 15;
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected Track Track => Beatmap.Value.Track;
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Drawable background;
private readonly Container content;
@ -42,9 +43,11 @@ namespace osu.Game.Screens.Edit.Components
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
background.Colour = colours.Gray1;
}
}

View File

@ -62,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components
}
};
Track?.AddAdjustment(AdjustableProperty.Tempo, tempo);
Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true);
}
protected override void Dispose(bool isDisposing)
{
Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
base.Dispose(isDisposing);
}

View File

@ -0,0 +1,112 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
internal class DrawableTernaryButton : TriangleButton
{
private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour;
private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour;
private Drawable icon;
public readonly TernaryButton Button;
public DrawableTernaryButton(TernaryButton button)
{
Button = button;
Text = button.Description;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
defaultBackgroundColour = colours.Gray3;
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Triangles.Alpha = 0;
Content.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 2,
Offset = new Vector2(0, 1),
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
}
protected override void LoadComplete()
{
base.LoadComplete();
Button.Bindable.BindValueChanged(selected => updateSelectionState(), true);
Action = onAction;
}
private void onAction()
{
Button.Toggle();
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
icon.Colour = selectedBubbleColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
icon.Colour = defaultBubbleColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
icon.Colour = selectedBubbleColour;
BackgroundColour = selectedBackgroundColour;
break;
}
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
}
}

View File

@ -0,0 +1,44 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public class TernaryButton
{
public readonly Bindable<TernaryState> Bindable;
public readonly string Description;
/// <summary>
/// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
/// </summary>
public readonly Func<Drawable> CreateIcon;
public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable> createIcon = null)
{
Bindable = bindable;
Description = description;
CreateIcon = createIcon;
}
public void Toggle()
{
switch (Bindable.Value)
{
case TernaryState.False:
case TernaryState.Indeterminate:
Bindable.Value = TernaryState.True;
break;
case TernaryState.True:
Bindable.Value = TernaryState.False;
break;
}
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osuTK;
using osu.Framework.Graphics;
@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
protected readonly IBindable<Track> Track = new Bindable<Track>();
private readonly Container<T> content;
protected override Container<T> Content => content;
@ -35,12 +38,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
updateRelativeChildSize();
LoadBeatmap(b.NewValue);
};
Track.ValueChanged += _ => updateRelativeChildSize();
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap)
private void load(IBindable<WorkingBeatmap> beatmap, EditorClock clock)
{
Beatmap.BindTo(beatmap);
Track.BindTo(clock.Track);
}
private void updateRelativeChildSize()

View File

@ -24,15 +24,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A container which provides a "blueprint" display of hitobjects.
/// Includes selection and manipulation support via a <see cref="SelectionHandler"/>.
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler"/>.
/// </summary>
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler<PlatformAction>
{
protected DragBox DragBox { get; private set; }
protected Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
public Container<SelectionBlueprint> SelectionBlueprints { get; private set; }
private SelectionHandler selectionHandler;
protected SelectionHandler SelectionHandler { get; private set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
protected EditorBeatmap Beatmap { get; private set; }
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
@ -56,22 +56,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
selectionHandler = CreateSelectionHandler();
selectionHandler.DeselectAll = deselectAll;
SelectionHandler = CreateSelectionHandler();
SelectionHandler.DeselectAll = deselectAll;
AddRangeInternal(new[]
{
DragBox = CreateDragBox(selectBlueprintsFromDragRectangle),
selectionHandler,
SelectionHandler,
SelectionBlueprints = CreateSelectionBlueprintContainer(),
selectionHandler.CreateProxy(),
SelectionHandler.CreateProxy(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
});
foreach (var obj in beatmap.HitObjects)
foreach (var obj in Beatmap.HitObjects)
AddBlueprintFor(obj);
selectedHitObjects.BindTo(beatmap.SelectedHitObjects);
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{
switch (args.Action)
@ -94,15 +94,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
beatmap.HitObjectAdded += AddBlueprintFor;
beatmap.HitObjectRemoved += removeBlueprintFor;
Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += removeBlueprintFor;
}
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
/// <summary>
/// Creates a <see cref="SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// </summary>
protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
// store for double-click handling
clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
clickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
@ -147,7 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return false;
// ensure the blueprint which was hovered for the first click is still the hovered blueprint.
if (clickedBlueprint == null || selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
return false;
editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
@ -208,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (DragBox.State == Visibility.Visible)
{
DragBox.Hide();
selectionHandler.UpdateVisibility();
SelectionHandler.UpdateVisibility();
}
}
@ -217,7 +217,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (e.Key)
{
case Key.Escape:
if (!selectionHandler.SelectedBlueprints.Any())
if (!SelectionHandler.SelectedBlueprints.Any())
return false;
deselectAll();
@ -271,7 +271,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
blueprint.Selected += onBlueprintSelected;
blueprint.Deselected += onBlueprintDeselected;
if (beatmap.SelectedHitObjects.Contains(hitObject))
if (Beatmap.SelectedHitObjects.Contains(hitObject))
blueprint.Select();
SelectionBlueprints.Add(blueprint);
@ -298,14 +298,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
bool allowDeselection = e.ControlPressed && e.Button == MouseButton.Left;
// Todo: This is probably incorrectly disallowing multiple selections on stacked objects
if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
if (!allowDeselection && SelectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
return;
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren)
{
if (blueprint.IsHovered)
{
selectionHandler.HandleSelectionRequested(blueprint, e.CurrentState);
SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState);
clickSelectionBegan = true;
break;
}
@ -358,23 +358,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void selectAll()
{
SelectionBlueprints.ToList().ForEach(m => m.Select());
selectionHandler.UpdateVisibility();
SelectionHandler.UpdateVisibility();
}
/// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s.
/// </summary>
private void deselectAll() => selectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect());
private void onBlueprintSelected(SelectionBlueprint blueprint)
{
selectionHandler.HandleSelected(blueprint);
SelectionHandler.HandleSelected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 1);
}
private void onBlueprintDeselected(SelectionBlueprint blueprint)
{
selectionHandler.HandleDeselected(blueprint);
SelectionHandler.HandleDeselected(blueprint);
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
}
@ -391,16 +391,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
private void prepareSelectionMovement()
{
if (!selectionHandler.SelectedBlueprints.Any())
if (!SelectionHandler.SelectedBlueprints.Any())
return;
// Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement
// A special case is added for when a click selection occurred before the drag
if (!clickSelectionBegan && !selectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
return;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct
}
@ -425,14 +425,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects.
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
return true;
if (result.Time.HasValue)
{
// Apply the start time at the newly snapped-to position
double offset = result.Time.Value - draggedObject.StartTime;
foreach (HitObject obj in selectionHandler.SelectedHitObjects)
foreach (HitObject obj in SelectionHandler.SelectedHitObjects)
obj.StartTime += offset;
}
@ -460,10 +460,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.Dispose(isDisposing);
if (beatmap != null)
if (Beatmap != null)
{
beatmap.HitObjectAdded -= AddBlueprintFor;
beatmap.HitObjectRemoved -= removeBlueprintFor;
Beatmap.HitObjectAdded -= AddBlueprintFor;
Beatmap.HitObjectRemoved -= removeBlueprintFor;
}
}
}

View File

@ -3,14 +3,21 @@
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
@ -46,6 +53,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load()
{
TernaryStates = CreateTernaryButtons().ToArray();
AddInternal(placementBlueprintContainer);
}
@ -54,6 +63,92 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.LoadComplete();
inputManager = GetContainingInputManager();
// updates to selected are handled for us by SelectionHandler.
NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
// we are responsible for current placement blueprint updated based on state changes.
NewCombo.ValueChanged += _ => updatePlacementNewCombo();
// we own SelectionHandler so don't need to worry about making bindable copies (for simplicity)
foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
kvp.Value.BindValueChanged(_ => updatePlacementSamples());
}
}
private void updatePlacementNewCombo()
{
if (currentPlacement == null) return;
if (currentPlacement.HitObject is IHasComboInformation c)
c.NewCombo = NewCombo.Value == TernaryState.True;
}
private void updatePlacementSamples()
{
if (currentPlacement == null) return;
foreach (var kvp in SelectionHandler.SelectionSampleStates)
sampleChanged(kvp.Key, kvp.Value.Value);
}
private void sampleChanged(string sampleName, TernaryState state)
{
if (currentPlacement == null) return;
var samples = currentPlacement.HitObject.Samples;
var existingSample = samples.FirstOrDefault(s => s.Name == sampleName);
switch (state)
{
case TernaryState.False:
if (existingSample != null)
samples.Remove(existingSample);
break;
case TernaryState.True:
if (existingSample == null)
samples.Add(new HitSampleInfo { Name = sampleName });
break;
}
}
public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
/// <summary>
/// A collection of states which will be displayed to the user in the toolbox.
/// </summary>
public TernaryButton[] TernaryStates { get; private set; }
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
{
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle });
foreach (var kvp in SelectionHandler.SelectionSampleStates)
yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key));
}
private Drawable getIconForSample(string sampleName)
{
switch (sampleName)
{
case HitSampleInfo.HIT_CLAP:
return new SpriteIcon { Icon = FontAwesome.Solid.Hands };
case HitSampleInfo.HIT_WHISTLE:
return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn };
case HitSampleInfo.HIT_FINISH:
return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan };
}
return null;
}
#region Placement
@ -86,7 +181,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
removePlacement();
if (currentPlacement != null)
{
updatePlacementPosition();
}
}
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
@ -119,6 +216,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
updatePlacementPosition();
updatePlacementSamples();
updatePlacementNewCombo();
}
}

View File

@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -35,6 +37,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
private readonly List<SelectionBlueprint> selectedBlueprints;
public int SelectedCount => selectedBlueprints.Count;
public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject);
private Drawable content;
@ -59,6 +63,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
createStateBindables();
InternalChild = content = new Container
{
Children = new Drawable[]
@ -283,7 +289,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
var comboInfo = h as IHasComboInformation;
if (comboInfo == null)
throw new InvalidOperationException($"Tried to change combo state of a {h.GetType()}, which doesn't implement {nameof(IHasComboInformation)}");
continue;
comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h);
@ -308,6 +314,93 @@ namespace osu.Game.Screens.Edit.Compose.Components
#endregion
#region Selection State
/// <summary>
/// The state of "new combo" for all selected hitobjects.
/// </summary>
public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
/// <summary>
/// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
private void createStateBindables()
{
foreach (var sampleName in HitSampleInfo.AllAdditions)
{
var bindable = new Bindable<TernaryState>
{
Description = sampleName.Replace("hit", string.Empty).Titleize()
};
bindable.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
};
SelectionSampleStates[sampleName] = bindable;
}
// new combo
SelectionNewComboState.ValueChanged += state =>
{
switch (state.NewValue)
{
case TernaryState.False:
SetNewCombo(false);
break;
case TernaryState.True:
SetNewCombo(true);
break;
}
};
// bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates();
}
/// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
}
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
#endregion
#region Context Menu
public MenuItem[] ContextMenuItems
@ -322,7 +415,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints));
if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
items.Add(createNewComboMenuItem());
{
items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
}
if (selectedBlueprints.Count == 1)
items.AddRange(selectedBlueprints[0].ContextMenuItems);
@ -331,12 +426,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
new OsuMenuItem("Sound")
{
Items = new[]
{
createHitSampleMenuItem("Whistle", HitSampleInfo.HIT_WHISTLE),
createHitSampleMenuItem("Clap", HitSampleInfo.HIT_CLAP),
createHitSampleMenuItem("Finish", HitSampleInfo.HIT_FINISH)
}
Items = SelectionSampleStates.Select(kvp =>
new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
},
new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),
});
@ -353,76 +444,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
=> Enumerable.Empty<MenuItem>();
private MenuItem createNewComboMenuItem()
{
return new TernaryStateMenuItem("New combo", MenuItemType.Standard, setNewComboState)
{
State = { Value = getHitSampleState() }
};
void setNewComboState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
SetNewCombo(false);
break;
case TernaryState.True:
SetNewCombo(true);
break;
}
}
TernaryState getHitSampleState()
{
int countExisting = selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo);
if (countExisting == 0)
return TernaryState.False;
if (countExisting < SelectedHitObjects.Count())
return TernaryState.Indeterminate;
return TernaryState.True;
}
}
private MenuItem createHitSampleMenuItem(string name, string sampleName)
{
return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState)
{
State = { Value = getHitSampleState() }
};
void setHitSampleState(TernaryState state)
{
switch (state)
{
case TernaryState.False:
RemoveHitSample(sampleName);
break;
case TernaryState.True:
AddHitSample(sampleName);
break;
}
}
TernaryState getHitSampleState()
{
int countExisting = SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName));
if (countExisting == 0)
return TernaryState.False;
if (countExisting < SelectedHitObjects.Count())
return TernaryState.Indeterminate;
return TernaryState.True;
}
}
#endregion
}
}

View File

@ -13,6 +13,11 @@ namespace osu.Game.Screens.Edit.Compose
{
private HitObjectComposer composer;
public ComposeScreen()
: base(EditorScreenMode.Compose)
{
}
protected override Drawable CreateMainContent()
{
var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance();

View File

@ -6,6 +6,7 @@ namespace osu.Game.Screens.Edit.Design
public class DesignScreen : EditorScreen
{
public DesignScreen()
: base(EditorScreenMode.Design)
{
Child = new ScreenWhiteBox.UnderConstructionMessage("Design mode");
}

View File

@ -43,6 +43,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
[Cached]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider
{
public override float BackgroundParallaxAmount => 0.1f;
@ -68,7 +69,7 @@ namespace osu.Game.Screens.Edit
private string lastSavedHash;
private Box bottomBackground;
private Container screenContainer;
private Container<EditorScreen> screenContainer;
private EditorScreen currentScreen;
@ -91,6 +92,9 @@ namespace osu.Game.Screens.Edit
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private MusicController music { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours, GameHost host)
{
@ -98,9 +102,9 @@ namespace osu.Game.Screens.Edit
beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue);
// Todo: should probably be done at a DrawableRuleset level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
UpdateClockSource();
dependencies.CacheAs(clock);
AddInternal(clock);
@ -163,7 +167,7 @@ namespace osu.Game.Screens.Edit
Name = "Screen container",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 40, Bottom = 60 },
Child = screenContainer = new Container
Child = screenContainer = new Container<EditorScreen>
{
RelativeSizeAxes = Axes.Both,
Masking = true
@ -271,6 +275,15 @@ namespace osu.Game.Screens.Edit
bottomBackground.Colour = colours.Gray2;
}
/// <summary>
/// If the beatmap's track has changed, this method must be called to keep the editor in a valid state.
/// </summary>
public void UpdateClockSource()
{
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock.ChangeSource(sourceClock);
}
protected void Save()
{
// apply any set-level metadata changes.
@ -512,7 +525,21 @@ namespace osu.Game.Screens.Edit
private void onModeChanged(ValueChangedEvent<EditorScreenMode> e)
{
currentScreen?.Exit();
var lastScreen = currentScreen;
lastScreen?
.ScaleTo(0.98f, 200, Easing.OutQuint)
.FadeOut(200, Easing.OutQuint);
if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null)
{
screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);
currentScreen
.ScaleTo(1, 200, Easing.OutQuint)
.FadeIn(200, Easing.OutQuint);
return;
}
switch (e.NewValue)
{

View File

@ -3,6 +3,8 @@
using System;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
@ -17,7 +19,11 @@ namespace osu.Game.Screens.Edit
/// </summary>
public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock
{
public readonly double TrackLength;
public IBindable<Track> Track => track;
private readonly Bindable<Track> track = new Bindable<Track>();
public double TrackLength => track.Value?.Length ?? 60000;
public ControlPointInfo ControlPointInfo;
@ -35,7 +41,6 @@ namespace osu.Game.Screens.Edit
this.beatDivisor = beatDivisor;
ControlPointInfo = controlPointInfo;
TrackLength = trackLength;
underlyingClock = new DecoupleableInterpolatingFramedClock();
}
@ -190,7 +195,11 @@ namespace osu.Game.Screens.Edit
public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source);
public void ChangeSource(IClock source)
{
track.Value = source as Track;
underlyingClock.ChangeSource(source);
}
public IClock Source => underlyingClock.Source;

View File

@ -23,8 +23,12 @@ namespace osu.Game.Screens.Edit
protected override Container<Drawable> Content => content;
private readonly Container content;
protected EditorScreen()
public readonly EditorScreenMode Type;
protected EditorScreen(EditorScreenMode type)
{
Type = type;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;

View File

@ -25,6 +25,11 @@ namespace osu.Game.Screens.Edit
private Container timelineContainer;
protected EditorScreenWithTimeline(EditorScreenMode type)
: base(type)
{
}
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{

View File

@ -1,17 +1,24 @@
// 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.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
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.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Setup
@ -23,10 +30,31 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledTextBox titleTextBox;
private LabelledTextBox creatorTextBox;
private LabelledTextBox difficultyTextBox;
private LabelledTextBox audioTrackTextBox;
[Resolved]
private MusicController music { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved(canBeNull: true)]
private Editor editor { get; set; }
public SetupScreen()
: base(EditorScreenMode.SongSetup)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Container audioTrackFileChooserContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -70,6 +98,18 @@ namespace osu.Game.Screens.Edit.Setup
},
},
new OsuSpriteText
{
Text = "Resources"
},
audioTrackTextBox = new FileChooserLabelledTextBox
{
Label = "Audio Track",
Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" },
Target = audioTrackFileChooserContainer,
TabbableContentContainer = this
},
audioTrackFileChooserContainer,
new OsuSpriteText
{
Text = "Beatmap metadata"
},
@ -104,10 +144,47 @@ namespace osu.Game.Screens.Edit.Setup
}
};
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
foreach (var item in flow.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
}
public bool ChangeAudioTrack(string path)
{
var info = new FileInfo(path);
if (!info.Exists)
return false;
var set = Beatmap.Value.BeatmapSetInfo;
// remove the previous audio track for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile);
using (var stream = info.OpenRead())
{
if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
else
beatmaps.AddFile(set, stream, info.Name);
}
Beatmap.Value.Metadata.AudioFile = info.Name;
music.ReloadCurrentTrack();
editor?.UpdateClockSource();
return true;
}
private void audioTrackChanged(ValueChangedEvent<string> filePath)
{
if (!ChangeAudioTrack(filePath.NewValue))
audioTrackTextBox.Current.Value = filePath.OldValue;
}
private void onCommit(TextBox sender, bool newText)
{
if (!newText) return;
@ -120,4 +197,60 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value;
}
}
internal class FileChooserLabelledTextBox : LabelledTextBox
{
public Container Target;
private readonly IBindable<FileInfo> currentFile = new Bindable<FileInfo>();
public FileChooserLabelledTextBox()
{
currentFile.BindValueChanged(onFileSelected);
}
private void onFileSelected(ValueChangedEvent<FileInfo> file)
{
if (file.NewValue == null)
return;
Target.Clear();
Current.Value = file.NewValue.FullName;
}
protected override OsuTextBox CreateTextBox() =>
new FileChooserOsuTextBox
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
CornerRadius = CORNER_RADIUS,
OnFocused = DisplayFileChooser
};
public void DisplayFileChooser()
{
Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" })
{
RelativeSizeAxes = Axes.X,
Height = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
CurrentFile = { BindTarget = currentFile }
};
}
internal class FileChooserOsuTextBox : OsuTextBox
{
public Action OnFocused;
protected override void OnFocus(FocusEvent e)
{
OnFocused?.Invoke();
base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this);
}
}
}
}

View File

@ -24,6 +24,11 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private EditorClock clock { get; set; }
public TimingScreen()
: base(EditorScreenMode.Timing)
{
}
protected override Drawable CreateMainContent() => new GridContainer
{
RelativeSizeAxes = Axes.Both,

View File

@ -26,13 +26,15 @@ namespace osu.Game.Tests.Visual
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
}
protected virtual bool EditorComponentsReady => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
AddUntilStep("wait for editor to load", () => EditorComponentsReady);
AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
AddStep("get clock", () => EditorClock = Editor.ChildrenOfType<EditorClock>().Single());
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.923.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.925.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.923.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.925.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.923.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.925.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -909,6 +909,7 @@ private void load()
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bindable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bindables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue">True</s:Boolean>