make code testable by moving input handling logic out of MainForm

This commit is contained in:
SuuperW 2025-07-26 20:22:59 -05:00
parent 27974dc84c
commit 433f0c30e9
No known key found for this signature in database
4 changed files with 186 additions and 180 deletions

View File

@ -0,0 +1,13 @@
#nullable enable
using System.Collections.Generic;
namespace BizHawk.Client.Common
{
public interface IInputSource
{
InputEvent? DequeueEvent();
KeyValuePair<string, int>[] GetAxisValues();
}
}

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using BizHawk.Emulation.Common;
@ -147,5 +148,142 @@ namespace BizHawk.Client.Common
return ret;
}
// input state which has been destined for client hotkey consumption are colesced here
private readonly InputCoalescer _hotkeyCoalescer = new InputCoalescer();
/// <param name="processUnboundInput">Events that did not do anything are forwarded out here.
/// This allows things like Windows' standard alt hotkeys (for menu items) to be handled by the
/// caller if the input didn't alrady do something else.</param>
public void ProcessInput(IInputSource source, Func<string, bool> processHotkey, Config config, Action<InputEvent> processUnboundInput, Func<string, bool> isInternalHotkey)
{
// loop through all available events
InputEvent ie;
while ((ie = source.DequeueEvent()) != null)
{
// useful debugging:
// Console.WriteLine(ie);
// TODO - wonder what happens if we pop up something interactive as a response to one of these hotkeys? may need to purge further processing
// look for hotkey bindings for this key
var triggers = ClientControls.SearchBindings(ie.LogicalButton.ToString());
if (triggers.Count == 0)
{
processUnboundInput(ie);
}
switch (config.InputHotkeyOverrideOptions)
{
default:
case 0: // Both allowed
{
ControllerInputCoalescer.Receive(ie);
var handled = false;
if (ie.EventType is InputEventType.Press)
{
handled = triggers.Aggregate(handled, (current, trigger) => current | processHotkey(trigger));
}
// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
if (!handled)
{
_hotkeyCoalescer.Receive(ie);
}
break;
}
case 1: // Input overrides Hotkeys
{
ControllerInputCoalescer.Receive(ie);
if (!ie.LogicalButton.ToString().Split('+').Any(ActiveController.HasBinding))
{
var handled = false;
if (ie.EventType is InputEventType.Press)
{
handled = triggers.Aggregate(false, (current, trigger) => current | processHotkey(trigger));
}
// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
if (!handled)
{
_hotkeyCoalescer.Receive(ie);
}
}
break;
}
case 2: // Hotkeys override Input
{
var handled = false;
if (ie.EventType is InputEventType.Press)
{
handled = triggers.Aggregate(false, (current, trigger) => current | processHotkey(trigger));
}
// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
if (!handled)
{
_hotkeyCoalescer.Receive(ie);
// Check for hotkeys that may not be handled through processHotkey() method, reject controller input mapped to these
if (!triggers.Exists((t) => isInternalHotkey(t)))
{
ControllerInputCoalescer.Receive(ie);
}
}
break;
}
}
} // foreach event
// also handle axes
// we'll need to isolate the mouse coordinates so we can translate them
int? mouseDeltaX = null, mouseDeltaY = null;
foreach (var f in source.GetAxisValues())
{
if (f.Key == "RMouse X")
mouseDeltaX = f.Value;
else if (f.Key == "RMouse Y")
mouseDeltaY = f.Value;
else ControllerInputCoalescer.AcceptNewAxis(f.Key, f.Value);
}
if (mouseDeltaX != null && mouseDeltaY != null)
{
var mouseSensitivity = config.RelativeMouseSensitivity / 100.0f;
var x = mouseDeltaX.Value * mouseSensitivity;
var y = mouseDeltaY.Value * mouseSensitivity;
const int MAX_REL_MOUSE_RANGE = 120; // arbitrary
x = Math.Min(Math.Max(x, -MAX_REL_MOUSE_RANGE), MAX_REL_MOUSE_RANGE) / MAX_REL_MOUSE_RANGE;
y = Math.Min(Math.Max(y, -MAX_REL_MOUSE_RANGE), MAX_REL_MOUSE_RANGE) / MAX_REL_MOUSE_RANGE;
ControllerInputCoalescer.AcceptNewAxis("RMouse X", (int)(x * 10000));
ControllerInputCoalescer.AcceptNewAxis("RMouse Y", (int)(y * 10000));
}
ClientControls.LatchFromPhysical(_hotkeyCoalescer);
ActiveController.LatchFromPhysical(ControllerInputCoalescer);
ActiveController.OR_FromLogical(ClickyVirtualPadController);
AutoFireController.LatchFromPhysical(ControllerInputCoalescer);
if (config.N64UseCircularAnalogConstraint)
{
ActiveController.ApplyAxisConstraints("Natural Circle");
}
if (ClientControls["Autohold"])
{
ToggleStickies();
}
else if (ClientControls["Autofire"])
{
ToggleAutoStickies();
}
// autohold/autofire must not be affected by the following inputs
ActiveController.Overrides(ButtonOverrideAdapter);
}
}
}

View File

@ -9,7 +9,7 @@ using BizHawk.Common.CollectionExtensions;
namespace BizHawk.Client.EmuHawk
{
public class Input
public class Input : IInputSource
{
/// <summary>
/// If your form needs this kind of input focus, be sure to say so.
@ -30,7 +30,7 @@ namespace BizHawk.Client.EmuHawk
private readonly Thread _updateThread;
public readonly IHostInputAdapter Adapter;
public IHostInputAdapter Adapter { get; }
private Config _currentConfig;

View File

@ -915,34 +915,45 @@ namespace BizHawk.Client.EmuHawk
// ...but prepare haptics first, those get read in ProcessInput
var finalHostController = InputManager.ControllerInputCoalescer;
InputManager.ActiveController.PrepareHapticsForHost(finalHostController);
ProcessInput(
_hotkeyCoalescer,
finalHostController,
InputManager.ClientControls.SearchBindings,
InputManager.ActiveController.HasBinding);
InputManager.ClientControls.LatchFromPhysical(_hotkeyCoalescer);
Input.Instance.Adapter.SetHaptics(finalHostController.GetHapticsSnapshot());
InputManager.ActiveController.LatchFromPhysical(finalHostController);
if (Config.N64UseCircularAnalogConstraint)
InputManager.ProcessInput(Input.Instance, CheckHotkey, Config, (ie) =>
{
InputManager.ActiveController.ApplyAxisConstraints("Natural Circle");
}
// Alt key for menu items.
if (ie.EventType is InputEventType.Press && (ie.LogicalButton.Modifiers & LogicalButton.MASK_ALT) is not 0U)
{
if (ie.LogicalButton.Button.Length == 1)
{
var c = ie.LogicalButton.Button.ToLowerInvariant()[0];
if ((c >= 'a' && c <= 'z') || c == ' ')
{
SendAltKeyChar(c);
}
}
else if (ie.LogicalButton.Button == "Space")
{
SendPlainAltKey(32);
}
}
InputManager.ActiveController.OR_FromLogical(InputManager.ClickyVirtualPadController);
InputManager.AutoFireController.LatchFromPhysical(finalHostController);
// same as right-click
if (ie.ToString() == "Press:Apps" && Config.ShowContextMenu && ContainsFocus)
{
MainFormContextMenu.Show(PointToScreen(new(0, MainformMenu.Height)));
}
}, IsInternalHotkey);
if (InputManager.ClientControls["Autohold"])
// translate mouse coordinates
// NOTE: these must go together, because in the case of screen rotation, X and Y are transformed together
{
InputManager.ToggleStickies();
var p = DisplayManager.UntransformPoint(new Point(
finalHostController.AxisValue("WMouse X"),
finalHostController.AxisValue("WMouse Y")));
var x = p.X / (float)_currentVideoProvider.BufferWidth;
var y = p.Y / (float)_currentVideoProvider.BufferHeight;
finalHostController.AcceptNewAxis("WMouse X", (int)((x * 20000) - 10000));
finalHostController.AcceptNewAxis("WMouse Y", (int)((y * 20000) - 10000));
}
else if (InputManager.ClientControls["Autofire"])
{
InputManager.ToggleAutoStickies();
}
// autohold/autofire must not be affected by the following inputs
InputManager.ActiveController.Overrides(InputManager.ButtonOverrideAdapter);
// emu.yield()'ing scripts
if (Tools.Has<LuaConsole>())
@ -1255,160 +1266,6 @@ namespace BizHawk.Client.EmuHawk
base.OnDeactivate(e);
}
private void ProcessInput(
InputCoalescer hotkeyCoalescer,
ControllerInputCoalescer finalHostController,
Func<string, List<string>> searchHotkeyBindings,
Func<string, bool> activeControllerHasBinding)
{
Input.Instance.Adapter.SetHaptics(finalHostController.GetHapticsSnapshot());
// loop through all available events
InputEvent ie;
while ((ie = Input.Instance.DequeueEvent()) != null)
{
// useful debugging:
// Console.WriteLine(ie);
// TODO - wonder what happens if we pop up something interactive as a response to one of these hotkeys? may need to purge further processing
// look for hotkey bindings for this key
var triggers = searchHotkeyBindings(ie.LogicalButton.ToString());
if (triggers.Count == 0)
{
// Maybe it is a system alt-key which hasn't been overridden
if (ie.EventType is InputEventType.Press && (ie.LogicalButton.Modifiers & LogicalButton.MASK_ALT) is not 0U)
{
if (ie.LogicalButton.Button.Length == 1)
{
var c = ie.LogicalButton.Button.ToLowerInvariant()[0];
if ((c >= 'a' && c <= 'z') || c == ' ')
{
SendAltKeyChar(c);
}
}
else if (ie.LogicalButton.Button == "Space")
{
SendPlainAltKey(32);
}
}
// ordinarily, an alt release with nothing else would move focus to the MenuBar. but that is sort of useless, and hard to implement exactly right.
if (Config.ShowContextMenu && ie.ToString() == "Press:Apps" && ContainsFocus)
{
// same as right-click
MainFormContextMenu.Show(PointToScreen(new(0, MainformMenu.Height)));
}
}
// zero 09-sep-2012 - all input is eligible for controller input. not sure why the above was done.
// maybe because it doesn't make sense to me to bind hotkeys and controller inputs to the same keystrokes
switch (Config.InputHotkeyOverrideOptions)
{
default:
case 0: // Both allowed
{
finalHostController.Receive(ie);
var handled = false;
if (ie.EventType is InputEventType.Press)
{
handled = triggers.Aggregate(handled, (current, trigger) => current | CheckHotkey(trigger));
}
// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
if (!handled)
{
hotkeyCoalescer.Receive(ie);
}
break;
}
case 1: // Input overrides Hotkeys
{
finalHostController.Receive(ie);
// don't check hotkeys when any of the pressed keys are input
if (!ie.LogicalButton.ToString().Split('+').Any(activeControllerHasBinding))
{
var handled = false;
if (ie.EventType is InputEventType.Press)
{
handled = triggers.Aggregate(false, (current, trigger) => current | CheckHotkey(trigger));
}
// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
if (!handled)
{
hotkeyCoalescer.Receive(ie);
}
}
break;
}
case 2: // Hotkeys override Input
{
var handled = false;
if (ie.EventType is InputEventType.Press)
{
handled = triggers.Aggregate(false, (current, trigger) => current | CheckHotkey(trigger));
}
// hotkeys which aren't handled as actions get coalesced as pollable virtual client buttons
if (!handled)
{
hotkeyCoalescer.Receive(ie);
// Check for hotkeys that may not be handled through CheckHotkey() method, reject controller input mapped to these
if (!triggers.Exists(IsInternalHotkey)) finalHostController.Receive(ie);
}
break;
}
}
} // foreach event
// also handle axes
// we'll need to isolate the mouse coordinates so we can translate them
int? mouseX = null, mouseY = null, mouseDeltaX = null, mouseDeltaY = null;
foreach (var f in Input.Instance.GetAxisValues())
{
if (f.Key == "WMouse X")
mouseX = f.Value;
else if (f.Key == "WMouse Y")
mouseY = f.Value;
else if (f.Key == "RMouse X")
mouseDeltaX = f.Value;
else if (f.Key == "RMouse Y")
mouseDeltaY = f.Value;
else finalHostController.AcceptNewAxis(f.Key, f.Value);
}
// if we found mouse coordinates (and why wouldn't we?) then translate them now
// NOTE: these must go together, because in the case of screen rotation, X and Y are transformed together
if (mouseX != null && mouseY != null)
{
var p = DisplayManager.UntransformPoint(new Point(mouseX.Value, mouseY.Value));
var x = p.X / (float)_currentVideoProvider.BufferWidth;
var y = p.Y / (float)_currentVideoProvider.BufferHeight;
finalHostController.AcceptNewAxis("WMouse X", (int) ((x * 20000) - 10000));
finalHostController.AcceptNewAxis("WMouse Y", (int) ((y * 20000) - 10000));
}
if (mouseDeltaX != null && mouseDeltaY != null)
{
var mouseSensitivity = Config.RelativeMouseSensitivity / 100.0f;
var x = mouseDeltaX.Value * mouseSensitivity;
var y = mouseDeltaY.Value * mouseSensitivity;
const int MAX_REL_MOUSE_RANGE = 120; // arbitrary
x = Math.Min(Math.Max(x, -MAX_REL_MOUSE_RANGE), MAX_REL_MOUSE_RANGE) / MAX_REL_MOUSE_RANGE;
y = Math.Min(Math.Max(y, -MAX_REL_MOUSE_RANGE), MAX_REL_MOUSE_RANGE) / MAX_REL_MOUSE_RANGE;
finalHostController.AcceptNewAxis("RMouse X", (int)(x * 10000));
finalHostController.AcceptNewAxis("RMouse Y", (int)(y * 10000));
}
}
public bool RebootCore()
{
if (ToolControllingReboot is { } tool)
@ -1840,8 +1697,6 @@ namespace BizHawk.Client.EmuHawk
// input state which has been destined for game controller inputs are coalesced here
// public static ControllerInputCoalescer ControllerInputCoalescer = new ControllerInputCoalescer();
// input state which has been destined for client hotkey consumption are colesced here
private readonly InputCoalescer _hotkeyCoalescer = new InputCoalescer();
private readonly PresentationPanel _presentationPanel;