Allow left/right modifier keys to be used separately (no UI)

Should allow for any key to be used as a modifier (see #2981), simply by
populating `Config.ModifierKeys`. Flip the assignment on `Input.cs:45` to try
out this change specifically.
This commit is contained in:
YoshiRulz 2021-11-24 08:12:58 +10:00
parent 3f9fb0eaef
commit accf0f038c
No known key found for this signature in database
GPG Key ID: C4DE31C245353FB7
11 changed files with 123 additions and 75 deletions

View File

@ -5,6 +5,8 @@ using BizHawk.Common;
using BizHawk.Common.PathExtensions; using BizHawk.Common.PathExtensions;
using BizHawk.Emulation.Common; using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores; using BizHawk.Emulation.Cores;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace BizHawk.Client.Common namespace BizHawk.Client.Common
@ -332,5 +334,12 @@ namespace BizHawk.Client.Common
public EHostInputMethod HostInputMethod { get; set; } = HostCapabilityDetector.HasDirectX ? EHostInputMethod.DirectInput : EHostInputMethod.OpenTK; public EHostInputMethod HostInputMethod { get; set; } = HostCapabilityDetector.HasDirectX ? EHostInputMethod.DirectInput : EHostInputMethod.OpenTK;
public bool UseStaticWindowTitles { get; set; } public bool UseStaticWindowTitles { get; set; }
public List<string> ModifierKeys { get; set; } = new();
[JsonIgnore]
public IReadOnlyList<string> ModifierKeysEffective;
public bool MergeLAndRModifierKeys { get; set; } = true;
} }
} }

View File

@ -1,6 +1,8 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Text;
namespace BizHawk.Client.Common namespace BizHawk.Client.Common
{ {
@ -29,25 +31,31 @@ namespace BizHawk.Client.Common
Press, Release Press, Release
} }
public readonly struct LogicalButton public struct LogicalButton
{ {
public const uint MASK_ALT = 1U << 2;
public const uint MASK_CTRL = 1U << 1;
public const uint MASK_SHIFT = 1U << 3;
public const uint MASK_WIN = 1U << 0;
public static bool operator ==(LogicalButton lhs, LogicalButton rhs) public static bool operator ==(LogicalButton lhs, LogicalButton rhs)
=> lhs.Button == rhs.Button && lhs.Modifiers == rhs.Modifiers; => lhs.Button == rhs.Button && lhs.Modifiers == rhs.Modifiers;
public static bool operator !=(LogicalButton lhs, LogicalButton rhs) => !(lhs == rhs); public static bool operator !=(LogicalButton lhs, LogicalButton rhs) => !(lhs == rhs);
public bool Alt => (Modifiers & ModifierKey.Alt) != 0; /// <remarks>pretty sure these are always consumed during the same iteration of the main program loop, but ¯\_(ツ)_/¯ better safe than sorry --yoshi</remarks>
private readonly Func<IReadOnlyList<string>> _getEffectiveModListCallback;
public readonly string Button; public readonly string Button;
public bool Control => (Modifiers & ModifierKey.Control) != 0; public readonly uint Modifiers;
public readonly ModifierKey Modifiers; public LogicalButton(string button, uint modifiers, Func<IReadOnlyList<string>> getEffectiveModListCallback)
public bool Shift => (Modifiers & ModifierKey.Shift) != 0;
public LogicalButton(string button, ModifierKey modifiers)
{ {
_getEffectiveModListCallback = getEffectiveModListCallback;
Button = button; Button = button;
Modifiers = modifiers; Modifiers = modifiers;
} }
@ -58,12 +66,19 @@ namespace BizHawk.Client.Common
public override readonly string ToString() public override readonly string ToString()
{ {
var ret = ""; if (Modifiers is 0U) return Button;
if (Control) ret += "Ctrl+"; var allMods = _getEffectiveModListCallback();
if (Alt) ret += "Alt+"; StringBuilder ret = new();
if (Shift) ret += "Shift+"; for (var i = 0; i < allMods.Count; i++)
ret += Button; {
return ret; var b = 1U << i;
if ((Modifiers & b) is not 0U)
{
ret.Append(allMods[i]);
ret.Append('+');
}
}
return ret + Button;
} }
} }
} }

View File

@ -1,5 +1,3 @@
using System;
namespace BizHawk.Client.Common namespace BizHawk.Client.Common
{ {
public enum AllowInput public enum AllowInput
@ -8,19 +6,4 @@ namespace BizHawk.Client.Common
All = 1, All = 1,
OnlyController = 2 OnlyController = 2
} }
[Flags]
public enum ModifierKey
{
/// <summary>The bitmask to extract modifiers from a key value.</summary>
Modifiers = -65536,
/// <summary>No key pressed.</summary>
None = 0,
/// <summary>The SHIFT modifier key.</summary>
Shift = 65536,
/// <summary>The CTRL modifier key.</summary>
Control = 131072,
/// <summary>The ALT modifier key.</summary>
Alt = 262144,
}
} }

View File

@ -8,6 +8,7 @@ using BizHawk.Bizware.DirectX;
using BizHawk.Bizware.OpenTK3; using BizHawk.Bizware.OpenTK3;
using BizHawk.Common; using BizHawk.Common;
using BizHawk.Client.Common; using BizHawk.Client.Common;
using BizHawk.Common.CollectionExtensions;
namespace BizHawk.Client.EmuHawk namespace BizHawk.Client.EmuHawk
{ {
@ -33,15 +34,20 @@ namespace BizHawk.Client.EmuHawk
public readonly IHostInputAdapter Adapter; public readonly IHostInputAdapter Adapter;
private Config _currentConfig;
private readonly Func<Config> _getConfigCallback; private readonly Func<Config> _getConfigCallback;
internal Input(IntPtr mainFormHandle, Func<Config> getConfigCallback, Func<bool, AllowInput> mainFormInputAllowedCallback) internal Input(IntPtr mainFormHandle, Func<Config> getConfigCallback, Func<bool, AllowInput> mainFormInputAllowedCallback)
{ {
_getConfigCallback = getConfigCallback; _getConfigCallback = getConfigCallback;
_currentConfig = _getConfigCallback();
_currentConfig.MergeLAndRModifierKeys = true; // for debugging
UpdateModifierKeysEffective();
MainFormInputAllowedCallback = mainFormInputAllowedCallback; MainFormInputAllowedCallback = mainFormInputAllowedCallback;
var config = _getConfigCallback(); Adapter = _currentConfig.HostInputMethod switch
Adapter = config.HostInputMethod switch
{ {
EHostInputMethod.OpenTK => new OpenTKInputAdapter(), EHostInputMethod.OpenTK => new OpenTKInputAdapter(),
_ when OSTailoredCode.IsUnixHost => new OpenTKInputAdapter(), _ when OSTailoredCode.IsUnixHost => new OpenTKInputAdapter(),
@ -49,7 +55,7 @@ namespace BizHawk.Client.EmuHawk
_ => throw new Exception() _ => throw new Exception()
}; };
Console.WriteLine($"Using {Adapter.Desc} for host input (keyboard + gamepads)"); Console.WriteLine($"Using {Adapter.Desc} for host input (keyboard + gamepads)");
Adapter.UpdateConfig(config); Adapter.UpdateConfig(_currentConfig);
Adapter.FirstInitAll(mainFormHandle); Adapter.FirstInitAll(mainFormHandle);
_updateThread = new Thread(UpdateThreadProc) _updateThread = new Thread(UpdateThreadProc)
{ {
@ -66,26 +72,36 @@ namespace BizHawk.Client.EmuHawk
private bool _trackDeltas; private bool _trackDeltas;
private bool _ignoreEventsNextPoll; private bool _ignoreEventsNextPoll;
private static readonly IReadOnlyList<string> ModifierKeysBase = new[] { "Win", "Ctrl", "Alt", "Shift" };
private static readonly IReadOnlyList<string> ModifierKeysBaseUnmerged = new[] { "Win", "Ctrl", "Alt", "Shift", "LeftWin", "RightWin", "LeftCtrl", "RightCtrl", "LeftAlt", "RightAlt", "LeftShift", "RightShift" };
public void UpdateModifierKeysEffective()
=> _currentConfig.ModifierKeysEffective = (_currentConfig.MergeLAndRModifierKeys ? ModifierKeysBase : ModifierKeysBaseUnmerged)
.Concat(_currentConfig.ModifierKeys)
.Take(32).ToArray();
private readonly IReadOnlyDictionary<string, string> _modifierKeyPreMap = new Dictionary<string, string>
{
["LeftWin"] = "Win",
["RightWin"] = "Win",
["LeftCtrl"] = "Ctrl",
["RightCtrl"] = "Ctrl",
["LeftAlt"] = "Alt",
["RightAlt"] = "Alt",
["LeftShift"] = "Shift",
["RightShift"] = "Shift",
};
private void HandleButton(string button, bool newState, ClientInputFocus source) private void HandleButton(string button, bool newState, ClientInputFocus source)
{ {
var currentModifier = button switch if (!(_currentConfig.MergeLAndRModifierKeys &&_modifierKeyPreMap.TryGetValue(button, out var button1))) button1 = button;
{ var modIndex = _currentConfig.ModifierKeysEffective.IndexOf(button1);
// "LeftWin" => ModifierKey.Win, var currentModifier = modIndex is -1 ? 0U : 1U << modIndex;
// "RightWin" => ModifierKey.Win, if (EnableIgnoreModifiers && currentModifier is not 0U) return;
"LeftShift" => ModifierKey.Shift, if (newState == _lastState[button1]) return;
"RightShift" => ModifierKey.Shift,
"LeftCtrl" => ModifierKey.Control,
"RightCtrl" => ModifierKey.Control,
"LeftAlt" => ModifierKey.Alt,
"RightAlt" => ModifierKey.Alt,
_ => ModifierKey.None
};
if (EnableIgnoreModifiers && currentModifier != ModifierKey.None) return;
if (_lastState[button] == newState) return;
// apply if (currentModifier is not 0U)
// NOTE: this is not quite right. if someone held leftshift+rightshift it would be broken. seems unlikely, though.
if (currentModifier != ModifierKey.None)
{ {
if (newState) if (newState)
_modifiers |= currentModifier; _modifiers |= currentModifier;
@ -94,17 +110,17 @@ namespace BizHawk.Client.EmuHawk
} }
// don't generate events for things like Ctrl+LeftControl // don't generate events for things like Ctrl+LeftControl
ModifierKey mods = _modifiers; var mods = _modifiers;
if (currentModifier != ModifierKey.None) if (currentModifier is not 0U)
mods &= ~currentModifier; mods &= ~currentModifier;
var ie = new InputEvent var ie = new InputEvent
{ {
EventType = newState ? InputEventType.Press : InputEventType.Release, EventType = newState ? InputEventType.Press : InputEventType.Release,
LogicalButton = new LogicalButton(button, mods), LogicalButton = new(button1, mods, () => _getConfigCallback().ModifierKeysEffective),
Source = source Source = source
}; };
_lastState[button] = newState; _lastState[button1] = newState;
// track the pressed events with modifiers that we send so that we can send corresponding unpresses with modifiers // track the pressed events with modifiers that we send so that we can send corresponding unpresses with modifiers
// this is an interesting idea, which we may need later, but not yet. // this is an interesting idea, which we may need later, but not yet.
@ -115,11 +131,11 @@ namespace BizHawk.Client.EmuHawk
// so, i am adding it as of 11-sep-2011 // so, i am adding it as of 11-sep-2011
if (newState) if (newState)
{ {
_modifierState[button] = ie.LogicalButton; _modifierState[button1] = ie.LogicalButton;
} }
else else
{ {
if (_modifierState.TryGetValue(button, out var buttonModifierState)) if (_modifierState.TryGetValue(button1, out var buttonModifierState))
{ {
if (buttonModifierState != ie.LogicalButton && !_ignoreEventsNextPoll) if (buttonModifierState != ie.LogicalButton && !_ignoreEventsNextPoll)
{ {
@ -131,7 +147,7 @@ namespace BizHawk.Client.EmuHawk
Source = source Source = source
}); });
} }
_modifierState.Remove(button); _modifierState.Remove(button1);
} }
} }
@ -147,7 +163,7 @@ namespace BizHawk.Client.EmuHawk
_axisValues[axis] = newValue; _axisValues[axis] = newValue;
} }
private ModifierKey _modifiers; private uint _modifiers;
private readonly List<InputEvent> _newEvents = new List<InputEvent>(); private readonly List<InputEvent> _newEvents = new List<InputEvent>();
public void ClearEvents() public void ClearEvents()
@ -245,7 +261,8 @@ namespace BizHawk.Client.EmuHawk
}; };
while (true) while (true)
{ {
Adapter.UpdateConfig(_getConfigCallback()); _currentConfig = _getConfigCallback();
Adapter.UpdateConfig(_currentConfig);
var keyEvents = Adapter.ProcessHostKeyboards(); var keyEvents = Adapter.ProcessHostKeyboards();
Adapter.PreprocessHostGamepads(); Adapter.PreprocessHostGamepads();
@ -307,7 +324,7 @@ namespace BizHawk.Client.EmuHawk
foreach (var ie in _newEvents) foreach (var ie in _newEvents)
{ {
//events are swallowed in some cases: //events are swallowed in some cases:
if (ie.LogicalButton.Alt && ShouldSwallow(MainFormInputAllowedCallback(true), ie)) if ((ie.LogicalButton.Modifiers & LogicalButton.MASK_ALT) is not 0U && ShouldSwallow(MainFormInputAllowedCallback(true), ie))
continue; continue;
if (ie.EventType == InputEventType.Press && ShouldSwallow(allowInput, ie)) if (ie.EventType == InputEventType.Press && ShouldSwallow(allowInput, ie))
continue; continue;

View File

@ -992,9 +992,9 @@ namespace BizHawk.Client.EmuHawk
if (triggers.Count == 0) if (triggers.Count == 0)
{ {
// Maybe it is a system alt-key which hasn't been overridden // Maybe it is a system alt-key which hasn't been overridden
if (ie.EventType is InputEventType.Press) if (ie.EventType is InputEventType.Press && (ie.LogicalButton.Modifiers & LogicalButton.MASK_ALT) is not 0U)
{ {
if (ie.LogicalButton.Alt && ie.LogicalButton.Button.Length == 1) if (ie.LogicalButton.Button.Length == 1)
{ {
var c = ie.LogicalButton.Button.ToLower()[0]; var c = ie.LogicalButton.Button.ToLower()[0];
if ((c >= 'a' && c <= 'z') || c == ' ') if ((c >= 'a' && c <= 'z') || c == ' ')
@ -1002,8 +1002,7 @@ namespace BizHawk.Client.EmuHawk
SendAltKeyChar(c); SendAltKeyChar(c);
} }
} }
else if (ie.LogicalButton.Button == "Space")
if (ie.LogicalButton.Alt && ie.LogicalButton.Button == "Space")
{ {
SendPlainAltKey(32); SendPlainAltKey(32);
} }

View File

@ -81,7 +81,7 @@ namespace BizHawk.Client.EmuHawk
private Control CreateNormalPanel(Dictionary<string, string> settings, List<string> buttons, Size size) private Control CreateNormalPanel(Dictionary<string, string> settings, List<string> buttons, Size size)
{ {
var cp = new ControllerConfigPanel { Dock = DockStyle.Fill, AutoScroll = true, Tooltip = toolTip1 }; ControllerConfigPanel cp = new(_config.ModifierKeysEffective) { Dock = DockStyle.Fill, AutoScroll = true, Tooltip = toolTip1 };
cp.LoadSettings(settings, checkBoxAutoTab.Checked, buttons, size.Width, size.Height); cp.LoadSettings(settings, checkBoxAutoTab.Checked, buttons, size.Width, size.Height);
return cp; return cp;
} }

View File

@ -4,6 +4,8 @@ using System.Drawing;
using System.Linq; using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
using BizHawk.Client.Common;
namespace BizHawk.Client.EmuHawk namespace BizHawk.Client.EmuHawk
{ {
// this is a little messy right now because of remnants of the old config system // this is a little messy right now because of remnants of the old config system
@ -33,8 +35,11 @@ namespace BizHawk.Client.EmuHawk
private bool _autoTab; private bool _autoTab;
public ControllerConfigPanel() private readonly IReadOnlyList<string> _effectiveModList;
public ControllerConfigPanel(IReadOnlyList<string> effectiveModList)
{ {
_effectiveModList = effectiveModList;
InitializeComponent(); InitializeComponent();
} }
@ -121,7 +126,7 @@ namespace BizHawk.Client.EmuHawk
x += columnWidth; x += columnWidth;
} }
var iw = new InputCompositeWidget var iw = new InputCompositeWidget(_effectiveModList)
{ {
Location = new Point(x, y), Location = new Point(x, y),
Size = new Size(_inputSize, UIHelper.ScaleY(23)), Size = new Size(_inputSize, UIHelper.ScaleY(23)),

View File

@ -110,7 +110,7 @@ namespace BizHawk.Client.EmuHawk
Size = new Size(iwOffsetX - UIHelper.ScaleX(2), UIHelper.ScaleY(15)) Size = new Size(iwOffsetX - UIHelper.ScaleX(2), UIHelper.ScaleY(15))
}; };
var w = new InputCompositeWidget var w = new InputCompositeWidget(_config.ModifierKeysEffective)
{ {
Location = new Point(x + iwOffsetX, y + iwOffsetY), Location = new Point(x + iwOffsetX, y + iwOffsetY),
AutoTab = AutoTabCheckBox.Checked, AutoTab = AutoTabCheckBox.Checked,

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Windows.Forms; using System.Windows.Forms;
using BizHawk.Client.Common; using BizHawk.Client.Common;
@ -7,8 +8,12 @@ namespace BizHawk.Client.EmuHawk
{ {
public partial class InputCompositeWidget : UserControl public partial class InputCompositeWidget : UserControl
{ {
public InputCompositeWidget() private readonly IReadOnlyList<string> _effectiveModList;
public InputCompositeWidget(IReadOnlyList<string> effectiveModList)
{ {
_effectiveModList = effectiveModList;
InitializeComponent(); InitializeComponent();
btnSpecial.Image = Properties.Resources.ArrowBlackDown; btnSpecial.Image = Properties.Resources.ArrowBlackDown;
@ -82,24 +87,24 @@ namespace BizHawk.Client.EmuHawk
private void DropdownMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e) private void DropdownMenu_ItemClicked(object sender, ToolStripItemClickedEventArgs e)
{ {
var mods = ModifierKey.None; var mods = 0U;
if ((ModifierKeys & Keys.Shift) != 0) if ((ModifierKeys & Keys.Shift) != 0)
{ {
mods |= ModifierKey.Shift; mods |= LogicalButton.MASK_SHIFT;
} }
if ((ModifierKeys & Keys.Control) != 0) if ((ModifierKeys & Keys.Control) != 0)
{ {
mods |= ModifierKey.Control; mods |= LogicalButton.MASK_CTRL;
} }
if ((ModifierKeys & Keys.Alt) != 0) if ((ModifierKeys & Keys.Alt) != 0)
{ {
mods |= ModifierKey.Alt; mods |= LogicalButton.MASK_ALT;
} }
LogicalButton lb = new(e.ClickedItem.Text, mods); LogicalButton lb = new(e.ClickedItem.Text, mods, () => _effectiveModList);
widget.SetBinding(lb.ToString()); widget.SetBinding(lb.ToString());
} }

View File

@ -120,6 +120,19 @@ namespace BizHawk.Common.CollectionExtensions
foreach (var item in collection) list.Add(item); foreach (var item in collection) list.Add(item);
} }
/// <inheritdoc cref="IList{T}.IndexOf"/>
/// <remarks>
/// (This is an extension method which reimplements <see cref="IList{T}.IndexOf"/> for other <see cref="IReadOnlyList{T}">collections</see>.
/// It defers to the existing <see cref="IList{T}.IndexOf">IndexOf</see> if the receiver's type is <see cref="IList{T}"/> or a subtype.)
/// </remarks>
public static int IndexOf<T>(this IReadOnlyList<T> list, T elem)
where T : IEquatable<T>
{
if (list is IList<T> listImpl) return listImpl.IndexOf(elem);
for (int i = 0, l = list.Count; i < l; i++) if (elem.Equals(list[i])) return i;
return -1;
}
public static T? FirstOrNull<T>(this IEnumerable<T> list, Func<T, bool> predicate) public static T? FirstOrNull<T>(this IEnumerable<T> list, Func<T, bool> predicate)
where T : struct where T : struct
{ {

View File

@ -33,6 +33,7 @@ namespace BizHawk.Tests.Client.Common.config
typeof(Dictionary<,>), typeof(Dictionary<,>),
typeof(int), typeof(int),
typeof(JToken), typeof(JToken),
typeof(List<>),
typeof(Nullable<>), typeof(Nullable<>),
typeof(object), typeof(object),
typeof(float), typeof(float),
@ -78,6 +79,7 @@ namespace BizHawk.Tests.Client.Common.config
groupDesc ??= t.Name; groupDesc ??= t.Name;
foreach (var mi in t.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) foreach (var mi in t.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{ {
if (mi.GetCustomAttribute<JsonIgnoreAttribute>() is not null) continue;
if (mi is PropertyInfo pi) CheckMemberAndTypeParams(pi.PropertyType, groupDesc); if (mi is PropertyInfo pi) CheckMemberAndTypeParams(pi.PropertyType, groupDesc);
else if (mi is FieldInfo fi) CheckMemberAndTypeParams(fi.FieldType, groupDesc); else if (mi is FieldInfo fi) CheckMemberAndTypeParams(fi.FieldType, groupDesc);
} }