1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-07 22:22:59 +08:00

Merge branch 'master' into async-deadlock-safety

This commit is contained in:
Dean Herbert 2022-01-07 17:28:59 +09:00
commit 3cd996eb4a
34 changed files with 972 additions and 354 deletions

View File

@ -52,16 +52,25 @@ namespace osu.Game.Rulesets.Catch.Edit
return true; return true;
} }
public override bool HandleFlip(Direction direction) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{ {
if (SelectedItems.Count == 0)
return false;
// This could be implemented in the future if there's a requirement for it.
if (direction == Direction.Vertical)
return false;
var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems); var selectionRange = CatchHitObjectUtils.GetPositionRange(SelectedItems);
bool changed = false; bool changed = false;
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
if (h is CatchHitObject catchObject) if (h is CatchHitObject catchObject)
changed |= handleFlip(selectionRange, catchObject); changed |= handleFlip(selectionRange, catchObject, flipOverOrigin);
}); });
return changed; return changed;
} }
@ -116,7 +125,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return Math.Clamp(deltaX, lowerBound, upperBound); return Math.Clamp(deltaX, lowerBound, upperBound);
} }
private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject) private bool handleFlip(PositionRange selectionRange, CatchHitObject hitObject, bool flipOverOrigin)
{ {
switch (hitObject) switch (hitObject)
{ {
@ -124,7 +133,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return false; return false;
case JuiceStream juiceStream: case JuiceStream juiceStream:
juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX); juiceStream.OriginalX = getFlippedPosition(juiceStream.OriginalX);
foreach (var point in juiceStream.Path.ControlPoints) foreach (var point in juiceStream.Path.ControlPoints)
point.Position *= new Vector2(-1, 1); point.Position *= new Vector2(-1, 1);
@ -133,9 +142,11 @@ namespace osu.Game.Rulesets.Catch.Edit
return true; return true;
default: default:
hitObject.OriginalX = selectionRange.GetFlippedPosition(hitObject.OriginalX); hitObject.OriginalX = getFlippedPosition(hitObject.OriginalX);
return true; return true;
} }
float getFlippedPosition(float original) => flipOverOrigin ? CatchPlayfield.WIDTH - original : selectionRange.GetFlippedPosition(original);
} }
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Game.Extensions;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -84,18 +85,28 @@ namespace osu.Game.Rulesets.Osu.Edit
return true; return true;
} }
public override bool HandleFlip(Direction direction) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects); var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
bool didFlip = false;
foreach (var h in hitObjects) foreach (var h in hitObjects)
{ {
h.Position = GetFlippedPosition(direction, selectedObjectsQuad, h.Position); var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
h.Position = flippedPosition;
didFlip = true;
}
if (h is Slider slider) if (h is Slider slider)
{ {
didFlip = true;
foreach (var point in slider.Path.ControlPoints) foreach (var point in slider.Path.ControlPoints)
{ {
point.Position = new Vector2( point.Position = new Vector2(
@ -106,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
return true; return didFlip;
} }
public override bool HandleScale(Vector2 scale, Anchor reference) public override bool HandleScale(Vector2 scale, Anchor reference)

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -36,7 +37,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new NonImportingPlayer(false); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeImportingPlayer(false);
protected new FakeImportingPlayer Player => (FakeImportingPlayer)base.Player;
protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
@ -207,6 +210,25 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false); AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
} }
[Test]
public void TestSubmissionOnExitDuringImport()
{
prepareTokenResponse(true);
createPlayerTest();
AddStep("block imports", () => Player.AllowImportCompletion.Wait());
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
addFakeHit();
AddUntilStep("wait for import to start", () => Player.ScoreImportStarted);
AddStep("exit", () => Player.Exit());
AddStep("allow import to proceed", () => Player.AllowImportCompletion.Release(1));
AddAssert("ensure submission", () => Player.SubmittedScore != null && Player.ImportedScore != null);
}
[Test] [Test]
public void TestNoSubmissionOnLocalBeatmap() public void TestNoSubmissionOnLocalBeatmap()
{ {
@ -288,15 +310,26 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
private class NonImportingPlayer : TestPlayer protected class FakeImportingPlayer : TestPlayer
{ {
public NonImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) public bool ScoreImportStarted { get; set; }
public SemaphoreSlim AllowImportCompletion { get; }
public Score ImportedScore { get; private set; }
public FakeImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base(allowPause, showResults, pauseOnFocusLost) : base(allowPause, showResults, pauseOnFocusLost)
{ {
AllowImportCompletion = new SemaphoreSlim(1);
} }
protected override Task ImportScore(Score score) protected override async Task ImportScore(Score score)
{ {
ScoreImportStarted = true;
await AllowImportCompletion.WaitAsync().ConfigureAwait(false);
ImportedScore = score;
// It was discovered that Score members could sometimes be half-populated. // It was discovered that Score members could sometimes be half-populated.
// In particular, the RulesetID property could be set to 0 even on non-osu! maps. // In particular, the RulesetID property could be set to 0 even on non-osu! maps.
// We want to test that the state of that property is consistent in this test. // We want to test that the state of that property is consistent in this test.
@ -311,8 +344,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context, // In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
// RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3. // RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
// //
// For the above reasons, importing is disabled in this test. // For the above reasons, actual importing is disabled in this test.
return Task.CompletedTask;
} }
} }
} }

View File

@ -107,19 +107,31 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden); AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
} }
[Test]
public void TestCorrectOldContentExpiration()
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(100);
AddStep("show more results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 30).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(30);
}
[Test] [Test]
public void TestCardSizeSwitching() public void TestCardSizeSwitching()
{ {
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray())); AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(); assertAllCardsOfType<BeatmapCardNormal>(100);
setCardSize(BeatmapCardSize.Extra); setCardSize(BeatmapCardSize.Extra);
assertAllCardsOfType<BeatmapCardExtra>(); assertAllCardsOfType<BeatmapCardExtra>(100);
setCardSize(BeatmapCardSize.Normal); setCardSize(BeatmapCardSize.Normal);
assertAllCardsOfType<BeatmapCardNormal>(); assertAllCardsOfType<BeatmapCardNormal>(100);
AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
@ -323,13 +335,12 @@ namespace osu.Game.Tests.Visual.Online
private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize); private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize);
private void assertAllCardsOfType<T>() private void assertAllCardsOfType<T>(int expectedCount)
where T : BeatmapCard => where T : BeatmapCard =>
AddUntilStep($"all loaded beatmap cards are {typeof(T)}", () => AddUntilStep($"all loaded beatmap cards are {typeof(T)}", () =>
{ {
int loadedCorrectCount = this.ChildrenOfType<BeatmapCard>().Count(card => card.IsLoaded && card.GetType() == typeof(T)); int loadedCorrectCount = this.ChildrenOfType<BeatmapCard>().Count(card => card.IsLoaded && card.GetType() == typeof(T));
int totalCount = this.ChildrenOfType<BeatmapCard>().Count(); return loadedCorrectCount > 0 && loadedCorrectCount == expectedCount;
return loadedCorrectCount > 0 && loadedCorrectCount == totalCount;
}); });
} }
} }

View File

@ -0,0 +1,79 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface.PageSelector;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestScenePageSelector : OsuTestScene
{
[Cached]
private OverlayColourProvider provider { get; } = new OverlayColourProvider(OverlayColourScheme.Green);
private readonly PageSelector pageSelector;
public TestScenePageSelector()
{
AddRange(new Drawable[]
{
pageSelector = new PageSelector
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
});
}
[Test]
public void TestOmittedPages()
{
setAvailablePages(100);
selectPageIndex(0);
checkVisiblePageNumbers(new[] { 1, 2, 3, 100 });
selectPageIndex(6);
checkVisiblePageNumbers(new[] { 1, 5, 6, 7, 8, 9, 100 });
selectPageIndex(49);
checkVisiblePageNumbers(new[] { 1, 48, 49, 50, 51, 52, 100 });
selectPageIndex(99);
checkVisiblePageNumbers(new[] { 1, 98, 99, 100 });
}
[Test]
public void TestResetCurrentPage()
{
setAvailablePages(10);
selectPageIndex(6);
setAvailablePages(11);
AddAssert("Page 1 is current", () => pageSelector.CurrentPage.Value == 0);
}
[Test]
public void TestOutOfBoundsSelection()
{
setAvailablePages(10);
selectPageIndex(11);
AddAssert("Page 10 is current", () => pageSelector.CurrentPage.Value == pageSelector.AvailablePages.Value - 1);
selectPageIndex(-1);
AddAssert("Page 1 is current", () => pageSelector.CurrentPage.Value == 0);
}
private void checkVisiblePageNumbers(int[] expected) => AddAssert($"Sequence is {string.Join(',', expected.Select(i => i.ToString()))}", () => pageSelector.ChildrenOfType<PageSelectorPageButton>().Select(p => p.PageNumber).SequenceEqual(expected));
private void selectPageIndex(int pageIndex) =>
AddStep($"Select page {pageIndex}", () => pageSelector.CurrentPage.Value = pageIndex);
private void setAvailablePages(int availablePages) =>
AddStep($"Set available pages to {availablePages}", () => pageSelector.AvailablePages.Value = availablePages);
}
}

View File

@ -3,16 +3,13 @@
#nullable enable #nullable enable
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
@ -156,54 +153,5 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Hollow = true, Hollow = true,
}, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); }, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
} }
private class ExpandedContentScrollContainer : OsuScrollContainer
{
public ExpandedContentScrollContainer()
{
ScrollbarVisible = false;
}
protected override void Update()
{
base.Update();
Height = Math.Min(Content.DrawHeight, 400);
}
private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize);
protected override bool OnDragStart(DragStartEvent e)
{
if (!allowScroll)
return false;
return base.OnDragStart(e);
}
protected override void OnDrag(DragEvent e)
{
if (!allowScroll)
return;
base.OnDrag(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (!allowScroll)
return;
base.OnDragEnd(e);
}
protected override bool OnScroll(ScrollEvent e)
{
if (!allowScroll)
return false;
return base.OnScroll(e);
}
}
} }
} }

View File

@ -0,0 +1,61 @@
// 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.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class ExpandedContentScrollContainer : OsuScrollContainer
{
public const float HEIGHT = 200;
public ExpandedContentScrollContainer()
{
ScrollbarVisible = false;
}
protected override void Update()
{
base.Update();
Height = Math.Min(Content.DrawHeight, HEIGHT);
}
private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize);
protected override bool OnDragStart(DragStartEvent e)
{
if (!allowScroll)
return false;
return base.OnDragStart(e);
}
protected override void OnDrag(DragEvent e)
{
if (!allowScroll)
return;
base.OnDrag(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (!allowScroll)
return;
base.OnDragEnd(e);
}
protected override bool OnScroll(ScrollEvent e)
{
if (!allowScroll)
return false;
return base.OnScroll(e);
}
}
}

View File

@ -1,6 +1,8 @@
// 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.Globalization;
using Newtonsoft.Json.Linq;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -16,7 +18,7 @@ namespace osu.Game.Extensions
{ {
cursor?.Properties.ForEach(x => cursor?.Properties.ForEach(x =>
{ {
webRequest.AddParameter("cursor[" + x.Key + "]", x.Value.ToString()); webRequest.AddParameter("cursor[" + x.Key + "]", (x.Value as JValue)?.ToString(CultureInfo.InvariantCulture) ?? x.Value.ToString());
}); });
} }
} }

View File

@ -0,0 +1,33 @@
// 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 osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface.PageSelector
{
internal class PageEllipsis : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
InternalChildren = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = "...",
Colour = colourProvider.Light3,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
}
}

View File

@ -0,0 +1,102 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
namespace osu.Game.Graphics.UserInterface.PageSelector
{
public class PageSelector : CompositeDrawable
{
public readonly BindableInt CurrentPage = new BindableInt { MinValue = 0, };
public readonly BindableInt AvailablePages = new BindableInt(1) { MinValue = 1, };
private readonly FillFlowContainer itemsFlow;
private readonly PageSelectorPrevNextButton previousPageButton;
private readonly PageSelectorPrevNextButton nextPageButton;
public PageSelector()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
previousPageButton = new PageSelectorPrevNextButton(false, "prev")
{
Action = () => CurrentPage.Value -= 1,
},
itemsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
},
nextPageButton = new PageSelectorPrevNextButton(true, "next")
{
Action = () => CurrentPage.Value += 1
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentPage.BindValueChanged(_ => Scheduler.AddOnce(redraw));
AvailablePages.BindValueChanged(_ =>
{
CurrentPage.Value = 0;
// AddOnce as the reset of CurrentPage may also trigger a redraw.
Scheduler.AddOnce(redraw);
}, true);
}
private void redraw()
{
if (CurrentPage.Value >= AvailablePages.Value)
{
CurrentPage.Value = AvailablePages.Value - 1;
return;
}
previousPageButton.Enabled.Value = CurrentPage.Value != 0;
nextPageButton.Enabled.Value = CurrentPage.Value < AvailablePages.Value - 1;
itemsFlow.Clear();
int totalPages = AvailablePages.Value;
bool lastWasEllipsis = false;
for (int i = 0; i < totalPages; i++)
{
int pageIndex = i;
bool shouldShowPage = pageIndex == 0 || pageIndex == totalPages - 1 || Math.Abs(pageIndex - CurrentPage.Value) <= 2;
if (shouldShowPage)
{
lastWasEllipsis = false;
itemsFlow.Add(new PageSelectorPageButton(pageIndex + 1)
{
Action = () => CurrentPage.Value = pageIndex,
Selected = CurrentPage.Value == pageIndex,
});
}
else if (!lastWasEllipsis)
{
lastWasEllipsis = true;
itemsFlow.Add(new PageEllipsis());
}
}
}
}
}

View File

@ -0,0 +1,71 @@
// 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.Containers;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Framework.Input.Events;
using JetBrains.Annotations;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface.PageSelector
{
public abstract class PageSelectorButton : OsuClickableContainer
{
protected const int DURATION = 200;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; }
protected Box Background;
protected PageSelectorButton()
{
AutoSizeAxes = Axes.X;
Height = 20;
}
[BackgroundDependencyLoader]
private void load()
{
Add(new CircularContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
Children = new[]
{
Background = new Box
{
RelativeSizeAxes = Axes.Both
},
CreateContent().With(content =>
{
content.Anchor = Anchor.Centre;
content.Origin = Anchor.Centre;
content.Margin = new MarginPadding { Horizontal = 10 };
})
}
});
}
[NotNull]
protected abstract Drawable CreateContent();
protected override bool OnHover(HoverEvent e)
{
UpdateHoverState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
UpdateHoverState();
}
protected abstract void UpdateHoverState();
}
}

View File

@ -0,0 +1,70 @@
// 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.Bindables;
using osu.Framework.Allocation;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface.PageSelector
{
public class PageSelectorPageButton : PageSelectorButton
{
private readonly BindableBool selected = new BindableBool();
public bool Selected
{
set => selected.Value = value;
}
public int PageNumber { get; }
private OsuSpriteText text;
public PageSelectorPageButton(int pageNumber)
{
PageNumber = pageNumber;
Action = () =>
{
if (!selected.Value)
selected.Value = true;
};
}
protected override Drawable CreateContent() => text = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = PageNumber.ToString(),
};
[BackgroundDependencyLoader]
private void load()
{
Background.Colour = ColourProvider.Highlight1;
Background.Alpha = 0;
}
protected override void LoadComplete()
{
base.LoadComplete();
selected.BindValueChanged(onSelectedChanged, true);
}
private void onSelectedChanged(ValueChangedEvent<bool> selected)
{
Background.FadeTo(selected.NewValue ? 1 : 0, DURATION, Easing.OutQuint);
text.FadeColour(selected.NewValue ? ColourProvider.Dark4 : ColourProvider.Light3, DURATION, Easing.OutQuint);
text.Font = text.Font.With(weight: IsHovered ? FontWeight.SemiBold : FontWeight.Regular);
}
protected override void UpdateHoverState()
{
if (selected.Value)
return;
text.FadeColour(IsHovered ? ColourProvider.Light2 : ColourProvider.Light1, DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,78 @@
// 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.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.UserInterface.PageSelector
{
public class PageSelectorPrevNextButton : PageSelectorButton
{
private readonly bool rightAligned;
private readonly string text;
private SpriteIcon icon;
private OsuSpriteText name;
public PageSelectorPrevNextButton(bool rightAligned, string text)
{
this.rightAligned = rightAligned;
this.text = text;
}
protected override Drawable CreateContent() => new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(3, 0),
Children = new Drawable[]
{
name = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12),
Anchor = rightAligned ? Anchor.CentreLeft : Anchor.CentreRight,
Origin = rightAligned ? Anchor.CentreLeft : Anchor.CentreRight,
Text = text.ToUpper(),
},
icon = new SpriteIcon
{
Icon = rightAligned ? FontAwesome.Solid.ChevronRight : FontAwesome.Solid.ChevronLeft,
Size = new Vector2(8),
Anchor = rightAligned ? Anchor.CentreLeft : Anchor.CentreRight,
Origin = rightAligned ? Anchor.CentreLeft : Anchor.CentreRight,
},
}
},
}
};
[BackgroundDependencyLoader]
private void load()
{
Background.Colour = ColourProvider.Dark4;
name.Colour = icon.Colour = ColourProvider.Light1;
}
protected override void LoadComplete()
{
base.LoadComplete();
Enabled.BindValueChanged(enabled => Background.FadeTo(enabled.NewValue ? 1 : 0.5f, DURATION), true);
}
protected override void UpdateHoverState() =>
Background.FadeColour(IsHovered ? ColourProvider.Dark3 : ColourProvider.Dark4, DURATION, Easing.OutQuint);
}
}

View File

@ -77,6 +77,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode), new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay), new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
}; };
public IEnumerable<KeyBinding> InGameKeyBindings => new[] public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -292,6 +294,12 @@ namespace osu.Game.Input.Bindings
EditorCycleGridDisplayMode, EditorCycleGridDisplayMode,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestGameplay))]
EditorTestGameplay EditorTestGameplay,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorFlipHorizontally))]
EditorFlipHorizontally,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorFlipVertically))]
EditorFlipVertically,
} }
} }

View File

@ -229,6 +229,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorNudgeRight => new TranslatableString(getKey(@"editor_nudge_right"), @"Nudge selection right"); public static LocalisableString EditorNudgeRight => new TranslatableString(getKey(@"editor_nudge_right"), @"Nudge selection right");
/// <summary>
/// "Flip selection horizontally"
/// </summary>
public static LocalisableString EditorFlipHorizontally => new TranslatableString(getKey(@"editor_flip_horizontally"), @"Flip selection horizontally");
/// <summary>
/// "Flip selection vertically"
/// </summary>
public static LocalisableString EditorFlipVertically => new TranslatableString(getKey(@"editor_flip_vertically"), @"Flip selection vertically");
/// <summary> /// <summary>
/// "Toggle skin editor" /// "Toggle skin editor"
/// </summary> /// </summary>

View File

@ -79,7 +79,6 @@ namespace osu.Game.Overlays
Padding = new MarginPadding { Horizontal = 20 }, Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[] Children = new Drawable[]
{ {
foundContent = new FillFlowContainer<BeatmapCard>(),
notFoundContent = new NotFoundDrawable(), notFoundContent = new NotFoundDrawable(),
supporterRequiredContent = new SupporterRequiredDrawable(), supporterRequiredContent = new SupporterRequiredDrawable(),
} }
@ -140,7 +139,7 @@ namespace osu.Game.Overlays
if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters) if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters)
{ {
supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed); supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed);
addContentToPlaceholder(supporterRequiredContent); addContentToResultsArea(supporterRequiredContent);
return; return;
} }
@ -151,13 +150,13 @@ namespace osu.Game.Overlays
//No matches case //No matches case
if (!newCards.Any()) if (!newCards.Any())
{ {
addContentToPlaceholder(notFoundContent); addContentToResultsArea(notFoundContent);
return; return;
} }
var content = createCardContainerFor(newCards); var content = createCardContainerFor(newCards);
panelLoadTask = LoadComponentAsync(foundContent = content, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token); panelLoadTask = LoadComponentAsync(foundContent = content, addContentToResultsArea, (cancellationToken = new CancellationTokenSource()).Token);
} }
else else
{ {
@ -186,13 +185,17 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10), Spacing = new Vector2(10),
Alpha = 0, Alpha = 0,
Margin = new MarginPadding { Vertical = 15 }, Margin = new MarginPadding
{
Vertical = 15,
Bottom = ExpandedContentScrollContainer.HEIGHT
},
ChildrenEnumerable = newCards ChildrenEnumerable = newCards
}; };
return content; return content;
} }
private void addContentToPlaceholder(Drawable content) private void addContentToResultsArea(Drawable content)
{ {
Loading.Hide(); Loading.Hide();
lastFetchDisplayedTime = Time.Current; lastFetchDisplayedTime = Time.Current;
@ -204,37 +207,27 @@ namespace osu.Game.Overlays
if (lastContent != null) if (lastContent != null)
{ {
lastContent.FadeOut(100, Easing.OutQuint); lastContent.FadeOut();
if (!isPlaceholderContent(lastContent))
// Consider the case when the new content is smaller than the last content. lastContent.Expire();
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
var sequence = lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
if (lastContent == foundContent)
{
sequence.Then().Schedule(() =>
{
foundContent.Expire();
foundContent = null;
});
}
} }
if (!content.IsAlive) if (!content.IsAlive)
panelTarget.Add(content); panelTarget.Add(content);
content.FadeInFromZero(200, Easing.OutQuint); content.FadeInFromZero();
currentContent = content; currentContent = content;
// currentContent may be one of the placeholders, and still have BypassAutoSizeAxes set to Y from the last fade-out.
// restore to the initial state.
currentContent.BypassAutoSizeAxes = Axes.None;
} }
/// <summary>
/// Whether <paramref name="drawable"/> is a static placeholder reused multiple times by this overlay.
/// </summary>
private bool isPlaceholderContent(Drawable drawable)
=> drawable == notFoundContent || drawable == supporterRequiredContent;
private void onCardSizeChanged() private void onCardSizeChanged()
{ {
if (foundContent == null || !foundContent.Any()) if (foundContent?.IsAlive != true || !foundContent.Any())
return; return;
Loading.Show(); Loading.Show();
@ -259,10 +252,6 @@ namespace osu.Game.Overlays
public class NotFoundDrawable : CompositeDrawable public class NotFoundDrawable : CompositeDrawable
{ {
// required for scheduled tasks to complete correctly
// (see `addContentToPlaceholder()` and the scheduled `BypassAutoSizeAxes` set during fade-out in outer class above)
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public NotFoundDrawable() public NotFoundDrawable()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -307,10 +296,6 @@ namespace osu.Game.Overlays
// (https://github.com/ppy/osu-framework/issues/4530) // (https://github.com/ppy/osu-framework/issues/4530)
public class SupporterRequiredDrawable : CompositeDrawable public class SupporterRequiredDrawable : CompositeDrawable
{ {
// required for scheduled tasks to complete correctly
// (see `addContentToPlaceholder()` and the scheduled `BypassAutoSizeAxes` set during fade-out in outer class above)
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private LinkFlowContainer supporterRequiredText; private LinkFlowContainer supporterRequiredText;
public SupporterRequiredDrawable() public SupporterRequiredDrawable()

View File

@ -1,47 +1,46 @@
// 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;
using System.Linq; using System.Linq;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays
{ {
public class Sidebar : Container<SidebarIconButton>, IStateful<ExpandedState> public abstract class ExpandingButtonContainer : Container, IStateful<ExpandedState>
{ {
private readonly Box background; private readonly float contractedWidth;
private readonly FillFlowContainer<SidebarIconButton> content; private readonly float expandedWidth;
public const float DEFAULT_WIDTH = 70;
public const int EXPANDED_WIDTH = 200;
public event Action<ExpandedState> StateChanged; public event Action<ExpandedState> StateChanged;
protected override Container<SidebarIconButton> Content => content; protected override Container<Drawable> Content => FillFlow;
public Sidebar() protected FillFlowContainer FillFlow { get; }
protected ExpandingButtonContainer(float contractedWidth, float expandedWidth)
{ {
this.contractedWidth = contractedWidth;
this.expandedWidth = expandedWidth;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = contractedWidth;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
background = new Box
{
Colour = OsuColour.Gray(0.02f),
RelativeSizeAxes = Axes.Both,
},
new SidebarScrollContainer new SidebarScrollContainer
{ {
Children = new[] Children = new[]
{ {
content = new FillFlowContainer<SidebarIconButton> FillFlow = new FillFlowContainer
{ {
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
@ -54,12 +53,6 @@ namespace osu.Game.Overlays.Settings
}; };
} }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background5;
}
private ScheduledDelegate expandEvent; private ScheduledDelegate expandEvent;
private ExpandedState state; private ExpandedState state;
@ -72,7 +65,7 @@ namespace osu.Game.Overlays.Settings
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
expandEvent?.Cancel(); expandEvent?.Cancel();
lastHoveredButton = null; hoveredButton = null;
State = ExpandedState.Contracted; State = ExpandedState.Contracted;
base.OnHoverLost(e); base.OnHoverLost(e);
@ -107,11 +100,11 @@ namespace osu.Game.Overlays.Settings
switch (state) switch (state)
{ {
default: default:
this.ResizeTo(new Vector2(DEFAULT_WIDTH, Height), 500, Easing.OutQuint); this.ResizeTo(new Vector2(contractedWidth, Height), 500, Easing.OutQuint);
break; break;
case ExpandedState.Expanded: case ExpandedState.Expanded:
this.ResizeTo(new Vector2(EXPANDED_WIDTH, Height), 500, Easing.OutQuint); this.ResizeTo(new Vector2(expandedWidth, Height), 500, Easing.OutQuint);
break; break;
} }
@ -119,24 +112,24 @@ namespace osu.Game.Overlays.Settings
} }
} }
private Drawable lastHoveredButton; private Drawable hoveredButton;
private Drawable hoveredButton => content.Children.FirstOrDefault(c => c.IsHovered);
private void queueExpandIfHovering() private void queueExpandIfHovering()
{ {
// only expand when we hover a different button. // if the same button is hovered, let the scheduled expand play out..
if (lastHoveredButton == hoveredButton) return; if (hoveredButton?.IsHovered == true)
return;
if (!IsHovered) return; // ..otherwise check whether a new button is hovered, and if so, queue a new hover operation.
if (State != ExpandedState.Expanded) // usually we wouldn't use ChildrenOfType in implementations, but this is the simplest way
{ // to handle cases like the editor where the buttons may be nested within a child hierarchy.
expandEvent?.Cancel(); hoveredButton = FillFlow.ChildrenOfType<OsuButton>().FirstOrDefault(c => c.IsHovered);
expandEvent?.Cancel();
if (hoveredButton?.IsHovered == true && State != ExpandedState.Expanded)
expandEvent = Scheduler.AddDelayed(() => State = ExpandedState.Expanded, 750); expandEvent = Scheduler.AddDelayed(() => State = ExpandedState.Expanded, 750);
}
lastHoveredButton = hoveredButton;
} }
} }

View File

@ -140,6 +140,7 @@ namespace osu.Game.Overlays.Rankings
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Bottom = ExpandedContentScrollContainer.HEIGHT },
Spacing = new Vector2(10), Spacing = new Vector2(10),
Children = response.BeatmapSets.Select(b => new BeatmapCardNormal(b) Children = response.BeatmapSets.Select(b => new BeatmapCardNormal(b)
{ {

View File

@ -0,0 +1,31 @@
// 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.Shapes;
namespace osu.Game.Overlays.Settings
{
public class SettingsSidebar : ExpandingButtonContainer
{
public const float DEFAULT_WIDTH = 70;
public const int EXPANDED_WIDTH = 200;
public SettingsSidebar()
: base(DEFAULT_WIDTH, EXPANDED_WIDTH)
{
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
AddInternal(new Box
{
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
});
}
}
}

View File

@ -62,14 +62,14 @@ namespace osu.Game.Overlays.Settings
{ {
textIconContent = new Container textIconContent = new Container
{ {
Width = Sidebar.DEFAULT_WIDTH, Width = SettingsSidebar.DEFAULT_WIDTH,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Colour = OsuColour.Gray(0.6f), Colour = OsuColour.Gray(0.6f),
Children = new Drawable[] Children = new Drawable[]
{ {
headerText = new OsuSpriteText headerText = new OsuSpriteText
{ {
Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0), Position = new Vector2(SettingsSidebar.DEFAULT_WIDTH + 10, 0),
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}, },

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays
public const float TRANSITION_LENGTH = 600; public const float TRANSITION_LENGTH = 600;
private const float sidebar_width = Sidebar.DEFAULT_WIDTH; private const float sidebar_width = SettingsSidebar.DEFAULT_WIDTH;
/// <summary> /// <summary>
/// The width of the settings panel content, excluding the sidebar. /// The width of the settings panel content, excluding the sidebar.
@ -43,7 +43,7 @@ namespace osu.Game.Overlays
protected override Container<Drawable> Content => ContentContainer; protected override Container<Drawable> Content => ContentContainer;
protected Sidebar Sidebar; protected SettingsSidebar Sidebar;
private SidebarIconButton selectedSidebarButton; private SidebarIconButton selectedSidebarButton;
public SettingsSectionsContainer SectionsContainer { get; private set; } public SettingsSectionsContainer SectionsContainer { get; private set; }
@ -129,7 +129,7 @@ namespace osu.Game.Overlays
if (showSidebar) if (showSidebar)
{ {
AddInternal(Sidebar = new Sidebar { Width = sidebar_width }); AddInternal(Sidebar = new SettingsSidebar { Width = sidebar_width });
} }
CreateSections()?.ForEach(AddSection); CreateSections()?.ForEach(AddSection);
@ -244,7 +244,7 @@ namespace osu.Game.Overlays
if (selectedSidebarButton != null) if (selectedSidebarButton != null)
selectedSidebarButton.Selected = false; selectedSidebarButton.Selected = false;
selectedSidebarButton = Sidebar.Children.FirstOrDefault(b => b.Section == section.NewValue); selectedSidebarButton = Sidebar.Children.OfType<SidebarIconButton>().FirstOrDefault(b => b.Section == section.NewValue);
if (selectedSidebarButton != null) if (selectedSidebarButton != null)
selectedSidebarButton.Selected = true; selectedSidebarButton.Selected = true;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Size = new Vector2(Sidebar.DEFAULT_WIDTH); Size = new Vector2(SettingsSidebar.DEFAULT_WIDTH);
AddRange(new Drawable[] AddRange(new Drawable[]
{ {

View File

@ -0,0 +1,190 @@
// 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.Caching;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public abstract class SettingsToolboxGroup : Container
{
private const float transition_duration = 250;
private const int container_width = 270;
private const int border_thickness = 2;
private const int header_height = 30;
private const int corner_radius = 5;
private const float fade_duration = 800;
private const float inactive_alpha = 0.5f;
private readonly Cached headerTextVisibilityCache = new Cached();
private readonly FillFlowContainer content;
private readonly IconButton button;
private bool expanded = true;
public bool Expanded
{
get => expanded;
set
{
if (expanded == value) return;
expanded = value;
content.ClearTransforms();
if (expanded)
content.AutoSizeAxes = Axes.Y;
else
{
content.AutoSizeAxes = Axes.None;
content.ResizeHeightTo(0, transition_duration, Easing.OutQuint);
}
updateExpanded();
}
}
private Color4 expandedColour;
private readonly OsuSpriteText headerText;
/// <summary>
/// Create a new instance.
/// </summary>
/// <param name="title">The title to be displayed in the header of this group.</param>
protected SettingsToolboxGroup(string title)
{
AutoSizeAxes = Axes.Y;
Width = container_width;
Masking = true;
CornerRadius = corner_radius;
BorderColour = Color4.Black;
BorderThickness = border_thickness;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Container
{
Name = @"Header",
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = header_height,
Children = new Drawable[]
{
headerText = new OsuSpriteText
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Text = title.ToUpperInvariant(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 },
},
button = new IconButton
{
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
Position = new Vector2(-15, 0),
Icon = FontAwesome.Solid.Bars,
Scale = new Vector2(0.75f),
Action = () => Expanded = !Expanded,
},
}
},
content = new FillFlowContainer
{
Name = @"Content",
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeDuration = transition_duration,
AutoSizeEasing = Easing.OutQuint,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(15),
Spacing = new Vector2(0, 15),
}
}
},
};
}
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
{
if (invalidation.HasFlagFast(Invalidation.DrawSize))
headerTextVisibilityCache.Invalidate();
return base.OnInvalidate(invalidation, source);
}
protected override void Update()
{
base.Update();
if (!headerTextVisibilityCache.IsValid)
// These toolbox grouped may be contracted to only show icons.
// For now, let's hide the header to avoid text truncation weirdness in such cases.
headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint);
}
protected override void LoadComplete()
{
base.LoadComplete();
this.Delay(600).FadeTo(inactive_alpha, fade_duration, Easing.OutQuint);
updateExpanded();
}
protected override bool OnHover(HoverEvent e)
{
this.FadeIn(fade_duration, Easing.OutQuint);
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.FadeTo(inactive_alpha, fade_duration, Easing.OutQuint);
base.OnHoverLost(e);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
expandedColour = colours.Yellow;
}
private void updateExpanded() => button.FadeColour(expanded ? expandedColour : Color4.White, 200, Easing.InOutQuint);
protected override Container<Drawable> Content => content;
protected override bool OnMouseDown(MouseDownEvent e) => true;
}
}

View File

@ -2,13 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Overlays;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
public class ToolboxGroup : PlayerSettingsGroup public class EditorToolboxGroup : SettingsToolboxGroup
{ {
public ToolboxGroup(string title) public EditorToolboxGroup(string title)
: base(title) : base(title)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;

View File

@ -13,6 +13,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -98,14 +99,11 @@ namespace osu.Game.Rulesets.Edit
dependencies.CacheAs(Playfield); dependencies.CacheAs(Playfield);
const float toolbar_width = 200;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container new Container
{ {
Name = "Content", Name = "Content",
Padding = new MarginPadding { Left = toolbar_width },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
@ -117,20 +115,15 @@ namespace osu.Game.Rulesets.Edit
.WithChild(BlueprintContainer = CreateBlueprintContainer()) .WithChild(BlueprintContainer = CreateBlueprintContainer())
} }
}, },
new FillFlowContainer new LeftToolboxFlow
{ {
Name = "Sidebar",
RelativeSizeAxes = Axes.Y,
Width = toolbar_width,
Padding = new MarginPadding { Right = 10 },
Spacing = new Vector2(10),
Children = new Drawable[] Children = new Drawable[]
{ {
new ToolboxGroup("toolbox (1-9)") new EditorToolboxGroup("toolbox (1-9)")
{ {
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X } Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
}, },
new ToolboxGroup("toggles (Q~P)") new EditorToolboxGroup("toggles (Q~P)")
{ {
Child = togglesCollection = new FillFlowContainer Child = togglesCollection = new FillFlowContainer
{ {
@ -427,6 +420,18 @@ namespace osu.Game.Rulesets.Edit
} }
#endregion #endregion
private class LeftToolboxFlow : ExpandingButtonContainer
{
public LeftToolboxFlow()
: base(80, 200)
{
RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding { Right = 10 };
FillFlow.Spacing = new Vector2(10);
}
}
} }
/// <summary> /// <summary>

View File

@ -7,7 +7,7 @@ using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
{ {
public class ScrollingToolboxGroup : ToolboxGroup public class ScrollingToolboxGroup : EditorToolboxGroup
{ {
protected readonly OsuScrollContainer Scroll; protected readonly OsuScrollContainer Scroll;

View File

@ -21,7 +21,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Func<float, bool> OnRotation; public Func<float, bool> OnRotation;
public Func<Vector2, Anchor, bool> OnScale; public Func<Vector2, Anchor, bool> OnScale;
public Func<Direction, bool> OnFlip; public Func<Direction, bool, bool> OnFlip;
public Func<bool> OnReverse; public Func<bool> OnReverse;
public Action OperationStarted; public Action OperationStarted;
@ -174,12 +174,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
case Key.G: case Key.G:
return CanReverse && runOperationFromHotkey(OnReverse); return CanReverse && runOperationFromHotkey(OnReverse);
case Key.H:
return CanFlipX && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Horizontal) ?? false);
case Key.J:
return CanFlipY && runOperationFromHotkey(() => OnFlip?.Invoke(Direction.Vertical) ?? false);
} }
return base.OnKeyDown(e); return base.OnKeyDown(e);
@ -287,12 +281,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void addXFlipComponents() private void addXFlipComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal)); addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal, false));
} }
private void addYFlipComponents() private void addYFlipComponents()
{ {
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical)); addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical, false));
} }
private void addButton(IconUsage icon, string tooltip, Action action) private void addButton(IconUsage icon, string tooltip, Action action)

View File

@ -17,6 +17,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// A component which outlines items and handles movement of selections. /// A component which outlines items and handles movement of selections.
/// </summary> /// </summary>
public abstract class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu public abstract class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu
{ {
/// <summary> /// <summary>
/// The currently selected blueprints. /// The currently selected blueprints.
@ -127,9 +128,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Handles the selected items being flipped. /// Handles the selected items being flipped.
/// </summary> /// </summary>
/// <param name="direction">The direction to flip</param> /// <param name="direction">The direction to flip.</param>
/// <param name="flipOverOrigin">Whether the flip operation should be global to the playfield's origin or local to the selected pattern.</param>
/// <returns>Whether any items could be flipped.</returns> /// <returns>Whether any items could be flipped.</returns>
public virtual bool HandleFlip(Direction direction) => false; public virtual bool HandleFlip(Direction direction, bool flipOverOrigin) => false;
/// <summary> /// <summary>
/// Handles the selected items being reversed pattern-wise. /// Handles the selected items being reversed pattern-wise.
@ -137,6 +139,27 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether any items could be reversed.</returns> /// <returns>Whether any items could be reversed.</returns>
public virtual bool HandleReverse() => false; public virtual bool HandleReverse() => false;
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.EditorFlipHorizontally:
return HandleFlip(Direction.Horizontal, true);
case GlobalAction.EditorFlipVertically:
return HandleFlip(Direction.Vertical, true);
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e) public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{ {
switch (e.Action) switch (e.Action)

View File

@ -7,7 +7,6 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -17,7 +16,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
internal class TimelineSelectionHandler : EditorSelectionHandler, IKeyBindingHandler<GlobalAction> internal class TimelineSelectionHandler : EditorSelectionHandler
{ {
[Resolved] [Resolved]
private Timeline timeline { get; set; } private Timeline timeline { get; set; }
@ -27,7 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation // for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true; public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) => true;
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
switch (e.Action) switch (e.Action)
{ {
@ -40,11 +39,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return true; return true;
} }
return false; return base.OnPressed(e);
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
} }
/// <summary> /// <summary>

View File

@ -381,7 +381,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected virtual void UpdateMods() protected virtual void UpdateMods()
{ {
if (SelectedItem.Value == null) if (SelectedItem.Value == null || !this.IsCurrentScreen())
return; return;
Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList();

View File

@ -241,7 +241,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override void UpdateMods() protected override void UpdateMods()
{ {
if (SelectedItem.Value == null || client.LocalUser == null) if (SelectedItem.Value == null || client.LocalUser == null || !this.IsCurrentScreen())
return; return;
// update local mods based on room's reported status for the local user (omitting the base call implementation). // update local mods based on room's reported status for the local user (omitting the base call implementation).

View File

@ -1,165 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Overlays;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.PlayerSettings namespace osu.Game.Screens.Play.PlayerSettings
{ {
public abstract class PlayerSettingsGroup : Container public class PlayerSettingsGroup : SettingsToolboxGroup
{ {
private const float transition_duration = 250; public PlayerSettingsGroup(string title)
private const int container_width = 270; : base(title)
private const int border_thickness = 2;
private const int header_height = 30;
private const int corner_radius = 5;
private readonly FillFlowContainer content;
private readonly IconButton button;
private bool expanded = true;
public bool Expanded
{ {
get => expanded;
set
{
if (expanded == value) return;
expanded = value;
content.ClearTransforms();
if (expanded)
content.AutoSizeAxes = Axes.Y;
else
{
content.AutoSizeAxes = Axes.None;
content.ResizeHeightTo(0, transition_duration, Easing.OutQuint);
}
updateExpanded();
}
}
private Color4 expandedColour;
/// <summary>
/// Create a new instance.
/// </summary>
/// <param name="title">The title to be displayed in the header of this group.</param>
protected PlayerSettingsGroup(string title)
{
AutoSizeAxes = Axes.Y;
Width = container_width;
Masking = true;
CornerRadius = corner_radius;
BorderColour = Color4.Black;
BorderThickness = border_thickness;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Container
{
Name = @"Header",
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
Height = header_height,
Children = new Drawable[]
{
new OsuSpriteText
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Text = title.ToUpperInvariant(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Margin = new MarginPadding { Left = 10 },
},
button = new IconButton
{
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
Position = new Vector2(-15, 0),
Icon = FontAwesome.Solid.Bars,
Scale = new Vector2(0.75f),
Action = () => Expanded = !Expanded,
},
}
},
content = new FillFlowContainer
{
Name = @"Content",
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeDuration = transition_duration,
AutoSizeEasing = Easing.OutQuint,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(15),
Spacing = new Vector2(0, 15),
}
}
},
};
}
private const float fade_duration = 800;
private const float inactive_alpha = 0.5f;
protected override void LoadComplete()
{
base.LoadComplete();
this.Delay(600).FadeTo(inactive_alpha, fade_duration, Easing.OutQuint);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
this.FadeIn(fade_duration, Easing.OutQuint); base.OnHover(e);
// Importantly, return true to correctly take focus away from PlayerLoader.
return true; return true;
} }
protected override void OnHoverLost(HoverLostEvent e)
{
this.FadeTo(inactive_alpha, fade_duration, Easing.OutQuint);
base.OnHoverLost(e);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
expandedColour = colours.Yellow;
updateExpanded();
}
private void updateExpanded() => button.FadeColour(expanded ? expandedColour : Color4.White, 200, Easing.InOutQuint);
protected override Container<Drawable> Content => content;
protected override bool OnMouseDown(MouseDownEvent e) => true;
} }
} }

View File

@ -1,7 +1,6 @@
// 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.Diagnostics;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -67,18 +66,34 @@ namespace osu.Game.Skinning.Editor
public override void Show() public override void Show()
{ {
// base call intentionally omitted. // base call intentionally omitted as we have custom behaviour.
if (skinEditor == null)
if (skinEditor != null)
{ {
skinEditor = new SkinEditor(target);
skinEditor.State.BindValueChanged(editorVisibilityChanged);
Debug.Assert(skinEditor != null);
LoadComponentAsync(skinEditor, AddInternal);
}
else
skinEditor.Show(); skinEditor.Show();
return;
}
var editor = new SkinEditor(target);
editor.State.BindValueChanged(editorVisibilityChanged);
skinEditor = editor;
// Schedule ensures that if `Show` is called before this overlay is loaded,
// it will not throw (LoadComponentAsync requires the load target to be in a loaded state).
Schedule(() =>
{
if (editor != skinEditor)
return;
LoadComponentAsync(editor, _ =>
{
if (editor != skinEditor)
return;
AddInternal(editor);
});
});
} }
private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility) private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility)

View File

@ -126,7 +126,7 @@ namespace osu.Game.Skinning.Editor
return true; return true;
} }
public override bool HandleFlip(Direction direction) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
{ {
var selectionQuad = getSelectionQuad(); var selectionQuad = getSelectionQuad();
Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1); Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1);
@ -135,7 +135,7 @@ namespace osu.Game.Skinning.Editor
{ {
var drawableItem = (Drawable)b.Item; var drawableItem = (Drawable)b.Item;
var flippedPosition = GetFlippedPosition(direction, selectionQuad, b.ScreenSpaceSelectionPoint); var flippedPosition = GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
updateDrawablePosition(drawableItem, flippedPosition); updateDrawablePosition(drawableItem, flippedPosition);