refactor: tidy up attachement flow

TODO: find better naming and improve XMLDocs
This commit is contained in:
tsrk 2023-06-18 22:57:21 +02:00
parent 141f9efad5
commit f83a4f4952
No known key found for this signature in database
GPG Key ID: EBD46BB3049B56D6
6 changed files with 126 additions and 60 deletions

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.UI
/// Displays an interactive ruleset gameplay instance.
/// </summary>
/// <typeparam name="TObject">The type of HitObject contained by this DrawableRuleset.</typeparam>
public abstract partial class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, ICanAttachHUDPieces
public abstract partial class DrawableRuleset<TObject> : DrawableRuleset, IProvideCursor, IKeybindingEventsEmitter
where TObject : HitObject
{
public override event Action<JudgementResult> NewResult;
@ -327,8 +327,8 @@ namespace osu.Game.Rulesets.UI
/// <returns>The representing <see cref="DrawableHitObject{TObject}"/>.</returns>
public abstract DrawableHitObject<TObject> CreateDrawableRepresentation(TObject h);
public void Attach(IAttachableSkinComponent skinComponent) =>
(KeyBindingInputManager as ICanAttachHUDPieces)?.Attach(skinComponent);
public void Attach(IKeybindingListener skinComponent) =>
(KeyBindingInputManager as IKeybindingEventsEmitter)?.Attach(skinComponent);
/// <summary>
/// Creates a key conversion input manager. An exception will be thrown if a valid <see cref="RulesetInputManager{T}"/> is not returned.

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// Listens to <see cref="IKeyBinding"/> events emitted by an <see cref="IKeybindingEventsEmitter"/>.
/// Alternative to <see cref="IKeyBindingHandler{T}"/> for classes that need to not depend on type parameters.
/// </summary>
public interface IKeybindingListener
{
/// <summary>
/// This class or a member of this class can already handle keybindings.
/// Signals to the <see cref="IKeybindingEventsEmitter"/> that <see cref="OnPressed{T}"/> and <see cref="OnReleased{T}"/>
/// don't necessarily need to be called.
/// </summary>
/// <remarks>
/// This is usually true for <see cref="CompositeDrawable"/>s and <see cref="CompositeComponent"/>s that need to
/// pass <see cref="IKeyBinding"/>s events to children that can already handle them.
/// </remarks>
public bool CanHandleKeybindings { get; }
/// <summary>
/// Prepares this class to receive events.
/// </summary>
/// <param name="actions">The list of possible actions that can occur.</param>
/// <typeparam name="T">The type actions, commonly enums.</typeparam>
public void Setup<T>(IEnumerable<T> actions) where T : struct;
/// <summary>
/// Called when an action is pressed.
/// </summary>
/// <param name="action">The event containing information about the pressed action.</param>
/// <typeparam name="T">The type of binding, commonly enums.</typeparam>
public void OnPressed<T>(KeyBindingPressEvent<T> action) where T : struct;
/// <summary>
/// Called when an action is released.
/// </summary>
/// <param name="action">The event containing information about the released action.</param>
/// <typeparam name="T">The type of binding, commonly enums.</typeparam>
public void OnReleased<T>(KeyBindingReleaseEvent<T> action) where T : struct;
}
}

View File

@ -19,13 +19,11 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.ClicksPerSecond;
using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.UI
{
public abstract partial class RulesetInputManager<T> : PassThroughInputManager, ICanAttachHUDPieces, IHasReplayHandler, IHasRecordingHandler
public abstract partial class RulesetInputManager<T> : PassThroughInputManager, IKeybindingEventsEmitter, IHasReplayHandler, IHasRecordingHandler
where T : struct
{
protected override bool AllowRightClickFromLongTouch => false;
@ -66,6 +64,7 @@ namespace osu.Game.Rulesets.UI
InternalChild = KeyBindingContainer =
CreateKeyBindingContainer(ruleset, variant, unique)
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
KeyBindingContainer.Add(actionListener = new ActionListener());
}
[BackgroundDependencyLoader(true)]
@ -160,63 +159,47 @@ namespace osu.Game.Rulesets.UI
#region Component attachement
public void Attach(IAttachableSkinComponent skinComponent)
private readonly ActionListener actionListener;
public void Attach(IKeybindingListener skinComponent)
{
switch (skinComponent)
{
case KeyCounterController keyCounterDisplay:
attachKeyCounter(keyCounterDisplay);
break;
case ClicksPerSecondCalculator clicksPerSecondCalculator:
attachClicksPerSecond(clicksPerSecondCalculator);
break;
}
}
#endregion
#region Key Counter Attachment
private void attachKeyCounter(KeyCounterController keyCounter)
{
KeyBindingContainer.Add(keyCounter);
keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings
skinComponent.Setup(KeyBindingContainer.DefaultKeyBindings
.Select(b => b.GetAction<T>())
.Distinct()
.OrderBy(action => action)
.Select(action => new KeyCounterActionTrigger<T>(action)));
}
.OrderBy(a => a));
#endregion
if (skinComponent.CanHandleKeybindings && skinComponent is Drawable component)
{
try
{
KeyBindingContainer.Add(component);
return;
}
catch (Exception)
{
return;
}
}
#region Keys per second Counter Attachment
private void attachClicksPerSecond(ClicksPerSecondCalculator calculator)
{
var listener = new ActionListener(calculator);
KeyBindingContainer.Add(listener);
actionListener.OnPressedEvent += skinComponent.OnPressed;
actionListener.OnReleasedEvent += skinComponent.OnReleased;
}
private partial class ActionListener : Component, IKeyBindingHandler<T>
{
private readonly ClicksPerSecondCalculator calculator;
public event Action<KeyBindingPressEvent<T>> OnPressedEvent;
public ActionListener(ClicksPerSecondCalculator calculator)
{
this.calculator = calculator;
}
public event Action<KeyBindingReleaseEvent<T>> OnReleasedEvent;
public bool OnPressed(KeyBindingPressEvent<T> e)
{
calculator.AddInputTimestamp();
OnPressedEvent?.Invoke(e);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<T> e)
{
OnReleasedEvent?.Invoke(e);
}
}
@ -257,16 +240,11 @@ namespace osu.Game.Rulesets.UI
}
/// <summary>
/// Supports attaching various HUD pieces.
/// Keys will be populated automatically and a receptor will be injected inside.
/// Sends <see cref="IKeyBinding"/> events to a <see cref="IKeybindingListener"/>
/// </summary>
public interface ICanAttachHUDPieces
{
void Attach(IAttachableSkinComponent component);
}
public interface IAttachableSkinComponent
public interface IKeybindingEventsEmitter
{
void Attach(IKeybindingListener component);
}
public class RulesetInputManagerInputState<T> : InputState

View File

@ -4,11 +4,12 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
{
public partial class ClicksPerSecondCalculator : Component, IAttachableSkinComponent
public partial class ClicksPerSecondCalculator : Component, IKeybindingListener
{
private readonly List<double> timestamps = new List<double>();
@ -53,5 +54,21 @@ namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
Value = count;
}
#region IKeybindingListener
bool IKeybindingListener.CanHandleKeybindings => false;
void IKeybindingListener.Setup<T>(IEnumerable<T> actions)
{
}
void IKeybindingListener.OnPressed<T>(KeyBindingPressEvent<T> action) => AddInputTimestamp();
void IKeybindingListener.OnReleased<T>(KeyBindingReleaseEvent<T> action)
{
}
#endregion
}
}

View File

@ -3,14 +3,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Play.HUD
{
public partial class KeyCounterController : CompositeComponent, IAttachableSkinComponent
public partial class KeyCounterController : CompositeComponent, IKeybindingListener
{
public readonly Bindable<bool> IsCounting = new BindableBool(true);
@ -36,5 +38,22 @@ namespace osu.Game.Screens.Play.HUD
public override bool HandleNonPositionalInput => true;
public override bool HandlePositionalInput => true;
#region IKeybindingListener
bool IKeybindingListener.CanHandleKeybindings => true;
void IKeybindingListener.Setup<T>(IEnumerable<T> actions)
=> AddRange(actions.Select(a => new KeyCounterActionTrigger<T>(a)));
void IKeybindingListener.OnPressed<T>(KeyBindingPressEvent<T> action)
{
}
void IKeybindingListener.OnReleased<T>(KeyBindingReleaseEvent<T> action)
{
}
#endregion
}
}

View File

@ -10,6 +10,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
@ -102,6 +103,8 @@ namespace osu.Game.Screens.Play
private readonly List<Drawable> hideTargets;
private readonly IEnumerable<IKeybindingListener> actionInjectionCandidates;
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
{
Drawable rulesetComponents;
@ -163,6 +166,8 @@ namespace osu.Game.Screens.Play
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, topRightElements };
actionInjectionCandidates = new IKeybindingListener[] { clicksPerSecondCalculator, KeyCounter };
if (!alwaysShowLeaderboard)
hideTargets.Add(LeaderboardFlow);
}
@ -319,11 +324,8 @@ namespace osu.Game.Screens.Play
protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset)
{
if (drawableRuleset is ICanAttachHUDPieces attachTarget)
{
attachTarget.Attach(KeyCounter);
attachTarget.Attach(clicksPerSecondCalculator);
}
if (drawableRuleset is IKeybindingEventsEmitter attachTarget)
actionInjectionCandidates.ForEach(attachTarget.Attach);
replayLoaded.BindTo(drawableRuleset.HasReplayLoaded);
}