diff --git a/src/BizHawk.Client.Common/input/IInputSource.cs b/src/BizHawk.Client.Common/input/IInputSource.cs new file mode 100644 index 0000000000..ebae1a0556 --- /dev/null +++ b/src/BizHawk.Client.Common/input/IInputSource.cs @@ -0,0 +1,13 @@ +#nullable enable + +using System.Collections.Generic; + +namespace BizHawk.Client.Common +{ + public interface IInputSource + { + InputEvent? DequeueEvent(); + + KeyValuePair[] GetAxisValues(); + } +} diff --git a/src/BizHawk.Client.Common/inputAdapters/InputManager.cs b/src/BizHawk.Client.Common/inputAdapters/InputManager.cs index a8466a232b..9f87070190 100644 --- a/src/BizHawk.Client.Common/inputAdapters/InputManager.cs +++ b/src/BizHawk.Client.Common/inputAdapters/InputManager.cs @@ -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(); + + /// 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. + public void ProcessInput(IInputSource source, Func processHotkey, Config config, Action processUnboundInput, Func 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); + } } } diff --git a/src/BizHawk.Client.EmuHawk/Input/Input.cs b/src/BizHawk.Client.EmuHawk/Input/Input.cs index 23c47126f9..d40015378a 100644 --- a/src/BizHawk.Client.EmuHawk/Input/Input.cs +++ b/src/BizHawk.Client.EmuHawk/Input/Input.cs @@ -9,7 +9,7 @@ using BizHawk.Common.CollectionExtensions; namespace BizHawk.Client.EmuHawk { - public class Input + public class Input : IInputSource { /// /// 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; diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 0389598938..1737a691af 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -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()) @@ -1255,160 +1266,6 @@ namespace BizHawk.Client.EmuHawk base.OnDeactivate(e); } - private void ProcessInput( - InputCoalescer hotkeyCoalescer, - ControllerInputCoalescer finalHostController, - Func> searchHotkeyBindings, - Func 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;