diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 08b7684677..4d1d9d7e5d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Judgements; @@ -21,14 +20,8 @@ namespace osu.Game.Rulesets.Catch.UI private readonly CatcherArea catcherArea; - protected override bool UserScrollSpeedAdjustment => false; - - protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant; - public CatchPlayfield(BeatmapDifficulty difficulty, Func> getVisualRepresentation) { - Direction.Value = ScrollingDirection.Down; - Container explodingFruitContainer; Anchor = Anchor.TopCentre; @@ -55,8 +48,6 @@ namespace osu.Game.Rulesets.Catch.UI HitObjectContainer } }; - - VisibleTimeRange.Value = BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); } public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj); diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 94233bc9f0..ef1bb7767f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Input; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; @@ -18,9 +19,15 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchRulesetContainer : ScrollingRulesetContainer { + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant; + + protected override bool UserScrollSpeedAdjustment => false; + public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { + Direction.Value = ScrollingDirection.Down; + TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); diff --git a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs deleted file mode 100644 index 29663c2093..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI.Scrolling; - -namespace osu.Game.Rulesets.Mania.Tests -{ - /// - /// A container which provides a to children. - /// - public class ScrollingTestContainer : Container - { - [Cached(Type = typeof(IScrollingInfo))] - private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); - - public ScrollingTestContainer(ScrollingDirection direction) - { - scrollingInfo.Direction.Value = direction; - } - - public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up; - } - - public class TestScrollingInfo : IScrollingInfo - { - public readonly Bindable Direction = new Bindable(); - IBindable IScrollingInfo.Direction => Direction; - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs index cceee718ca..d044b48553 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; using OpenTK; using OpenTK.Graphics; @@ -93,7 +94,6 @@ namespace osu.Game.Rulesets.Mania.Tests Height = 0.85f, AccentColour = Color4.OrangeRed, Action = { Value = action }, - VisibleTimeRange = { Value = 2000 } }; columns.Add(column); @@ -104,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.Tests Origin = Anchor.Centre, AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, + TimeRange = 2000, Child = column }; } diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseHoldNoteSelectionBlueprint.cs new file mode 100644 index 0000000000..993f7520e8 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseHoldNoteSelectionBlueprint.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Edit.Blueprints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestCaseHoldNoteSelectionBlueprint : SelectionBlueprintTestCase + { + private readonly DrawableHoldNote drawableObject; + + protected override Container Content => content ?? base.Content; + private readonly Container content; + + public TestCaseHoldNoteSelectionBlueprint() + { + var holdNote = new HoldNote { Column = 0, Duration = 1000 }; + holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 50, + Child = drawableObject = new DrawableHoldNote(holdNote) + { + Height = 300, + AccentColour = OsuColour.Gray(0.3f) + } + }; + } + + protected override void Update() + { + base.Update(); + + foreach (var nested in drawableObject.NestedHitObjects) + { + double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration; + nested.Y = (float)(-finalPosition * content.DrawHeight); + } + } + + protected override SelectionBlueprint CreateBlueprint() => new HoldNoteSelectionBlueprint(drawableObject); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseNoteSelectionBlueprint.cs new file mode 100644 index 0000000000..fd26b93e5c --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNoteSelectionBlueprint.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Edit.Blueprints; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using OpenTK; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestCaseNoteSelectionBlueprint : SelectionBlueprintTestCase + { + private readonly DrawableNote drawableObject; + + protected override Container Content => content ?? base.Content; + private readonly Container content; + + public TestCaseNoteSelectionBlueprint() + { + var note = new Note { Column = 0 }; + note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(50, 20), + Child = drawableObject = new DrawableNote(note) + }; + } + + protected override SelectionBlueprint CreateBlueprint() => new NoteSelectionBlueprint(drawableObject); + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs index 5c5d955168..02d5b13100 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; using OpenTK; namespace osu.Game.Rulesets.Mania.Tests @@ -122,7 +123,7 @@ namespace osu.Game.Rulesets.Mania.Tests { var specialAction = ManiaAction.Special1; - var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction) { VisibleTimeRange = { Value = 2000 } }; + var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction); stages.Add(stage); return new ScrollingTestContainer(direction) @@ -131,6 +132,7 @@ namespace osu.Game.Rulesets.Mania.Tests Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, + TimeRange = 2000, Child = stage }; } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs new file mode 100644 index 0000000000..424ff1118c --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; + +namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components +{ + public class EditNotePiece : CompositeDrawable + { + public EditNotePiece() + { + Height = NotePiece.NOTE_HEIGHT; + + CornerRadius = 5; + Masking = true; + + InternalChild = new NotePiece(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs index 3da2538497..35ce38dadb 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs @@ -4,10 +4,10 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using OpenTK; using OpenTK.Graphics; @@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints Y -= HitObject.Tail.DrawHeight; } + public override Quad SelectionQuad => ScreenSpaceDrawQuad; + private class HoldNoteNoteSelectionBlueprint : NoteSelectionBlueprint { public HoldNoteNoteSelectionBlueprint(DrawableNote note) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs index 474b8c662e..53f9dd8752 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Graphics; using osu.Framework.Input.Events; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; @@ -12,6 +13,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public ManiaSelectionBlueprint(DrawableHitObject hitObject) : base(hitObject) { + RelativeSizeAxes = Axes.None; } public override void AdjustPosition(DragEvent dragEvent) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs index 7c0337dc4e..7df7924c51 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs @@ -1,10 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Mania.Edit.Blueprints { @@ -13,18 +12,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints public NoteSelectionBlueprint(DrawableNote note) : base(note) { - Scale = note.Scale; - - CornerRadius = 5; - Masking = true; - - AddInternal(new NotePiece()); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Colour = colours.Yellow; + AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X }); } protected override void Update() diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs index 138a2c0273..2404297cc3 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs @@ -6,11 +6,14 @@ using OpenTK; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Edit { public class ManiaEditRulesetContainer : ManiaRulesetContainer { + public new IScrollingInfo ScrollingInfo => base.ScrollingInfo; + public ManiaEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 06d67821a9..3531b81e68 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -10,31 +10,32 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Edit.Blueprints; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Edit { public class ManiaHitObjectComposer : HitObjectComposer { - protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; - public ManiaHitObjectComposer(Ruleset ruleset) : base(ruleset) { } + private DependencyContainer dependencies; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new ManiaScrollingInfo(Config)); - return dependencies; - } + => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) - => new ManiaEditRulesetContainer(ruleset, beatmap); + { + var rulesetContainer = new ManiaEditRulesetContainer(ruleset, beatmap); + + // This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it + dependencies.CacheAs(rulesetContainer.ScrollingInfo); + + return rulesetContainer; + } protected override IReadOnlyList CompositionTools => Array.Empty(); diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs new file mode 100644 index 0000000000..81a2728ad4 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Edit.Masks +{ + public abstract class ManiaSelectionBlueprint : SelectionBlueprint + { + protected ManiaSelectionBlueprint(DrawableHitObject hitObject) + : base(hitObject) + { + RelativeSizeAxes = Axes.None; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index cb6196a890..8c96c6dfe7 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -5,7 +5,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs index 2c74f5b168..b7c90e5144 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 09976e5994..576af6d93a 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -1,12 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Input.Bindings; @@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Mania.UI { - public class Column : ManiaScrollingPlayfield, IKeyBindingHandler, IHasAccentColour + public class Column : ScrollingPlayfield, IKeyBindingHandler, IHasAccentColour { private const float column_width = 45; private const float special_column_width = 70; @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.UI hitObject.AccentColour = AccentColour; hitObject.OnNewResult += OnNewResult; - HitObjects.Add(hitObject); + HitObjectContainer.Add(hitObject); } internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) @@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Mania.UI explosionContainer.Add(new HitExplosion(judgedObject) { - Anchor = Direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre + Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre }); } @@ -154,10 +154,10 @@ namespace osu.Game.Rulesets.Mania.UI return false; var nextObject = - HitObjects.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? + HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? // fallback to non-alive objects to find next off-screen object - HitObjects.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? - HitObjects.Objects.LastOrDefault(); + HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? + HitObjectContainer.Objects.LastOrDefault(); nextObject?.PlaySamples(); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 5c3a618a19..c59917056d 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -1,20 +1,19 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; using OpenTK; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaPlayfield : ManiaScrollingPlayfield + public class ManiaPlayfield : ScrollingPlayfield { private readonly List stages = new List(); @@ -41,7 +40,6 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < stageDefinitions.Count; i++) { var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction); - newStage.VisibleTimeRange.BindTo(VisibleTimeRange); playfieldGrid.Content[0][i] = newStage; @@ -68,11 +66,5 @@ namespace osu.Game.Rulesets.Mania.UI return null; } - - [BackgroundDependencyLoader] - private void load(ManiaConfigManager maniaConfig) - { - maniaConfig.BindWith(ManiaSetting.ScrollTime, VisibleTimeRange); - } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 500fb5a631..321dd4e1cb 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input; @@ -35,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; + private readonly Bindable configDirection = new Bindable(); + public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -70,18 +73,11 @@ namespace osu.Game.Rulesets.Mania.UI private void load() { BarLines.ForEach(Playfield.Add); - } - private DependencyContainer dependencies; + Config.BindWith(ManiaSetting.ScrollDirection, configDirection); + configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true); - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - if (dependencies.Get() == null) - dependencies.CacheAs(new ManiaScrollingInfo(Config)); - - return dependencies; + Config.BindWith(ManiaSetting.ScrollTime, TimeRange); } protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs deleted file mode 100644 index 624ea13e1b..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Configuration; -using osu.Game.Rulesets.Mania.Configuration; -using osu.Game.Rulesets.UI.Scrolling; - -namespace osu.Game.Rulesets.Mania.UI -{ - public class ManiaScrollingInfo : IScrollingInfo - { - private readonly Bindable configDirection = new Bindable(); - - public readonly Bindable Direction = new Bindable(); - IBindable IScrollingInfo.Direction => Direction; - - public ManiaScrollingInfo(ManiaConfigManager config) - { - config.BindWith(ManiaSetting.ScrollDirection, configDirection); - configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true); - } - } -} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs deleted file mode 100644 index 8ee0fbf7fe..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Game.Rulesets.UI.Scrolling; - -namespace osu.Game.Rulesets.Mania.UI -{ - public abstract class ManiaScrollingPlayfield : ScrollingPlayfield - { - private readonly IBindable direction = new Bindable(); - - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) - { - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(direction => Direction.Value = direction, true); - } - } -} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 8cf49686b9..19e930f530 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// - public class ManiaStage : ManiaScrollingPlayfield + public class ManiaStage : ScrollingPlayfield { public const float HIT_TARGET_POSITION = 50; @@ -144,8 +144,6 @@ namespace osu.Game.Rulesets.Mania.UI public void AddColumn(Column c) { - c.VisibleTimeRange.BindTo(VisibleTimeRange); - topLevelContainer.Add(c.TopLevelContainer.CreateProxy()); columnFlow.Add(c); AddNested(c); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs index c97adde427..804a4fcba0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs @@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners isPlacingEnd = true; piece.FadeTo(1f, 150, Easing.OutQuint); + + BeginPlacement(); } return true; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 398680cb8d..a94d1e9039 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -84,5 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 824c1f817a..561afb0180 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; @@ -39,10 +38,6 @@ namespace osu.Game.Rulesets.Taiko.UI /// private const float left_area_size = 240; - protected override bool UserScrollSpeedAdjustment => false; - - protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; - private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; private readonly JudgementContainer judgementContainer; @@ -59,8 +54,6 @@ namespace osu.Game.Rulesets.Taiko.UI public TaikoPlayfield(ControlPointInfo controlPoints) { - Direction.Value = ScrollingDirection.Left; - InternalChild = new PlayfieldAdjustmentContainer { Anchor = Anchor.CentreLeft, @@ -200,8 +193,6 @@ namespace osu.Game.Rulesets.Taiko.UI } } }; - - VisibleTimeRange.Value = 7000; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index c94ced3390..99c83c243b 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Taiko.Replays; using System.Linq; using osu.Framework.Input; +using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Rulesets.UI.Scrolling; @@ -21,9 +22,15 @@ namespace osu.Game.Rulesets.Taiko.UI { public class TaikoRulesetContainer : ScrollingRulesetContainer { + protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping; + + protected override bool UserScrollSpeedAdjustment => false; + public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { + Direction.Value = ScrollingDirection.Left; + TimeRange.Value = 7000; } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs new file mode 100644 index 0000000000..5e01213a48 --- /dev/null +++ b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; + +namespace osu.Game.Tests.ScrollAlgorithms +{ + [TestFixture] + public class ConstantScrollTest + { + private IScrollAlgorithm algorithm; + + [SetUp] + public void Setup() + { + algorithm = new ConstantScrollAlgorithm(); + } + + [Test] + public void TestDisplayStartTime() + { + Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 10000)); + Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 5000)); + Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 5000)); + Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 10000)); + } + + [Test] + public void TestLength() + { + Assert.AreEqual(1f / 5, algorithm.GetLength(0, 1000, 5000, 1)); + Assert.AreEqual(1f / 5, algorithm.GetLength(6000, 7000, 5000, 1)); + } + + [Test] + public void TestPosition() + { + Assert.AreEqual(1f / 5, algorithm.PositionAt(1000, 0, 5000, 1)); + Assert.AreEqual(1f / 5, algorithm.PositionAt(6000, 5000, 5000, 1)); + } + + [TestCase(1000)] + [TestCase(10000)] + [TestCase(15000)] + [TestCase(20000)] + [TestCase(25000)] + public void TestTime(double time) + { + Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 0, 5000, 1), 0, 5000, 1), 0.001); + Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 5000, 5000, 1), 5000, 5000, 1), 0.001); + } + } +} diff --git a/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs new file mode 100644 index 0000000000..c1a5a0f3c9 --- /dev/null +++ b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Lists; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; + +namespace osu.Game.Tests.ScrollAlgorithms +{ + [TestFixture] + public class OverlappingScrollTest + { + private IScrollAlgorithm algorithm; + + [SetUp] + public void Setup() + { + var controlPoints = new SortedList + { + new MultiplierControlPoint(0) { Velocity = 1 }, + new MultiplierControlPoint(10000) { Velocity = 2f }, + new MultiplierControlPoint(20000) { Velocity = 0.5f } + }; + + algorithm = new OverlappingScrollAlgorithm(controlPoints); + } + + [Test] + public void TestDisplayStartTime() + { + Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 1000)); // Like constant + Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 1000)); // 10500 - (1000 * 0.5) + Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 1000)); // 23000 - (1000 / 0.5) + } + + [Test] + public void TestLength() + { + Assert.AreEqual(1f / 5, algorithm.GetLength(0, 1000, 5000, 1)); // Like constant + Assert.AreEqual(1f / 5, algorithm.GetLength(10000, 10500, 5000, 1)); // (10500 - 10000) / 0.5 / 5000 + Assert.AreEqual(1f / 5, algorithm.GetLength(20000, 22000, 5000, 1)); // (22000 - 20000) * 0.5 / 5000 + } + + [Test] + public void TestPosition() + { + // Basically same calculations as TestLength() + Assert.AreEqual(1f / 5, algorithm.PositionAt(1000, 0, 5000, 1)); + Assert.AreEqual(1f / 5, algorithm.PositionAt(10500, 10000, 5000, 1)); + Assert.AreEqual(1f / 5, algorithm.PositionAt(22000, 20000, 5000, 1)); + } + + [TestCase(1000)] + [TestCase(10000)] + [TestCase(15000)] + [TestCase(20000)] + [TestCase(25000)] + [Ignore("Disabled for now because overlapping control points have multiple time values under the same position." + + "Ideally, scrolling should be changed to constant or sequential during editing of hitobjects.")] + public void TestTime(double time) + { + Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 0, 5000, 1), 0, 5000, 1), 0.001); + Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 5000, 5000, 1), 5000, 5000, 1), 0.001); + } + } +} diff --git a/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs new file mode 100644 index 0000000000..990fb92e6c --- /dev/null +++ b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Lists; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; + +namespace osu.Game.Tests.ScrollAlgorithms +{ + [TestFixture] + public class SequentialScrollTest + { + private IScrollAlgorithm algorithm; + + [SetUp] + public void Setup() + { + var controlPoints = new SortedList + { + new MultiplierControlPoint(0) { Velocity = 1 }, + new MultiplierControlPoint(10000) { Velocity = 2f }, + new MultiplierControlPoint(20000) { Velocity = 0.5f } + }; + + algorithm = new SequentialScrollAlgorithm(controlPoints); + } + + [Test] + public void TestDisplayStartTime() + { + // Sequential scroll algorithm approximates the start time + // This should be fixed in the future + } + + [Test] + public void TestLength() + { + Assert.AreEqual(1f / 5, algorithm.GetLength(0, 1000, 5000, 1)); // Like constant + Assert.AreEqual(1f / 5, algorithm.GetLength(10000, 10500, 5000, 1)); // (10500 - 10000) / 0.5 / 5000 + Assert.AreEqual(1f / 5, algorithm.GetLength(20000, 22000, 5000, 1)); // (22000 - 20000) * 0.5 / 5000 + } + + [Test] + public void TestPosition() + { + // Basically same calculations as TestLength() + Assert.AreEqual(1f / 5, algorithm.PositionAt(1000, 0, 5000, 1)); + Assert.AreEqual(1f / 5, algorithm.PositionAt(10500, 10000, 5000, 1)); + Assert.AreEqual(1f / 5, algorithm.PositionAt(22000, 20000, 5000, 1)); + } + + [TestCase(1000)] + [TestCase(10000)] + [TestCase(15000)] + [TestCase(20000)] + [TestCase(25000)] + public void TestTime(double time) + { + Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 0, 5000, 1), 0, 5000, 1), 0.001); + Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 5000, 5000, 1), 5000, 5000, 1), 0.001); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs new file mode 100644 index 0000000000..e6a04bf5c6 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.MathUtils; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat.Tabs; +using osu.Game.Users; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseChannelTabControl : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ChannelTabControl), + }; + + private readonly ChannelTabControl channelTabControl; + + public TestCaseChannelTabControl() + { + SpriteText currentText; + Add(new Container + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + channelTabControl = new ChannelTabControl + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Height = 50 + }, + new Box + { + Colour = Color4.Black.Opacity(0.1f), + RelativeSizeAxes = Axes.X, + Height = 50, + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + } + } + }); + + Add(new Container + { + Origin = Anchor.TopLeft, + Anchor = Anchor.TopLeft, + Children = new Drawable[] + { + currentText = new SpriteText + { + Text = "Currently selected channel:" + } + } + }); + + channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel); + channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.ToString(); + + AddStep("Add random private channel", addRandomUser); + AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2); + AddRepeatStep("Add 3 random private channels", addRandomUser, 3); + AddAssert("There are four channels", () => channelTabControl.Items.Count() == 5); + AddStep("Add random public channel", () => addChannel(RNG.Next().ToString())); + + AddRepeatStep("Select a random channel", () => channelTabControl.Current.Value = channelTabControl.Items.ElementAt(RNG.Next(channelTabControl.Items.Count())), 20); + } + + private List users; + + private void addRandomUser() + { + channelTabControl.AddChannel(new Channel + { + Users = + { + users?.Count > 0 + ? users[RNG.Next(0, users.Count - 1)] + : new User + { + Id = RNG.Next(), + Username = "testuser" + RNG.Next(1000) + } + } + }); + } + + private void addChannel(string name) + { + channelTabControl.AddChannel(new Channel + { + Type = ChannelType.Public, + Name = name + }); + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + GetUsersRequest req = new GetUsersRequest(); + req.Success += list => users = list.Select(e => e.User).ToList(); + + api.Queue(req); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseChatDisplay.cs b/osu.Game.Tests/Visual/TestCaseChatDisplay.cs index c03b12bdc1..e3bd4026b3 100644 --- a/osu.Game.Tests/Visual/TestCaseChatDisplay.cs +++ b/osu.Game.Tests/Visual/TestCaseChatDisplay.cs @@ -1,21 +1,45 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using System.ComponentModel; +using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Tabs; namespace osu.Game.Tests.Visual { [Description("Testing chat api and overlay")] public class TestCaseChatDisplay : OsuTestCase { - public TestCaseChatDisplay() + public override IReadOnlyList RequiredTypes => new[] { - Add(new ChatOverlay + typeof(ChatOverlay), + typeof(ChatLine), + typeof(DrawableChannel), + typeof(ChannelSelectorTabItem), + typeof(ChannelTabControl), + typeof(ChannelTabItem), + typeof(PrivateChannelTabItem), + typeof(TabCloseButton) + }; + + [Cached] + private readonly ChannelManager channelManager = new ChannelManager(); + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] { - State = Visibility.Visible - }); + channelManager, + new ChatOverlay { State = Visibility.Visible } + }; } } } diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs index 12a7ee9c12..51def9be7d 100644 --- a/osu.Game.Tests/Visual/TestCaseChatLink.cs +++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs @@ -50,14 +50,13 @@ namespace osu.Game.Tests.Visual private void load(OsuColour colours) { linkColour = colours.Blue; - Dependencies.Cache(new ChatOverlay - { - AvailableChannels = - { - new Channel { Name = "#english" }, - new Channel { Name = "#japanese" } - } - }); + + var chatManager = new ChannelManager(); + chatManager.AvailableChannels.Add(new Channel { Name = "#english"}); + chatManager.AvailableChannels.Add(new Channel { Name = "#japanese" }); + Dependencies.Cache(chatManager); + + Dependencies.Cache(new ChatOverlay()); testLinksGeneral(); testEcho(); diff --git a/osu.Game.Tests/Visual/TestCaseQuitButton.cs b/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs similarity index 76% rename from osu.Game.Tests/Visual/TestCaseQuitButton.cs rename to osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs index a427b7a20a..019472f127 100644 --- a/osu.Game.Tests/Visual/TestCaseQuitButton.cs +++ b/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs @@ -13,25 +13,25 @@ using OpenTK.Input; namespace osu.Game.Tests.Visual { [Description("'Hold to Quit' UI element")] - public class TestCaseQuitButton : ManualInputManagerTestCase + public class TestCaseHoldForMenuButton : ManualInputManagerTestCase { private bool exitAction; [BackgroundDependencyLoader] private void load() { - QuitButton quitButton; + HoldForMenuButton holdForMenuButton; - Add(quitButton = new QuitButton + Add(holdForMenuButton = new HoldForMenuButton { Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, Action = () => exitAction = true }); - var text = quitButton.Children.OfType().First(); + var text = holdForMenuButton.Children.OfType().First(); - AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(quitButton)); + AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton)); AddUntilStep(() => text.IsPresent && !exitAction, "Text visible"); AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One)); AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible"); @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual AddStep("Trigger exit action", () => { exitAction = false; - InputManager.MoveMouseTo(quitButton); + InputManager.MoveMouseTo(holdForMenuButton); InputManager.PressButton(MouseButton.Left); }); @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual AddAssert("action not triggered", () => !exitAction); AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left)); - AddUntilStep(() => exitAction, $"{nameof(quitButton.Action)} was triggered"); + AddUntilStep(() => exitAction, $"{nameof(holdForMenuButton.Action)} was triggered"); } } } diff --git a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs index eb322df185..a486abb9e8 100644 --- a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs @@ -9,6 +9,7 @@ using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Timing; @@ -22,6 +23,7 @@ namespace osu.Game.Tests.Visual { public override IReadOnlyList RequiredTypes => new[] { typeof(Playfield) }; + private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4]; private readonly TestPlayfield[] playfields = new TestPlayfield[4]; public TestCaseScrollingHitObjects() @@ -33,18 +35,38 @@ namespace osu.Game.Tests.Visual { new Drawable[] { - playfields[0] = new TestPlayfield(ScrollingDirection.Up), - playfields[1] = new TestPlayfield(ScrollingDirection.Down) + scrollContainers[0] = new ScrollingTestContainer(ScrollingDirection.Up) + { + RelativeSizeAxes = Axes.Both, + Child = playfields[0] = new TestPlayfield() + }, + scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Up) + { + RelativeSizeAxes = Axes.Both, + Child = playfields[1] = new TestPlayfield() + }, }, new Drawable[] { - playfields[2] = new TestPlayfield(ScrollingDirection.Left), - playfields[3] = new TestPlayfield(ScrollingDirection.Right) + scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Up) + { + RelativeSizeAxes = Axes.Both, + Child = playfields[2] = new TestPlayfield() + }, + scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Up) + { + RelativeSizeAxes = Axes.Both, + Child = playfields[3] = new TestPlayfield() + } } } }); - AddSliderStep("Time range", 100, 10000, 5000, v => playfields.ForEach(p => p.VisibleTimeRange.Value = v)); + AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); + AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); + AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); + + AddSliderStep("Time range", 100, 10000, 5000, v => scrollContainers.ForEach(c => c.TimeRange = v)); AddStep("Add control point", () => addControlPoint(Time.Current + 5000)); } @@ -52,7 +74,7 @@ namespace osu.Game.Tests.Visual { base.LoadComplete(); - playfields.ForEach(p => p.HitObjects.AddControlPoint(new MultiplierControlPoint(0))); + scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); for (int i = 0; i <= 5000; i += 1000) addHitObject(Time.Current + i); @@ -73,12 +95,15 @@ namespace osu.Game.Tests.Visual private void addControlPoint(double time) { + scrollContainers.ForEach(c => + { + c.ControlPoints.Add(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } }); + c.ControlPoints.Add(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } }); + c.ControlPoints.Add(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } }); + }); + playfields.ForEach(p => { - p.HitObjects.AddControlPoint(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } }); - p.HitObjects.AddControlPoint(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } }); - p.HitObjects.AddControlPoint(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } }); - TestDrawableControlPoint createDrawablePoint(double t) { var obj = new TestDrawableControlPoint(p.Direction, t); @@ -111,15 +136,14 @@ namespace osu.Game.Tests.Visual } } + private void setScrollAlgorithm(ScrollVisualisationMethod algorithm) => scrollContainers.ForEach(c => c.ScrollAlgorithm = algorithm); private class TestPlayfield : ScrollingPlayfield { - public new ScrollingDirection Direction => base.Direction; + public new ScrollingDirection Direction => base.Direction.Value; - public TestPlayfield(ScrollingDirection direction) + public TestPlayfield() { - base.Direction.Value = direction; - Padding = new MarginPadding(2); InternalChildren = new Drawable[] diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index e4e7828d0e..5c8fddc342 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using System.Collections.Generic; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -21,16 +22,17 @@ namespace osu.Game.Graphics.Containers } private OsuGame game; - + private ChannelManager channelManager; private Action showNotImplementedError; private GameHost host; [BackgroundDependencyLoader(true)] - private void load(OsuGame game, NotificationOverlay notifications, GameHost host) + private void load(OsuGame game, NotificationOverlay notifications, GameHost host, ChannelManager channelManager) { // will be null in tests this.game = game; this.host = host; + this.channelManager = channelManager; showNotImplementedError = () => notifications?.Post(new SimpleNotification { @@ -80,7 +82,15 @@ namespace osu.Game.Graphics.Containers game?.ShowBeatmapSet(setId); break; case LinkAction.OpenChannel: - game?.OpenChannel(linkArgument); + try + { + channelManager?.OpenChannel(linkArgument); + } + catch (ChannelNotFoundException e) + { + Logger.Log($"The requested channel \"{linkArgument}\" does not exist"); + } + break; case LinkAction.OpenEditorTimestamp: case LinkAction.JoinMultiplayerMatch: diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs index 502f468ec9..5c6a2a0569 100644 --- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs @@ -2,9 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.ComponentModel; -using System.Reflection; -using System.Collections.Generic; namespace osu.Game.Graphics.UserInterface { @@ -15,18 +12,7 @@ namespace osu.Game.Graphics.UserInterface if (!typeof(T).IsEnum) throw new InvalidOperationException("OsuEnumDropdown only supports enums as the generic type argument"); - List> items = new List>(); - foreach (var val in (T[])Enum.GetValues(typeof(T))) - { - var field = typeof(T).GetField(Enum.GetName(typeof(T), val)); - items.Add( - new KeyValuePair( - field.GetCustomAttribute()?.Description ?? Enum.GetName(typeof(T), val), - val - ) - ); - } - Items = items; + Items = (T[])Enum.GetValues(typeof(T)); } } } diff --git a/osu.Game/Online/API/APIMessagesRequest.cs b/osu.Game/Online/API/APIMessagesRequest.cs new file mode 100644 index 0000000000..991096c0af --- /dev/null +++ b/osu.Game/Online/API/APIMessagesRequest.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.IO.Network; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.API +{ + public abstract class APIMessagesRequest : APIRequest> + { + private readonly long? sinceId; + + protected APIMessagesRequest(long? sinceId) + { + this.sinceId = sinceId; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + if (sinceId.HasValue) req.AddParameter(@"since", sinceId.Value.ToString()); + + return req; + } + } +} diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs new file mode 100644 index 0000000000..00dab10c75 --- /dev/null +++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Net.Http; +using osu.Framework.IO.Network; +using osu.Game.Online.Chat; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests +{ + public class CreateNewPrivateMessageRequest : APIRequest + { + private readonly User user; + private readonly Message message; + + public CreateNewPrivateMessageRequest(User user, Message message) + { + this.user = user; + this.message = message; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Method = HttpMethod.Post; + req.AddParameter(@"target_id", user.Id.ToString()); + req.AddParameter(@"message", message.Content); + req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant()); + return req; + } + + protected override string Target => @"chat/new"; + } +} diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageResponse.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageResponse.cs new file mode 100644 index 0000000000..84f1593c31 --- /dev/null +++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageResponse.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Online.Chat; + +namespace osu.Game.Online.API.Requests +{ + public class CreateNewPrivateMessageResponse + { + [JsonProperty("new_channel_id")] + public int ChannelID; + + public Message Message; + } +} diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index bbe74fcac0..9d3b7b5cc9 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -3,15 +3,64 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Newtonsoft.Json; using osu.Framework.Configuration; using osu.Framework.Lists; +using osu.Game.Users; namespace osu.Game.Online.Chat { public class Channel { + public readonly int MaxHistory = 300; + + /// + /// Contains every joined user except the current logged in user. Currently only returned for PM channels. + /// + public readonly ObservableCollection Users = new ObservableCollection(); + + [JsonProperty(@"users")] + private long[] userIds + { + set + { + foreach (var id in value) + Users.Add(new User { Id = id }); + } + } + + /// + /// Contains all the messages send in the channel. + /// + public readonly SortedList Messages = new SortedList(Comparer.Default); + + /// + /// Contains all the messages that are still pending for submission to the server. + /// + private readonly List pendingMessages = new List(); + + + /// + /// An event that fires when new messages arrived. + /// + public event Action> NewMessagesArrived; + + /// + /// An event that fires when a pending message gets resolved. + /// + public event Action PendingMessageResolved; + + /// + /// An event that fires when a pending message gets removed. + /// + public event Action MessageRemoved; + + public bool ReadOnly => false; //todo not yet used. + + public override string ToString() => Name; + [JsonProperty(@"name")] public string Name; @@ -22,19 +71,16 @@ namespace osu.Game.Online.Chat public ChannelType Type; [JsonProperty(@"channel_id")] - public int Id; + public long Id; [JsonProperty(@"last_message_id")] public long? LastMessageId; - public readonly SortedList Messages = new SortedList(Comparer.Default); - - private readonly List pendingMessages = new List(); - + /// + /// Signalles if the current user joined this channel or not. Defaults to false. + /// public Bindable Joined = new Bindable(); - public bool ReadOnly => false; - public const int MAX_HISTORY = 300; [JsonConstructor] @@ -42,10 +88,10 @@ namespace osu.Game.Online.Chat { } - public event Action> NewMessagesArrived; - public event Action PendingMessageResolved; - public event Action MessageRemoved; - + /// + /// Adds the argument message as a local echo. When this local echo is resolved will get called. + /// + /// public void AddLocalEcho(LocalEchoMessage message) { pendingMessages.Add(message); @@ -54,8 +100,12 @@ namespace osu.Game.Online.Chat NewMessagesArrived?.Invoke(new[] { message }); } - public bool MessagesLoaded { get; private set; } + public bool MessagesLoaded; + /// + /// Adds new messages to the channel and purges old messages. Triggers the event. + /// + /// public void AddNewMessages(params Message[] messages) { messages = messages.Except(Messages).ToArray(); @@ -63,7 +113,6 @@ namespace osu.Game.Online.Chat if (messages.Length == 0) return; Messages.AddRange(messages); - MessagesLoaded = true; var maxMessageId = messages.Max(m => m.Id); if (maxMessageId > LastMessageId) @@ -74,14 +123,6 @@ namespace osu.Game.Online.Chat NewMessagesArrived?.Invoke(messages); } - private void purgeOldMessages() - { - // never purge local echos - int messageCount = Messages.Count - pendingMessages.Count; - if (messageCount > MAX_HISTORY) - Messages.RemoveRange(0, messageCount - MAX_HISTORY); - } - /// /// Replace or remove a message from the channel. /// @@ -101,17 +142,18 @@ namespace osu.Game.Online.Chat } if (Messages.Contains(final)) - { - // message already inserted, so let's throw away this update. - // we may want to handle this better in the future, but for the time being api requests are single-threaded so order is assumed. - MessageRemoved?.Invoke(echo); - return; - } + throw new InvalidOperationException("Attempted to add the same message again"); Messages.Add(final); PendingMessageResolved?.Invoke(echo, final); } - public override string ToString() => Name; + private void purgeOldMessages() + { + // never purge local echos + int messageCount = Messages.Count - pendingMessages.Count; + if (messageCount > MaxHistory) + Messages.RemoveRange(0, messageCount - MaxHistory); + } } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs new file mode 100644 index 0000000000..9014fce18d --- /dev/null +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -0,0 +1,409 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Framework.Threading; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Users; + +namespace osu.Game.Online.Chat +{ + /// + /// Manages everything channel related + /// + public class ChannelManager : Component, IOnlineComponent + { + /// + /// The channels the player joins on startup + /// + private readonly string[] defaultChannels = + { + @"#lazer", + @"#osu", + @"#lobby" + }; + + /// + /// The currently opened channel + /// + public Bindable CurrentChannel { get; } = new Bindable(); + + /// + /// The Channels the player has joined + /// + public ObservableCollection JoinedChannels { get; } = new ObservableCollection(); //todo: should be publicly readonly + + /// + /// The channels available for the player to join + /// + public ObservableCollection AvailableChannels { get; } = new ObservableCollection(); //todo: should be publicly readonly + + private IAPIProvider api; + private ScheduledDelegate fetchMessagesScheduleder; + + public ChannelManager() + { + CurrentChannel.ValueChanged += currentChannelChanged; + } + + /// + /// Opens a channel or switches to the channel if already opened. + /// + /// If the name of the specifed channel was not found this exception will be thrown. + /// + public void OpenChannel(string name) + { + if (name == null) + throw new ArgumentNullException(nameof(name)); + + CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name); + } + + /// + /// Opens a new private channel. + /// + /// The user the private channel is opened with. + public void OpenPrivateChannel(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id)) + ?? new Channel { Name = user.Username, Users = { user } }; + } + + private void currentChannelChanged(Channel channel) => JoinChannel(channel); + + /// + /// Ensure we run post actions in sequence, once at a time. + /// + private readonly Queue postQueue = new Queue(); + + /// + /// Posts a message to the currently opened channel. + /// + /// The message text that is going to be posted + /// Is true if the message is an action, e.g.: user is currently eating + public void PostMessage(string text, bool isAction = false) + { + if (CurrentChannel.Value == null) + return; + + var currentChannel = CurrentChannel.Value; + + void dequeueAndRun() + { + if (postQueue.Count > 0) + postQueue.Dequeue().Invoke(); + } + + postQueue.Enqueue(() => + { + if (!api.IsLoggedIn) + { + currentChannel.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); + return; + } + + var message = new LocalEchoMessage + { + Sender = api.LocalUser.Value, + Timestamp = DateTimeOffset.Now, + ChannelId = CurrentChannel.Value.Id, + IsAction = isAction, + Content = text + }; + + currentChannel.AddLocalEcho(message); + + // if this is a PM and the first message, we need to do a special request to create the PM channel + if (currentChannel.Type == ChannelType.PM && !currentChannel.Joined) + { + var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(currentChannel.Users.First(), message); + + createNewPrivateMessageRequest.Success += createRes => + { + currentChannel.Id = createRes.ChannelID; + currentChannel.ReplaceMessage(message, createRes.Message); + dequeueAndRun(); + }; + + createNewPrivateMessageRequest.Failure += exception => + { + Logger.Error(exception, "Posting message failed."); + currentChannel.ReplaceMessage(message, null); + dequeueAndRun(); + }; + + api.Queue(createNewPrivateMessageRequest); + return; + } + + var req = new PostMessageRequest(message); + + req.Success += m => + { + currentChannel.ReplaceMessage(message, m); + dequeueAndRun(); + }; + + req.Failure += exception => + { + Logger.Error(exception, "Posting message failed."); + currentChannel.ReplaceMessage(message, null); + dequeueAndRun(); + }; + + api.Queue(req); + }); + + // always run if the queue is empty + if (postQueue.Count == 1) + dequeueAndRun(); + } + + /// + /// Posts a command locally. Commands like /help will result in a help message written in the current channel. + /// + /// the text containing the command identifier and command parameters. + public void PostCommand(string text) + { + if (CurrentChannel.Value == null) + return; + + var parameters = text.Split(new[] { ' ' }, 2); + string command = parameters[0]; + string content = parameters.Length == 2 ? parameters[1] : string.Empty; + + switch (command) + { + case "me": + if (string.IsNullOrWhiteSpace(content)) + { + CurrentChannel.Value.AddNewMessages(new ErrorMessage("Usage: /me [action]")); + break; + } + + PostMessage(content, true); + break; + + case "help": + CurrentChannel.Value.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]")); + break; + + default: + CurrentChannel.Value.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help")); + break; + } + } + + private void handleChannelMessages(IEnumerable messages) + { + var channels = JoinedChannels.ToList(); + + foreach (var group in messages.GroupBy(m => m.ChannelId)) + channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); + } + + private void initializeChannels() + { + var req = new ListChannelsRequest(); + + var joinDefaults = JoinedChannels.Count == 0; + + req.Success += channels => + { + foreach (var channel in channels) + { + // add as available if not already + if (AvailableChannels.All(c => c.Id != channel.Id)) + AvailableChannels.Add(channel); + + // join any channels classified as "defaults" + if (joinDefaults && defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase))) + JoinChannel(channel); + } + }; + req.Failure += error => + { + Logger.Error(error, "Fetching channel list failed"); + initializeChannels(); + }; + + api.Queue(req); + } + + /// + /// Fetches inital messages of a channel + /// + /// TODO: remove this when the API supports returning initial fetch messages for more than one channel by specifying the last message id per channel instead of one last message id globally. + /// right now it caps out at 50 messages and therefore only returns one channel's worth of content. + /// + /// The channel + private void fetchInitalMessages(Channel channel) + { + if (channel.Id <= 0) return; + + var fetchInitialMsgReq = new GetMessagesRequest(channel); + fetchInitialMsgReq.Success += messages => + { + handleChannelMessages(messages); + channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none. + }; + + api.Queue(fetchInitialMsgReq); + } + + public void JoinChannel(Channel channel) + { + if (channel == null) return; + + // ReSharper disable once AccessToModifiedClosure + var existing = JoinedChannels.FirstOrDefault(c => c.Id == channel.Id); + + if (existing != null) + { + // if we already have this channel loaded, we don't want to make a second one. + channel = existing; + } + else + { + var foundSelf = channel.Users.FirstOrDefault(u => u.Id == api.LocalUser.Value.Id); + if (foundSelf != null) + channel.Users.Remove(foundSelf); + + JoinedChannels.Add(channel); + + if (channel.Type == ChannelType.Public && !channel.Joined) + { + var req = new JoinChannelRequest(channel, api.LocalUser); + req.Success += () => + { + channel.Joined.Value = true; + JoinChannel(channel); + }; + req.Failure += ex => LeaveChannel(channel); + api.Queue(req); + return; + } + } + + if (CurrentChannel.Value == null) + CurrentChannel.Value = channel; + + if (!channel.MessagesLoaded) + { + // let's fetch a small number of messages to bring us up-to-date with the backlog. + fetchInitalMessages(channel); + } + } + + public void LeaveChannel(Channel channel) + { + if (channel == null) return; + + if (channel == CurrentChannel.Value) CurrentChannel.Value = null; + + JoinedChannels.Remove(channel); + + if (channel.Joined.Value) + { + api.Queue(new LeaveChannelRequest(channel, api.LocalUser)); + channel.Joined.Value = false; + } + } + + public void APIStateChanged(APIAccess api, APIState state) + { + switch (state) + { + case APIState.Online: + fetchUpdates(); + break; + default: + fetchMessagesScheduleder?.Cancel(); + fetchMessagesScheduleder = null; + break; + } + } + + private long lastMessageId; + private const int update_poll_interval = 1000; + + private bool channelsInitialised; + + private void fetchUpdates() + { + fetchMessagesScheduleder?.Cancel(); + fetchMessagesScheduleder = Scheduler.AddDelayed(() => + { + var fetchReq = new GetUpdatesRequest(lastMessageId); + + fetchReq.Success += updates => + { + if (updates?.Presence != null) + { + foreach (var channel in updates.Presence) + { + if (!channel.Joined.Value) + { + // we received this from the server so should mark the channel already joined. + channel.Joined.Value = true; + + JoinChannel(channel); + } + } + + if (!channelsInitialised) + { + channelsInitialised = true; + // we want this to run after the first presence so we can see if the user is in any channels already. + initializeChannels(); + } + + //todo: handle left channels + + handleChannelMessages(updates.Messages); + + foreach (var group in updates.Messages.GroupBy(m => m.ChannelId)) + JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); + + lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId; + } + + fetchUpdates(); + }; + + fetchReq.Failure += delegate { fetchUpdates(); }; + + api.Queue(fetchReq); + }, update_poll_interval); + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + this.api = api; + api.Register(this); + } + } + + /// + /// An exception thrown when a channel could not been found. + /// + public class ChannelNotFoundException : Exception + { + public ChannelNotFoundException(string channelName) + : base($"A channel with the name {channelName} could not be found.") + { + } + } +} diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 65e0415cd3..3f0f352ac7 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Newtonsoft.Json; using osu.Game.Users; @@ -16,10 +15,10 @@ namespace osu.Game.Online.Chat //todo: this should be inside sender. [JsonProperty(@"sender_id")] - public int UserId; + public long UserId; [JsonProperty(@"channel_id")] - public int ChannelId; + public long ChannelId; [JsonProperty(@"is_action")] public bool IsAction; @@ -69,12 +68,4 @@ namespace osu.Game.Online.Chat // ReSharper disable once ImpureMethodCallOnReadonlyValueField public override int GetHashCode() => Id.GetHashCode(); } - - public enum TargetType - { - [Description(@"channel")] - Channel, - [Description(@"user")] - User - } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b75a37e9df..76a9102c5e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -31,6 +31,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Input.Bindings; +using osu.Game.Online.Chat; using osu.Game.Rulesets.Mods; using osu.Game.Skinning; using OpenTK.Graphics; @@ -180,12 +181,6 @@ namespace osu.Game private ScheduledDelegate scoreLoad; - /// - /// Open chat to a channel matching the provided name, if present. - /// - /// The name of the channel. - public void OpenChannel(string channelName) => chat.OpenChannel(chat.AvailableChannels.Find(c => c.Name == channelName)); - /// /// Show a beatmap set as an overlay. /// @@ -343,6 +338,11 @@ namespace osu.Game //overlay elements loadComponentSingleFile(direct = new DirectOverlay { Depth = -1 }, mainContent.Add); loadComponentSingleFile(social = new SocialOverlay { Depth = -1 }, mainContent.Add); + loadComponentSingleFile(new ChannelManager(), channelManager => + { + dependencies.Cache(channelManager); + AddInternal(channelManager); + }); loadComponentSingleFile(chat = new ChatOverlay { Depth = -1 }, mainContent.Add); loadComponentSingleFile(settings = new MainSettings { diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 1fccaeb123..770f528e17 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using OpenTK; using OpenTK.Graphics; @@ -81,6 +82,8 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Left = padding, Right = padding }; } + private ChannelManager chatManager; + private Message message; private OsuSpriteText username; private LinkFlowContainer contentFlow; @@ -104,9 +107,9 @@ namespace osu.Game.Overlays.Chat } [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, ChatOverlay chat) + private void load(OsuColour colours, ChannelManager chatManager) { - this.chat = chat; + this.chatManager = chatManager; customUsernameColour = colours.ChatBlue; } @@ -215,8 +218,6 @@ namespace osu.Game.Overlays.Chat FinishTransforms(true); } - private ChatOverlay chat; - private void updateMessageContent() { this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); @@ -226,7 +227,7 @@ namespace osu.Game.Overlays.Chat username.Text = $@"{message.Sender.Username}" + (senderHasBackground || message.IsAction ? "" : ":"); // remove non-existent channels from the link list - message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chat?.AvailableChannels.Any(c => c.Name == link.Argument) != true); + message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument) != true); contentFlow.Clear(); contentFlow.AddLinks(message.DisplayContent, message.Links); @@ -236,20 +237,24 @@ namespace osu.Game.Overlays.Chat { private readonly User sender; + private Action startChatAction; + public MessageSender(User sender) { this.sender = sender; } [BackgroundDependencyLoader(true)] - private void load(UserProfileOverlay profile) + private void load(UserProfileOverlay profile, ChannelManager chatManager) { Action = () => profile?.ShowUser(sender); + startChatAction = () => chatManager?.OpenPrivateChannel(sender); } public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action), + new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction), }; } } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index c57e71b5ad..fedfd788ce 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenTK.Graphics; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; @@ -55,15 +54,11 @@ namespace osu.Game.Overlays.Chat Channel.PendingMessageResolved += pendingMessageResolved; } - [BackgroundDependencyLoader] - private void load() - { - newMessagesArrived(Channel.Messages); - } - protected override void LoadComplete() { base.LoadComplete(); + + newMessagesArrived(Channel.Messages); scrollToEnd(); } @@ -79,7 +74,7 @@ namespace osu.Game.Overlays.Chat private void newMessagesArrived(IEnumerable newMessages) { // Add up to last Channel.MAX_HISTORY messages - var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); + var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory)); flow.AddRange(displayMessages.Select(m => new ChatLine(m))); @@ -89,7 +84,7 @@ namespace osu.Game.Overlays.Chat scrollToEnd(); var staleMessages = flow.Children.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); - int count = staleMessages.Length - Channel.MAX_HISTORY; + int count = staleMessages.Length - Channel.MaxHistory; for (int i = 0; i < count; i++) { diff --git a/osu.Game/Overlays/Chat/ChannelListItem.cs b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs similarity index 99% rename from osu.Game/Overlays/Chat/ChannelListItem.cs rename to osu.Game/Overlays/Chat/Selection/ChannelListItem.cs index 8df29c89f2..cb6caf1506 100644 --- a/osu.Game/Overlays/Chat/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs @@ -15,7 +15,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Game.Graphics.Containers; -namespace osu.Game.Overlays.Chat +namespace osu.Game.Overlays.Chat.Selection { public class ChannelListItem : OsuClickableContainer, IFilterable { diff --git a/osu.Game/Overlays/Chat/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs similarity index 97% rename from osu.Game/Overlays/Chat/ChannelSection.cs rename to osu.Game/Overlays/Chat/Selection/ChannelSection.cs index 85fdecaaba..ac790b424e 100644 --- a/osu.Game/Overlays/Chat/ChannelSection.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -namespace osu.Game.Overlays.Chat +namespace osu.Game.Overlays.Chat.Selection { public class ChannelSection : Container, IHasFilterableChildren { diff --git a/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs similarity index 93% rename from osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs rename to osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index a86465b77b..9766c0f2c5 100644 --- a/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -18,7 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Chat; using osu.Game.Graphics.Containers; -namespace osu.Game.Overlays.Chat +namespace osu.Game.Overlays.Chat.Selection { public class ChannelSelectionOverlay : OsuFocusedOverlayContainer { @@ -35,23 +35,6 @@ namespace osu.Game.Overlays.Chat public Action OnRequestJoin; public Action OnRequestLeave; - public IEnumerable Sections - { - set - { - sectionsFlow.ChildrenEnumerable = value; - - foreach (ChannelSection s in sectionsFlow.Children) - { - foreach (ChannelListItem c in s.ChannelFlow.Children) - { - c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); }; - c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); }; - } - } - } - } - public ChannelSelectionOverlay() { RelativeSizeAxes = Axes.X; @@ -140,6 +123,30 @@ namespace osu.Game.Overlays.Chat search.Current.ValueChanged += newValue => sectionsFlow.SearchTerm = newValue; } + public void UpdateAvailableChannels(IEnumerable channels) + { + Scheduler.Add(() => + { + sectionsFlow.ChildrenEnumerable = new[] + { + new ChannelSection + { + Header = "All Channels", + Channels = channels, + }, + }; + + foreach (ChannelSection s in sectionsFlow.Children) + { + foreach (ChannelListItem c in s.ChannelFlow.Children) + { + c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); }; + c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); }; + } + } + }); + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs new file mode 100644 index 0000000000..0b1721741a --- /dev/null +++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.Chat.Tabs +{ + public class ChannelSelectorTabItem : ChannelTabItem + { + public override bool IsRemovable => false; + + public ChannelSelectorTabItem(Channel value) : base(value) + { + Depth = float.MaxValue; + Width = 45; + + Icon.Alpha = 0; + + Text.TextSize = 45; + TextBold.TextSize = 45; + } + + [BackgroundDependencyLoader] + private new void load(OsuColour colour) + { + BackgroundInactive = colour.Gray2; + BackgroundActive = colour.Gray3; + } + } +} diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs new file mode 100644 index 0000000000..08d4e40a64 --- /dev/null +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Chat; +using OpenTK; +using osu.Framework.Configuration; +using System; +using System.Linq; + +namespace osu.Game.Overlays.Chat.Tabs +{ + public class ChannelTabControl : OsuTabControl + { + public static readonly float SHEAR_WIDTH = 10; + + public Action OnRequestLeave; + + public readonly Bindable ChannelSelectorActive = new Bindable(); + + private readonly ChannelSelectorTabItem selectorTab; + + public ChannelTabControl() + { + TabContainer.Margin = new MarginPadding { Left = 50 }; + TabContainer.Spacing = new Vector2(-SHEAR_WIDTH, 0); + TabContainer.Masking = false; + + AddInternal(new SpriteIcon + { + Icon = FontAwesome.fa_comments, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + Margin = new MarginPadding(10), + }); + + AddTabItem(selectorTab = new ChannelSelectorTabItem(new Channel { Name = "+" })); + + ChannelSelectorActive.BindTo(selectorTab.Active); + } + + protected override void AddTabItem(TabItem item, bool addToDropdown = true) + { + if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue) + // performTabSort might've made selectorTab's position wonky, fix it + TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); + + base.AddTabItem(item, addToDropdown); + } + + protected override TabItem CreateTabItem(Channel value) + { + switch (value.Type) + { + case ChannelType.Public: + return new ChannelTabItem(value) { OnRequestClose = tabCloseRequested }; + case ChannelType.PM: + return new PrivateChannelTabItem(value) { OnRequestClose = tabCloseRequested }; + default: + throw new InvalidOperationException("Only TargetType User and Channel are supported."); + } + } + + /// + /// Adds a channel to the ChannelTabControl. + /// The first channel added will automaticly selected. + /// + /// The channel that is going to be added. + public void AddChannel(Channel channel) + { + if (!Items.Contains(channel)) + AddItem(channel); + + if (Current.Value == null) + Current.Value = channel; + } + + /// + /// Removes a channel from the ChannelTabControl. + /// If the selected channel is the one that is beeing removed, the next available channel will be selected. + /// + /// The channel that is going to be removed. + public void RemoveChannel(Channel channel) + { + RemoveItem(channel); + + if (Current.Value == channel) + Current.Value = Items.FirstOrDefault(); + } + + protected override void SelectTab(TabItem tab) + { + if (tab is ChannelSelectorTabItem) + { + tab.Active.Toggle(); + return; + } + + selectorTab.Active.Value = false; + + base.SelectTab(tab); + } + + private void tabCloseRequested(TabItem tab) + { + int totalTabs = TabContainer.Count - 1; // account for selectorTab + int currentIndex = MathHelper.Clamp(TabContainer.IndexOf(tab), 1, totalTabs); + + if (tab == SelectedTab && totalTabs > 1) + // Select the tab after tab-to-be-removed's index, or the tab before if current == last + SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]); + else if (totalTabs == 1 && !selectorTab.Active) + // Open channel selection overlay if all channel tabs will be closed after removing this tab + SelectTab(selectorTab); + + OnRequestLeave?.Invoke(tab.Value); + } + } +} diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs new file mode 100644 index 0000000000..e6de55f9b2 --- /dev/null +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs @@ -0,0 +1,212 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Chat.Tabs +{ + public class ChannelTabItem : TabItem + { + protected Color4 BackgroundInactive; + private Color4 backgroundHover; + protected Color4 BackgroundActive; + + public override bool IsRemovable => !Pinned; + + protected readonly SpriteText Text; + protected readonly SpriteText TextBold; + protected readonly ClickableContainer CloseButton; + private readonly Box box; + private readonly Box highlightBox; + protected readonly SpriteIcon Icon; + + public Action OnRequestClose; + private readonly Container content; + + protected override Container Content => content; + + public ChannelTabItem(Channel value) + : base(value) + { + Width = 150; + + RelativeSizeAxes = Axes.Y; + + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + Shear = new Vector2(ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0); + + Masking = true; + + InternalChildren = new Drawable[] + { + box = new Box + { + EdgeSmoothness = new Vector2(1, 0), + RelativeSizeAxes = Axes.Both, + }, + highlightBox = new Box + { + Width = 5, + Alpha = 0, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + EdgeSmoothness = new Vector2(1, 0), + RelativeSizeAxes = Axes.Y, + }, + content = new Container + { + Shear = new Vector2(-ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0), + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + Icon = new SpriteIcon + { + Icon = DisplayIcon, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.Black, + X = -10, + Alpha = 0.2f, + Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT), + }, + Text = new OsuSpriteText + { + Margin = new MarginPadding(5), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Text = value.ToString(), + TextSize = 18, + }, + TextBold = new OsuSpriteText + { + Alpha = 0, + Margin = new MarginPadding(5), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Text = value.ToString(), + Font = @"Exo2.0-Bold", + TextSize = 18, + }, + CloseButton = new TabCloseButton + { + Alpha = 0, + Margin = new MarginPadding { Right = 20 }, + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Action = delegate + { + if (IsRemovable) OnRequestClose?.Invoke(this); + }, + }, + }, + }, + }; + } + + protected virtual FontAwesome DisplayIcon => FontAwesome.fa_hashtag; + + protected virtual bool ShowCloseOnHover => true; + + protected override bool OnHover(HoverEvent e) + { + if (IsRemovable && ShowCloseOnHover) + CloseButton.FadeIn(200, Easing.OutQuint); + + if (!Active) + box.FadeColour(backgroundHover, TRANSITION_LENGTH, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + CloseButton.FadeOut(200, Easing.OutQuint); + updateState(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundActive = colours.ChatBlue; + BackgroundInactive = colours.Gray4; + backgroundHover = colours.Gray7; + + highlightBox.Colour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateState(); + FinishTransforms(true); + } + + private void updateState() + { + if (Active) + FadeActive(); + else + FadeInactive(); + } + + protected const float TRANSITION_LENGTH = 400; + + private readonly EdgeEffectParameters activateEdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 15, + Colour = Color4.Black.Opacity(0.4f), + }; + + private readonly EdgeEffectParameters deactivateEdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 10, + Colour = Color4.Black.Opacity(0.2f), + }; + + protected virtual void FadeActive() + { + this.ResizeHeightTo(1.1f, TRANSITION_LENGTH, Easing.OutQuint); + + TweenEdgeEffectTo(activateEdgeEffect, TRANSITION_LENGTH); + + box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint); + highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + + Text.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + TextBold.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + } + + protected virtual void FadeInactive() + { + this.ResizeHeightTo(1, TRANSITION_LENGTH, Easing.OutQuint); + + TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH); + + box.FadeColour(BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint); + highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + + Text.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + TextBold.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + } + + protected override void OnActivated() => updateState(); + protected override void OnDeactivated() => updateState(); + } +} diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs new file mode 100644 index 0000000000..c7ca1cb073 --- /dev/null +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Users; +using OpenTK; + +namespace osu.Game.Overlays.Chat.Tabs +{ + public class PrivateChannelTabItem : ChannelTabItem + { + private readonly OsuSpriteText username; + private readonly Avatar avatarContainer; + + protected override FontAwesome DisplayIcon => FontAwesome.fa_at; + + public PrivateChannelTabItem(Channel value) + : base(value) + { + if (value.Type != ChannelType.PM) + throw new ArgumentException("Argument value needs to have the targettype user!"); + + AddRange(new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Margin = new MarginPadding + { + Horizontal = 3 + }, + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + Children = new Drawable[] + { + new CircularContainer + { + Scale = new Vector2(0.95f), + Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Masking = true, + Child = new DelayedLoadWrapper(new Avatar(value.Users.First()) + { + RelativeSizeAxes = Axes.Both, + OnLoadComplete = d => d.FadeInFromZero(300, Easing.OutQuint), + }) + { + RelativeSizeAxes = Axes.Both, + } + }, + } + }, + }); + + Text.X = ChatOverlay.TAB_AREA_HEIGHT; + TextBold.X = ChatOverlay.TAB_AREA_HEIGHT; + } + + protected override bool ShowCloseOnHover => false; + + protected override void FadeActive() + { + base.FadeActive(); + + this.ResizeWidthTo(200, TRANSITION_LENGTH, Easing.OutQuint); + CloseButton.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); + } + + + protected override void FadeInactive() + { + base.FadeInactive(); + + this.ResizeWidthTo(ChatOverlay.TAB_AREA_HEIGHT + 10, TRANSITION_LENGTH, Easing.OutQuint); + CloseButton.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + var user = Value.Users.First(); + + BackgroundActive = user.Colour != null ? OsuColour.FromHex(user.Colour) : colours.BlueDark; + BackgroundInactive = BackgroundActive.Darken(0.5f); + } + } +} diff --git a/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs b/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs new file mode 100644 index 0000000000..0adaa40889 --- /dev/null +++ b/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Chat.Tabs +{ + public class TabCloseButton : OsuClickableContainer + { + private readonly SpriteIcon icon; + + public TabCloseButton() + { + Size = new Vector2(20); + + Child = icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.75f), + Icon = FontAwesome.fa_close, + RelativeSizeAxes = Axes.Both, + }; + } + + protected override bool OnMouseDown(MouseDownEvent e) + { + icon.ScaleTo(0.5f, 1000, Easing.OutQuint); + return base.OnMouseDown(e); + } + + protected override bool OnMouseUp(MouseUpEvent e) + { + icon.ScaleTo(0.75f, 1000, Easing.OutElastic); + return base.OnMouseUp(e); + } + + protected override bool OnHover(HoverEvent e) + { + icon.FadeColour(Color4.Red, 200, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + icon.FadeColour(Color4.White, 200, Easing.OutQuint); + base.OnHoverLost(e); + } + } +} diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index ff2ff9af14..e45373c36f 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -1,9 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using System.Collections.Generic; -using System.Linq; +using System.Collections.Specialized; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -13,42 +12,38 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; using osu.Game.Overlays.Chat; +using osu.Game.Overlays.Chat.Selection; +using osu.Game.Overlays.Chat.Tabs; namespace osu.Game.Overlays { - public class ChatOverlay : OsuFocusedOverlayContainer, IOnlineComponent + public class ChatOverlay : OsuFocusedOverlayContainer { private const float textbox_height = 60; private const float channel_selection_min_height = 0.3f; - private ScheduledDelegate messageRequest; + private ChannelManager channelManager; private readonly Container currentChannelContainer; + private readonly List loadedChannels = new List(); private readonly LoadingAnimation loading; private readonly FocusedTextBox textbox; - private APIAccess api; - private const int transition_length = 500; public const float DEFAULT_HEIGHT = 0.4f; public const float TAB_AREA_HEIGHT = 50; - private GetUpdatesRequest fetchReq; - - private readonly ChatTabControl channelTabs; + private readonly ChannelTabControl channelTabControl; private readonly Container chatContainer; private readonly TabsArea tabsArea; @@ -57,7 +52,6 @@ namespace osu.Game.Overlays public Bindable ChatHeight { get; set; } - public List AvailableChannels { get; private set; } = new List(); private readonly Container channelSelectionContainer; private readonly ChannelSelectionOverlay channelSelection; @@ -154,10 +148,12 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - channelTabs = new ChatTabControl + channelTabControl = new ChannelTabControl { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - OnRequestLeave = removeChannel, + OnRequestLeave = channel => channelManager.LeaveChannel(channel) }, } }, @@ -165,11 +161,11 @@ namespace osu.Game.Overlays }, }; - channelTabs.Current.ValueChanged += newChannel => CurrentChannel = newChannel; - channelTabs.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden; + channelTabControl.Current.ValueChanged += chat => channelManager.CurrentChannel.Value = chat; + channelTabControl.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden; channelSelection.StateChanged += state => { - channelTabs.ChannelSelectorActive.Value = state == Visibility.Visible; + channelTabControl.ChannelSelectorActive.Value = state == Visibility.Visible; if (state == Visibility.Visible) { @@ -180,13 +176,75 @@ namespace osu.Game.Overlays else textbox.HoldFocus = true; }; + + channelSelection.OnRequestJoin = channel => channelManager.JoinChannel(channel); + channelSelection.OnRequestLeave = channel => channelManager.LeaveChannel(channel); + } + + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (Channel newChannel in args.NewItems) + { + channelTabControl.AddChannel(newChannel); + } + + break; + case NotifyCollectionChangedAction.Remove: + foreach (Channel removedChannel in args.OldItems) + { + channelTabControl.RemoveChannel(removedChannel); + loadedChannels.Remove(loadedChannels.Find(c => c.Channel == removedChannel)); + } + + break; + } + } + + private void currentChannelChanged(Channel channel) + { + if (channel == null) + { + textbox.Current.Disabled = true; + currentChannelContainer.Clear(false); + channelTabControl.Current.Value = null; + return; + } + + textbox.Current.Disabled = channel.ReadOnly; + + if (channelTabControl.Current.Value != channel) + Scheduler.Add(() => channelTabControl.Current.Value = channel); + + var loaded = loadedChannels.Find(d => d.Channel == channel); + if (loaded == null) + { + currentChannelContainer.FadeOut(500, Easing.OutQuint); + loading.Show(); + + loaded = new DrawableChannel(channel); + loadedChannels.Add(loaded); + LoadComponentAsync(loaded, l => + { + loading.Hide(); + + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loaded); + currentChannelContainer.FadeIn(500, Easing.OutQuint); + }); + } + else + { + currentChannelContainer.Clear(false); + Scheduler.Add(() => currentChannelContainer.Add(loaded)); + } } private double startDragChatHeight; private bool isDragging; - public void OpenChannel(Channel channel) => addChannel(channel); - protected override bool OnDragStart(DragStartEvent e) { isDragging = tabsArea.IsHovered; @@ -220,19 +278,6 @@ namespace osu.Game.Overlays return base.OnDragEnd(e); } - public void APIStateChanged(APIAccess api, APIState state) - { - switch (state) - { - case APIState.Online: - initializeChannels(); - break; - default: - messageRequest?.Cancel(); - break; - } - } - public override bool AcceptsFocus => true; protected override void OnFocus(FocusEvent e) @@ -261,11 +306,8 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(APIAccess api, OsuConfigManager config, OsuColour colours) + private void load(OsuConfigManager config, OsuColour colours, ChannelManager channelManager) { - this.api = api; - api.Register(this); - ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); ChatHeight.ValueChanged += h => { @@ -276,255 +318,49 @@ namespace osu.Game.Overlays ChatHeight.TriggerChange(); chatBackground.Colour = colours.ChatBlue; - } - private long lastMessageId; - - private readonly List careChannels = new List(); - - private readonly List loadedChannels = new List(); - - private void initializeChannels() - { loading.Show(); - messageRequest?.Cancel(); + this.channelManager = channelManager; + channelManager.CurrentChannel.ValueChanged += currentChannelChanged; + channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; + channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; - ListChannelsRequest req = new ListChannelsRequest(); - req.Success += delegate(List channels) - { - AvailableChannels = channels; - - Scheduler.Add(delegate - { - //todo: decide how to handle default channels for a user now that they are saved server-side. - addChannel(channels.Find(c => c.Name == @"#lazer")); - addChannel(channels.Find(c => c.Name == @"#osu")); - - channelSelection.OnRequestJoin = addChannel; - channelSelection.OnRequestLeave = removeChannel; - channelSelection.Sections = new[] - { - new ChannelSection - { - Header = "All Channels", - Channels = channels, - }, - }; - }); - - messageRequest = Scheduler.AddDelayed(fetchUpdates, 1000, true); - }; - - api.Queue(req); + //for the case that channelmanager was faster at fetching the channels than our attachment to CollectionChanged. + channelSelection.UpdateAvailableChannels(channelManager.AvailableChannels); + joinedChannelsChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, channelManager.JoinedChannels)); } - private Channel currentChannel; - - protected Channel CurrentChannel + private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs e) { - get { return currentChannel; } + channelSelection.UpdateAvailableChannels(channelManager.AvailableChannels); + } - set + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (channelManager != null) { - if (currentChannel == value) return; - - if (value == null) - { - currentChannel = null; - textbox.Current.Disabled = true; - currentChannelContainer.Clear(false); - return; - } - - currentChannel = value; - - textbox.Current.Disabled = currentChannel.ReadOnly; - channelTabs.Current.Value = value; - - var loaded = loadedChannels.Find(d => d.Channel == value); - if (loaded == null) - { - currentChannelContainer.FadeOut(500, Easing.OutQuint); - loading.Show(); - - loaded = new DrawableChannel(currentChannel); - loadedChannels.Add(loaded); - LoadComponentAsync(loaded, l => - { - if (currentChannel.MessagesLoaded) - loading.Hide(); - - currentChannelContainer.Clear(false); - currentChannelContainer.Add(loaded); - currentChannelContainer.FadeIn(500, Easing.OutQuint); - }); - } - else - { - currentChannelContainer.Clear(false); - currentChannelContainer.Add(loaded); - } + channelManager.CurrentChannel.ValueChanged -= currentChannelChanged; + channelManager.JoinedChannels.CollectionChanged -= joinedChannelsChanged; + channelManager.AvailableChannels.CollectionChanged -= availableChannelsChanged; } } - private void addChannel(Channel channel) - { - if (channel == null) return; - - // ReSharper disable once AccessToModifiedClosure - var existing = careChannels.Find(c => c.Id == channel.Id); - - if (existing != null) - { - // if we already have this channel loaded, we don't want to make a second one. - channel = existing; - } - else - { - careChannels.Add(channel); - channelTabs.AddItem(channel); - - if (channel.Type == ChannelType.Public && !channel.Joined) - { - var req = new JoinChannelRequest(channel, api.LocalUser); - req.Success += () => addChannel(channel); - req.Failure += ex => removeChannel(channel); - api.Queue(req); - return; - } - } - - // let's fetch a small number of messages to bring us up-to-date with the backlog. - fetchInitialMessages(channel); - - if (CurrentChannel == null) - CurrentChannel = channel; - - channel.Joined.Value = true; - } - - private void removeChannel(Channel channel) - { - if (channel == null) return; - - if (channel == CurrentChannel) CurrentChannel = null; - - careChannels.Remove(channel); - loadedChannels.Remove(loadedChannels.Find(c => c.Channel == channel)); - channelTabs.RemoveItem(channel); - - api.Queue(new LeaveChannelRequest(channel, api.LocalUser)); - channel.Joined.Value = false; - } - - private void fetchInitialMessages(Channel channel) - { - var req = new GetMessagesRequest(channel); - req.Success += messages => - { - channel.AddNewMessages(messages.ToArray()); - if (channel == currentChannel) - loading.Hide(); - }; - - api.Queue(req); - } - - private void fetchUpdates() - { - if (fetchReq != null) return; - - fetchReq = new GetUpdatesRequest(lastMessageId); - - fetchReq.Success += updates => - { - if (updates?.Presence != null) - { - foreach (var channel in updates.Presence) - addChannel(AvailableChannels.Find(c => c.Id == channel.Id)); - - foreach (var group in updates.Messages.GroupBy(m => m.ChannelId)) - careChannels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray()); - - lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId; - } - - fetchReq = null; - }; - - fetchReq.Failure += delegate { fetchReq = null; }; - - api.Queue(fetchReq); - } - private void postMessage(TextBox textbox, bool newText) { - var postText = textbox.Text; + var text = textbox.Text.Trim(); + + if (string.IsNullOrWhiteSpace(text)) + return; + + if (text[0] == '/') + channelManager.PostCommand(text.Substring(1)); + else + channelManager.PostMessage(text); textbox.Text = string.Empty; - - if (string.IsNullOrWhiteSpace(postText)) - return; - - var target = currentChannel; - - if (target == null) return; - - if (!api.IsLoggedIn) - { - target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); - return; - } - - bool isAction = false; - - if (postText[0] == '/') - { - string[] parameters = postText.Substring(1).Split(new[] { ' ' }, 2); - string command = parameters[0]; - string content = parameters.Length == 2 ? parameters[1] : string.Empty; - - switch (command) - { - case "me": - - if (string.IsNullOrWhiteSpace(content)) - { - currentChannel.AddNewMessages(new ErrorMessage("Usage: /me [action]")); - return; - } - - isAction = true; - postText = content; - break; - - case "help": - currentChannel.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]")); - return; - - default: - currentChannel.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help")); - return; - } - } - - var message = new LocalEchoMessage - { - Sender = api.LocalUser.Value, - Timestamp = DateTimeOffset.Now, - ChannelId = target.Id, - IsAction = isAction, - Content = postText - }; - - var req = new PostMessageRequest(message); - - target.AddLocalEcho(message); - req.Failure += e => target.ReplaceMessage(message, null); - req.Success += m => target.ReplaceMessage(message, m); - - api.Queue(req); } private class TabsArea : Container diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index 2d492dcf1e..e4807baeb4 100644 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -37,7 +36,7 @@ namespace osu.Game.Overlays.Music new CollectionsDropdown { RelativeSizeAxes = Axes.X, - Items = new[] { new KeyValuePair(@"All", PlaylistCollection.All) }, + Items = new[] { PlaylistCollection.All }, } }, }, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index d637eb96f6..ad79024f62 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.Audio { @@ -35,12 +36,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private void updateItems() { - var deviceItems = new List> { new KeyValuePair("Default", string.Empty) }; - deviceItems.AddRange(audio.AudioDeviceNames.Select(d => new KeyValuePair(d, d))); + var deviceItems = new List { string.Empty }; + deviceItems.AddRange(audio.AudioDeviceNames); var preferredDeviceName = audio.AudioDevice.Value; - if (deviceItems.All(kv => kv.Value != preferredDeviceName)) - deviceItems.Add(new KeyValuePair(preferredDeviceName, preferredDeviceName)); + if (deviceItems.All(kv => kv != preferredDeviceName)) + deviceItems.Add(preferredDeviceName); // The option dropdown for audio device selection lists all audio // device names. Dropdowns, however, may not have multiple identical @@ -59,7 +60,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Children = new Drawable[] { - dropdown = new SettingsDropdown() + dropdown = new AudioDeviceSettingsDropdown() }; updateItems(); @@ -69,5 +70,16 @@ namespace osu.Game.Overlays.Settings.Sections.Audio audio.OnNewDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged; } + + private class AudioDeviceSettingsDropdown : SettingsDropdown + { + protected override OsuDropdown CreateDropdown() => new AudioDeviceDropdownControl { Items = Items }; + + private class AudioDeviceDropdownControl : DropdownControl + { + protected override string GenerateItemText(string item) + => string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item); + } + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 254de6c6f7..685244e06b 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -6,9 +6,9 @@ using System.Drawing; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.Graphics { @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics if (resolutions.Count > 1) { - resolutionSettingsContainer.Child = resolutionDropdown = new SettingsDropdown + resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown { LabelText = "Resolution", ShowsDefaultIndicator = false, @@ -115,18 +115,36 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, true); } - private IReadOnlyList> getResolutions() + private IReadOnlyList getResolutions() { - var resolutions = new KeyValuePair("Default", new Size(9999, 9999)).Yield(); + var resolutions = new List { new Size(9999, 9999) }; if (game.Window != null) - resolutions = resolutions.Concat(game.Window.AvailableResolutions - .Where(r => r.Width >= 800 && r.Height >= 600) - .OrderByDescending(r => r.Width) - .ThenByDescending(r => r.Height) - .Select(res => new KeyValuePair($"{res.Width}x{res.Height}", new Size(res.Width, res.Height))) - .Distinct()); - return resolutions.ToList(); + { + resolutions.AddRange(game.Window.AvailableResolutions + .Where(r => r.Width >= 800 && r.Height >= 600) + .OrderByDescending(r => r.Width) + .ThenByDescending(r => r.Height) + .Select(res => new Size(res.Width, res.Height)) + .Distinct()); + } + + return resolutions; + } + + private class ResolutionSettingsDropdown : SettingsDropdown + { + protected override OsuDropdown CreateDropdown() => new ResolutionDropdownControl { Items = Items }; + + private class ResolutionDropdownControl : DropdownControl + { + protected override string GenerateItemText(Size item) + { + if (item == new Size(9999, 9999)) + return "Default"; + return $"{item.Width}x{item.Height}"; + } + } } } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 15787d29f7..af7864836b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,9 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics; @@ -15,12 +15,15 @@ namespace osu.Game.Overlays.Settings.Sections { public class SkinSection : SettingsSection { - private SettingsDropdown skinDropdown; + private SkinSettingsDropdown skinDropdown; public override string Header => "Skin"; public override FontAwesome Icon => FontAwesome.fa_paint_brush; + private readonly Bindable dropdownBindable = new Bindable(); + private readonly Bindable configBindable = new Bindable(); + private SkinManager skins; [BackgroundDependencyLoader] @@ -31,7 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { - skinDropdown = new SettingsDropdown(), + skinDropdown = new SkinSettingsDropdown(), new SettingsSlider { LabelText = "Menu cursor size", @@ -54,19 +57,21 @@ namespace osu.Game.Overlays.Settings.Sections skins.ItemAdded += itemAdded; skins.ItemRemoved += itemRemoved; - skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); + config.BindWith(OsuSetting.Skin, configBindable); - var skinBindable = config.GetBindable(OsuSetting.Skin); + skinDropdown.Bindable = dropdownBindable; + skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); // Todo: This should not be necessary when OsuConfigManager is databased - if (skinDropdown.Items.All(s => s.Value != skinBindable.Value)) - skinBindable.Value = 0; + if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) + configBindable.Value = 0; - skinDropdown.Bindable = skinBindable; + configBindable.BindValueChanged(v => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == v), true); + dropdownBindable.BindValueChanged(v => configBindable.Value = v.ID); } - private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.Value != s.ID); - private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(new KeyValuePair(s.ToString(), s.ID)); + private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray(); + private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray(); protected override void Dispose(bool isDisposing) { @@ -83,5 +88,15 @@ namespace osu.Game.Overlays.Settings.Sections { public override string TooltipText => Current.Value.ToString(@"0.##x"); } + + private class SkinSettingsDropdown : SettingsDropdown + { + protected override OsuDropdown CreateDropdown() => new SkinDropdownControl { Items = Items }; + + private class SkinDropdownControl : DropdownControl + { + protected override string GenerateItemText(SkinInfo item) => item.ToString(); + } + } } } diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index 33a8af7d91..1c3eb6c245 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -2,36 +2,41 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { public class SettingsDropdown : SettingsItem { - private Dropdown dropdown; + protected new OsuDropdown Control => (OsuDropdown)base.Control; - private IEnumerable> items = new KeyValuePair[] { }; - public IEnumerable> Items + private IEnumerable items = Enumerable.Empty(); + + public IEnumerable Items { - get - { - return items; - } + get => items; set { items = value; - if (dropdown != null) - dropdown.Items = value; + + if (Control != null) + Control.Items = value; } } - protected override Drawable CreateControl() => dropdown = new OsuDropdown + protected sealed override Drawable CreateControl() => CreateDropdown(); + + protected virtual OsuDropdown CreateDropdown() => new DropdownControl { Items = Items }; + + protected class DropdownControl : OsuDropdown { - Margin = new MarginPadding { Top = 5 }, - RelativeSizeAxes = Axes.X, - Items = Items, - }; + public DropdownControl() + { + Margin = new MarginPadding { Top = 5 }; + RelativeSizeAxes = Axes.X; + } + } } } diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index 64811137a6..2a98b80816 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -8,10 +8,15 @@ namespace osu.Game.Overlays.Settings { public class SettingsEnumDropdown : SettingsDropdown { - protected override Drawable CreateControl() => new OsuEnumDropdown + protected override OsuDropdown CreateDropdown() => new DropdownControl(); + + protected class DropdownControl : OsuEnumDropdown { - Margin = new MarginPadding { Top = 5 }, - RelativeSizeAxes = Axes.X, - }; + public DropdownControl() + { + Margin = new MarginPadding { Top = 5 }; + RelativeSizeAxes = Axes.X; + } + } } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 932cfe5789..86ecbc0bb0 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -37,6 +38,8 @@ namespace osu.Game.Rulesets.Edit private BlueprintContainer blueprintContainer; + private InputManager inputManager; + internal HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; @@ -114,6 +117,13 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items[0].Select(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + inputManager = GetContainingInputManager(); + } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -137,6 +147,11 @@ namespace osu.Game.Rulesets.Edit }); } + /// + /// Whether the user's cursor is currently in an area of the that is valid for placement. + /// + public virtual bool CursorInPlacementArea => rulesetContainer.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); + /// /// Adds a to the and visualises it. /// diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index b726b683ea..45dc7e4a05 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -18,8 +20,18 @@ namespace osu.Game.Rulesets.Edit /// /// A blueprint which governs the creation of a new to actualisation. /// - public abstract class PlacementBlueprint : CompositeDrawable, IRequireHighFrequencyMousePosition + public abstract class PlacementBlueprint : CompositeDrawable, IStateful, IRequireHighFrequencyMousePosition { + /// + /// Invoked when has changed. + /// + public event Action StateChanged; + + /// + /// Whether the is currently being placed, but has not necessarily finished being placed. + /// + public bool PlacementBegun { get; private set; } + /// /// The that is being placed. /// @@ -37,6 +49,8 @@ namespace osu.Game.Rulesets.Edit HitObject = hitObject; RelativeSizeAxes = Axes.Both; + + Alpha = 0; } [BackgroundDependencyLoader] @@ -49,7 +63,25 @@ namespace osu.Game.Rulesets.Edit ApplyDefaultsToHitObject(); } - private bool placementBegun; + private PlacementState state; + + public PlacementState State + { + get => state; + set + { + if (state == value) + return; + state = value; + + if (state == PlacementState.Shown) + Show(); + else + Hide(); + + StateChanged?.Invoke(value); + } + } /// /// Signals that the placement of has started. @@ -57,7 +89,7 @@ namespace osu.Game.Rulesets.Edit protected void BeginPlacement() { placementHandler.BeginPlacement(HitObject); - placementBegun = true; + PlacementBegun = true; } /// @@ -66,7 +98,7 @@ namespace osu.Game.Rulesets.Edit /// protected void EndPlacement() { - if (!placementBegun) + if (!PlacementBegun) BeginPlacement(); placementHandler.EndPlacement(HitObject); } @@ -93,4 +125,10 @@ namespace osu.Game.Rulesets.Edit } } } + + public enum PlacementState + { + Hidden, + Shown, + } } diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs index ed61ed7022..5628fb51f3 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs @@ -17,6 +17,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) => (float)((time - currentTime) / timeRange * scrollLength); + public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) + => position * timeRange / scrollLength + currentTime; + public void Reset() { } diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs index 43bc1b2a2a..2ece9bef9b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs @@ -36,6 +36,16 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms /// The absolute spatial position. float PositionAt(double time, double currentTime, double timeRange, float scrollLength); + /// + /// Computes the time which brings a point to a provided spatial position given the current time. + /// + /// The absolute spatial position. + /// The current time. + /// The amount of visible time. + /// The absolute spatial length through . + /// The time at which == . + double TimeAt(float position, double currentTime, double timeRange, float scrollLength); + /// /// Resets this to a default state. /// diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs index f7c097e81d..4d9659c820 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs @@ -3,6 +3,7 @@ using osu.Framework.Lists; using osu.Game.Rulesets.Timing; +using OpenTK; namespace osu.Game.Rulesets.UI.Scrolling.Algorithms { @@ -36,6 +37,33 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) => (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength); + public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) + { + // Find the control point relating to the position. + // Note: Due to velocity adjustments, overlapping control points will provide multiple valid time values for a single position + // As such, this operation provides unexpected results by using the latter of the control points. + + int i = 0; + float pos = 0; + + for (; i < controlPoints.Count; i++) + { + float lastPos = pos; + pos = PositionAt(controlPoints[i].StartTime, currentTime, timeRange, scrollLength); + + if (pos > position) + { + i--; + pos = lastPos; + break; + } + } + + i = MathHelper.Clamp(i, 0, controlPoints.Count - 1); + + return controlPoints[i].StartTime + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength; + } + public void Reset() { } diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs index 54494cfe63..8f8f546992 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs @@ -35,6 +35,36 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms return (float)((relativePositionAtCached(time, timeRange) - timelinePosition) * scrollLength); } + public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) + { + // Convert the position to a length relative to time = 0 + double length = position / scrollLength + relativePositionAt(currentTime, timeRange); + + // We need to consider all timing points until the specified time and not just the currently-active one, + // since each timing point individually affects the positions of _all_ hitobjects after its start time + for (int i = 0; i < controlPoints.Count; i++) + { + var current = controlPoints[i]; + var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; + + // Duration of the current control point + var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime; + + // Figure out the length of control point + var currentLength = currentDuration / timeRange * current.Multiplier; + + if (currentLength > length) + { + // The point is within this control point + return current.StartTime + length * timeRange / current.Multiplier; + } + + length -= currentLength; + } + + return 0; // Should never occur + } + private double relativePositionAtCached(double time, double timeRange) { if (!positionCache.TryGetValue(time, out double existing)) diff --git a/osu.Game.Rulesets.Mania/UI/IScrollingInfo.cs b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs similarity index 54% rename from osu.Game.Rulesets.Mania/UI/IScrollingInfo.cs rename to osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs index ee65e9f1a5..21cbd855a9 100644 --- a/osu.Game.Rulesets.Mania/UI/IScrollingInfo.cs +++ b/osu.Game/Rulesets/UI/Scrolling/IScrollingInfo.cs @@ -3,9 +3,9 @@ using osu.Framework.Configuration; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; -namespace osu.Game.Rulesets.Mania.UI +namespace osu.Game.Rulesets.UI.Scrolling { public interface IScrollingInfo { @@ -13,5 +13,15 @@ namespace osu.Game.Rulesets.Mania.UI /// The direction s should scroll in. /// IBindable Direction { get; } + + /// + /// + /// + IBindable TimeRange { get; } + + /// + /// The algorithm which controls positions and sizes. + /// + IScrollAlgorithm Algorithm { get; } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 489604afc9..00642b3d41 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -1,59 +1,39 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Lists; -using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Timing; -using osu.Game.Rulesets.UI.Scrolling.Algorithms; namespace osu.Game.Rulesets.UI.Scrolling { public class ScrollingHitObjectContainer : HitObjectContainer { - /// - /// The duration required to scroll through one length of the before any control point adjustments. - /// - public readonly BindableDouble TimeRange = new BindableDouble - { - MinValue = 0, - MaxValue = double.MaxValue - }; + private readonly IBindable timeRange = new BindableDouble(); - /// - /// The control points that adjust the scrolling speed. - /// - protected readonly SortedList ControlPoints = new SortedList(); + private readonly IBindable direction = new Bindable(); - public readonly Bindable Direction = new Bindable(); - - private readonly IScrollAlgorithm algorithm; + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } private Cached initialStateCache = new Cached(); - public ScrollingHitObjectContainer(ScrollVisualisationMethod visualisationMethod) + public ScrollingHitObjectContainer() { RelativeSizeAxes = Axes.Both; + } - TimeRange.ValueChanged += _ => initialStateCache.Invalidate(); - Direction.ValueChanged += _ => initialStateCache.Invalidate(); + [BackgroundDependencyLoader] + private void load() + { + direction.BindTo(scrollingInfo.Direction); + timeRange.BindTo(scrollingInfo.TimeRange); - switch (visualisationMethod) - { - case ScrollVisualisationMethod.Sequential: - algorithm = new SequentialScrollAlgorithm(ControlPoints); - break; - case ScrollVisualisationMethod.Overlapping: - algorithm = new OverlappingScrollAlgorithm(ControlPoints); - break; - case ScrollVisualisationMethod.Constant: - algorithm = new ConstantScrollAlgorithm(); - break; - } + direction.ValueChanged += _ => initialStateCache.Invalidate(); + timeRange.ValueChanged += _ => initialStateCache.Invalidate(); } public override void Add(DrawableHitObject hitObject) @@ -70,20 +50,6 @@ namespace osu.Game.Rulesets.UI.Scrolling return result; } - public void AddControlPoint(MultiplierControlPoint controlPoint) - { - ControlPoints.Add(controlPoint); - initialStateCache.Invalidate(); - } - - public bool RemoveControlPoint(MultiplierControlPoint controlPoint) - { - var result = ControlPoints.Remove(controlPoint); - if (result) - initialStateCache.Invalidate(); - return result; - } - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) { if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0) @@ -100,7 +66,7 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!initialStateCache.IsValid) { - switch (Direction.Value) + switch (direction.Value) { case ScrollingDirection.Up: case ScrollingDirection.Down: @@ -111,7 +77,7 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - algorithm.Reset(); + scrollingInfo.Algorithm.Reset(); foreach (var obj in Objects) computeInitialStateRecursive(obj); @@ -121,19 +87,19 @@ namespace osu.Game.Rulesets.UI.Scrolling private void computeInitialStateRecursive(DrawableHitObject hitObject) { - hitObject.LifetimeStart = algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, TimeRange); + hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value); if (hitObject.HitObject is IHasEndTime endTime) { - switch (Direction.Value) + switch (direction.Value) { case ScrollingDirection.Up: case ScrollingDirection.Down: - hitObject.Height = algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, TimeRange, scrollLength); + hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, timeRange.Value, scrollLength); break; case ScrollingDirection.Left: case ScrollingDirection.Right: - hitObject.Width = algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, TimeRange, scrollLength); + hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, timeRange.Value, scrollLength); break; } } @@ -158,19 +124,19 @@ namespace osu.Game.Rulesets.UI.Scrolling private void updatePosition(DrawableHitObject hitObject, double currentTime) { - switch (Direction.Value) + switch (direction.Value) { case ScrollingDirection.Up: - hitObject.Y = algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + hitObject.Y = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); break; case ScrollingDirection.Down: - hitObject.Y = -algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + hitObject.Y = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); break; case ScrollingDirection.Left: - hitObject.X = algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + hitObject.X = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); break; case ScrollingDirection.Right: - hitObject.X = -algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, TimeRange, scrollLength); + hitObject.X = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength); break; } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 5e2704c9ee..0eb67b8bb1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -3,10 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Input.Bindings; -using osu.Game.Configuration; -using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI.Scrolling @@ -14,88 +10,19 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// A type of specialized towards scrolling s. /// - public abstract class ScrollingPlayfield : Playfield, IKeyBindingHandler + public abstract class ScrollingPlayfield : Playfield { - /// - /// The default span of time visible by the length of the scrolling axes. - /// This is clamped between and . - /// - private const double time_span_default = 1500; + protected readonly IBindable Direction = new Bindable(); - /// - /// The minimum span of time that may be visible by the length of the scrolling axes. - /// - private const double time_span_min = 50; - - /// - /// The maximum span of time that may be visible by the length of the scrolling axes. - /// - private const double time_span_max = 10000; - - /// - /// The step increase/decrease of the span of time visible by the length of the scrolling axes. - /// - private const double time_span_step = 200; - - /// - /// The span of time that is visible by the length of the scrolling axes. - /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. - /// - public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default) - { - Default = time_span_default, - MinValue = time_span_min, - MaxValue = time_span_max - }; - - /// - /// Whether the player can change . - /// - protected virtual bool UserScrollSpeedAdjustment => true; - - /// - /// The container that contains the s. - /// - public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)HitObjectContainer; - - /// - /// The direction in which s in this should scroll. - /// - protected readonly Bindable Direction = new Bindable(); - - protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential; + [Resolved] + private IScrollingInfo scrollingInfo { get; set; } [BackgroundDependencyLoader] private void load() { - HitObjects.TimeRange.BindTo(VisibleTimeRange); + Direction.BindTo(scrollingInfo.Direction); } - public bool OnPressed(GlobalAction action) - { - if (!UserScrollSpeedAdjustment) - return false; - - switch (action) - { - case GlobalAction.IncreaseScrollSpeed: - this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint); - return true; - case GlobalAction.DecreaseScrollSpeed: - this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) => false; - - protected sealed override HitObjectContainer CreateHitObjectContainer() - { - var container = new ScrollingHitObjectContainer(VisualisationMethod); - container.Direction.BindTo(Direction); - return container; - } + protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs index 41cdd6c06f..83b9e31a46 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs @@ -4,13 +4,18 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; namespace osu.Game.Rulesets.UI.Scrolling { @@ -18,20 +23,82 @@ namespace osu.Game.Rulesets.UI.Scrolling /// A type of that supports a . /// s inside this will scroll within the playfield. /// - public abstract class ScrollingRulesetContainer : RulesetContainer + public abstract class ScrollingRulesetContainer : RulesetContainer, IKeyBindingHandler where TObject : HitObject where TPlayfield : ScrollingPlayfield { + /// + /// The default span of time visible by the length of the scrolling axes. + /// This is clamped between and . + /// + private const double time_span_default = 1500; + + /// + /// The minimum span of time that may be visible by the length of the scrolling axes. + /// + private const double time_span_min = 50; + + /// + /// The maximum span of time that may be visible by the length of the scrolling axes. + /// + private const double time_span_max = 10000; + + /// + /// The step increase/decrease of the span of time visible by the length of the scrolling axes. + /// + private const double time_span_step = 200; + + protected readonly Bindable Direction = new Bindable(); + + /// + /// The span of time that is visible by the length of the scrolling axes. + /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. + /// + protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default) + { + Default = time_span_default, + MinValue = time_span_min, + MaxValue = time_span_max + }; + + protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential; + + /// + /// Whether the player can change . + /// + protected virtual bool UserScrollSpeedAdjustment => true; + /// /// Provides the default s that adjust the scrolling rate of s /// inside this . /// /// - protected readonly SortedList DefaultControlPoints = new SortedList(Comparer.Default); + private readonly SortedList controlPoints = new SortedList(Comparer.Default); + + protected IScrollingInfo ScrollingInfo => scrollingInfo; + + [Cached(Type = typeof(IScrollingInfo))] + private readonly LocalScrollingInfo scrollingInfo; protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { + scrollingInfo = new LocalScrollingInfo(); + scrollingInfo.Direction.BindTo(Direction); + scrollingInfo.TimeRange.BindTo(TimeRange); + + switch (VisualisationMethod) + { + case ScrollVisualisationMethod.Sequential: + scrollingInfo.Algorithm = new SequentialScrollAlgorithm(controlPoints); + break; + case ScrollVisualisationMethod.Overlapping: + scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(controlPoints); + break; + case ScrollVisualisationMethod.Constant: + scrollingInfo.Algorithm = new ConstantScrollAlgorithm(); + break; + } } [BackgroundDependencyLoader] @@ -75,19 +142,40 @@ namespace osu.Game.Rulesets.UI.Scrolling // Collapse sections with the same start time .GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime); - DefaultControlPoints.AddRange(timingChanges); + controlPoints.AddRange(timingChanges); // If we have no control points, add a default one - if (DefaultControlPoints.Count == 0) - DefaultControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); - - DefaultControlPoints.ForEach(c => applySpeedAdjustment(c, Playfield)); + if (controlPoints.Count == 0) + controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); } - private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield) + public bool OnPressed(GlobalAction action) { - playfield.HitObjects.AddControlPoint(controlPoint); - playfield.NestedPlayfields?.OfType().ForEach(p => applySpeedAdjustment(controlPoint, p)); + if (!UserScrollSpeedAdjustment) + return false; + + switch (action) + { + case GlobalAction.IncreaseScrollSpeed: + this.TransformBindableTo(TimeRange, TimeRange - time_span_step, 200, Easing.OutQuint); + return true; + case GlobalAction.DecreaseScrollSpeed: + this.TransformBindableTo(TimeRange, TimeRange + time_span_step, 200, Easing.OutQuint); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; + + private class LocalScrollingInfo : IScrollingInfo + { + public IBindable Direction { get; } = new Bindable(); + + public IBindable TimeRange { get; } = new BindableDouble(); + + public IScrollAlgorithm Algorithm { get; set; } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index acbfd1f1d6..1623ef0100 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -18,7 +18,10 @@ namespace osu.Game.Screens.Edit.Compose.Components public class BlueprintContainer : CompositeDrawable { private SelectionBlueprintContainer selectionBlueprints; + private Container placementBlueprintContainer; + private PlacementBlueprint currentPlacement; + private SelectionBox selectionBox; private IEnumerable selections => selectionBlueprints.Children.Where(c => c.IsAlive); @@ -117,19 +120,32 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } + protected override void Update() + { + base.Update(); + + if (currentPlacement != null) + { + if (composer.CursorInPlacementArea) + currentPlacement.State = PlacementState.Shown; + else if (currentPlacement?.PlacementBegun == false) + currentPlacement.State = PlacementState.Hidden; + } + } + /// /// Refreshes the current placement tool. /// private void refreshTool() { placementBlueprintContainer.Clear(); + currentPlacement = null; var blueprint = CurrentTool?.CreatePlacementBlueprint(); if (blueprint != null) - placementBlueprintContainer.Child = blueprint; + placementBlueprintContainer.Child = currentPlacement = blueprint; } - /// /// Select all masks in a given rectangle selection area. /// diff --git a/osu.Game/Screens/Play/HUD/QuitButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs similarity index 88% rename from osu.Game/Screens/Play/HUD/QuitButton.cs rename to osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 88547e0169..01f4551689 100644 --- a/osu.Game/Screens/Play/HUD/QuitButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -8,16 +8,18 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using OpenTK; namespace osu.Game.Screens.Play.HUD { - public class QuitButton : FillFlowContainer + public class HoldForMenuButton : FillFlowContainer { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -30,7 +32,7 @@ namespace osu.Game.Screens.Play.HUD private readonly OsuSpriteText text; - public QuitButton() + public HoldForMenuButton() { Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); @@ -79,7 +81,7 @@ namespace osu.Game.Screens.Play.HUD Alpha, MathHelper.Clamp(1 - positionalAdjust, 0.04f, 1), 0, 200, Easing.OutQuint); } - private class Button : HoldToConfirmContainer + private class Button : HoldToConfirmContainer, IKeyBindingHandler { private SpriteIcon icon; private CircularProgress circularProgress; @@ -90,6 +92,30 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + BeginConfirm(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) + { + switch (action) + { + case GlobalAction.Back: + AbortConfirm(); + return true; + } + + return false; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 0f9f4fc20a..93d92d062d 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; - public readonly QuitButton HoldToQuit; + public readonly HoldForMenuButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; private Bindable showHud; @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { KeyCounter = CreateKeyCounter(adjustableClock as IFrameBasedClock), - HoldToQuit = CreateQuitButton(), + HoldToQuit = CreateHoldForMenuButton(), } } } @@ -219,7 +219,7 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.X, }; - protected virtual QuitButton CreateQuitButton() => new QuitButton + protected virtual HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b3cbeb3850..a220a05971 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play { public class Player : ScreenWithBeatmapBackground, IProvideCursor { + protected override bool AllowBackButton => false; // handled by HoldForMenuButton + protected override float BackgroundParallaxAmount => 0.1f; protected override bool HideOverlaysOnEnter => true; diff --git a/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs index 9a654d3bfd..da47de8a9b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; @@ -25,7 +24,7 @@ namespace osu.Game.Screens.Play.PlayerSettings new CollectionsDropdown { RelativeSizeAxes = Axes.X, - Items = new[] { new KeyValuePair(@"All", PlaylistCollection.All) }, + Items = new[] { PlaylistCollection.All }, }, }; } diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs new file mode 100644 index 0000000000..1819fdb2a0 --- /dev/null +++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Lists; +using osu.Game.Configuration; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; + +namespace osu.Game.Tests.Visual +{ + /// + /// A container which provides a to children. + /// This should only be used when testing + /// + public class ScrollingTestContainer : Container + { + public SortedList ControlPoints => scrollingInfo.Algorithm.ControlPoints; + + public ScrollVisualisationMethod ScrollAlgorithm { set => scrollingInfo.Algorithm.Algorithm = value; } + + public double TimeRange { set => scrollingInfo.TimeRange.Value = value; } + + [Cached(Type = typeof(IScrollingInfo))] + private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); + + public ScrollingTestContainer(ScrollingDirection direction) + { + scrollingInfo.Direction.Value = direction; + } + + public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up; + + private class TestScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; + + public readonly Bindable TimeRange = new Bindable(1000) { Value = 1000 }; + IBindable IScrollingInfo.TimeRange => TimeRange; + + public readonly TestScrollAlgorithm Algorithm = new TestScrollAlgorithm(); + IScrollAlgorithm IScrollingInfo.Algorithm => Algorithm; + } + + private class TestScrollAlgorithm : IScrollAlgorithm + { + public readonly SortedList ControlPoints = new SortedList(); + + private IScrollAlgorithm implementation; + + public TestScrollAlgorithm() + { + Algorithm = ScrollVisualisationMethod.Constant; + } + + public ScrollVisualisationMethod Algorithm + { + set + { + switch (value) + { + case ScrollVisualisationMethod.Constant: + implementation = new ConstantScrollAlgorithm(); + break; + case ScrollVisualisationMethod.Overlapping: + implementation = new OverlappingScrollAlgorithm(ControlPoints); + break; + case ScrollVisualisationMethod.Sequential: + implementation = new SequentialScrollAlgorithm(ControlPoints); + break; + } + } + } + + public double GetDisplayStartTime(double time, double timeRange) + => implementation.GetDisplayStartTime(time, timeRange); + + public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) + => implementation.GetLength(startTime, endTime, timeRange, scrollLength); + + public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) + => implementation.PositionAt(time, currentTime, timeRange, scrollLength); + + public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) + => implementation.TimeAt(position, currentTime, timeRange, scrollLength); + + public void Reset() + => implementation.Reset(); + } + } +} diff --git a/osu.Game/Tests/Visual/SelectionBlueprintTestCase.cs b/osu.Game/Tests/Visual/SelectionBlueprintTestCase.cs index b1df849a67..183bef7602 100644 --- a/osu.Game/Tests/Visual/SelectionBlueprintTestCase.cs +++ b/osu.Game/Tests/Visual/SelectionBlueprintTestCase.cs @@ -29,9 +29,12 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - base.Content.Add(blueprint = CreateBlueprint()); + blueprint = CreateBlueprint(); + blueprint.Depth = float.MinValue; blueprint.SelectionRequested += (_, __) => blueprint.Select(); + Add(blueprint); + AddStep("Select", () => blueprint.Select()); AddStep("Deselect", () => blueprint.Deselect()); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9f7996a5fd..8c47df654a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - +