Files
osu-framework/osu.Framework/Input/Bindings/KeyBindingContainer.cs
2025-12-06 17:22:05 +08:00

494 lines
20 KiB
C#

// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.ListExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osuTK;
namespace osu.Framework.Input.Bindings
{
/// <summary>
/// Maps input actions to custom action data of type <typeparamref name="T"/>. Use in conjunction with <see cref="Drawable"/>s implementing <see cref="IKeyBindingHandler{T}"/>.
/// </summary>
/// <typeparam name="T">The type of the custom action.</typeparam>
public abstract partial class KeyBindingContainer<T> : KeyBindingContainer
where T : struct
{
private readonly SimultaneousBindingMode simultaneousMode;
private readonly KeyCombinationMatchingMode matchingMode;
/// <summary>
/// Create a new instance.
/// </summary>
/// <param name="simultaneousMode">Specify how to deal with multiple matches of <see cref="KeyCombination"/>s and <typeparamref name="T"/>s.</param>
/// <param name="matchingMode">Specify how to deal with exact <see cref="KeyCombination"/> matches.</param>
protected KeyBindingContainer(SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None, KeyCombinationMatchingMode matchingMode = KeyCombinationMatchingMode.Any)
{
RelativeSizeAxes = Axes.Both;
this.simultaneousMode = simultaneousMode;
this.matchingMode = matchingMode;
}
private readonly List<IKeyBinding> pressedBindings = new List<IKeyBinding>();
private readonly List<T> pressedActions = new List<T>();
/// <summary>
/// All actions in a currently pressed state.
/// </summary>
public SlimReadOnlyListWrapper<T> PressedActions => pressedActions.AsSlimReadOnly();
private readonly Dictionary<IKeyBinding, List<Drawable>> keyBindingQueues = new Dictionary<IKeyBinding, List<Drawable>>();
private readonly List<Drawable> queue = new List<Drawable>();
private List<Drawable> keyRepeatInputQueue;
/// <summary>
/// The input queue to be used for processing key bindings. Based on the non-positional <see cref="InputManager.NonPositionalInputQueue"/>.
/// Can be overridden to change priorities.
/// </summary>
protected virtual IEnumerable<Drawable> KeyBindingInputQueue
{
get
{
queue.Clear();
BuildNonPositionalInputQueue(queue, false);
queue.Reverse();
return queue;
}
}
protected override void Update()
{
base.Update();
// aggressively clear to avoid holding references.
queue.Clear();
}
/// <summary>
/// Whether this <see cref="KeyBindingContainer"/> should attempt to handle input before any of its children.
/// </summary>
protected virtual bool Prioritised => false;
internal override bool BuildNonPositionalInputQueue(List<Drawable> queue, bool allowBlocking = true)
{
if (!base.BuildNonPositionalInputQueue(queue, allowBlocking))
return false;
if (Prioritised)
{
queue.Remove(this);
queue.Add(this);
}
return true;
}
internal override bool BuildPositionalInputQueue(Vector2 screenSpacePos, List<Drawable> queue)
{
if (!base.BuildPositionalInputQueue(screenSpacePos, queue))
return false;
if (Prioritised)
{
queue.Remove(this);
queue.Add(this);
}
return true;
}
/// <summary>
/// All input keys which are currently pressed and have reached this <see cref="KeyBindingContainer"/>.
/// </summary>
private readonly HashSet<InputKey> pressedInputKeys = new HashSet<InputKey>();
protected override bool Handle(UIEvent e)
{
var state = e.CurrentState;
switch (e)
{
case MouseDownEvent mouseDown:
return handleNewPressed(state, KeyCombination.FromMouseButton(mouseDown.Button));
case MouseUpEvent mouseUp:
handleNewReleased(state, KeyCombination.FromMouseButton(mouseUp.Button));
return false;
case KeyDownEvent keyDown:
if (keyDown.Repeat)
return handleRepeat(state);
else
return handleNewPressed(state, KeyCombination.FromKey(keyDown.Key));
case KeyUpEvent keyUp:
handleNewReleased(state, KeyCombination.FromKey(keyUp.Key));
return false;
case JoystickPressEvent joystickPress:
return handleNewPressed(state, KeyCombination.FromJoystickButton(joystickPress.Button));
case JoystickReleaseEvent joystickRelease:
handleNewReleased(state, KeyCombination.FromJoystickButton(joystickRelease.Button));
return false;
case MidiDownEvent midiDown:
return handleNewPressed(state, KeyCombination.FromMidiKey(midiDown.Key));
case MidiUpEvent midiUp:
handleNewReleased(state, KeyCombination.FromMidiKey(midiUp.Key));
return false;
case TabletPenButtonPressEvent tabletPenButtonPress:
return handleNewPressed(state, KeyCombination.FromTabletPenButton(tabletPenButtonPress.Button));
case TabletPenButtonReleaseEvent tabletPenButtonRelease:
handleNewReleased(state, KeyCombination.FromTabletPenButton(tabletPenButtonRelease.Button));
return false;
case TabletAuxiliaryButtonPressEvent tabletAuxiliaryButtonPress:
return handleNewPressed(state, KeyCombination.FromTabletAuxiliaryButton(tabletAuxiliaryButtonPress.Button));
case TabletAuxiliaryButtonReleaseEvent tabletAuxiliaryButtonRelease:
handleNewReleased(state, KeyCombination.FromTabletAuxiliaryButton(tabletAuxiliaryButtonRelease.Button));
return false;
case ScrollEvent scroll:
{
var keys = KeyCombination.FromScrollDelta(scroll.ScrollDelta);
bool handled = false;
foreach (var key in keys)
{
handled |= handleNewPressed(state, key, scroll.ScrollDelta, scroll.IsPrecise);
handleNewReleased(state, key);
}
return handled;
}
}
return false;
}
private bool handleRepeat(InputState state)
{
if (!HandleRepeats)
return false;
if (pressedActions.Count == 0)
return false;
// A simplistic approach to key repeat (that mostly matches OS level implementations) is that the last binding - or action - to
// trigger is the one and only action to repeat.
T action = pressedActions.Last();
var pressEvent = new KeyBindingPressEvent<T>(state, action, true);
// Only drawables that can still handle input should handle the repeat
var drawables = keyRepeatInputQueue.Intersect(KeyBindingInputQueue).Where(t => t.IsAlive && t.IsPresent);
return drawables.FirstOrDefault(d => triggerKeyBindingEvent(d, pressEvent)) != null;
}
private readonly List<IKeyBinding> newlyPressed = new List<IKeyBinding>();
private bool handleNewPressed(InputState state, InputKey newKey, Vector2? scrollDelta = null, bool isPrecise = false)
{
pressedInputKeys.Add(newKey);
float scrollAmount = getScrollAmount(newKey, scrollDelta);
var pressedCombination = new KeyCombination(pressedInputKeys);
bool handled = false;
newlyPressed.Clear();
if (KeyBindings != null)
{
foreach (IKeyBinding binding in KeyBindings)
{
if (pressedBindings.Contains(binding))
continue;
if (binding.KeyCombination.IsPressed(pressedCombination, state, matchingMode))
newlyPressed.Add(binding);
}
}
if (KeyCombination.IsModifierKey(newKey))
{
// if the current key pressed was a modifier, only handle modifier-only bindings.
// lambda expression is used so that the delegate is cached (see: https://github.com/dotnet/roslyn/issues/5835)
// TODO: remove when we switch to .NET 7.
// ReSharper disable once ConvertClosureToMethodGroup
for (int i = 0; i < newlyPressed.Count; i++)
{
if (!newlyPressed[i].KeyCombination.Keys.All(key => KeyCombination.IsModifierKey(key)))
newlyPressed.RemoveAt(i--);
}
}
// we want to always handle bindings with more keys before bindings with less.
newlyPressed.Sort(static (a, b) => b.KeyCombination.Keys.Length.CompareTo(a.KeyCombination.Keys.Length));
pressedBindings.AddRange(newlyPressed);
// exact matching may result in no pressed (new or old) bindings, in which case we want to trigger releases for existing actions
if (simultaneousMode == SimultaneousBindingMode.None && (matchingMode == KeyCombinationMatchingMode.Exact || matchingMode == KeyCombinationMatchingMode.Modifiers))
{
// only want to release pressed actions if no existing bindings would still remain pressed
if (pressedBindings.Count > 0 && !pressedBindings.Any(m => m.KeyCombination.IsPressed(pressedCombination, state, matchingMode)))
releasePressedActions(state);
}
foreach (var newBinding in newlyPressed)
{
// we handled a new binding and there is an existing one. if we don't want concurrency, let's propagate a released event.
if (simultaneousMode == SimultaneousBindingMode.None)
{
releasePressedActions(state);
}
List<Drawable> inputQueue = getInputQueue(newBinding, true);
Drawable handledBy = PropagatePressed(inputQueue, state, newBinding.GetAction<T>(), scrollAmount, isPrecise);
if (handledBy != null)
{
// only drawables up to the one that handled the press should handle the release, so remove all subsequent drawables from the queue (for future use).
int count = inputQueue.IndexOf(handledBy) + 1;
inputQueue.RemoveRange(count, inputQueue.Count - count);
handled = true;
}
keyRepeatInputQueue = inputQueue;
// we only want to handle the first valid binding (the one with the most keys) in non-simultaneous mode.
if (simultaneousMode == SimultaneousBindingMode.None && handled)
break;
}
return handled;
}
private static float getScrollAmount(InputKey newKey, Vector2? scrollDelta)
{
switch (newKey)
{
case InputKey.MouseWheelUp:
return scrollDelta?.Y ?? 0;
case InputKey.MouseWheelDown:
return -(scrollDelta?.Y ?? 0);
case InputKey.MouseWheelRight:
return scrollDelta?.X ?? 0;
case InputKey.MouseWheelLeft:
return -(scrollDelta?.X ?? 0);
default:
return 0;
}
}
protected virtual Drawable PropagatePressed(IEnumerable<Drawable> drawables, InputState state, T pressed, float scrollAmount = 0, bool isPrecise = false, bool repeat = false)
{
Drawable handled = null;
// only handle if we are a new non-pressed action (or a concurrency mode that supports multiple simultaneous triggers).
if (simultaneousMode == SimultaneousBindingMode.All || !pressedActions.Contains(pressed))
{
pressedActions.Add(pressed);
if (scrollAmount != 0)
{
var scrollEvent = new KeyBindingScrollEvent<T>(state, pressed, scrollAmount, isPrecise);
handled = drawables.FirstOrDefault(d => triggerKeyBindingEvent(d, scrollEvent));
}
if (handled == null)
{
var pressEvent = new KeyBindingPressEvent<T>(state, pressed, repeat);
handled = drawables.FirstOrDefault(d => triggerKeyBindingEvent(d, pressEvent));
}
}
// if (handled != null)
// Logger.Log($"Pressed ({pressed}) handled by {handled}.", LoggingTarget.Runtime, LogLevel.Debug);
return handled;
}
/// <summary>
/// Releases all pressed actions.
/// Note that the relevant key bindings remain in a pressed state by the user and are not released by this method.
/// </summary>
private void releasePressedActions(InputState state)
{
foreach (var action in pressedActions)
{
var releaseEvent = new KeyBindingReleaseEvent<T>(state, action);
foreach (var kvp in keyBindingQueues.Where(k => EqualityComparer<T>.Default.Equals(k.Key.GetAction<T>(), action)))
kvp.Value.ForEach(d => triggerKeyBindingEvent(d, releaseEvent));
}
pressedActions.Clear();
}
private void handleNewReleased(InputState state, InputKey releasedKey)
{
pressedInputKeys.Remove(releasedKey);
if (pressedBindings.Count == 0)
return;
// we don't want to consider exact matching here as we are dealing with bindings, not actions.
var pressedCombination = new KeyCombination(pressedInputKeys);
for (int i = 0; i < pressedBindings.Count; i++)
{
var binding = pressedBindings[i];
if (pressedInputKeys.Count == 0 || !binding.KeyCombination.IsPressed(pressedCombination, state, KeyCombinationMatchingMode.Any))
{
pressedBindings.RemoveAt(i--);
PropagateReleased(getInputQueue(binding).Where(d => d.IsRootedAt(this)), state, binding.GetAction<T>());
keyBindingQueues[binding].Clear();
}
}
}
protected virtual void PropagateReleased(IEnumerable<Drawable> drawables, InputState state, T released)
{
// we either want multiple release events due to the simultaneous mode, or we only want one when we
// - were pressed (as an action)
// - are the last pressed binding with this action
if (simultaneousMode == SimultaneousBindingMode.All || (pressedActions.Contains(released) && pressedBindings.All(b => !EqualityComparer<T>.Default.Equals(b.GetAction<T>(), released))))
{
var releaseEvent = new KeyBindingReleaseEvent<T>(state, released);
foreach (var d in drawables.OfType<IKeyBindingHandler<T>>())
triggerKeyBindingEvent(d, releaseEvent);
pressedActions.Remove(released);
}
}
public void TriggerReleased(T released) => PropagateReleased(KeyBindingInputQueue, GetContainingInputManager()?.CurrentState ?? new InputState(), released);
public Drawable TriggerPressed(T pressed)
{
var state = GetContainingInputManager()?.CurrentState ?? new InputState();
if (simultaneousMode == SimultaneousBindingMode.None)
releasePressedActions(state);
return PropagatePressed(KeyBindingInputQueue, state, pressed);
}
private List<Drawable> getInputQueue(IKeyBinding binding, bool rebuildIfEmpty = false)
{
if (!keyBindingQueues.ContainsKey(binding))
keyBindingQueues.Add(binding, new List<Drawable>());
var currentQueue = keyBindingQueues[binding];
if (rebuildIfEmpty && currentQueue.Count == 0)
currentQueue.AddRange(KeyBindingInputQueue);
return currentQueue;
}
private bool triggerKeyBindingEvent(IDrawable drawable, KeyBindingEvent<T> e)
{
e.Target = (Drawable)drawable;
switch (e)
{
case KeyBindingPressEvent<T> press:
return (drawable as IKeyBindingHandler<T>)?.OnPressed(press) ?? false;
case KeyBindingReleaseEvent<T> release:
(drawable as IKeyBindingHandler<T>)?.OnReleased(release);
return false;
case KeyBindingScrollEvent<T> scroll:
return (drawable as IScrollBindingHandler<T>)?.OnScroll(scroll) ?? false;
default:
throw new ArgumentException($"Invalid event type: {e.GetType()}", nameof(e));
}
}
}
/// <summary>
/// Maps input actions to custom action data.
/// </summary>
public abstract partial class KeyBindingContainer : Container
{
protected IEnumerable<IKeyBinding> KeyBindings;
public abstract IEnumerable<IKeyBinding> DefaultKeyBindings { get; }
/// <summary>
/// Whether key repeat events should be sent. Defaults to <c>true</c>.
/// </summary>
/// <remarks>
/// Key repeats will invoke actions at the same rates as <see cref="Drawable.OnKeyDown"/> events.
/// Disabling this is recommended if you either don't require key repeats (quite often applicable to gameplay input),
/// or want a custom repeat rate.
/// </remarks>
protected virtual bool HandleRepeats => true;
protected override void LoadComplete()
{
base.LoadComplete();
ReloadMappings();
}
protected virtual void ReloadMappings()
{
KeyBindings = DefaultKeyBindings;
}
}
public enum SimultaneousBindingMode
{
/// <summary>
/// One action can be in a pressed state at once.
/// If a new matching binding is encountered, any existing binding is first released.
/// </summary>
None,
/// <summary>
/// Unique actions are allowed to be pressed at the same time. There may therefore be more than one action in an actuated state at once.
/// If one action has multiple bindings, only the first will trigger an <see cref="IKeyBindingHandler{T}.OnPressed"/>.
/// The last binding to be released will trigger an <see cref="IKeyBindingHandler{T}.OnReleased"/>.
/// </summary>
Unique,
/// <summary>
/// Unique actions are allowed to be pressed at the same time, as well as multiple times from different bindings. There may therefore be
/// more than one action in an pressed state at once, as well as multiple consecutive <see cref="IKeyBindingHandler{T}.OnPressed"/> events
/// for a single action (followed by an eventual balancing number of <see cref="IKeyBindingHandler{T}.OnReleased"/> events).
/// </summary>
All,
}
}