1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 15:23:14 +08:00

Merge remote-tracking branch 'origin/master' into mania-selection-movement

This commit is contained in:
smoogipoo 2018-11-15 20:47:53 +09:00
commit 63642586c4
72 changed files with 1950 additions and 737 deletions

6
.gitignore vendored
View File

@ -252,7 +252,11 @@ paket-files/
.fake/
# JetBrains Rider
.idea/
.idea/.idea.osu/.idea/*.xml
.idea/.idea.osu/.idea/codeStyles/*.xml
.idea/.idea.osu/.idea/dataSources/*.xml
.idea/.idea.osu/.idea/dictionaries/*.xml
.idea/.idea.osu/*.iml
*.sln.iml
# CodeRush

@ -1 +1 @@
Subproject commit 9ee64e369fe6fdafc6aed40f5a35b5f01eb82c53
Subproject commit 651e598b016b43e31ab1c1b29d5b30c92361b8d9

View File

@ -5,6 +5,7 @@ using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
@ -37,13 +38,11 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new JuiceStream
{
X = 0.5f - width / 2,
ControlPoints = new[]
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
},
PathType = PathType.Linear,
Distance = width * CatchPlayfield.BASE_WIDTH,
}),
StartTime = i * 2000,
NewCombo = i % 8 == 0
});

View File

@ -34,9 +34,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
ControlPoints = curveData.ControlPoints,
PathType = curveData.PathType,
Distance = curveData.Distance,
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,

View File

@ -10,7 +10,6 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
namespace osu.Game.Rulesets.Catch.Objects
{
@ -138,28 +137,18 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration => EndTime - StartTime;
public double Distance
private SliderPath path;
public SliderPath Path
{
get { return Path.Distance; }
set { Path.Distance = value; }
get => path;
set => path = value;
}
public SliderPath Path { get; } = new SliderPath();
public Vector2[] ControlPoints
{
get { return Path.ControlPoints; }
set { Path.ControlPoints = value; }
}
public double Distance => Path.Distance;
public List<List<SampleInfo>> NodeSamples { get; set; } = new List<List<SampleInfo>>();
public PathType PathType
{
get { return Path.PathType; }
set { Path.PathType = value; }
}
public double? LegacyLastTickOffset { get; set; }
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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;
}
}
}

View File

@ -1,10 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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()

View File

@ -18,6 +18,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
@ -108,13 +109,12 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(239, 176),
ControlPoints = new[]
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(154, 28),
new Vector2(52, -34)
},
Distance = 700,
}, 700),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats),
StackHeight = 10
@ -141,12 +141,11 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-(distance / 2), 0),
ControlPoints = new[]
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(distance, 0),
},
Distance = distance,
}, distance),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats),
StackHeight = stackHeight
@ -161,13 +160,12 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new[]
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(200, 200),
new Vector2(400, 0)
},
Distance = 600,
}, 600),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
@ -181,10 +179,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
PathType = PathType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new[]
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(150, 75),
@ -192,8 +189,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(300, -200),
new Vector2(400, 0),
new Vector2(430, 0)
},
Distance = 793.4417,
}),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
@ -207,18 +203,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
PathType = PathType.Bezier,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new[]
Path = new SliderPath(PathType.Bezier, new[]
{
Vector2.Zero,
new Vector2(150, 75),
new Vector2(200, 100),
new Vector2(300, -200),
new Vector2(430, 0)
},
Distance = 480,
}),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
@ -232,10 +226,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
PathType = PathType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(0, 0),
ControlPoints = new[]
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(-200, 0),
@ -243,8 +236,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(0, -200),
new Vector2(-200, -200),
new Vector2(0, -200)
},
Distance = 1000,
}),
RepeatCount = repeats,
NodeSamples = createEmptySamples(repeats)
};
@ -264,15 +256,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-100, 0),
PathType = PathType.Catmull,
ControlPoints = new[]
Path = new SliderPath(PathType.Catmull, new[]
{
Vector2.Zero,
new Vector2(50, -50),
new Vector2(150, 50),
new Vector2(200, 0)
},
Distance = 300,
}),
RepeatCount = repeats,
NodeSamples = repeatSamples
};

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
@ -35,14 +36,12 @@ namespace osu.Game.Rulesets.Osu.Tests
var slider = new Slider
{
Position = new Vector2(256, 192),
ControlPoints = new[]
Path = new SliderPath(PathType.Bezier, new[]
{
Vector2.Zero,
new Vector2(150, 150),
new Vector2(300, 0)
},
PathType = PathType.Bezier,
Distance = 350
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });

View File

@ -35,9 +35,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
StartTime = original.StartTime,
Samples = original.Samples,
ControlPoints = curveData.ControlPoints,
PathType = curveData.PathType,
Distance = curveData.Distance,
Path = curveData.Path,
NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -9,6 +8,7 @@ using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using OpenTK;
@ -55,16 +55,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
Position = slider.StackedPosition + slider.ControlPoints[index];
Position = slider.StackedPosition + slider.Path.ControlPoints[index];
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
path.ClearVertices();
if (index != slider.ControlPoints.Length - 1)
if (index != slider.Path.ControlPoints.Length - 1)
{
path.AddVertex(Vector2.Zero);
path.AddVertex(slider.ControlPoints[index + 1] - slider.ControlPoints[index]);
path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
}
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnDrag(DragEvent e)
{
var newControlPoints = slider.ControlPoints.ToArray();
var newControlPoints = slider.Path.ControlPoints.ToArray();
if (index == 0)
{
@ -96,8 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (isSegmentSeparatorWithPrevious)
newControlPoints[index - 1] = newControlPoints[index];
slider.ControlPoints = newControlPoints;
slider.Path.Calculate(true);
slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
return true;
}
@ -106,8 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
private bool isSegmentSeparatorWithNext => index < slider.ControlPoints.Length - 1 && slider.ControlPoints[index + 1] == slider.ControlPoints[index];
private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.ControlPoints[index - 1] == slider.ControlPoints[index];
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
}
}

View File

@ -19,15 +19,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
slider.ControlPointsChanged += _ => updatePathControlPoints();
slider.PathChanged += _ => updatePathControlPoints();
updatePathControlPoints();
}
private void updatePathControlPoints()
{
while (slider.ControlPoints.Length > pieces.Count)
while (slider.Path.ControlPoints.Length > pieces.Count)
pieces.Add(new PathControlPointPiece(slider, pieces.Count));
while (slider.ControlPoints.Length < pieces.Count)
while (slider.Path.ControlPoints.Length < pieces.Count)
pieces.Remove(pieces[pieces.Count - 1]);
}
}

View File

@ -45,8 +45,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
base.Update();
slider.Path.Calculate();
var vertices = new List<Vector2>();
slider.Path.GetPathToProgress(vertices, 0, 1);

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
this.position = position;
slider.ControlPointsChanged += _ => UpdatePosition();
slider.PathChanged += _ => UpdatePosition();
}
protected override void UpdatePosition()

View File

@ -1,16 +1,15 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.MathUtils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using OpenTK;
@ -119,12 +118,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
for (int i = 0; i < segments.Count; i++)
segments[i].Calculate(i == segments.Count - 1 ? (Vector2?)cursor : null);
HitObject.ControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
HitObject.PathType = HitObject.ControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear;
HitObject.Distance = segments.Sum(s => s.Distance);
var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
}
private void setState(PlacementState newState)
@ -140,41 +135,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private class Segment
{
public float Distance { get; private set; }
public readonly List<Vector2> ControlPoints = new List<Vector2>();
public Segment(Vector2 offset)
{
ControlPoints.Add(offset);
}
public void Calculate(Vector2? cursor = null)
{
Span<Vector2> allControlPoints = stackalloc Vector2[ControlPoints.Count + (cursor.HasValue ? 1 : 0)];
for (int i = 0; i < ControlPoints.Count; i++)
allControlPoints[i] = ControlPoints[i];
if (cursor.HasValue)
allControlPoints[allControlPoints.Length - 1] = cursor.Value;
List<Vector2> result;
switch (allControlPoints.Length)
{
case 1:
case 2:
result = PathApproximator.ApproximateLinear(allControlPoints);
break;
default:
result = PathApproximator.ApproximateBezier(allControlPoints);
break;
}
Distance = 0;
for (int i = 0; i < result.Count - 1; i++)
Distance += Vector2.Distance(result[i], result[i + 1]);
}
}
}
}

View File

@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
isPlacingEnd = true;
piece.FadeTo(1f, 150, Easing.OutQuint);
BeginPlacement();
}
return true;

View File

@ -32,12 +32,11 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var newControlPoints = new Vector2[slider.ControlPoints.Length];
for (int i = 0; i < slider.ControlPoints.Length; i++)
newControlPoints[i] = new Vector2(slider.ControlPoints[i].X, -slider.ControlPoints[i].Y);
var newControlPoints = new Vector2[slider.Path.ControlPoints.Length];
for (int i = 0; i < slider.Path.ControlPoints.Length; i++)
newControlPoints[i] = new Vector2(slider.Path.ControlPoints[i].X, -slider.Path.ControlPoints[i].Y);
slider.ControlPoints = newControlPoints;
slider.Path?.Calculate(); // Recalculate the slider curve
slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance);
}
}
}

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Ball.Scale = new Vector2(HitObject.Scale);
};
slider.ControlPointsChanged += _ => Body.Refresh();
slider.PathChanged += _ => Body.Refresh();
}
public override Color4 AccentColour

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.slider = slider;
h.PositionChanged += _ => updatePosition();
slider.ControlPointsChanged += _ => updatePosition();
slider.PathChanged += _ => updatePosition();
updatePosition();
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true;
hitCircle.PositionChanged += _ => updatePosition();
slider.ControlPointsChanged += _ => updatePosition();
slider.PathChanged += _ => updatePosition();
updatePosition();
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
private const float base_scoring_distance = 100;
public event Action<Vector2[]> ControlPointsChanged;
public event Action<SliderPath> PathChanged;
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double Duration => EndTime - StartTime;
@ -52,35 +52,23 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
public SliderPath Path { get; } = new SliderPath();
private SliderPath path;
public Vector2[] ControlPoints
public SliderPath Path
{
get => Path.ControlPoints;
get => path;
set
{
if (Path.ControlPoints == value)
return;
Path.ControlPoints = value;
path = value;
ControlPointsChanged?.Invoke(value);
PathChanged?.Invoke(value);
if (TailCircle != null)
TailCircle.Position = EndPosition;
}
}
public PathType PathType
{
get { return Path.PathType; }
set { Path.PathType = value; }
}
public double Distance
{
get { return Path.Distance; }
set { Path.Distance = value; }
}
public double Distance => Path.Distance;
public override Vector2 Position
{

View File

@ -84,5 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI
judgementLayer.Add(explosion);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<Type> 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<User> 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);
}
}
}

View File

@ -1,21 +1,45 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<Type> 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 }
};
}
}
}

View File

@ -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();

View File

@ -11,6 +11,7 @@ using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@ -53,12 +54,11 @@ namespace osu.Game.Tests.Visual
new Slider
{
Position = new Vector2(128, 256),
ControlPoints = new[]
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(216, 0),
},
Distance = 216,
}),
Scale = 0.5f,
}
},

View File

@ -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<SpriteText>().First();
var text = holdForMenuButton.Children.OfType<SpriteText>().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");
}
}
}

View File

@ -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:

View File

@ -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<KeyValuePair<string, T>> items = new List<KeyValuePair<string, T>>();
foreach (var val in (T[])Enum.GetValues(typeof(T)))
{
var field = typeof(T).GetField(Enum.GetName(typeof(T), val));
items.Add(
new KeyValuePair<string, T>(
field.GetCustomAttribute<DescriptionAttribute>()?.Description ?? Enum.GetName(typeof(T), val),
val
)
);
}
Items = items;
Items = (T[])Enum.GetValues(typeof(T));
}
}
}

View File

@ -0,0 +1,41 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
namespace osu.Game.Input
{
public class IdleTracker : Component, IKeyBindingHandler<PlatformAction>
{
private double lastInteractionTime;
public double IdleTime => Clock.CurrentTime - lastInteractionTime;
private bool updateLastInteractionTime()
{
lastInteractionTime = Clock.CurrentTime;
return false;
}
public bool OnPressed(PlatformAction action) => updateLastInteractionTime();
public bool OnReleased(PlatformAction action) => updateLastInteractionTime();
protected override bool Handle(UIEvent e)
{
switch (e)
{
case KeyDownEvent _:
case KeyUpEvent _:
case MouseDownEvent _:
case MouseUpEvent _:
case MouseMoveEvent _:
return updateLastInteractionTime();
default:
return base.Handle(e);
}
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<List<Message>>
{
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;
}
}
}

View File

@ -0,0 +1,34 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<CreateNewPrivateMessageResponse>
{
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";
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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;
}
}

View File

@ -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;
/// <summary>
/// Contains every joined user except the current logged in user. Currently only returned for PM channels.
/// </summary>
public readonly ObservableCollection<User> Users = new ObservableCollection<User>();
[JsonProperty(@"users")]
private long[] userIds
{
set
{
foreach (var id in value)
Users.Add(new User { Id = id });
}
}
/// <summary>
/// Contains all the messages send in the channel.
/// </summary>
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
/// <summary>
/// Contains all the messages that are still pending for submission to the server.
/// </summary>
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
/// <summary>
/// An event that fires when new messages arrived.
/// </summary>
public event Action<IEnumerable<Message>> NewMessagesArrived;
/// <summary>
/// An event that fires when a pending message gets resolved.
/// </summary>
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
/// <summary>
/// An event that fires when a pending message gets removed.
/// </summary>
public event Action<Message> 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<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
/// <summary>
/// Signalles if the current user joined this channel or not. Defaults to false.
/// </summary>
public Bindable<bool> Joined = new Bindable<bool>();
public bool ReadOnly => false;
public const int MAX_HISTORY = 300;
[JsonConstructor]
@ -42,10 +88,10 @@ namespace osu.Game.Online.Chat
{
}
public event Action<IEnumerable<Message>> NewMessagesArrived;
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
public event Action<Message> MessageRemoved;
/// <summary>
/// Adds the argument message as a local echo. When this local echo is resolved <see cref="PendingMessageResolved"/> will get called.
/// </summary>
/// <param name="message"></param>
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;
/// <summary>
/// Adds new messages to the channel and purges old messages. Triggers the <see cref="NewMessagesArrived"/> event.
/// </summary>
/// <param name="messages"></param>
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);
}
/// <summary>
/// Replace or remove a message from the channel.
/// </summary>
@ -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);
}
}
}

View File

@ -0,0 +1,409 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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
{
/// <summary>
/// Manages everything channel related
/// </summary>
public class ChannelManager : Component, IOnlineComponent
{
/// <summary>
/// The channels the player joins on startup
/// </summary>
private readonly string[] defaultChannels =
{
@"#lazer",
@"#osu",
@"#lobby"
};
/// <summary>
/// The currently opened channel
/// </summary>
public Bindable<Channel> CurrentChannel { get; } = new Bindable<Channel>();
/// <summary>
/// The Channels the player has joined
/// </summary>
public ObservableCollection<Channel> JoinedChannels { get; } = new ObservableCollection<Channel>(); //todo: should be publicly readonly
/// <summary>
/// The channels available for the player to join
/// </summary>
public ObservableCollection<Channel> AvailableChannels { get; } = new ObservableCollection<Channel>(); //todo: should be publicly readonly
private IAPIProvider api;
private ScheduledDelegate fetchMessagesScheduleder;
public ChannelManager()
{
CurrentChannel.ValueChanged += currentChannelChanged;
}
/// <summary>
/// Opens a channel or switches to the channel if already opened.
/// </summary>
/// <exception cref="ChannelNotFoundException">If the name of the specifed channel was not found this exception will be thrown.</exception>
/// <param name="name"></param>
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);
}
/// <summary>
/// Opens a new private channel.
/// </summary>
/// <param name="user">The user the private channel is opened with.</param>
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);
/// <summary>
/// Ensure we run post actions in sequence, once at a time.
/// </summary>
private readonly Queue<Action> postQueue = new Queue<Action>();
/// <summary>
/// Posts a message to the currently opened channel.
/// </summary>
/// <param name="text">The message text that is going to be posted</param>
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
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();
}
/// <summary>
/// Posts a command locally. Commands like /help will result in a help message written in the current channel.
/// </summary>
/// <param name="text">the text containing the command identifier and command parameters.</param>
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<Message> 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="channel">The channel </param>
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);
}
}
/// <summary>
/// An exception thrown when a channel could not been found.
/// </summary>
public class ChannelNotFoundException : Exception
{
public ChannelNotFoundException(string channelName)
: base($"A channel with the name {channelName} could not be found.")
{
}
}
}

View File

@ -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
}
}

View File

@ -26,11 +26,13 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Input;
using osu.Game.Rulesets.Scoring;
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;
@ -85,6 +87,8 @@ namespace osu.Game
public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight;
private IdleTracker idleTracker;
public readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
private OsuScreen screenStack;
@ -180,12 +184,6 @@ namespace osu.Game
private ScheduledDelegate scoreLoad;
/// <summary>
/// Open chat to a channel matching the provided name, if present.
/// </summary>
/// <param name="channelName">The name of the channel.</param>
public void OpenChannel(string channelName) => chat.OpenChannel(chat.AvailableChannels.Find(c => c.Name == channelName));
/// <summary>
/// Show a beatmap set as an overlay.
/// </summary>
@ -316,6 +314,7 @@ namespace osu.Game
},
mainContent = new Container { RelativeSizeAxes = Axes.Both },
overlayContent = new Container { RelativeSizeAxes = Axes.Both, Depth = float.MinValue },
idleTracker = new IdleTracker { RelativeSizeAxes = Axes.Both }
});
loadComponentSingleFile(screenStack = new Loader(), d =>
@ -343,6 +342,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
{
@ -372,6 +376,7 @@ namespace osu.Game
Depth = -6,
}, overlayContent.Add);
dependencies.Cache(idleTracker);
dependencies.Cache(settings);
dependencies.Cache(onscreenDisplay);
dependencies.Cache(social);

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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),
};
}
}

View File

@ -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<Message> 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++)
{

View File

@ -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
{

View File

@ -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
{

View File

@ -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<Channel> OnRequestJoin;
public Action<Channel> OnRequestLeave;
public IEnumerable<ChannelSection> 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<Channel> 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)
{

View File

@ -0,0 +1,32 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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;
}
}
}

View File

@ -0,0 +1,123 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<Channel>
{
public static readonly float SHEAR_WIDTH = 10;
public Action<Channel> OnRequestLeave;
public readonly Bindable<bool> ChannelSelectorActive = new Bindable<bool>();
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<Channel> 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<Channel> 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.");
}
}
/// <summary>
/// Adds a channel to the ChannelTabControl.
/// The first channel added will automaticly selected.
/// </summary>
/// <param name="channel">The channel that is going to be added.</param>
public void AddChannel(Channel channel)
{
if (!Items.Contains(channel))
AddItem(channel);
if (Current.Value == null)
Current.Value = channel;
}
/// <summary>
/// Removes a channel from the ChannelTabControl.
/// If the selected channel is the one that is beeing removed, the next available channel will be selected.
/// </summary>
/// <param name="channel">The channel that is going to be removed.</param>
public void RemoveChannel(Channel channel)
{
RemoveItem(channel);
if (Current.Value == channel)
Current.Value = Items.FirstOrDefault();
}
protected override void SelectTab(TabItem<Channel> tab)
{
if (tab is ChannelSelectorTabItem)
{
tab.Active.Toggle();
return;
}
selectorTab.Active.Value = false;
base.SelectTab(tab);
}
private void tabCloseRequested(TabItem<Channel> 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);
}
}
}

View File

@ -0,0 +1,212 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<Channel>
{
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<ChannelTabItem> OnRequestClose;
private readonly Container content;
protected override Container<Drawable> 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();
}
}

View File

@ -0,0 +1,97 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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);
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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);
}
}
}

View File

@ -1,9 +1,8 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<DrawableChannel> currentChannelContainer;
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
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<double> ChatHeight { get; set; }
public List<Channel> AvailableChannels { get; private set; } = new List<Channel>();
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<double>(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<Channel> careChannels = new List<Channel>();
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
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<Channel> 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

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<PlaylistCollection>
{
RelativeSizeAxes = Axes.X,
Items = new[] { new KeyValuePair<string, PlaylistCollection>(@"All", PlaylistCollection.All) },
Items = new[] { PlaylistCollection.All },
}
},
},

View File

@ -245,10 +245,10 @@ namespace osu.Game.Overlays
{
base.Update();
if (current?.TrackLoaded ?? false)
{
var track = current.Track;
var track = current?.TrackLoaded ?? false ? current.Track : null;
if (track?.IsDummyDevice == false)
{
progressBar.EndTime = track.Length;
progressBar.CurrentTime = track.CurrentTime;
@ -258,7 +258,11 @@ namespace osu.Game.Overlays
next();
}
else
{
progressBar.CurrentTime = 0;
progressBar.EndTime = 1;
playButton.Icon = FontAwesome.fa_play_circle_o;
}
}
private void play()

View File

@ -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<KeyValuePair<string, string>> { new KeyValuePair<string, string>("Default", string.Empty) };
deviceItems.AddRange(audio.AudioDeviceNames.Select(d => new KeyValuePair<string, string>(d, d)));
var deviceItems = new List<string> { string.Empty };
deviceItems.AddRange(audio.AudioDeviceNames);
var preferredDeviceName = audio.AudioDevice.Value;
if (deviceItems.All(kv => kv.Value != preferredDeviceName))
deviceItems.Add(new KeyValuePair<string, string>(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<string>()
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<string>
{
protected override OsuDropdown<string> CreateDropdown() => new AudioDeviceDropdownControl { Items = Items };
private class AudioDeviceDropdownControl : DropdownControl
{
protected override string GenerateItemText(string item)
=> string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item);
}
}
}
}

View File

@ -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<Size>
resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown
{
LabelText = "Resolution",
ShowsDefaultIndicator = false,
@ -115,18 +115,36 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, true);
}
private IReadOnlyList<KeyValuePair<string, Size>> getResolutions()
private IReadOnlyList<Size> getResolutions()
{
var resolutions = new KeyValuePair<string, Size>("Default", new Size(9999, 9999)).Yield();
var resolutions = new List<Size> { 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<string, Size>($"{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<Size>
{
protected override OsuDropdown<Size> 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}";
}
}
}
}
}

View File

@ -1,9 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<int> skinDropdown;
private SkinSettingsDropdown skinDropdown;
public override string Header => "Skin";
public override FontAwesome Icon => FontAwesome.fa_paint_brush;
private readonly Bindable<SkinInfo> dropdownBindable = new Bindable<SkinInfo>();
private readonly Bindable<int> configBindable = new Bindable<int>();
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<int>(),
skinDropdown = new SkinSettingsDropdown(),
new SettingsSlider<double, SizeSlider>
{
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<string, int>(s.ToString(), s.ID));
config.BindWith(OsuSetting.Skin, configBindable);
var skinBindable = config.GetBindable<int>(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<string, int>(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<SkinInfo>
{
protected override OsuDropdown<SkinInfo> CreateDropdown() => new SkinDropdownControl { Items = Items };
private class SkinDropdownControl : DropdownControl
{
protected override string GenerateItemText(SkinInfo item) => item.ToString();
}
}
}
}

View File

@ -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<T> : SettingsItem<T>
{
private Dropdown<T> dropdown;
protected new OsuDropdown<T> Control => (OsuDropdown<T>)base.Control;
private IEnumerable<KeyValuePair<string, T>> items = new KeyValuePair<string, T>[] { };
public IEnumerable<KeyValuePair<string, T>> Items
private IEnumerable<T> items = Enumerable.Empty<T>();
public IEnumerable<T> 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<T>
protected sealed override Drawable CreateControl() => CreateDropdown();
protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl { Items = Items };
protected class DropdownControl : OsuDropdown<T>
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
Items = Items,
};
public DropdownControl()
{
Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
}
}
}

View File

@ -8,10 +8,15 @@ namespace osu.Game.Overlays.Settings
{
public class SettingsEnumDropdown<T> : SettingsDropdown<T>
{
protected override Drawable CreateControl() => new OsuEnumDropdown<T>
protected override OsuDropdown<T> CreateDropdown() => new DropdownControl();
protected class DropdownControl : OsuEnumDropdown<T>
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
};
public DropdownControl()
{
Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
}
}
}

View File

@ -8,6 +8,10 @@ namespace osu.Game.Overlays.Settings
{
public class SettingsTextBox : SettingsItem<string>
{
protected override Drawable CreateControl() => new OsuTextBox();
protected override Drawable CreateControl() => new OsuTextBox
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
};
}
}

View File

@ -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;
@ -64,7 +67,7 @@ namespace osu.Game.Rulesets.Edit
layerBelowRuleset.Child = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both };
var layerAboveRuleset = CreateLayerContainer();
layerAboveRuleset.Child = new BlueprintContainer();
layerAboveRuleset.Child = blueprintContainer = new BlueprintContainer();
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
@ -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
});
}
/// <summary>
/// Whether the user's cursor is currently in an area of the <see cref="HitObjectComposer"/> that is valid for placement.
/// </summary>
public virtual bool CursorInPlacementArea => rulesetContainer.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
/// <summary>
/// Adds a <see cref="HitObject"/> to the <see cref="Beatmaps.Beatmap"/> and visualises it.
/// </summary>

View File

@ -1,6 +1,8 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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
/// <summary>
/// A blueprint which governs the creation of a new <see cref="HitObject"/> to actualisation.
/// </summary>
public abstract class PlacementBlueprint : CompositeDrawable, IRequireHighFrequencyMousePosition
public abstract class PlacementBlueprint : CompositeDrawable, IStateful<PlacementState>, IRequireHighFrequencyMousePosition
{
/// <summary>
/// Invoked when <see cref="State"/> has changed.
/// </summary>
public event Action<PlacementState> StateChanged;
/// <summary>
/// Whether the <see cref="HitObject"/> is currently being placed, but has not necessarily finished being placed.
/// </summary>
public bool PlacementBegun { get; private set; }
/// <summary>
/// The <see cref="HitObject"/> that is being placed.
/// </summary>
@ -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);
}
}
/// <summary>
/// Signals that the placement of <see cref="HitObject"/> has started.
@ -57,7 +89,7 @@ namespace osu.Game.Rulesets.Edit
protected void BeginPlacement()
{
placementHandler.BeginPlacement(HitObject);
placementBegun = true;
PlacementBegun = true;
}
/// <summary>
@ -66,7 +98,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
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,
}
}

View File

@ -50,9 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
X = position.X,
NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset,
ControlPoints = controlPoints,
Distance = length,
PathType = pathType,
Path = new SliderPath(pathType, controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};

View File

@ -3,7 +3,6 @@
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using OpenTK;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -20,11 +19,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <summary>
/// <see cref="ConvertSlider"/>s don't need a curve since they're converted to ruleset-specific hitobjects.
/// </summary>
public SliderPath Path { get; } = null;
public Vector2[] ControlPoints { get; set; }
public PathType PathType { get; set; }
public SliderPath Path { get; set; }
public double Distance { get; set; }
public double Distance => Path.Distance;
public List<List<SampleInfo>> NodeSamples { get; set; }
public int RepeatCount { get; set; }

View File

@ -31,9 +31,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
return new ConvertSlider
{
X = position.X,
ControlPoints = controlPoints,
Distance = length,
PathType = pathType,
Path = new SliderPath(pathType, controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};

View File

@ -51,9 +51,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
Position = position,
NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset,
ControlPoints = controlPoints,
Distance = Math.Max(0, length),
PathType = pathType,
Path = new SliderPath(pathType, controlPoints, Math.Max(0, length)),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};

View File

@ -27,9 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
return new ConvertSlider
{
ControlPoints = controlPoints,
Distance = length,
PathType = pathType,
Path = new SliderPath(pathType, controlPoints, length),
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};

View File

@ -4,28 +4,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
namespace osu.Game.Rulesets.Objects
{
public class SliderPath
public struct SliderPath : IEquatable<SliderPath>
{
public double Distance;
/// <summary>
/// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value,
/// and the path will be shortened/lengthened to match this length.
/// </summary>
public readonly double? ExpectedDistance;
public Vector2[] ControlPoints = Array.Empty<Vector2>();
/// <summary>
/// The type of path.
/// </summary>
public readonly PathType Type;
public PathType PathType = PathType.PerfectCurve;
[JsonProperty]
private Vector2[] controlPoints;
public Vector2 Offset;
private List<Vector2> calculatedPath;
private List<double> cumulativeLength;
private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>();
private bool isInitialised;
/// <summary>
/// Creates a new <see cref="SliderPath"/>.
/// </summary>
/// <param name="type">The type of path.</param>
/// <param name="controlPoints">The control points of the path.</param>
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all
/// <paramref name="controlPoints"/>. The path will be shortened/lengthened to match this length.
/// If null, the path will use the true distance between all <paramref name="controlPoints"/>.</param>
[JsonConstructor]
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
{
this = default;
this.controlPoints = controlPoints;
Type = type;
ExpectedDistance = expectedDistance;
ensureInitialised();
}
/// <summary>
/// The control points of the path.
/// </summary>
[JsonIgnore]
public ReadOnlySpan<Vector2> ControlPoints
{
get
{
ensureInitialised();
return controlPoints.AsSpan();
}
}
/// <summary>
/// The distance of the path after lengthening/shortening to account for <see cref="ExpectedDistance"/>.
/// </summary>
[JsonIgnore]
public double Distance
{
get
{
ensureInitialised();
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1];
}
}
/// <summary>
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
/// to 1 (end of the slider) and stores the generated path in the given list.
/// </summary>
/// <param name="path">The list to be filled with the computed path.</param>
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
{
ensureInitialised();
double d0 = progressToDistance(p0);
double d1 = progressToDistance(p1);
path.Clear();
int i = 0;
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i)
{
}
path.Add(interpolateVertices(i, d0));
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
path.Add(calculatedPath[i]);
path.Add(interpolateVertices(i, d1));
}
/// <summary>
/// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
/// to 1 (end of the path).
/// </summary>
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
/// <returns></returns>
public Vector2 PositionAt(double progress)
{
ensureInitialised();
double d = progressToDistance(progress);
return interpolateVertices(indexOfDistance(d), d);
}
private void ensureInitialised()
{
if (isInitialised)
return;
isInitialised = true;
controlPoints = controlPoints ?? Array.Empty<Vector2>();
calculatedPath = new List<Vector2>();
cumulativeLength = new List<double>();
calculatePath();
calculateCumulativeLength();
}
private List<Vector2> calculateSubpath(ReadOnlySpan<Vector2> subControlPoints)
{
switch (PathType)
switch (Type)
{
case PathType.Linear:
return PathApproximator.ApproximateLinear(subControlPoints);
@ -66,7 +178,7 @@ namespace osu.Game.Rulesets.Objects
if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
{
ReadOnlySpan<Vector2> cpSpan = ControlPoints.AsSpan().Slice(start, end - start);
ReadOnlySpan<Vector2> cpSpan = ControlPoints.Slice(start, end - start);
foreach (Vector2 t in calculateSubpath(cpSpan))
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
@ -77,49 +189,6 @@ namespace osu.Game.Rulesets.Objects
}
}
private void calculateCumulativeLengthAndTrimPath()
{
double l = 0;
cumulativeLength.Clear();
cumulativeLength.Add(l);
for (int i = 0; i < calculatedPath.Count - 1; ++i)
{
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
double d = diff.Length;
// Shorten slider paths that are too long compared to what's
// in the .osu file.
if (Distance - l < d)
{
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Distance - l) / d);
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
l = Distance;
cumulativeLength.Add(l);
break;
}
l += d;
cumulativeLength.Add(l);
}
// Lengthen slider paths that are too short compared to what's
// in the .osu file.
if (l < Distance && calculatedPath.Count > 1)
{
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
double d = diff.Length;
if (d <= 0)
return;
calculatedPath[calculatedPath.Count - 1] += diff * (float)((Distance - l) / d);
cumulativeLength[calculatedPath.Count - 1] = Distance;
}
}
private void calculateCumulativeLength()
{
double l = 0;
@ -132,21 +201,33 @@ namespace osu.Game.Rulesets.Objects
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
double d = diff.Length;
// Shorted slider paths that are too long compared to the expected distance
if (ExpectedDistance.HasValue && ExpectedDistance - l < d)
{
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d);
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
l = ExpectedDistance.Value;
cumulativeLength.Add(l);
break;
}
l += d;
cumulativeLength.Add(l);
}
Distance = l;
}
// Lengthen slider paths that are too short compared to the expected distance
if (ExpectedDistance.HasValue && l < ExpectedDistance && calculatedPath.Count > 1)
{
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
double d = diff.Length;
public void Calculate(bool updateDistance = false)
{
calculatePath();
if (d <= 0)
return;
if (!updateDistance)
calculateCumulativeLengthAndTrimPath();
else
calculateCumulativeLength();
calculatedPath[calculatedPath.Count - 1] += diff * (float)((ExpectedDistance - l) / d);
cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value;
}
}
private int indexOfDistance(double d)
@ -169,7 +250,7 @@ namespace osu.Game.Rulesets.Objects
if (i <= 0)
return calculatedPath.First();
else if (i >= calculatedPath.Count)
if (i >= calculatedPath.Count)
return calculatedPath.Last();
Vector2 p0 = calculatedPath[i - 1];
@ -186,47 +267,20 @@ namespace osu.Game.Rulesets.Objects
return p0 + (p1 - p0) * (float)w;
}
/// <summary>
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
/// to 1 (end of the slider) and stores the generated path in the given list.
/// </summary>
/// <param name="path">The list to be filled with the computed path.</param>
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
public bool Equals(SliderPath other)
{
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
Calculate();
if (ControlPoints == null && other.ControlPoints != null)
return false;
if (other.ControlPoints == null && ControlPoints != null)
return false;
double d0 = progressToDistance(p0);
double d1 = progressToDistance(p1);
path.Clear();
int i = 0;
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i) { }
path.Add(interpolateVertices(i, d0) + Offset);
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
path.Add(calculatedPath[i] + Offset);
path.Add(interpolateVertices(i, d1) + Offset);
return ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance.Equals(other.ExpectedDistance) && Type == other.Type;
}
/// <summary>
/// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
/// to 1 (end of the path).
/// </summary>
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
/// <returns></returns>
public Vector2 PositionAt(double progress)
public override bool Equals(object obj)
{
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
Calculate();
double d = progressToDistance(progress);
return interpolateVertices(indexOfDistance(d), d) + Offset;
if (ReferenceEquals(null, obj)) return false;
return obj is SliderPath other && Equals(other);
}
}
}

View File

@ -14,16 +14,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// The curve.
/// </summary>
SliderPath Path { get; }
/// <summary>
/// The control points that shape the curve.
/// </summary>
Vector2[] ControlPoints { get; }
/// <summary>
/// The type of curve.
/// </summary>
PathType PathType { get; }
}
public static class HasCurveExtensions

View File

@ -18,7 +18,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
public class BlueprintContainer : CompositeDrawable
{
private SelectionBlueprintContainer selectionBlueprints;
private Container<PlacementBlueprint> placementBlueprintContainer;
private PlacementBlueprint currentPlacement;
private SelectionBox selectionBox;
private IEnumerable<SelectionBlueprint> 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;
}
}
/// <summary>
/// Refreshes the current placement tool.
/// </summary>
private void refreshTool()
{
placementBlueprintContainer.Clear();
currentPlacement = null;
var blueprint = CurrentTool?.CreatePlacementBlueprint();
if (blueprint != null)
placementBlueprintContainer.Child = blueprint;
placementBlueprintContainer.Child = currentPlacement = blueprint;
}
/// <summary>
/// Select all masks in a given rectangle selection area.
/// </summary>

View File

@ -14,6 +14,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using OpenTK;
@ -64,6 +65,8 @@ namespace osu.Game.Screens.Menu
private SampleChannel sampleBack;
private IdleTracker idleTracker;
public ButtonSystem()
{
RelativeSizeAxes = Axes.Both;
@ -102,9 +105,10 @@ namespace osu.Game.Screens.Menu
private OsuGame game;
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuGame game)
private void load(AudioManager audio, OsuGame game, IdleTracker idleTracker)
{
this.game = game;
this.idleTracker = idleTracker;
sampleBack = audio.Sample.Get(@"Menu/button-back-select");
}
@ -266,8 +270,8 @@ namespace osu.Game.Screens.Menu
protected override void Update()
{
//if (OsuGame.IdleTime > 6000 && State != MenuState.Exit)
// State = MenuState.Initial;
if (idleTracker?.IdleTime > 6000 && State != ButtonSystemState.Exit)
State = ButtonSystemState.Initial;
base.Update();

View File

@ -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<GlobalAction>
{
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)
{

View File

@ -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<bool> 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,

View File

@ -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;

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// 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<PlaylistCollection>
{
RelativeSizeAxes = Axes.X,
Items = new[] { new KeyValuePair<string, PlaylistCollection>(@"All", PlaylistCollection.All) },
Items = new[] { PlaylistCollection.All },
},
};
}

View File

@ -18,7 +18,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="ppy.osu.Framework" Version="2018.1105.0" />
<PackageReference Include="ppy.osu.Framework" Version="2018.1114.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />