Various modifications to the input manager to allow key rebinding.

This commit is contained in:
Pieter-Jan Briers
2020-09-07 10:55:31 +02:00
parent f3398853c1
commit 1eb03eb339
6 changed files with 396 additions and 100 deletions

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -18,7 +19,10 @@ using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
using static Robust.Client.Input.Keyboard;
@@ -26,24 +30,37 @@ namespace Robust.Client.Input
{
internal class InputManager : IInputManager
{
public bool Enabled { get; set; } = true;
// This is for both userdata and resources.
private const string KeybindsPath = "/keybinds.yml";
public virtual Vector2 MouseScreenPosition => Vector2.Zero;
[ViewVariables] public bool Enabled { get; set; } = true;
[ViewVariables] public virtual Vector2 MouseScreenPosition => Vector2.Zero;
[Dependency] private readonly IResourceManager _resourceMan = default!;
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManagerInternal = default!;
private readonly List<KeyBindingRegistration> _defaultRegistrations = new List<KeyBindingRegistration>();
private readonly Dictionary<BoundKeyFunction, InputCmdHandler> _commands =
new Dictionary<BoundKeyFunction, InputCmdHandler>();
private readonly List<KeyBinding> _bindings = new List<KeyBinding>();
private readonly Dictionary<BoundKeyFunction, List<KeyBinding>> _bindingsByFunction
= new Dictionary<BoundKeyFunction, List<KeyBinding>>();
// For knowing what to write to config.
private readonly HashSet<BoundKeyFunction> _modifiedKeyFunctions = new HashSet<BoundKeyFunction>();
[ViewVariables] private readonly List<KeyBinding> _bindings = new List<KeyBinding>();
private readonly bool[] _keysPressed = new bool[256];
/// <inheritdoc />
[ViewVariables]
public BoundKeyMap NetworkBindMap { get; private set; } = default!;
/// <inheritdoc />
[ViewVariables]
public IInputContextContainer Contexts { get; } = new InputContextContainer();
/// <inheritdoc />
@@ -64,12 +81,7 @@ namespace Robust.Client.Input
public string GetKeyFunctionButtonString(BoundKeyFunction function)
{
IKeyBinding bind;
try
{
bind = GetKeyBinding(function);
}
catch (KeyNotFoundException)
if (!TryGetKeyBinding(function, out var bind))
{
return Loc.GetString("<not bound>");
}
@@ -77,6 +89,11 @@ namespace Robust.Client.Input
return bind.GetKeyString();
}
public IEnumerable<IKeyBinding> AllBindings => _bindings;
public event KeyEventAction? FirstChanceOnKeyEvent;
public event Action<IKeyBinding>? OnKeyBindingAdded;
public event Action<IKeyBinding>? OnKeyBindingRemoved;
/// <inheritdoc />
public void Initialize()
{
@@ -87,13 +104,55 @@ namespace Robust.Client.Input
Contexts.ContextChanged += OnContextChanged;
var path = new ResourcePath("/keybinds.yml");
var path = new ResourcePath(KeybindsPath);
if (_resourceMan.UserData.Exists(path))
{
LoadKeyFile(path, true);
}
if (_resourceMan.ContentFileExists(path))
{
LoadKeyFile(path);
LoadKeyFile(path, false);
}
}
public void SaveToUserData()
{
var mapping = new YamlMappingNode();
var ser = YamlObjectSerializer.NewWriter(mapping);
var modifiedBindings = _modifiedKeyFunctions
.Select(p => _bindingsByFunction[p])
.SelectMany(p => p)
.Select(p => new KeyBindingRegistration
{
Function = p.Function,
BaseKey = p.BaseKey,
Mod1 = p.Mod1,
Mod2 = p.Mod2,
Mod3 = p.Mod3,
Priority = p.Priority,
Type = p.BindingType,
CanFocus = p.CanFocus,
CanRepeat = p.CanRepeat
}).ToArray();
var leaveEmpty = _modifiedKeyFunctions
.Where(p => _bindingsByFunction[p].Count == 0)
.ToArray();
var version = 1;
ser.DataField(ref version, "version", 1);
ser.DataField(ref modifiedBindings, "binds", null);
ser.DataField(ref leaveEmpty, "leaveEmpty", null);
var path = new ResourcePath(KeybindsPath);
using var writer = new StreamWriter(_resourceMan.UserData.Create(path));
var stream = new YamlStream {new YamlDocument(mapping)};
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
}
private void OnContextChanged(object? sender, ContextChangedEventArgs args)
{
// keyup any commands that are not in the new contexts, because it will not exist in the new context and get filtered. Luckily
@@ -120,6 +179,13 @@ namespace Robust.Client.Input
return;
}
FirstChanceOnKeyEvent?.Invoke(args, args.IsRepeat ? KeyEventType.Repeat : KeyEventType.Down);
if (args.Handled)
{
return;
}
_keysPressed[(int) args.Key] = true;
PackedKeyCombo matchedCombo = default;
@@ -177,6 +243,8 @@ namespace Robust.Client.Input
return;
}
FirstChanceOnKeyEvent?.Invoke(args, KeyEventType.Up);
var hasCanFocus = false;
foreach (var binding in _bindings)
{
@@ -237,7 +305,7 @@ namespace Robust.Client.Input
SetBindState(binding, BoundKeyState.Up);
}
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly=false)
private bool SetBindState(KeyBinding binding, BoundKeyState state, bool uiOnly = false)
{
binding.State = state;
@@ -307,85 +375,101 @@ namespace Robust.Client.Input
return true;
}
private void LoadKeyFile(ResourcePath yamlFile)
private void LoadKeyFile(ResourcePath file, bool userData)
{
YamlDocument document;
using (var stream = _resourceMan.ContentFileRead(yamlFile))
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
TextReader reader;
if (userData)
{
var yamlStream = new YamlStream();
yamlStream.Load(reader);
document = yamlStream.Documents[0];
reader = _resourceMan.UserData.OpenText(file);
}
else
{
reader = _resourceMan.ContentFileReadText(file);
}
var mapping = (YamlMappingNode) document.RootNode;
foreach (var keyMapping in mapping.GetNode<YamlSequenceNode>("binds").Cast<YamlMappingNode>())
var yamlStream = new YamlStream();
yamlStream.Load(reader);
var mapping = (YamlMappingNode) yamlStream.Documents[0].RootNode;
var baseSerializer = YamlObjectSerializer.NewReader(mapping);
var baseKeyRegs = baseSerializer.ReadDataField<KeyBindingRegistration[]>("binds");
foreach (var reg in baseKeyRegs)
{
var function = keyMapping.GetNode("function").AsString();
if (!NetworkBindMap.FunctionExists(function))
if (!NetworkBindMap.FunctionExists(reg.Function.FunctionName))
{
Logger.ErrorS("input", "Key function in {0} does not exist: '{1}'", yamlFile, function);
Logger.ErrorS("input", "Key function in {0} does not exist: '{1}'", file,
reg.Function.FunctionName);
continue;
}
var key = keyMapping.GetNode("key").AsEnum<Key>();
var canFocus = false;
if (keyMapping.TryGetNode("canFocus", out var canFocusName))
if (!userData)
{
canFocus = canFocusName.AsBool();
_defaultRegistrations.Add(reg);
if (_modifiedKeyFunctions.Contains(reg.Function))
{
// Don't read key functions from preset files that have been modified.
// So that we don't bulldoze a user's saved preferences.
continue;
}
}
var canRepeat = false;
if (keyMapping.TryGetNode("canRepeat", out var canRepeatName))
{
canRepeat = canRepeatName.AsBool();
}
RegisterBinding(reg, markModified: userData);
}
var mod1 = Key.Unknown;
if (keyMapping.TryGetNode("mod1", out var mod1Name))
{
mod1 = mod1Name.AsEnum<Key>();
}
var mod2 = Key.Unknown;
if (keyMapping.TryGetNode("mod2", out var mod2Name))
{
mod2 = mod2Name.AsEnum<Key>();
}
var mod3 = Key.Unknown;
if (keyMapping.TryGetNode("mod3", out var mod3Name))
{
mod3 = mod3Name.AsEnum<Key>();
}
var priority = 0;
if (keyMapping.TryGetNode("priority", out var priorityValue))
{
priority = priorityValue.AsInt();
}
var type = keyMapping.GetNode("type").AsEnum<KeyBindingType>();
var binding = new KeyBinding(this, function, type, key, canFocus, canRepeat, priority, mod1, mod2,
mod3);
RegisterBinding(binding);
if (userData)
{
// Adding to _modifiedKeyFunctions means that these keybinds won't be loaded from the base file.
// Because they've been explicitly cleared.
var leaveEmpty = baseSerializer.ReadDataField<BoundKeyFunction[]>("leaveEmpty");
_modifiedKeyFunctions.UnionWith(leaveEmpty);
}
}
/// <inheritdoc />
public void RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
public IKeyBinding RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
Key baseKey, Key? mod1, Key? mod2, Key? mod3)
{
var binding = new KeyBinding(this, function, bindingType, baseKey, false, false,
0, mod1 ?? Key.Unknown, mod2 ?? Key.Unknown, mod3 ?? Key.Unknown);
RegisterBinding(binding);
return binding;
}
private void RegisterBinding(KeyBinding binding)
public IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified = true)
{
var binding = new KeyBinding(this, reg.Function, reg.Type, reg.BaseKey, reg.CanFocus, reg.CanRepeat,
reg.Priority, reg.Mod1, reg.Mod2, reg.Mod3);
RegisterBinding(binding, markModified);
return binding;
}
public void RemoveBinding(IKeyBinding binding, bool markModified = true)
{
var bindings = _bindingsByFunction[binding.Function];
var cast = (KeyBinding) binding;
if (!bindings.Remove(cast))
{
// Keybind does not exist.
return;
}
if (markModified)
{
_modifiedKeyFunctions.Add(binding.Function);
}
_bindings.Remove(cast);
OnKeyBindingRemoved?.Invoke(binding);
}
private void RegisterBinding(KeyBinding binding, bool markModified = true)
{
// we sort larger combos first so they take priority over smaller (single key) combos,
// so they get processed first in KeyDown and such.
@@ -395,7 +479,14 @@ namespace Robust.Client.Input
pos = ~pos;
}
if (markModified)
{
_modifiedKeyFunctions.Add(binding.Function);
}
_bindings.Insert(pos, binding);
_bindingsByFunction.GetOrNew(binding.Function).Add(binding);
OnKeyBindingAdded?.Invoke(binding);
}
/// <inheritdoc />
@@ -409,10 +500,50 @@ namespace Robust.Client.Input
throw new KeyNotFoundException($"No keys are bound for function '{function}'");
}
/// <inheritdoc />
public bool TryGetKeyBinding(BoundKeyFunction function, out IKeyBinding binding)
public IReadOnlyList<IKeyBinding> GetKeyBindings(BoundKeyFunction function)
{
binding = _bindings.FirstOrDefault(k => k.Function == function);
return _bindingsByFunction.GetOrNew(function);
}
public void ResetBindingsFor(BoundKeyFunction function)
{
foreach (var binding in GetKeyBindings(function).ToArray())
{
RemoveBinding(binding);
}
// Mark as unmodified.
_modifiedKeyFunctions.Remove(function);
foreach (var defaultBinding in _defaultRegistrations.Where(p => p.Function == function))
{
RegisterBinding(defaultBinding, markModified: false);
}
}
public void ResetAllBindings()
{
foreach (var modified in _modifiedKeyFunctions.ToArray())
{
ResetBindingsFor(modified);
}
}
public bool IsKeyFunctionModified(BoundKeyFunction function)
{
return _modifiedKeyFunctions.Contains(function);
}
/// <inheritdoc />
public bool TryGetKeyBinding(BoundKeyFunction function, [NotNullWhen(true)] out IKeyBinding? binding)
{
if (!_bindingsByFunction.TryGetValue(function, out var bindings))
{
binding = null;
return false;
}
binding = bindings.FirstOrDefault();
return binding != null;
}
@@ -445,22 +576,29 @@ namespace Robust.Client.Input
{
private readonly InputManager _inputManager;
public BoundKeyState State { get; set; }
[ViewVariables] public BoundKeyState State { get; set; }
public PackedKeyCombo PackedKeyCombo { get; }
public BoundKeyFunction Function { get; }
public KeyBindingType BindingType { get; }
[ViewVariables] public BoundKeyFunction Function { get; }
[ViewVariables] public KeyBindingType BindingType { get; }
[ViewVariables] public Key BaseKey => PackedKeyCombo.BaseKey;
[ViewVariables] public Key Mod1 => PackedKeyCombo.Mod1;
[ViewVariables] public Key Mod2 => PackedKeyCombo.Mod2;
[ViewVariables] public Key Mod3 => PackedKeyCombo.Mod3;
/// <summary>
/// Whether the BoundKey can change the focused control.
/// </summary>
[ViewVariables]
public bool CanFocus { get; internal set; }
/// <summary>
/// Whether the BoundKey still triggers while held down.
/// </summary>
[ViewVariables]
public bool CanRepeat { get; internal set; }
public int Priority { get; internal set; }
[ViewVariables] public int Priority { get; internal set; }
public KeyBinding(InputManager inputManager, BoundKeyFunction function,
KeyBindingType bindingType,
@@ -518,7 +656,28 @@ namespace Robust.Client.Input
}
}
public static IComparer<KeyBinding> ProcessPriorityComparer { get; } = new ProcessPriorityRelationalComparer();
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendFormat("{0}: {1}", Function.FunctionName, BaseKey);
if (Mod1 != Key.Unknown)
{
sb.AppendFormat("+{0}", Mod1);
if (Mod2 != Key.Unknown)
{
sb.AppendFormat("+{0}", Mod2);
if (Mod3 != Key.Unknown)
{
sb.AppendFormat("+{0}", Mod3);
}
}
}
return sb.ToString();
}
public static IComparer<KeyBinding> ProcessPriorityComparer { get; } =
new ProcessPriorityRelationalComparer();
}
[StructLayout(LayoutKind.Explicit)]
@@ -612,6 +771,7 @@ namespace Robust.Client.Input
public string Command => "bind";
public string Description => "Binds an input key to an input command.";
public string Help => "bind <KeyName> <BindMode> <InputCommand>";
public bool Execute(IDebugConsole console, params string[] args)
{
if (args.Length < 3)
@@ -628,26 +788,50 @@ namespace Robust.Client.Input
var keyName = args[0];
if (!Enum.TryParse(typeof(Keyboard.Key), keyName, true, out var keyIdObj))
if (!Enum.TryParse(typeof(Key), keyName, true, out var keyIdObj))
{
console.AddLine($"Key '{keyName}' is unrecognized.");
return false;
}
var keyId = (Keyboard.Key) keyIdObj!;
var keyId = (Key) keyIdObj!;
if (!Enum.TryParse(typeof(KeyBindingType), args[1], true, out var keyModeObj))
{
console.AddLine($"BindMode '{args[1]}' is unrecognized.");
return false;
}
var keyMode = (KeyBindingType)keyModeObj!;
var keyMode = (KeyBindingType) keyModeObj!;
var inputCommand = args[2];
var inputMan = IoCManager.Resolve<IInputManager>();
inputMan.RegisterBinding(new BoundKeyFunction(inputCommand), keyMode, keyId, null, null, null);
var registration = new KeyBindingRegistration
{
Function = new BoundKeyFunction(inputCommand),
BaseKey = keyId,
Type = keyMode
};
inputMan.RegisterBinding(registration);
return false;
}
}
[UsedImplicitly]
internal class SaveBindCommand : IConsoleCommand
{
public string Command => "svbind";
public string Description => "";
public string Help => "";
public bool Execute(IDebugConsole console, params string[] args)
{
IoCManager.Resolve<IInputManager>()
.SaveToUserData();
return false;
}

View File

@@ -0,0 +1,20 @@
namespace Robust.Client.Input
{
public enum KeyEventType : byte
{
/// <summary>
/// This key is pressed down.
/// </summary>
Down,
/// <summary>
/// This key was repeated by the operating system while already being pressed down.
/// </summary>
Repeat,
/// <summary>
/// This key has been released.
/// </summary>
Up
}
}

View File

@@ -1,7 +1,6 @@
using Robust.Shared;
using Robust.Shared.IoC;
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Client.Input;
using Robust.Shared.Maths;
using Robust.Shared.Input;
@@ -9,6 +8,8 @@ using Robust.Shared.Input.Binding;
namespace Robust.Client.Interfaces.Input
{
public delegate void KeyEventAction(KeyEventArgs keyEvent, KeyEventType type);
/// <summary>
/// Manages key bindings, input commands and other misc. input systems.
/// </summary>
@@ -23,30 +24,32 @@ namespace Robust.Client.Interfaces.Input
IInputContextContainer Contexts { get; }
void Initialize();
void SaveToUserData();
void KeyDown(KeyEventArgs e);
void KeyUp(KeyEventArgs e);
/// <summary>
/// Registers a new key binding in the input manager.
/// </summary>
/// <param name="function">The function the key binding is bound to.</param>
/// <param name="bindingType"></param>
/// <param name="baseKey"></param>
/// <param name="mod1"></param>
/// <param name="mod2"></param>
/// <param name="mod3"></param>
void RegisterBinding(BoundKeyFunction function, KeyBindingType bindingType,
Keyboard.Key baseKey, Keyboard.Key? mod1, Keyboard.Key? mod2, Keyboard.Key? mod3);
IKeyBinding RegisterBinding(in KeyBindingRegistration reg, bool markModified=true);
void RemoveBinding(IKeyBinding binding, bool markModified=true);
/// <summary>
/// Gets a key binding according to the function it is bound to.
/// </summary>
/// <remarks>
/// Which keybind is returned if there are multiple for a key function is unspecified.
/// </remarks>
/// <param name="function">The function the key binding is bound to.</param>
/// <returns>The key binding.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if no there is no keybind for the specified function.
/// </exception>
IKeyBinding GetKeyBinding(BoundKeyFunction function);
bool TryGetKeyBinding(BoundKeyFunction function, out IKeyBinding binding);
/// <remarks>
/// Which keybind is returned if there are multiple for a key function is unspecified.
/// </remarks>
bool TryGetKeyBinding(BoundKeyFunction function, [NotNullWhen(true)] out IKeyBinding? binding);
/// <summary>
/// Returns the input command bound to a key function.
@@ -79,5 +82,41 @@ namespace Robust.Client.Interfaces.Input
/// of buttons is bound to the specified key function.
/// </summary>
string GetKeyFunctionButtonString(BoundKeyFunction function);
/// <summary>
/// Gets all the key bindings currently registered into the manager.
/// </summary>
IEnumerable<IKeyBinding> AllBindings { get; }
/// <summary>
/// An event that gets fired before everything else when a key event comes in.
/// For key down events, the event can be handled to block it.
/// </summary>
/// <remarks>
/// Do not use this for regular input handling.
/// This is a low-level API intended solely for stuff like the key rebinding menu.
/// </remarks>
event KeyEventAction FirstChanceOnKeyEvent;
event Action<IKeyBinding> OnKeyBindingAdded;
event Action<IKeyBinding> OnKeyBindingRemoved;
/// <summary>
/// Gets all the keybinds bound to a specific function.
/// </summary>
IReadOnlyList<IKeyBinding> GetKeyBindings(BoundKeyFunction function);
/// <summary>
/// Resets the bindings for a specific BoundKeyFunction to the defaults from the resource files.
/// </summary>
/// <param name="function">The key function to reset the bindings for.</param>
void ResetBindingsFor(BoundKeyFunction function);
/// <summary>
/// Resets ALL the keybinds to the defaults from the resource files.
/// </summary>
void ResetAllBindings();
bool IsKeyFunctionModified(BoundKeyFunction function);
}
}

View File

@@ -9,6 +9,15 @@ namespace Robust.Client.Interfaces.Input
BoundKeyFunction Function { get; }
KeyBindingType BindingType { get; }
Keyboard.Key BaseKey { get; }
Keyboard.Key Mod1 { get; }
Keyboard.Key Mod2 { get; }
Keyboard.Key Mod3 { get; }
bool CanFocus { get; }
bool CanRepeat { get; }
int Priority { get; }
/// <summary>
/// Gets a user-presentable, localized & keyboard-adjusted string for which buttons the user has to press.
/// </summary>

View File

@@ -0,0 +1,33 @@
using Robust.Client.Input;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
namespace Robust.Client.Interfaces.Input
{
public struct KeyBindingRegistration : IExposeData
{
public BoundKeyFunction Function;
public KeyBindingType Type;
public Keyboard.Key BaseKey;
public Keyboard.Key Mod1;
public Keyboard.Key Mod2;
public Keyboard.Key Mod3;
public int Priority;
public bool CanFocus;
public bool CanRepeat;
public void ExposeData(ObjectSerializer serializer)
{
serializer.DataField(ref Function, "function", default);
serializer.DataField(ref Type, "type", KeyBindingType.State);
serializer.DataField(ref BaseKey, "key", default);
serializer.DataField(ref Mod1, "mod1", default);
serializer.DataField(ref Mod2, "mod2", default);
serializer.DataField(ref Mod3, "mod3", default);
serializer.DataField(ref Priority, "priority", 0);
serializer.DataField(ref CanFocus, "canFocus", false);
serializer.DataField(ref CanRepeat, "canRepeat", false);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using Robust.Shared.Interfaces.Serialization;
using Robust.Shared.Serialization;
namespace Robust.Shared.Input
@@ -72,7 +73,7 @@ namespace Robust.Shared.Input
}
[Serializable, NetSerializable]
public struct BoundKeyFunction : IComparable, IComparable<BoundKeyFunction>, IEquatable<BoundKeyFunction>
public struct BoundKeyFunction : IComparable, IComparable<BoundKeyFunction>, IEquatable<BoundKeyFunction>, ISelfSerialize
{
public readonly string FunctionName;
@@ -86,14 +87,14 @@ namespace Robust.Shared.Input
return new BoundKeyFunction(name);
}
public override string ToString()
public override readonly string ToString()
{
return $"KeyFunction({FunctionName})";
}
#region Code for easy equality and sorting.
public int CompareTo(object? obj)
public readonly int CompareTo(object? obj)
{
if (!(obj is BoundKeyFunction func))
{
@@ -102,23 +103,23 @@ namespace Robust.Shared.Input
return CompareTo(func);
}
public int CompareTo(BoundKeyFunction other)
public readonly int CompareTo(BoundKeyFunction other)
{
return string.Compare(FunctionName, other.FunctionName, StringComparison.InvariantCultureIgnoreCase);
}
// Could maybe go dirty and optimize these on the assumption that they're singletons.
public override bool Equals(object? obj)
public override readonly bool Equals(object? obj)
{
return obj is BoundKeyFunction func && Equals(func);
}
public bool Equals(BoundKeyFunction other)
public readonly bool Equals(BoundKeyFunction other)
{
return other.FunctionName == FunctionName;
}
public override int GetHashCode()
public override readonly int GetHashCode()
{
return FunctionName.GetHashCode();
}
@@ -134,6 +135,16 @@ namespace Robust.Shared.Input
}
#endregion
public void Deserialize(string value)
{
this = new BoundKeyFunction(value);
}
public readonly string Serialize()
{
return FunctionName;
}
}
/// <summary>