Merge remote-tracking branch 'refs/remotes/ppy/master' into no-control-overlay-headers

This commit is contained in:
Andrei Zavatski
2020-01-26 12:48:01 +03:00
45 changed files with 902 additions and 434 deletions

View File

@ -111,7 +111,6 @@ platform :ios do
souyuz( souyuz(
platform: "ios", platform: "ios",
build_target: "osu_iOS",
plist_path: "../osu.iOS/Info.plist" plist_path: "../osu.iOS/Info.plist"
) )
end end

View File

@ -54,6 +54,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.122.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.125.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
public class ManiaSelectionBlueprint : SelectionBlueprint public class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{ {
public Vector2 ScreenSpaceDragPosition { get; private set; } public Vector2 ScreenSpaceDragPosition { get; private set; }
public Vector2 DragPosition { get; private set; } public Vector2 DragPosition { get; private set; }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{ {
} }
public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{ {
switch (hitObject) switch (hitObject)
{ {

View File

@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -70,10 +71,12 @@ namespace osu.Game.Rulesets.Mania.Edit
// When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen. // When scrolling downwards the anchor position is at the bottom of the screen, however the movement event assumes the anchor is at the top of the screen.
// This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height. // This causes the delta to assume a positive hitobject position, and which can be corrected for by subtracting the parent height.
if (scrollingInfo.Direction.Value == ScrollingDirection.Down) if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
delta -= moveEvent.Blueprint.DrawableObject.Parent.DrawHeight; delta -= moveEvent.Blueprint.Parent.DrawHeight; // todo: probably wrong
foreach (var b in SelectedBlueprints) foreach (var selectionBlueprint in SelectedBlueprints)
{ {
var b = (OverlaySelectionBlueprint)selectionBlueprint;
var hitObject = b.DrawableObject; var hitObject = b.DrawableObject;
var objectParent = (HitObjectContainer)hitObject.Parent; var objectParent = (HitObjectContainer)hitObject.Parent;

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Edit.Masks namespace osu.Game.Rulesets.Mania.Edit.Masks
{ {
public abstract class ManiaSelectionBlueprint : SelectionBlueprint public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{ {
protected ManiaSelectionBlueprint(DrawableHitObject drawableObject) protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject) : base(drawableObject)

View File

@ -7,10 +7,10 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{ {
public abstract class OsuSelectionBlueprint<T> : SelectionBlueprint public abstract class OsuSelectionBlueprint<T> : OverlaySelectionBlueprint
where T : OsuHitObject where T : OsuHitObject
{ {
protected T HitObject => (T)DrawableObject.HitObject; protected new T HitObject => (T)DrawableObject.HitObject;
protected OsuSelectionBlueprint(DrawableHitObject drawableObject) protected OsuSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject) : base(drawableObject)

View File

@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
: base(slider) : base(slider)
{ {
this.position = position; this.position = position;
InternalChild = CirclePiece = new HitCirclePiece(); InternalChild = CirclePiece = new HitCirclePiece();
Select(); Select();

View File

@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
}; };
public override Vector2 SelectionPoint => HeadBlueprint.SelectionPoint; public override Vector2 SelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler(); protected override SelectionHandler CreateSelectionHandler() => new OsuSelectionHandler();
public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{ {
switch (hitObject) switch (hitObject)
{ {

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
@ -22,12 +23,11 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editor namespace osu.Game.Tests.Visual.Editor
{ {
[TestFixture] [TestFixture]
public class TestSceneEditorComposeTimeline : EditorClockTestScene public class TestSceneTimelineBlueprintContainer : EditorClockTestScene
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(TimelineArea), typeof(TimelineArea),
typeof(TimelineHitObjectDisplay),
typeof(Timeline), typeof(Timeline),
typeof(TimelineButton), typeof(TimelineButton),
typeof(CentreMarker) typeof(CentreMarker)
@ -38,9 +38,10 @@ namespace osu.Game.Tests.Visual.Editor
{ {
Beatmap.Value = new WaveformTestBeatmap(audio); Beatmap.Value = new WaveformTestBeatmap(audio);
var editorBeatmap = new EditorBeatmap((Beatmap<HitObject>)Beatmap.Value.Beatmap); var editorBeatmap = new EditorBeatmap((Beatmap<HitObject>)Beatmap.Value.Beatmap, BeatDivisor);
Dependencies.Cache(editorBeatmap); Dependencies.Cache(editorBeatmap);
Dependencies.CacheAs<IBeatSnapProvider>(editorBeatmap);
Children = new Drawable[] Children = new Drawable[]
{ {
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editor
}, },
new TimelineArea new TimelineArea
{ {
Child = new TimelineHitObjectDisplay(editorBeatmap), Child = new TimelineBlueprintContainer(),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,

View File

@ -0,0 +1,36 @@
// 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.Overlays;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneSongTicker : OsuTestScene
{
[Cached]
private MusicController musicController = new MusicController();
public TestSceneSongTicker()
{
AddRange(new Drawable[]
{
musicController,
new SongTicker
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new NowPlayingOverlay
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
State = { Value = Visibility.Visible }
}
});
}
}
}

View File

@ -437,6 +437,53 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1); AddAssert("Selection was random", () => eagerSelectedIDs.Count > 1);
} }
[Test]
public void TestFilteringByUserStarDifficulty()
{
BeatmapSetInfo set = null;
loadBeatmaps(new List<BeatmapSetInfo>());
AddStep("add mixed difficulty set", () =>
{
set = createTestBeatmapSet(1);
set.Beatmaps.Clear();
for (int i = 1; i <= 15; i++)
{
set.Beatmaps.Add(new BeatmapInfo
{
Version = $"Stars: {i}",
StarDifficulty = i,
});
}
carousel.UpdateBeatmapSet(set);
});
AddStep("select added set", () => carousel.SelectBeatmap(set.Beatmaps[0], false));
AddStep("filter [5..]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 5 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 11);
AddStep("filter to [0..7]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Max = 7 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 7);
AddStep("filter to [5..7]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 5, Max = 7 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 3);
AddStep("filter [2..2]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 2, Max = 2 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 1);
AddStep("filter to [0..]", () => carousel.Filter(new FilterCriteria { UserStarDifficulty = { Min = 0 } }));
AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(true, 15);
}
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null) private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null)
{ {
createCarousel(); createCarousel();

View File

@ -239,11 +239,11 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"Source": case @"Source":
beatmap.BeatmapInfo.Metadata.Source = pair.Value; metadata.Source = pair.Value;
break; break;
case @"Tags": case @"Tags":
beatmap.BeatmapInfo.Metadata.Tags = pair.Value; metadata.Tags = pair.Value;
break; break;
case @"BeatmapID": case @"BeatmapID":
@ -300,13 +300,11 @@ namespace osu.Game.Beatmaps.Formats
switch (type) switch (type)
{ {
case LegacyEventType.Background: case LegacyEventType.Background:
string bgFilename = split[2].Trim('"'); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
beatmap.BeatmapInfo.Metadata.BackgroundFile = bgFilename.ToStandardisedPath();
break; break;
case LegacyEventType.Video: case LegacyEventType.Video:
string videoFilename = split[2].Trim('"'); beatmap.BeatmapInfo.Metadata.VideoFile = CleanFilename(split[2]);
beatmap.BeatmapInfo.Metadata.VideoFile = videoFilename.ToStandardisedPath();
break; break;
case LegacyEventType.Break: case LegacyEventType.Break:

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -115,7 +116,7 @@ namespace osu.Game.Beatmaps.Formats
protected KeyValuePair<string, string> SplitKeyVal(string line, char separator = ':') protected KeyValuePair<string, string> SplitKeyVal(string line, char separator = ':')
{ {
var split = line.Trim().Split(new[] { separator }, 2); var split = line.Split(separator, 2);
return new KeyValuePair<string, string> return new KeyValuePair<string, string>
( (
@ -124,6 +125,8 @@ namespace osu.Game.Beatmaps.Formats
); );
} }
protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath();
protected enum Section protected enum Section
{ {
None, None,

View File

@ -7,7 +7,6 @@ using System.Globalization;
using System.IO; using System.IO;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -65,13 +64,16 @@ namespace osu.Game.Beatmaps.Formats
private void handleEvents(string line) private void handleEvents(string line)
{ {
var depth = 0; var depth = 0;
var lineSpan = line.AsSpan();
while (line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal)) while (lineSpan.StartsWith(" ", StringComparison.Ordinal) || lineSpan.StartsWith("_", StringComparison.Ordinal))
{ {
lineSpan = lineSpan.Slice(1);
++depth; ++depth;
line = line.Substring(1);
} }
line = lineSpan.ToString();
decodeVariables(ref line); decodeVariables(ref line);
string[] split = line.Split(','); string[] split = line.Split(',');
@ -89,7 +91,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
var layer = parseLayer(split[1]); var layer = parseLayer(split[1]);
var origin = parseOrigin(split[2]); var origin = parseOrigin(split[2]);
var path = cleanFilename(split[3]); var path = CleanFilename(split[3]);
var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo);
var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo);
storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y));
@ -101,7 +103,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
var layer = parseLayer(split[1]); var layer = parseLayer(split[1]);
var origin = parseOrigin(split[2]); var origin = parseOrigin(split[2]);
var path = cleanFilename(split[3]); var path = CleanFilename(split[3]);
var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo);
var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo);
var frameCount = int.Parse(split[6]); var frameCount = int.Parse(split[6]);
@ -116,7 +118,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
var time = double.Parse(split[1], CultureInfo.InvariantCulture); var time = double.Parse(split[1], CultureInfo.InvariantCulture);
var layer = parseLayer(split[2]); var layer = parseLayer(split[2]);
var path = cleanFilename(split[3]); var path = CleanFilename(split[3]);
var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100;
storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume)); storyboard.GetLayer(layer).Add(new StoryboardSampleInfo(path, time, (int)volume));
break; break;
@ -331,7 +333,5 @@ namespace osu.Game.Beatmaps.Formats
break; break;
} }
} }
private string cleanFilename(string path) => path.Trim('"').ToStandardisedPath();
} }
} }

View File

@ -24,7 +24,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ShowConvertedBeatmaps, true); Set(OsuSetting.ShowConvertedBeatmaps, true);
Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1);
Set(OsuSetting.DisplayStarsMaximum, 10.0, 0, 10, 0.1); Set(OsuSetting.DisplayStarsMaximum, 10.1, 0, 10.1, 0.1);
Set(OsuSetting.SongSelectGroupingMode, GroupMode.All); Set(OsuSetting.SongSelectGroupingMode, GroupMode.All);
Set(OsuSetting.SongSelectSortingMode, SortMode.Title); Set(OsuSetting.SongSelectSortingMode, SortMode.Title);

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -85,14 +84,6 @@ namespace osu.Game.Graphics.UserInterface
set => strip.Colour = value; set => strip.Colour = value;
} }
protected override TabFillFlowContainer CreateTabFlow() => new OsuTabFillFlowContainer
{
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.Both,
Depth = -1,
Masking = true
};
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
@ -284,10 +275,5 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
} }
private class OsuTabFillFlowContainer : TabFillFlowContainer
{
protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y);
}
} }
} }

View File

@ -9,6 +9,7 @@ using osuTK;
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Overlays.Chat.Tabs namespace osu.Game.Overlays.Chat.Tabs
{ {
@ -113,5 +114,18 @@ namespace osu.Game.Overlays.Chat.Tabs
OnRequestLeave?.Invoke(tab.Value); OnRequestLeave?.Invoke(tab.Value);
} }
protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer
{
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.Both,
Depth = -1,
Masking = true
};
private class ChannelTabFillFlowContainer : TabFillFlowContainer
{
protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y);
}
} }
} }

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -10,11 +12,20 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{ {
public class SongSelectSettings : SettingsSubsection public class SongSelectSettings : SettingsSubsection
{ {
private Bindable<double> minStars;
private Bindable<double> maxStars;
protected override string Header => "Song Select"; protected override string Header => "Song Select";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
minStars = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum);
maxStars = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum);
minStars.ValueChanged += min => maxStars.Value = Math.Max(min.NewValue, maxStars.Value);
maxStars.ValueChanged += max => minStars.Value = Math.Min(max.NewValue, minStars.Value);
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox new SettingsCheckbox
@ -27,19 +38,19 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = "Show converted beatmaps", LabelText = "Show converted beatmaps",
Bindable = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps), Bindable = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
}, },
new SettingsSlider<double, StarSlider> new SettingsSlider<double, StarsSlider>
{ {
LabelText = "Display beatmaps from", LabelText = "Display beatmaps from",
Bindable = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum), Bindable = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum),
KeyboardStep = 0.1f, KeyboardStep = 0.1f,
Keywords = new[] { "star", "difficulty" } Keywords = new[] { "minimum", "maximum", "star", "difficulty" }
}, },
new SettingsSlider<double, StarSlider> new SettingsSlider<double, MaximumStarsSlider>
{ {
LabelText = "up to", LabelText = "up to",
Bindable = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum), Bindable = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum),
KeyboardStep = 0.1f, KeyboardStep = 0.1f,
Keywords = new[] { "star", "difficulty" } Keywords = new[] { "minimum", "maximum", "star", "difficulty" }
}, },
new SettingsEnumDropdown<RandomSelectAlgorithm> new SettingsEnumDropdown<RandomSelectAlgorithm>
{ {
@ -49,7 +60,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
}; };
} }
private class StarSlider : OsuSliderBar<double> private class MaximumStarsSlider : StarsSlider
{
public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
}
private class StarsSlider : OsuSliderBar<double>
{ {
public override string TooltipText => Current.Value.ToString(@"0.## stars"); public override string TooltipText => Current.Value.ToString(@"0.## stars");
} }

View File

@ -0,0 +1,34 @@
// 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.Primitives;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
public abstract class OverlaySelectionBlueprint : SelectionBlueprint
{
/// <summary>
/// The <see cref="DrawableHitObject"/> which this <see cref="OverlaySelectionBlueprint"/> applies to.
/// </summary>
public readonly DrawableHitObject DrawableObject;
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected;
protected OverlaySelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject.HitObject)
{
DrawableObject = drawableObject;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
public override Vector2 GetInstantDelta(Vector2 screenSpacePosition) => DrawableObject.Parent.ToLocalSpace(screenSpacePosition) - DrawableObject.Position;
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public abstract class SelectionBlueprint : CompositeDrawable, IStateful<SelectionState> public abstract class SelectionBlueprint : CompositeDrawable, IStateful<SelectionState>
{ {
public readonly HitObject HitObject;
/// <summary> /// <summary>
/// Invoked when this <see cref="SelectionBlueprint"/> has been selected. /// Invoked when this <see cref="SelectionBlueprint"/> has been selected.
/// </summary> /// </summary>
@ -30,26 +32,24 @@ namespace osu.Game.Rulesets.Edit
/// </summary> /// </summary>
public event Action<SelectionBlueprint> Deselected; public event Action<SelectionBlueprint> Deselected;
/// <summary>
/// The <see cref="DrawableHitObject"/> which this <see cref="SelectionBlueprint"/> applies to.
/// </summary>
public readonly DrawableHitObject DrawableObject;
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected;
public override bool HandlePositionalInput => ShouldBeAlive; public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; } private HitObjectComposer composer { get; set; }
protected SelectionBlueprint(DrawableHitObject drawableObject) protected SelectionBlueprint(HitObject hitObject)
{ {
DrawableObject = drawableObject; HitObject = hitObject;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AlwaysPresent = true; AlwaysPresent = true;
Alpha = 0; }
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
} }
private SelectionState state; private SelectionState state;
@ -66,58 +66,68 @@ namespace osu.Game.Rulesets.Edit
state = value; state = value;
switch (state) if (IsLoaded)
{ updateState();
case SelectionState.Selected:
Show();
Selected?.Invoke(this);
break;
case SelectionState.NotSelected:
Hide();
Deselected?.Invoke(this);
break;
}
StateChanged?.Invoke(state); StateChanged?.Invoke(state);
} }
} }
private void updateState()
{
switch (state)
{
case SelectionState.Selected:
OnSelected();
Selected?.Invoke(this);
break;
case SelectionState.NotSelected:
OnDeselected();
Deselected?.Invoke(this);
break;
}
}
protected virtual void OnDeselected() => Hide();
protected virtual void OnSelected() => Show();
// When not selected, input is only required for the blueprint itself to receive IsHovering // When not selected, input is only required for the blueprint itself to receive IsHovering
protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected; protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;
/// <summary> /// <summary>
/// Selects this <see cref="SelectionBlueprint"/>, causing it to become visible. /// Selects this <see cref="OverlaySelectionBlueprint"/>, causing it to become visible.
/// </summary> /// </summary>
public void Select() => State = SelectionState.Selected; public void Select() => State = SelectionState.Selected;
/// <summary> /// <summary>
/// Deselects this <see cref="SelectionBlueprint"/>, causing it to become invisible. /// Deselects this <see cref="OverlaySelectionBlueprint"/>, causing it to become invisible.
/// </summary> /// </summary>
public void Deselect() => State = SelectionState.NotSelected; public void Deselect() => State = SelectionState.NotSelected;
public bool IsSelected => State == SelectionState.Selected; public bool IsSelected => State == SelectionState.Selected;
/// <summary> /// <summary>
/// Updates the <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap. /// Updates the <see cref="Objects.HitObject"/>, invoking <see cref="Objects.HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary> /// </summary>
protected void UpdateHitObject() => composer?.UpdateHitObject(DrawableObject.HitObject); protected void UpdateHitObject() => composer?.UpdateHitObject(HitObject);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
/// <summary> /// <summary>
/// The <see cref="MenuItem"/>s to be displayed in the context menu for this <see cref="SelectionBlueprint"/>. /// The <see cref="MenuItem"/>s to be displayed in the context menu for this <see cref="OverlaySelectionBlueprint"/>.
/// </summary> /// </summary>
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>(); public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <summary> /// <summary>
/// The screen-space point that causes this <see cref="SelectionBlueprint"/> to be selected. /// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected.
/// </summary> /// </summary>
public virtual Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre; public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
/// <summary> /// <summary>
/// The screen-space quad that outlines this <see cref="SelectionBlueprint"/> for selections. /// The screen-space quad that outlines this <see cref="OverlaySelectionBlueprint"/> for selections.
/// </summary> /// </summary>
public virtual Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad; public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position;
} }
} }

View File

@ -11,20 +11,24 @@ using osu.Game.Beatmaps;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
public class TimelinePart : TimelinePart<Drawable>
{
}
/// <summary> /// <summary>
/// Represents a part of the summary timeline.. /// Represents a part of the summary timeline..
/// </summary> /// </summary>
public class TimelinePart : Container public class TimelinePart<T> : Container<T> where T : Drawable
{ {
protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); protected readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
private readonly Container timeline; private readonly Container<T> content;
protected override Container<Drawable> Content => timeline; protected override Container<T> Content => content;
public TimelinePart() public TimelinePart(Container<T> content = null)
{ {
AddInternal(timeline = new Container { RelativeSizeAxes = Axes.Both }); AddInternal(this.content = content ?? new Container<T> { RelativeSizeAxes = Axes.Both });
Beatmap.ValueChanged += b => Beatmap.ValueChanged += b =>
{ {
@ -44,17 +48,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
// the track may not be loaded completely (only has a length once it is). // the track may not be loaded completely (only has a length once it is).
if (!Beatmap.Value.Track.IsLoaded) if (!Beatmap.Value.Track.IsLoaded)
{ {
timeline.RelativeChildSize = Vector2.One; content.RelativeChildSize = Vector2.One;
Schedule(updateRelativeChildSize); Schedule(updateRelativeChildSize);
return; return;
} }
timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); content.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1);
} }
protected virtual void LoadBeatmap(WorkingBeatmap beatmap) protected virtual void LoadBeatmap(WorkingBeatmap beatmap)
{ {
timeline.Clear(); content.Clear();
} }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
@ -31,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected DragBox DragBox { get; private set; } protected DragBox DragBox { get; private set; }
private SelectionBlueprintContainer selectionBlueprints; private Container<SelectionBlueprint> selectionBlueprints;
private SelectionHandler selectionHandler; private SelectionHandler selectionHandler;
@ -41,6 +42,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private IDistanceSnapProvider snapProvider { get; set; } private IDistanceSnapProvider snapProvider { get; set; }
@ -59,12 +62,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
DragBox = CreateDragBox(select), DragBox = CreateDragBox(select),
selectionHandler, selectionHandler,
selectionBlueprints = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }, selectionBlueprints = CreateSelectionBlueprintContainer(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue) DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
}); });
foreach (var obj in beatmap.HitObjects) foreach (var obj in beatmap.HitObjects)
AddBlueprintFor(obj); AddBlueprintFor(obj);
selectedHitObjects.BindTo(beatmap.SelectedHitObjects);
selectedHitObjects.ItemsAdded += objects =>
{
foreach (var o in objects)
selectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Select();
};
selectedHitObjects.ItemsRemoved += objects =>
{
foreach (var o in objects)
selectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
};
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -75,6 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
beatmap.HitObjectRemoved += removeBlueprintFor; beatmap.HitObjectRemoved += removeBlueprintFor;
} }
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
/// <summary> /// <summary>
/// Creates a <see cref="SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections. /// Creates a <see cref="SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
/// </summary> /// </summary>
@ -91,6 +110,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
beginClickSelection(e); beginClickSelection(e);
prepareSelectionMovement();
return e.Button == MouseButton.Left; return e.Button == MouseButton.Left;
} }
@ -118,7 +139,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (clickedBlueprint == null) if (clickedBlueprint == null)
return false; return false;
adjustableClock?.Seek(clickedBlueprint.DrawableObject.HitObject.StartTime); adjustableClock?.Seek(clickedBlueprint.HitObject.StartTime);
return true; return true;
} }
@ -126,6 +147,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
// Special case for when a drag happened instead of a click // Special case for when a drag happened instead of a click
Schedule(() => endClickSelection()); Schedule(() => endClickSelection());
finishSelectionMovement();
} }
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
@ -133,15 +156,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Button == MouseButton.Right) if (e.Button == MouseButton.Right)
return false; return false;
if (!beginSelectionMovement()) if (movementBlueprint != null)
{ return true;
if (!DragBox.UpdateDrag(e))
return false;
DragBox.FadeIn(250, Easing.OutQuint); if (DragBox.HandleDrag(e))
{
DragBox.Show();
return true;
} }
return true; return false;
} }
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
@ -149,8 +173,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Button == MouseButton.Right) if (e.Button == MouseButton.Right)
return; return;
if (!moveCurrentSelection(e)) if (DragBox.State == Visibility.Visible)
DragBox.UpdateDrag(e); DragBox.HandleDrag(e);
moveCurrentSelection(e);
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
@ -158,9 +184,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Button == MouseButton.Right) if (e.Button == MouseButton.Right)
return; return;
if (!finishSelectionMovement()) if (DragBox.State == Visibility.Visible)
{ {
DragBox.FadeOut(250, Easing.OutQuint); DragBox.Hide();
selectionHandler.UpdateVisibility(); selectionHandler.UpdateVisibility();
} }
} }
@ -200,7 +226,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void removeBlueprintFor(HitObject hitObject) private void removeBlueprintFor(HitObject hitObject)
{ {
var blueprint = selectionBlueprints.SingleOrDefault(m => m.DrawableObject.HitObject == hitObject); var blueprint = selectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject);
if (blueprint == null) if (blueprint == null)
return; return;
@ -248,7 +274,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered))
return; return;
foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveBlueprints) foreach (SelectionBlueprint blueprint in selectionBlueprints.AliveChildren)
{ {
if (blueprint.IsHovered) if (blueprint.IsHovered)
{ {
@ -305,6 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
selectionHandler.HandleSelected(blueprint); selectionHandler.HandleSelected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 1); selectionBlueprints.ChangeChildDepth(blueprint, 1);
beatmap.SelectedHitObjects.Add(blueprint.HitObject);
SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects); SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
} }
@ -313,6 +340,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
selectionHandler.HandleDeselected(blueprint); selectionHandler.HandleDeselected(blueprint);
selectionBlueprints.ChangeChildDepth(blueprint, 0); selectionBlueprints.ChangeChildDepth(blueprint, 0);
beatmap.SelectedHitObjects.Remove(blueprint.HitObject);
SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects); SelectionChanged?.Invoke(selectionHandler.SelectedHitObjects);
} }
@ -321,27 +349,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement #region Selection Movement
private Vector2? screenSpaceMovementStartPosition; private Vector2? movementBlueprintOriginalPosition;
private SelectionBlueprint movementBlueprint; private SelectionBlueprint movementBlueprint;
/// <summary> /// <summary>
/// Attempts to begin the movement of any selected blueprints. /// Attempts to begin the movement of any selected blueprints.
/// </summary> /// </summary>
/// <returns>Whether movement began.</returns> private void prepareSelectionMovement()
private bool beginSelectionMovement()
{ {
Debug.Assert(movementBlueprint == null); 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 // 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 // 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 false; return;
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject // 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.DrawableObject.HitObject.StartTime).First(); movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
screenSpaceMovementStartPosition = movementBlueprint.DrawableObject.ToScreenSpace(movementBlueprint.DrawableObject.OriginPosition); movementBlueprintOriginalPosition = movementBlueprint.SelectionPoint; // todo: unsure if correct
return true;
} }
/// <summary> /// <summary>
@ -354,17 +380,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprint == null) if (movementBlueprint == null)
return false; return false;
Debug.Assert(screenSpaceMovementStartPosition != null); Debug.Assert(movementBlueprintOriginalPosition != null);
Vector2 startPosition = screenSpaceMovementStartPosition.Value; HitObject draggedObject = movementBlueprint.HitObject;
HitObject draggedObject = movementBlueprint.DrawableObject.HitObject;
// The final movement position, relative to screenSpaceMovementStartPosition // The final movement position, relative to screenSpaceMovementStartPosition
Vector2 movePosition = startPosition + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
(Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime); (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
// Move the hitobjects // Move the hitobjects
if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, startPosition, ToScreenSpace(snappedPosition)))) if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition))))
return true; return true;
// Apply the start time at the newly snapped-to position // Apply the start time at the newly snapped-to position
@ -384,7 +410,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprint == null) if (movementBlueprint == null)
return false; return false;
screenSpaceMovementStartPosition = null; movementBlueprintOriginalPosition = null;
movementBlueprint = null; movementBlueprint = null;
return true; return true;
@ -402,30 +428,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
beatmap.HitObjectRemoved -= removeBlueprintFor; beatmap.HitObjectRemoved -= removeBlueprintFor;
} }
} }
private class SelectionBlueprintContainer : Container<SelectionBlueprint>
{
public IEnumerable<SelectionBlueprint> AliveBlueprints => AliveInternalChildren.Cast<SelectionBlueprint>();
protected override int Compare(Drawable x, Drawable y)
{
if (!(x is SelectionBlueprint xBlueprint) || !(y is SelectionBlueprint yBlueprint))
return base.Compare(x, y);
return Compare(xBlueprint, yBlueprint);
}
public int Compare(SelectionBlueprint x, SelectionBlueprint y)
{
// dpeth is used to denote selected status (we always want selected blueprints to handle input first).
int d = x.Depth.CompareTo(y.Depth);
if (d != 0)
return d;
// Put earlier hitobjects towards the end of the list, so they handle input first
int i = y.DrawableObject.HitObject.StartTime.CompareTo(x.DrawableObject.HitObject.StartTime);
return i == 0 ? CompareReverseChildID(x, y) : i;
}
}
} }
} }

View File

@ -119,7 +119,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return CreateBlueprintFor(drawable); return CreateBlueprintFor(drawable);
} }
public virtual SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null;
protected override void AddBlueprintFor(HitObject hitObject) protected override void AddBlueprintFor(HitObject hitObject)
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -15,11 +16,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// A box that displays the drag selection and provides selection events for users to handle. /// A box that displays the drag selection and provides selection events for users to handle.
/// </summary> /// </summary>
public class DragBox : CompositeDrawable public class DragBox : CompositeDrawable, IStateful<Visibility>
{ {
private readonly Action<RectangleF> performSelection; protected readonly Action<RectangleF> PerformSelection;
private Drawable box; protected Drawable Box;
/// <summary> /// <summary>
/// Creates a new <see cref="DragBox"/>. /// Creates a new <see cref="DragBox"/>.
@ -27,7 +28,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="performSelection">A delegate that performs drag selection.</param> /// <param name="performSelection">A delegate that performs drag selection.</param>
public DragBox(Action<RectangleF> performSelection) public DragBox(Action<RectangleF> performSelection)
{ {
this.performSelection = performSelection; PerformSelection = performSelection;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
AlwaysPresent = true; AlwaysPresent = true;
@ -37,25 +38,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChild = box = new Container InternalChild = Box = CreateBox();
{
Masking = true,
BorderColour = Color4.White,
BorderThickness = SelectionHandler.BORDER_RADIUS,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f
}
};
} }
protected virtual Drawable CreateBox() => new Container
{
Masking = true,
BorderColour = Color4.White,
BorderThickness = SelectionHandler.BORDER_RADIUS,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f
}
};
/// <summary> /// <summary>
/// Handle a forwarded mouse event. /// Handle a forwarded mouse event.
/// </summary> /// </summary>
/// <param name="e">The mouse event.</param> /// <param name="e">The mouse event.</param>
/// <returns>Whether the event should be handled and blocking.</returns> /// <returns>Whether the event should be handled and blocking.</returns>
public virtual bool UpdateDrag(MouseButtonEvent e) public virtual bool HandleDrag(MouseButtonEvent e)
{ {
var dragPosition = e.ScreenSpaceMousePosition; var dragPosition = e.ScreenSpaceMousePosition;
var dragStartPosition = e.ScreenSpaceMouseDownPosition; var dragStartPosition = e.ScreenSpaceMouseDownPosition;
@ -68,11 +71,32 @@ namespace osu.Game.Screens.Edit.Compose.Components
var topLeft = ToLocalSpace(dragRectangle.TopLeft); var topLeft = ToLocalSpace(dragRectangle.TopLeft);
var bottomRight = ToLocalSpace(dragRectangle.BottomRight); var bottomRight = ToLocalSpace(dragRectangle.BottomRight);
box.Position = topLeft; Box.Position = topLeft;
box.Size = bottomRight - topLeft; Box.Size = bottomRight - topLeft;
performSelection?.Invoke(dragRectangle); PerformSelection?.Invoke(dragRectangle);
return true; return true;
} }
private Visibility state;
public Visibility State
{
get => state;
set
{
if (value == state) return;
state = value;
this.FadeTo(state == Visibility.Hidden ? 0 : 1, 250, Easing.OutQuint);
StateChanged?.Invoke(state);
}
}
public override void Hide() => State = Visibility.Hidden;
public override void Show() => State = Visibility.Visible;
public event Action<Visibility> StateChanged;
} }
} }

View File

@ -7,7 +7,7 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
/// <summary> /// <summary>
/// An event which occurs when a <see cref="SelectionBlueprint"/> is moved. /// An event which occurs when a <see cref="OverlaySelectionBlueprint"/> is moved.
/// </summary> /// </summary>
public class MoveSelectionEvent public class MoveSelectionEvent
{ {
@ -16,11 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
public readonly SelectionBlueprint Blueprint; public readonly SelectionBlueprint Blueprint;
/// <summary>
/// The starting screen-space position of the hitobject.
/// </summary>
public readonly Vector2 ScreenSpaceStartPosition;
/// <summary> /// <summary>
/// The expected screen-space position of the hitobject at the current cursor position. /// The expected screen-space position of the hitobject at the current cursor position.
/// </summary> /// </summary>
@ -29,18 +24,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// The distance between <see cref="ScreenSpacePosition"/> and the hitobject's current position, in the coordinate-space of the hitobject's parent. /// The distance between <see cref="ScreenSpacePosition"/> and the hitobject's current position, in the coordinate-space of the hitobject's parent.
/// </summary> /// </summary>
/// <remarks>
/// This does not use <see cref="ScreenSpaceStartPosition"/> and does not represent the cumulative movement distance.
/// </remarks>
public readonly Vector2 InstantDelta; public readonly Vector2 InstantDelta;
public MoveSelectionEvent(SelectionBlueprint blueprint, Vector2 screenSpaceStartPosition, Vector2 screenSpacePosition) public MoveSelectionEvent(SelectionBlueprint blueprint, Vector2 screenSpacePosition)
{ {
Blueprint = blueprint; Blueprint = blueprint;
ScreenSpaceStartPosition = screenSpaceStartPosition;
ScreenSpacePosition = screenSpacePosition; ScreenSpacePosition = screenSpacePosition;
InstantDelta = Blueprint.DrawableObject.Parent.ToLocalSpace(ScreenSpacePosition) - Blueprint.DrawableObject.Position; InstantDelta = Blueprint.GetInstantDelta(ScreenSpacePosition);
} }
} }
} }

View File

@ -33,12 +33,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints; public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
private readonly List<SelectionBlueprint> selectedBlueprints; private readonly List<SelectionBlueprint> selectedBlueprints;
public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.DrawableObject.HitObject); public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject);
private Drawable outline; private Drawable outline;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; } private EditorBeatmap editorBeatmap { get; set; }
public SelectionHandler() public SelectionHandler()
{ {
@ -104,7 +104,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handle a blueprint becoming selected. /// Handle a blueprint becoming selected.
/// </summary> /// </summary>
/// <param name="blueprint">The blueprint.</param> /// <param name="blueprint">The blueprint.</param>
internal void HandleSelected(SelectionBlueprint blueprint) => selectedBlueprints.Add(blueprint); internal void HandleSelected(SelectionBlueprint blueprint)
{
selectedBlueprints.Add(blueprint);
editorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
UpdateVisibility();
}
/// <summary> /// <summary>
/// Handle a blueprint becoming deselected. /// Handle a blueprint becoming deselected.
@ -113,6 +119,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
internal void HandleDeselected(SelectionBlueprint blueprint) internal void HandleDeselected(SelectionBlueprint blueprint)
{ {
selectedBlueprints.Remove(blueprint); selectedBlueprints.Remove(blueprint);
editorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
// We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection // We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection
if (selectedBlueprints.Count == 0) if (selectedBlueprints.Count == 0)
@ -141,14 +148,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
DeselectAll?.Invoke(); DeselectAll?.Invoke();
blueprint.Select(); blueprint.Select();
} }
UpdateVisibility();
} }
private void deleteSelected() private void deleteSelected()
{ {
foreach (var h in selectedBlueprints.ToList()) foreach (var h in selectedBlueprints.ToList())
placementHandler.Delete(h.DrawableObject.HitObject); editorBeatmap.Remove(h.HitObject);
} }
#endregion #endregion

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -11,10 +12,14 @@ using osu.Framework.Input.Events;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class Timeline : ZoomableScrollContainer [Cached(typeof(IDistanceSnapProvider))]
[Cached]
public class Timeline : ZoomableScrollContainer, IDistanceSnapProvider
{ {
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>(); public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>(); public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
@ -162,5 +167,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (trackWasPlaying) if (trackWasPlaying)
adjustableClock.Start(); adjustableClock.Start();
} }
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
{
var targetTime = (position.X / Content.DrawWidth) * track.Length;
return (position, beatSnapProvider.SnapTime(targetTime, targetTime));
}
public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException();
public float DurationToDistance(double referenceTime, double duration) => throw new NotImplementedException();
public double DistanceToDuration(double referenceTime, float distance) => throw new NotImplementedException();
public double GetSnappedDurationFromDistance(double referenceTime, float distance) => throw new NotImplementedException();
public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => throw new NotImplementedException();
} }
} }

View File

@ -0,0 +1,141 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
internal class TimelineBlueprintContainer : BlueprintContainer
{
[Resolved(CanBeNull = true)]
private Timeline timeline { get; set; }
private DragEvent lastDragEvent;
public TimelineBlueprintContainer()
{
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Height = 0.4f;
AddInternal(new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
});
}
protected override void LoadComplete()
{
base.LoadComplete();
DragBox.Alpha = 0;
}
protected override Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };
protected override void OnDrag(DragEvent e)
{
if (timeline != null)
{
var timelineQuad = timeline.ScreenSpaceDrawQuad;
var mouseX = e.ScreenSpaceMousePosition.X;
// scroll if in a drag and dragging outside visible extents
if (mouseX > timelineQuad.TopRight.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
else if (mouseX < timelineQuad.TopLeft.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
}
base.OnDrag(e);
lastDragEvent = e;
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
lastDragEvent = null;
}
protected override void Update()
{
// trigger every frame so drags continue to update selection while playback is scrolling the timeline.
if (IsDragged)
OnDrag(lastDragEvent);
base.Update();
}
protected override SelectionHandler CreateSelectionHandler() => new TimelineSelectionHandler();
protected override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) => new TimelineHitObjectBlueprint(hitObject);
protected override DragBox CreateDragBox(Action<RectangleF> performSelect) => new TimelineDragBox(performSelect);
internal class TimelineSelectionHandler : SelectionHandler
{
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
}
private class TimelineDragBox : DragBox
{
private Vector2 lastMouseDown;
private float localMouseDown;
public TimelineDragBox(Action<RectangleF> performSelect)
: base(performSelect)
{
}
protected override Drawable CreateBox() => new Box
{
RelativeSizeAxes = Axes.Y,
Alpha = 0.3f
};
public override bool HandleDrag(MouseButtonEvent e)
{
// store the original position of the mouse down, as we may be scrolled during selection.
if (lastMouseDown != e.ScreenSpaceMouseDownPosition)
{
lastMouseDown = e.ScreenSpaceMouseDownPosition;
localMouseDown = e.MouseDownPosition.X;
}
float selection1 = localMouseDown;
float selection2 = e.MousePosition.X;
Box.X = Math.Min(selection1, selection2);
Box.Width = Math.Abs(selection1 - selection2);
PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat);
return true;
}
}
protected class TimelineSelectionBlueprintContainer : Container<SelectionBlueprint>
{
protected override Container<SelectionBlueprint> Content { get; }
public TimelineSelectionBlueprintContainer()
{
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
}
}
}
}

View File

@ -0,0 +1,116 @@
// 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 JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineHitObjectBlueprint : SelectionBlueprint
{
private readonly Circle circle;
private readonly Container extensionBar;
[UsedImplicitly]
private readonly Bindable<double> startTime;
public const float THICKNESS = 3;
private const float circle_size = 16;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || circle.ReceivePositionalInputAt(screenSpacePos);
public TimelineHitObjectBlueprint(HitObject hitObject)
: base(hitObject)
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
startTime = hitObject.StartTimeBindable.GetBoundCopy();
startTime.BindValueChanged(time => X = (float)time.NewValue, true);
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
if (hitObject is IHasEndTime)
{
AddInternal(extensionBar = new Container
{
CornerRadius = 2,
Masking = true,
Size = new Vector2(1, THICKNESS),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.X,
Colour = Color4.Black,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
});
}
AddInternal(circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
BorderColour = Color4.Black,
BorderThickness = THICKNESS,
});
}
protected override void Update()
{
base.Update();
// no bindable so we perform this every update
Width = (float)(HitObject.GetEndTime() - HitObject.StartTime);
}
protected override void OnSelected()
{
circle.BorderColour = Color4.Orange;
if (extensionBar != null)
extensionBar.Colour = Color4.Orange;
}
protected override void OnDeselected()
{
circle.BorderColour = Color4.Black;
if (extensionBar != null)
extensionBar.Colour = Color4.Black;
}
public override Quad SelectionQuad
{
get
{
// correctly include the circle in the selection quad region, as it is usually outside the blueprint itself.
var circleQuad = circle.ScreenSpaceDrawQuad;
var actualQuad = ScreenSpaceDrawQuad;
return new Quad(circleQuad.TopLeft, Vector2.ComponentMax(actualQuad.TopRight, circleQuad.TopRight),
circleQuad.BottomLeft, Vector2.ComponentMax(actualQuad.BottomRight, circleQuad.BottomRight));
}
}
public override Vector2 SelectionPoint => ScreenSpaceDrawQuad.TopLeft;
}
}

View File

@ -1,140 +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;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
internal class TimelineHitObjectDisplay : BlueprintContainer
{
private EditorBeatmap beatmap { get; }
private readonly TimelinePart content;
public TimelineHitObjectDisplay(EditorBeatmap beatmap)
{
RelativeSizeAxes = Axes.Both;
this.beatmap = beatmap;
AddInternal(content = new TimelinePart { RelativeSizeAxes = Axes.Both });
}
[BackgroundDependencyLoader]
private void load()
{
foreach (var h in beatmap.HitObjects)
add(h);
beatmap.HitObjectAdded += add;
beatmap.HitObjectRemoved += remove;
beatmap.StartTimeChanged += h =>
{
remove(h);
add(h);
};
}
protected override void LoadComplete()
{
base.LoadComplete();
DragBox.Alpha = 0;
}
private void remove(HitObject h)
{
foreach (var d in content.OfType<TimelineHitObjectRepresentation>().Where(c => c.HitObject == h))
d.Expire();
}
private void add(HitObject h)
{
var yOffset = content.Count(d => d.X == h.StartTime);
content.Add(new TimelineHitObjectRepresentation(h) { Y = -yOffset * TimelineHitObjectRepresentation.THICKNESS });
}
protected override bool OnMouseDown(MouseDownEvent e)
{
base.OnMouseDown(e);
return false; // tempoerary until we correctly handle selections.
}
protected override DragBox CreateDragBox(Action<RectangleF> performSelect) => new NoDragDragBox(performSelect);
internal class NoDragDragBox : DragBox
{
public NoDragDragBox(Action<RectangleF> performSelect)
: base(performSelect)
{
}
public override bool UpdateDrag(MouseButtonEvent e) => false;
}
private class TimelineHitObjectRepresentation : CompositeDrawable
{
public const float THICKNESS = 3;
public readonly HitObject HitObject;
public TimelineHitObjectRepresentation(HitObject hitObject)
{
HitObject = hitObject;
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
Width = (float)(hitObject.GetEndTime() - hitObject.StartTime);
X = (float)hitObject.StartTime;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.X;
if (hitObject is IHasEndTime)
{
AddInternal(new Container
{
CornerRadius = 2,
Masking = true,
Size = new Vector2(1, THICKNESS),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.X,
Colour = Color4.Black,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
});
}
AddInternal(new Circle
{
Size = new Vector2(16),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
AlwaysPresent = true,
Colour = Color4.White,
BorderColour = Color4.Black,
BorderThickness = THICKNESS,
});
}
}
}
}

View File

@ -32,6 +32,6 @@ namespace osu.Game.Screens.Edit.Compose
return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer));
} }
protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(EditorBeatmap); protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer();
} }
} }

View File

@ -30,6 +30,8 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public event Action<HitObject> StartTimeChanged; public event Action<HitObject> StartTimeChanged;
public BindableList<HitObject> SelectedHitObjects { get; } = new BindableList<HitObject>();
public readonly IBeatmap PlayableBeatmap; public readonly IBeatmap PlayableBeatmap;
private readonly BindableBeatDivisor beatDivisor; private readonly BindableBeatDivisor beatDivisor;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private TimelineArea timelineArea; private Container timelineContainer;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor) private void load([CanBeNull] BindableBeatDivisor beatDivisor)
@ -62,11 +62,10 @@ namespace osu.Game.Screens.Edit
{ {
new Drawable[] new Drawable[]
{ {
new Container timelineContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Right = 5 },
Child = timelineArea = CreateTimelineArea()
}, },
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
}, },
@ -100,14 +99,16 @@ namespace osu.Game.Screens.Edit
mainContent.Add(content); mainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint); content.FadeInFromZero(300, Easing.OutQuint);
LoadComponentAsync(CreateTimelineContent(), timelineArea.Add); LoadComponentAsync(new TimelineArea
{
RelativeSizeAxes = Axes.Both,
Child = CreateTimelineContent()
}, timelineContainer.Add);
}); });
} }
protected abstract Drawable CreateMainContent(); protected abstract Drawable CreateMainContent();
protected virtual Drawable CreateTimelineContent() => new Container(); protected virtual Drawable CreateTimelineContent() => new Container();
protected TimelineArea CreateTimelineArea() => new TimelineArea { RelativeSizeAxes = Axes.Both };
} }
} }

View File

@ -69,6 +69,9 @@ namespace osu.Game.Screens.Menu
private ExitConfirmOverlay exitConfirmOverlay; private ExitConfirmOverlay exitConfirmOverlay;
private ParallaxContainer buttonsContainer;
private SongTicker songTicker;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
{ {
@ -89,9 +92,9 @@ namespace osu.Game.Screens.Menu
}); });
} }
AddRangeInternal(new Drawable[] AddRangeInternal(new[]
{ {
new ParallaxContainer buttonsContainer = new ParallaxContainer
{ {
ParallaxAmount = 0.01f, ParallaxAmount = 0.01f,
Children = new Drawable[] Children = new Drawable[]
@ -107,6 +110,13 @@ namespace osu.Game.Screens.Menu
} }
}, },
sideFlashes = new MenuSideFlashes(), sideFlashes = new MenuSideFlashes(),
songTicker = new SongTicker
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 }
},
exitConfirmOverlay?.CreateProxy() ?? Drawable.Empty()
}); });
buttons.StateChanged += state => buttons.StateChanged += state =>
@ -190,7 +200,7 @@ namespace osu.Game.Screens.Menu
buttons.State = ButtonSystemState.TopLevel; buttons.State = ButtonSystemState.TopLevel;
this.FadeIn(FADE_IN_DURATION, Easing.OutQuint); this.FadeIn(FADE_IN_DURATION, Easing.OutQuint);
this.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint); buttonsContainer.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint);
sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint); sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint);
} }
@ -227,7 +237,7 @@ namespace osu.Game.Screens.Menu
buttons.State = ButtonSystemState.EnteringMode; buttons.State = ButtonSystemState.EnteringMode;
this.FadeOut(FADE_OUT_DURATION, Easing.InSine); this.FadeOut(FADE_OUT_DURATION, Easing.InSine);
this.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine); buttonsContainer.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine);
sideFlashes.FadeOut(64, Easing.OutQuint); sideFlashes.FadeOut(64, Easing.OutQuint);
} }
@ -262,6 +272,9 @@ namespace osu.Game.Screens.Menu
} }
buttons.State = ButtonSystemState.Exit; buttons.State = ButtonSystemState.Exit;
songTicker.Hide();
this.FadeOut(3000); this.FadeOut(3000);
return base.OnExiting(next); return base.OnExiting(next);
} }

View File

@ -0,0 +1,72 @@
// 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.Sprites;
using osuTK;
using osu.Game.Graphics;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Framework.Localisation;
namespace osu.Game.Screens.Menu
{
public class SongTicker : Container
{
private const int fade_duration = 800;
[Resolved]
private Bindable<WorkingBeatmap> beatmap { get; set; }
private readonly OsuSpriteText title, artist;
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public SongTicker()
{
AutoSizeAxes = Axes.Both;
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
Children = new Drawable[]
{
title = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light, italics: true)
},
artist = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(size: 16)
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindValueChanged(_ => Scheduler.AddOnce(show), true);
}
private void show()
{
var metadata = beatmap.Value.Metadata;
title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title));
artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist));
this.FadeInFromZero(fade_duration / 2f)
.Delay(4000)
.Then().FadeOut(fade_duration);
}
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Screens.Select
{ {
private const float shear_width = 36.75f; private const float shear_width = 36.75f;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGED_CONTAINER_SIZE.Y, 0); private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0);
private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>(); private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();

View File

@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(Beatmap.Metadata.Artist) || match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(Beatmap.Metadata.Artist) ||
criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode); criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode);
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(Beatmap.StarDifficulty);
if (match) if (match)
{ {
var terms = new List<string>(); var terms = new List<string>();

View File

@ -42,9 +42,15 @@ namespace osu.Game.Screens.Select
Group = groupMode.Value, Group = groupMode.Value,
Sort = sortMode.Value, Sort = sortMode.Value,
AllowConvertedBeatmaps = showConverted.Value, AllowConvertedBeatmaps = showConverted.Value,
Ruleset = ruleset.Value Ruleset = ruleset.Value,
}; };
if (!minimumStars.IsDefault)
criteria.UserStarDifficulty.Min = minimumStars.Value;
if (!maximumStars.IsDefault)
criteria.UserStarDifficulty.Max = maximumStars.Value;
FilterQueryParser.ApplyQueries(criteria, query); FilterQueryParser.ApplyQueries(criteria, query);
return criteria; return criteria;
} }
@ -142,7 +148,9 @@ namespace osu.Game.Screens.Select
private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>(); private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
private Bindable<bool> showConverted; private readonly Bindable<bool> showConverted = new Bindable<bool>();
private readonly Bindable<double> minimumStars = new Bindable<double>();
private readonly Bindable<double> maximumStars = new Bindable<double>();
public readonly Box Background; public readonly Box Background;
@ -151,9 +159,15 @@ namespace osu.Game.Screens.Select
{ {
sortTabs.AccentColour = colours.GreenLight; sortTabs.AccentColour = colours.GreenLight;
showConverted = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps); config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
showConverted.ValueChanged += _ => updateCriteria(); showConverted.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
minimumStars.ValueChanged += _ => updateCriteria();
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
maximumStars.ValueChanged += _ => updateCriteria();
ruleset.BindTo(parentRuleset); ruleset.BindTo(parentRuleset);
ruleset.BindValueChanged(_ => updateCriteria()); ruleset.BindValueChanged(_ => updateCriteria());

View File

@ -26,6 +26,12 @@ namespace osu.Game.Screens.Select
public OptionalTextFilter Creator; public OptionalTextFilter Creator;
public OptionalTextFilter Artist; public OptionalTextFilter Artist;
public OptionalRange<double> UserStarDifficulty = new OptionalRange<double>
{
IsLowerInclusive = true,
IsUpperInclusive = true
};
public string[] SearchTerms = Array.Empty<string>(); public string[] SearchTerms = Array.Empty<string>();
public RulesetInfo Ruleset; public RulesetInfo Ruleset;

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select
{ {
public abstract class SongSelect : OsuScreen, IKeyBindingHandler<GlobalAction> public abstract class SongSelect : OsuScreen, IKeyBindingHandler<GlobalAction>
{ {
public static readonly Vector2 WEDGED_CONTAINER_SIZE = new Vector2(0.5f, 245); public static readonly float WEDGE_HEIGHT = 245;
protected const float BACKGROUND_BLUR = 20; protected const float BACKGROUND_BLUR = 20;
private const float left_area_padding = 20; private const float left_area_padding = 20;
@ -96,98 +96,116 @@ namespace osu.Game.Screens.Select
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new ParallaxContainer
{
Masking = true,
ParallaxAmount = 0.005f,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new WedgeBackground
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -150 },
Size = new Vector2(WEDGED_CONTAINER_SIZE.X, 1),
}
}
},
new Container
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(WEDGED_CONTAINER_SIZE.X, 1),
Padding = new MarginPadding
{
Bottom = Footer.HEIGHT,
Top = WEDGED_CONTAINER_SIZE.Y + left_area_padding,
Left = left_area_padding,
Right = left_area_padding * 2,
},
Child = BeatmapDetails = new BeatmapDetailArea
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 10, Right = 5 },
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 2, //avoid horizontal masking so the panels don't clip when screen stack is pushed.
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Top = FilterControl.HEIGHT,
Bottom = Footer.HEIGHT
},
Child = Carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1 - WEDGED_CONTAINER_SIZE.X, 1),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
},
},
FilterControl = new FilterControl
{
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
FilterChanged = ApplyFilterToCarousel,
Background = { Width = 2 },
},
}
},
},
beatmapInfoWedge = new BeatmapInfoWedge
{
Size = WEDGED_CONTAINER_SIZE,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding
{
Top = left_area_padding,
Right = left_area_padding,
},
},
new ResetScrollContainer(() => Carousel.ScrollToSelected()) new ResetScrollContainer(() => Carousel.ScrollToSelected())
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = 250, Width = 250,
} },
new VerticalMaskingContainer
{
Children = new Drawable[]
{
new GridContainer // used for max width implementation
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 850),
},
Content = new[]
{
new Drawable[]
{
new ParallaxContainer
{
ParallaxAmount = 0.005f,
RelativeSizeAxes = Axes.Both,
Child = new WedgeBackground
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -150 },
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Top = FilterControl.HEIGHT,
Bottom = Footer.HEIGHT
},
Child = Carousel = new BeatmapCarousel
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
SelectionChanged = updateSelectedBeatmap,
BeatmapSetsChanged = carouselBeatmapsLoaded,
},
}
},
}
},
FilterControl = new FilterControl
{
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
FilterChanged = ApplyFilterToCarousel,
Background = { Width = 2 },
},
new GridContainer // used for max width implementation
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 0.5f, maxSize: 650),
},
Content = new[]
{
new Drawable[]
{
new Container
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
beatmapInfoWedge = new BeatmapInfoWedge
{
Height = WEDGE_HEIGHT,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding
{
Top = left_area_padding,
Right = left_area_padding,
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Bottom = Footer.HEIGHT,
Top = WEDGE_HEIGHT + left_area_padding,
Left = left_area_padding,
Right = left_area_padding * 2,
},
Child = BeatmapDetails = new BeatmapDetailArea
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 10, Right = 5 },
},
},
}
},
},
}
}
}
},
}); });
if (ShowFooter) if (ShowFooter)
@ -705,6 +723,29 @@ namespace osu.Game.Screens.Select
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
private class VerticalMaskingContainer : Container
{
private const float panel_overflow = 1.2f;
protected override Container<Drawable> Content { get; }
public VerticalMaskingContainer()
{
RelativeSizeAxes = Axes.Both;
Masking = true;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Width = panel_overflow; //avoid horizontal masking so the panels don't clip when screen stack is pushed.
InternalChild = Content = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 1 / panel_overflow,
};
}
}
private class ResetScrollContainer : Container private class ResetScrollContainer : Container
{ {
private readonly Action onHoverAction; private readonly Action onHoverAction;

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual
}); });
} }
protected void AddBlueprint(SelectionBlueprint blueprint) protected void AddBlueprint(OverlaySelectionBlueprint blueprint)
{ {
Add(blueprint.With(d => Add(blueprint.With(d =>
{ {

View File

@ -23,7 +23,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.122.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.125.0" />
<PackageReference Include="Sentry" Version="1.2.0" /> <PackageReference Include="Sentry" Version="1.2.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -74,7 +74,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1230.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.122.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.125.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
@ -82,7 +82,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.122.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.125.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />