From 6b38fe9a4acec5ee7a549cd4353e823089f8f9b1 Mon Sep 17 00:00:00 2001 From: luttje <2738114+luttje@users.noreply.github.com> Date: Sun, 22 Oct 2023 10:23:12 +0200 Subject: [PATCH] Add XInput in preparation for gamepad triggers + add xmldoc (#50) * Add XInput in preparation for gamepad triggers + add xmldoc * add parent-child relation between mapped options * add remove child mappings option + remove debug context menu * x86 > AnyCPU (gives more sensible errors in WinForms Designer) - Fixed Designer failing on MappingForm * Fix polling gamepad * Add gamepad input trigger and config control + Fix mapping list not updating correctly * remove broken and quite useless test * update readme with warning regarding #46 + update readme special thanks * add warning on same gamepad id trigger and action * attempt to give github actions more time before timeout (tests sometimes fail) * use current default mapping profile from app (so we dont have to copy it to tests everytime manually) * Add gamepad button trigger * Add multi-property edit mode * Show physical gamepad connection warning * give even more time for tests so they dont fail * Block arming mappings if simulated gamepad index collides with physical gamepad * Cleanup + add GamePad Trigger Trigger * Fix combined trigger remove not working * Separate stick action and allow custom scaling * improve stick feeling * fix parent picker * update default mappings * add trigger and try work out conflicts between physical and simulated devices (no luck yet) * Fix simulated gamepad recognized as physical * Show gamepad devices in UI * fix mapping form + add more multi-edit type support * make reverse mapping tool more useful * easily setup/update reverse mappings while creating/updating a mapping * update readme screenshots + prefer 'Arm' terminology for enabling profile * support more nullable types in mapping profile * simplify groups * add configurable grouping * prevent wonky sorting across groups * Add easy switch group option * Fix being able to create corrupt profile * fail loading incorrect profile safely --- .../Key2Joy.Contracts.csproj | 4 +- .../Mapping/AbstractMappedOption.cs | 52 +- .../Mapping/AbstractMappingAspect.cs | 298 +- .../Mapping/Actions/AbstractAction.cs | 3 - .../Mapping/Actions/ActionAttribute.cs | 19 +- .../Mapping/Actions/ActionChangedEventArgs.cs | 12 + .../Actions/ActionOptionsChangeListener.cs | 38 +- .../Mapping/Actions/IActionOptionsControl.cs | 56 +- .../Actions/IPluginActionOptionsControl.cs | 29 + .../Mapping/IWndProcHandler.cs | 62 +- .../Mapping/MappingAttribute.cs | 66 +- .../Mapping/Triggers/AbstractTrigger.cs | 7 - .../Plugins/Remoting/RemoteEventSubscriber.cs | 126 +- .../Remoting/RemoteEventSubscriberHost.cs | 281 +- Core/Key2Joy.Contracts/Util/TimingHelper.cs | 18 + Core/Key2Joy.Core/Config/ConfigState.cs | 12 + .../Config/EnumConfigControlAttribute.cs | 12 + .../Config/ViewMappingGroupType.cs | 22 + Core/Key2Joy.Core/IKey2JoyManager.cs | 5 + Core/Key2Joy.Core/Key2Joy.Core.csproj | 8 +- Core/Key2Joy.Core/Key2JoyManager.cs | 83 +- .../LowLevelInput/GamePad/IGamePad.cs | 35 - .../LowLevelInput/GamePad/IGamePadService.cs | 18 - .../LowLevelInput/GamePad/SimulatedGamePad.cs | 48 - .../GamePad/SimulatedGamePadService.cs | 79 - .../Key2Joy.Core/LowLevelInput/GamePadInfo.cs | 51 + .../SimulatedGamePad/ISimulatedGamePad.cs | 71 + .../ISimulatedGamePadService.cs | 56 + .../SimulatedGamePad/SimulatedGamePad.cs | 87 + .../SimulatedGamePadService.cs | 126 + .../LowLevelInput/SimulatedKeyboard.cs | 1669 ++--- Core/Key2Joy.Core/LowLevelInput/Simulator.cs | 128 +- .../LowLevelInput/XInput/BatteryDeviceType.cs | 17 + .../LowLevelInput/XInput/BatteryLevel.cs | 27 + .../LowLevelInput/XInput/BatteryTypes.cs | 32 + .../LowLevelInput/XInput/CapabilityFlags.cs | 34 + .../XInput/CapabilityRequestFlag.cs | 13 + .../XInput/DevicePacketReceivedEventArgs.cs | 28 + .../XInput/DeviceStateChangedEventArgs.cs | 30 + .../LowLevelInput/XInput/DeviceSubType.cs | 90 + .../LowLevelInput/XInput/DeviceType.cs | 12 + .../LowLevelInput/XInput/GamePadButton.cs | 80 + .../LowLevelInput/XInput/IXInput.cs | 60 + .../LowLevelInput/XInput/IXInputService.cs | 82 + .../LowLevelInput/XInput/NativeXInput.cs | 54 + .../XInput/XInputBatteryInformation.cs | 35 + .../XInput/XInputCapabilities.cs | 68 + .../LowLevelInput/XInput/XInputGamepad.cs | 247 + .../LowLevelInput/XInput/XInputKeystroke.cs | 45 + .../LowLevelInput/XInput/XInputResultCode.cs | 40 + .../LowLevelInput/XInput/XInputService.cs | 229 + .../LowLevelInput/XInput/XInputState.cs | 55 + .../LowLevelInput/XInput/XInputVibration.cs | 64 + .../Mapping/Actions/CoreAction.cs | 3 - .../Mapping/Actions/DisabledAction.cs | 15 +- ...amePadAction.cs => GamePadButtonAction.cs} | 90 +- .../Actions/Input/GamePadResetAction.cs | 10 +- .../Actions/Input/GamePadStickAction.cs | 232 +- .../Actions/Input/GamePadTriggerAction.cs | 198 + .../Mapping/Actions/Input/KeyboardAction.cs | 237 +- .../Actions/Input/MouseButtonAction.cs | 186 +- .../Mapping/Actions/Input/MouseMoveAction.cs | 173 +- .../Actions/Scripting/BaseScriptAction.cs | 2 +- Core/Key2Joy.Core/Mapping/AxisDirection.cs | 25 +- .../Mapping/ExactAxisDirection.cs | 51 + Core/Key2Joy.Core/Mapping/IPressState.cs | 19 +- .../Mapping/IProvideReverseAspect.cs | 20 + .../Mapping/JsonMappingAspectConverter.cs | 372 +- Core/Key2Joy.Core/Mapping/MappedOption.cs | 123 +- Core/Key2Joy.Core/Mapping/MappingProfile.cs | 15 +- .../Mapping/Triggers/CoreTriggerListener.cs | 10 +- .../Mapping/Triggers/DisabledTrigger.cs | 14 +- .../Triggers/GamePad/GamePadButtonInputBag.cs | 23 + .../Triggers/GamePad/GamePadButtonTrigger.cs | 72 + .../GamePad/GamePadButtonTriggerListener.cs | 133 + .../Mapping/Triggers/GamePad/GamePadSide.cs | 7 + .../Triggers/GamePad/GamePadStickInputBag.cs | 16 + .../Triggers/GamePad/GamePadStickTrigger.cs | 78 + .../GamePad/GamePadStickTriggerListener.cs | 134 + .../GamePad/GamePadTriggerInputBag.cs | 16 + .../Triggers/GamePad/GamePadTriggerTrigger.cs | 103 + .../GamePad/GamePadTriggerTriggerListener.cs | 133 + .../Triggers/ITriggerOptionsControl.cs | 53 +- .../Triggers/Keyboard/KeyboardTrigger.cs | 154 +- .../Keyboard/KeyboardTriggerListener.cs | 25 +- .../Mapping/Triggers/Logic/CombinedTrigger.cs | 85 +- .../Triggers/Logic/CombinedTriggerListener.cs | 326 +- ...seMoveInputBag.cs => AxisDeltaInputBag.cs} | 18 +- .../Triggers/Mouse/MouseButtonTrigger.cs | 137 +- .../Mouse/MouseButtonTriggerListener.cs | 234 +- .../Triggers/Mouse/MouseMoveTrigger.cs | 94 +- .../Mouse/MouseMoveTriggerListener.cs | 335 +- .../Triggers/PressReleaseTriggerListener.cs | 104 +- .../Mapping/Triggers/TriggersRepository.cs | 168 +- .../MappingArmingFailedException.cs | 10 + Core/Key2Joy.Core/Plugins/ElementHostProxy.cs | 86 +- Core/Key2Joy.Core/Plugins/PluginSet.cs | 4 +- .../Plugins/PluginTriggerProxy.cs | 38 +- Core/Key2Joy.Core/Util/TypeExtensions.cs | 172 + Core/Key2Joy.Core/default-profile.k2j.json | 1878 +++--- .../Key2Joy.PluginHost.csproj | 2 +- Docs/Scripting.md | 21 +- Docs/screenshot-scripting.png | Bin 0 -> 66671 bytes Docs/screenshot.png | Bin 62124 -> 87790 bytes Key2Joy.Cmd/Key2Joy.Cmd.csproj | 2 +- Key2Joy.Gui/ConfigForm.Designer.cs | 162 +- Key2Joy.Gui/ConfigForm.cs | 62 +- Key2Joy.Gui/ConfigForm.resx | 238 +- Key2Joy.Gui/DeviceControl.Designer.cs | 104 + Key2Joy.Gui/DeviceControl.cs | 96 + Key2Joy.Gui/DeviceControl.resx | 120 + Key2Joy.Gui/DeviceListControl.Designer.cs | 103 + Key2Joy.Gui/DeviceListControl.cs | 74 + Key2Joy.Gui/DeviceListControl.resx | 120 + Key2Joy.Gui/Graphics/Icons/cross.png | Bin 0 -> 655 bytes Key2Joy.Gui/InitForm.cs | 4 +- Key2Joy.Gui/Key2Joy.Gui.csproj | 17 +- Key2Joy.Gui/MainForm.Designer.cs | 193 +- Key2Joy.Gui/MainForm.cs | 445 +- Key2Joy.Gui/Mapping/Actions/ActionControl.cs | 297 +- .../Actions/ActionPluginHostControl.cs | 76 +- .../Actions/Input/GamePadActionControl.cs | 25 +- .../Actions/Input/GamePadActionControl.resx | 238 +- .../GamePadStickActionControl.Designer.cs | 348 + .../Input/GamePadStickActionControl.cs | 124 + .../Input/GamePadStickActionControl.resx | 120 + .../GamePadTriggerActionControl.Designer.cs | 249 + .../Input/GamePadTriggerActionControl.cs | 101 + .../Input/GamePadTriggerActionControl.resx | 120 + .../Actions/Input/KeyboardActionControl.cs | 96 +- .../Actions/Logic/AppCommandActionControl.cs | 96 +- .../Logic/SequenceActionControl.Designer.cs | 396 +- .../Actions/Logic/SequenceActionControl.cs | 178 +- .../Actions/Logic/WaitActionControl.cs | 82 +- .../Actions/Scripting/ScriptActionControl.cs | 282 +- .../GamePadButtonTriggerControl.Designer.cs | 161 + .../GamePad/GamePadButtonTriggerControl.cs | 142 + .../GamePad/GamePadButtonTriggerControl.resx | 120 + .../GamePadStickTriggerControl.Designer.cs | 231 + .../GamePad/GamePadStickTriggerControl.cs | 102 + .../GamePad/GamePadStickTriggerControl.resx | 120 + .../GamePadTriggerTriggerControl.Designer.cs | 202 + .../GamePad/GamePadTriggerTriggerControl.cs | 100 + .../GamePad/GamePadTriggerTriggerControl.resx | 120 + .../Keyboard/KeyboardTriggerControl.cs | 19 + .../Triggers/Logic/CombinedTriggerControl.cs | 18 +- .../Logic/CombinedTriggerControlItem.cs | 65 +- .../Mouse/MouseButtonTriggerControl.cs | 202 +- .../Triggers/Mouse/MouseMoveTriggerControl.cs | 95 +- .../Mapping/Triggers/TriggerControl.cs | 308 +- Key2Joy.Gui/MappingContextMenuBuilder.cs | 271 + Key2Joy.Gui/MappingForm.Designer.cs | 387 +- Key2Joy.Gui/MappingForm.cs | 102 +- Key2Joy.Gui/MappingForm.resx | 238 +- Key2Joy.Gui/MappingGroupItemComparer.cs | 125 +- .../MappingPropertyEditorForm.Designer.cs | 103 + Key2Joy.Gui/MappingPropertyEditorForm.cs | 184 + Key2Joy.Gui/MappingPropertyEditorForm.resx | 120 + .../NotificationBannerControl.Designer.cs | 76 + Key2Joy.Gui/NotificationBannerControl.cs | 116 + Key2Joy.Gui/NotificationBannerControl.resx | 120 + Key2Joy.Gui/Program.cs | 152 +- Key2Joy.Gui/Properties/Resources.Designer.cs | 676 +- Key2Joy.Gui/Properties/Resources.resx | 405 +- .../Key2Joy.Plugin.Ffmpeg.csproj | 2 +- .../Key2Joy.Plugin.HelloWorld.csproj | 2 +- .../GetHelloWorldActionControl.xaml.cs | 86 +- .../Key2Joy.Plugin.Midi.csproj | 2 +- README.md | 38 +- .../BuildMarkdownDocs.csproj | 2 +- Support/Key2Joy.Setup/Key2Joy.Setup.csproj | 2 +- .../Key2Joy.Tests.Stubs.TestPlugin.csproj | 2 +- .../Util/FileServiceTests.cs | 1 - .../BuildMarkdownDocs/sample.xml | 5739 ++++++++++------- .../Mappings/Actions/ActionOptionsControl.cs | 58 + .../Core/Config/MockConfigManager.cs | 45 +- .../Core/Config/Stubs/current-config.json | 1 + .../Stubs/current-default-profile.k2j.json | 947 --- .../Core/Interop/Commands/CommandInfoTests.cs | 2 - .../Core/Interop/InteropClientTests.cs | 3 +- .../Core/Interop/InteropServerTests.cs | 2 +- .../Key2Joy.Tests/Core/Key2JoyManagerTests.cs | 2 - .../SimulatedGamePadTests.cs} | 21 +- .../LowLevelInput/XInput/XInputGamePad.cs | 66 + .../XInput/XInputServiceTests.cs | 96 + .../Core/Mapping/MappedOptionTests.cs | 23 +- .../Core/Mapping/MappingProfileLegacyTests.cs | 2 +- .../Core/Plugins/PluginSetTests.cs | 2 - .../Util/DependencyServiceLocatorTests.cs | 48 +- Support/Key2Joy.Tests/Key2Joy.Tests.csproj | 5 +- .../Key2Joy.Tests/Testing/TestUtilities.cs | 4 +- 191 files changed, 18460 insertions(+), 9768 deletions(-) create mode 100644 Core/Key2Joy.Contracts/Mapping/Actions/ActionChangedEventArgs.cs create mode 100644 Core/Key2Joy.Contracts/Mapping/Actions/IPluginActionOptionsControl.cs create mode 100644 Core/Key2Joy.Contracts/Util/TimingHelper.cs create mode 100644 Core/Key2Joy.Core/Config/EnumConfigControlAttribute.cs create mode 100644 Core/Key2Joy.Core/Config/ViewMappingGroupType.cs delete mode 100644 Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePad.cs delete mode 100644 Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePadService.cs delete mode 100644 Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePad.cs delete mode 100644 Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePadService.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/GamePadInfo.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePad.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePadService.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePad.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadService.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/BatteryDeviceType.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/BatteryLevel.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/BatteryTypes.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityFlags.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityRequestFlag.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/DevicePacketReceivedEventArgs.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/DeviceStateChangedEventArgs.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/DeviceSubType.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/DeviceType.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/GamePadButton.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/IXInput.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/IXInputService.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/NativeXInput.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputBatteryInformation.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputCapabilities.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputGamepad.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputKeystroke.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputResultCode.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputService.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputState.cs create mode 100644 Core/Key2Joy.Core/LowLevelInput/XInput/XInputVibration.cs rename Core/Key2Joy.Core/Mapping/Actions/Input/{GamePadAction.cs => GamePadButtonAction.cs} (66%) create mode 100644 Core/Key2Joy.Core/Mapping/Actions/Input/GamePadTriggerAction.cs create mode 100644 Core/Key2Joy.Core/Mapping/ExactAxisDirection.cs create mode 100644 Core/Key2Joy.Core/Mapping/IProvideReverseAspect.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonInputBag.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTrigger.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTriggerListener.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadSide.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickInputBag.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTrigger.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTriggerListener.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerInputBag.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTrigger.cs create mode 100644 Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTriggerListener.cs rename Core/Key2Joy.Core/Mapping/Triggers/Mouse/{MouseMoveInputBag.cs => AxisDeltaInputBag.cs} (54%) create mode 100644 Core/Key2Joy.Core/MappingArmingFailedException.cs create mode 100644 Docs/screenshot-scripting.png create mode 100644 Key2Joy.Gui/DeviceControl.Designer.cs create mode 100644 Key2Joy.Gui/DeviceControl.cs create mode 100644 Key2Joy.Gui/DeviceControl.resx create mode 100644 Key2Joy.Gui/DeviceListControl.Designer.cs create mode 100644 Key2Joy.Gui/DeviceListControl.cs create mode 100644 Key2Joy.Gui/DeviceListControl.resx create mode 100644 Key2Joy.Gui/Graphics/Icons/cross.png create mode 100644 Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.Designer.cs create mode 100644 Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.cs create mode 100644 Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.resx create mode 100644 Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.Designer.cs create mode 100644 Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.cs create mode 100644 Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.resx create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.Designer.cs create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.cs create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.resx create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.Designer.cs create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.cs create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.resx create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.Designer.cs create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.cs create mode 100644 Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.resx create mode 100644 Key2Joy.Gui/MappingContextMenuBuilder.cs create mode 100644 Key2Joy.Gui/MappingPropertyEditorForm.Designer.cs create mode 100644 Key2Joy.Gui/MappingPropertyEditorForm.cs create mode 100644 Key2Joy.Gui/MappingPropertyEditorForm.resx create mode 100644 Key2Joy.Gui/NotificationBannerControl.Designer.cs create mode 100644 Key2Joy.Gui/NotificationBannerControl.cs create mode 100644 Key2Joy.Gui/NotificationBannerControl.resx create mode 100644 Support/Key2Joy.Tests/Contracts/Mappings/Actions/ActionOptionsControl.cs delete mode 100644 Support/Key2Joy.Tests/Core/Config/Stubs/current-default-profile.k2j.json rename Support/Key2Joy.Tests/Core/LowLevelInput/{GamePad/GamePadTests.cs => SimulatedGamePad/SimulatedGamePadTests.cs} (76%) create mode 100644 Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputGamePad.cs create mode 100644 Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputServiceTests.cs diff --git a/Core/Key2Joy.Contracts/Key2Joy.Contracts.csproj b/Core/Key2Joy.Contracts/Key2Joy.Contracts.csproj index ff861986..591250a3 100644 --- a/Core/Key2Joy.Contracts/Key2Joy.Contracts.csproj +++ b/Core/Key2Joy.Contracts/Key2Joy.Contracts.csproj @@ -1,4 +1,4 @@ - + luttje @@ -14,7 +14,7 @@ NET48 latest Key2Joy.Contracts - x86 + AnyCPU False bin\$(MSBuildProjectName) diff --git a/Core/Key2Joy.Contracts/Mapping/AbstractMappedOption.cs b/Core/Key2Joy.Contracts/Mapping/AbstractMappedOption.cs index de3fac91..708327e1 100644 --- a/Core/Key2Joy.Contracts/Mapping/AbstractMappedOption.cs +++ b/Core/Key2Joy.Contracts/Mapping/AbstractMappedOption.cs @@ -1,16 +1,36 @@ -using System; -using System.Text.Json.Serialization; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Contracts.Mapping; - -public abstract class AbstractMappedOption : ICloneable -{ - [JsonInclude] - public AbstractAction Action { get; set; } - [JsonInclude] - public AbstractTrigger Trigger { get; set; } - - public abstract object Clone(); -} +using System; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Contracts.Mapping; + +public abstract class AbstractMappedOption : ICloneable +{ + /// + /// The unique identifier for this mapping. + /// This way it can be referenced by other mappings. + /// + [JsonInclude] + public Guid Guid { get; protected set; } + + /// + /// The action that is executed when this mapping is executed. + /// + [JsonInclude] + public AbstractAction Action { get; set; } + + /// + /// The trigger that causes this mapping to be executed. + /// + [JsonInclude] + public AbstractTrigger Trigger { get; set; } + + /// + /// The unique identifier of the parent mapped option. + /// + [JsonInclude] + public Guid? ParentGuid { get; set; } = null; + + public abstract object Clone(); +} diff --git a/Core/Key2Joy.Contracts/Mapping/AbstractMappingAspect.cs b/Core/Key2Joy.Contracts/Mapping/AbstractMappingAspect.cs index 5ac22e22..5e7dbaff 100644 --- a/Core/Key2Joy.Contracts/Mapping/AbstractMappingAspect.cs +++ b/Core/Key2Joy.Contracts/Mapping/AbstractMappingAspect.cs @@ -1,142 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json.Serialization; - -namespace Key2Joy.Contracts.Mapping; - -public abstract class AbstractMappingAspect : MarshalByRefObject, ICloneable, IComparable -{ - public string Name { get; set; } - - public AbstractMappingAspect(string name) => this.Name = name; - - private PropertyInfo[] GetProperties() - { - var type = this.GetType(); - var properties = type.GetProperties(); - - return properties.Where(p => p.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length == 0).ToArray(); - } - - public virtual MappingAspectOptions SaveOptions() - { - MappingAspectOptions options = new(); - var properties = this.GetProperties(); - - foreach (var property in properties) - { - this.SaveOptionsGetProperty(options, property); - } - - return options; - } - - /// - /// Can be overridden by child actions or triggers to allow saving more complex types. - /// - /// - /// - protected virtual void SaveOptionsGetProperty(MappingAspectOptions options, PropertyInfo property) - { - var value = property.GetValue(this); - - switch (value) - { - case DateTime dateTime: - value = dateTime.Ticks; - break; - - default: - break; - } - - options.Add(property.Name, value); - } - - public virtual void LoadOptions(MappingAspectOptions options) - { - var properties = this.GetProperties(); - - foreach (var property in properties) - { - if (!options.ContainsKey(property.Name)) - { - continue; - } - - this.LoadOptionSetProperty(options, property); - } - } - - /// - /// Can be overridden by child actions or triggers to allow loading more complex types. - /// - /// - /// - protected virtual void LoadOptionSetProperty(MappingAspectOptions options, PropertyInfo property) - { - var propertyType = property.PropertyType; - var value = options[property.Name]; - var genericTypeDefinition = propertyType.IsGenericType ? propertyType.GetGenericTypeDefinition() : null; - - if (propertyType.IsEnum) - { - value = Enum.Parse(propertyType, (string)value); - } - else if (propertyType == typeof(DateTime)) - { - value = new DateTime(Convert.ToInt64(value)); - } - else if (propertyType.IsGenericType - && (genericTypeDefinition == typeof(List<>) || genericTypeDefinition == typeof(IList<>))) - { - var constructedListType = typeof(List<>).MakeGenericType(propertyType.GetGenericArguments()); - var instance = Activator.CreateInstance(constructedListType); - - if (value is List list) - { - var addMethod = constructedListType.GetMethod("Add"); - - foreach (var item in list) - { - addMethod.Invoke(instance, new object[] { item }); - } - - value = instance; - } - else - { - throw new ArgumentException($"Expected value to be of type List<> to parse. But was: {value.GetType()}"); - } - } - else - { - value = Convert.ChangeType(value, propertyType); - } - - property.SetValue(this, value); - } - - public virtual int CompareTo(AbstractMappingAspect other) => this.ToString().CompareTo(other.ToString()); - - public static bool operator ==(AbstractMappingAspect a, AbstractMappingAspect b) - { - if (ReferenceEquals(a, b)) - { - return true; - } - - if (a is null || b is null) - { - return false; - } - - return a.Equals(b); - } - - public static bool operator !=(AbstractMappingAspect a, AbstractMappingAspect b) => !(a == b); - - public virtual object Clone() => this.MemberwiseClone(); -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; + +namespace Key2Joy.Contracts.Mapping; + +public abstract class AbstractMappingAspect : MarshalByRefObject, ICloneable, IComparable +{ + public string Name { get; set; } + + public AbstractMappingAspect(string name) => this.Name = name; + + public virtual string GetNameDisplay() => this.Name; + + public override string ToString() => this.GetNameDisplay(); + + private PropertyInfo[] GetProperties() + { + var type = this.GetType(); + var properties = type.GetProperties(); + + return properties.Where(p => p.GetCustomAttributes(typeof(JsonIgnoreAttribute), true).Length == 0).ToArray(); + } + + public virtual MappingAspectOptions SaveOptions() + { + MappingAspectOptions options = new(); + var properties = this.GetProperties(); + + foreach (var property in properties) + { + this.SaveOptionsGetProperty(options, property); + } + + return options; + } + + /// + /// Can be overridden by child actions or triggers to allow saving more complex types. + /// + /// + /// + protected virtual void SaveOptionsGetProperty(MappingAspectOptions options, PropertyInfo property) + { + var value = property.GetValue(this); + + switch (value) + { + case DateTime dateTime: + value = dateTime.Ticks; + break; + + default: + break; + } + + options.Add(property.Name, value); + } + + public virtual void LoadOptions(MappingAspectOptions options) + { + var properties = this.GetProperties(); + + foreach (var property in properties) + { + if (!options.ContainsKey(property.Name)) + { + continue; + } + + this.LoadOptionSetProperty(options, property); + } + } + + /// + /// Can be overridden by child actions or triggers to allow loading more complex types. + /// + /// + /// + protected virtual void LoadOptionSetProperty(MappingAspectOptions options, PropertyInfo property) + { + var propertyType = property.PropertyType; + var value = options[property.Name]; + var genericTypeDefinition = propertyType.IsGenericType ? propertyType.GetGenericTypeDefinition() : null; + + propertyType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + if (propertyType.IsEnum) + { + value = Enum.Parse(propertyType, (string)value); + } + else if (propertyType == typeof(DateTime)) + { + value = new DateTime(Convert.ToInt64(value)); + } + else if (propertyType == typeof(TimeSpan)) + { + value = TimeSpan.Parse((string)value); + } + else if (propertyType == typeof(short)) + { + value = Convert.ToInt16(value); + } + else if (propertyType.IsGenericType + && (genericTypeDefinition == typeof(List<>) || genericTypeDefinition == typeof(IList<>))) + { + var constructedListType = typeof(List<>).MakeGenericType(propertyType.GetGenericArguments()); + var instance = Activator.CreateInstance(constructedListType); + + if (value is List list) + { + var addMethod = constructedListType.GetMethod("Add"); + + foreach (var item in list) + { + addMethod.Invoke(instance, new object[] { item }); + } + + value = instance; + } + else + { + throw new ArgumentException($"Expected value to be of type List<> to parse. But was: {value.GetType()}"); + } + } + else if (value != null) + { + value = Convert.ChangeType(value, propertyType); + } + + property.SetValue(this, value); + } + + public virtual int CompareTo(AbstractMappingAspect other) => this.ToString().CompareTo(other.ToString()); + + public static bool operator ==(AbstractMappingAspect a, AbstractMappingAspect b) + { + if (ReferenceEquals(a, b)) + { + return true; + } + + if (a is null || b is null) + { + return false; + } + + return a.Equals(b); + } + + public static bool operator !=(AbstractMappingAspect a, AbstractMappingAspect b) => !(a == b); + + public virtual object Clone() => this.MemberwiseClone(); +} diff --git a/Core/Key2Joy.Contracts/Mapping/Actions/AbstractAction.cs b/Core/Key2Joy.Contracts/Mapping/Actions/AbstractAction.cs index 6b6980ee..b6f619b5 100644 --- a/Core/Key2Joy.Contracts/Mapping/Actions/AbstractAction.cs +++ b/Core/Key2Joy.Contracts/Mapping/Actions/AbstractAction.cs @@ -2,7 +2,6 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.Contracts.Util; namespace Key2Joy.Contracts.Mapping.Actions; @@ -25,8 +24,6 @@ public abstract class AbstractAction : AbstractMappingAspect /// public virtual Task Execute(AbstractInputBag inputBag = null) => throw new System.NotImplementedException(); - public virtual string GetNameDisplay() => this.Name; - public AbstractAction(string name) : base(name) { } diff --git a/Core/Key2Joy.Contracts/Mapping/Actions/ActionAttribute.cs b/Core/Key2Joy.Contracts/Mapping/Actions/ActionAttribute.cs index 299793f6..44049926 100644 --- a/Core/Key2Joy.Contracts/Mapping/Actions/ActionAttribute.cs +++ b/Core/Key2Joy.Contracts/Mapping/Actions/ActionAttribute.cs @@ -1,14 +1,5 @@ -namespace Key2Joy.Contracts.Mapping.Actions; - -public class ActionAttribute : MappingAttribute -{ - /// - /// Group the actions should be categorized under. - /// - public string GroupName { get; set; } - - /// - /// Image for the group the actions should be categorized under. - /// - public string GroupImage { get; set; } -} +namespace Key2Joy.Contracts.Mapping.Actions; + +public class ActionAttribute : MappingAttribute +{ +} diff --git a/Core/Key2Joy.Contracts/Mapping/Actions/ActionChangedEventArgs.cs b/Core/Key2Joy.Contracts/Mapping/Actions/ActionChangedEventArgs.cs new file mode 100644 index 00000000..7e2a3b4a --- /dev/null +++ b/Core/Key2Joy.Contracts/Mapping/Actions/ActionChangedEventArgs.cs @@ -0,0 +1,12 @@ +using System; + +namespace Key2Joy.Contracts.Mapping.Actions; + +public class ActionChangedEventArgs : EventArgs +{ + public static new readonly ActionChangedEventArgs Empty = new(); + + public AbstractAction Action { get; private set; } + + public ActionChangedEventArgs(AbstractAction action = null) => this.Action = action; +} diff --git a/Core/Key2Joy.Contracts/Mapping/Actions/ActionOptionsChangeListener.cs b/Core/Key2Joy.Contracts/Mapping/Actions/ActionOptionsChangeListener.cs index f6c94a82..9934cd7c 100644 --- a/Core/Key2Joy.Contracts/Mapping/Actions/ActionOptionsChangeListener.cs +++ b/Core/Key2Joy.Contracts/Mapping/Actions/ActionOptionsChangeListener.cs @@ -1,19 +1,19 @@ -using System; - -namespace Key2Joy.Contracts.Mapping.Actions; - -/// -/// This listener is used as a proxy, so we can listen to the event across the AppDomain boundary. -/// It works because both AppDomains need to know the class in which the event is defined. -/// -/// Source: https://stackoverflow.com/a/5871944 -/// -public class ActionOptionsChangeListener : MarshalByRefObject -{ - /// - /// Called when the options on an action change - /// - public event EventHandler OptionsChanged; - - public ActionOptionsChangeListener(IActionOptionsControl optionsControl) => optionsControl.OptionsChanged += new EventHandler((sender, e) => OptionsChanged?.Invoke(this, EventArgs.Empty)); -} +using System; + +namespace Key2Joy.Contracts.Mapping.Actions; + +/// +/// This listener is used as a proxy, so we can listen to the event across the AppDomain boundary. +/// It works because both AppDomains need to know the class in which the event is defined. +/// +/// Source: https://stackoverflow.com/a/5871944 +/// +public class ActionOptionsChangeListener : MarshalByRefObject +{ + /// + /// Called when the options on an action change + /// + public event EventHandler OptionsChanged; + + public ActionOptionsChangeListener(IActionOptionsControl optionsControl) => optionsControl.OptionsChanged += new EventHandler((sender, e) => OptionsChanged?.Invoke(this, EventArgs.Empty)); +} diff --git a/Core/Key2Joy.Contracts/Mapping/Actions/IActionOptionsControl.cs b/Core/Key2Joy.Contracts/Mapping/Actions/IActionOptionsControl.cs index e92d5f78..1c6c05a0 100644 --- a/Core/Key2Joy.Contracts/Mapping/Actions/IActionOptionsControl.cs +++ b/Core/Key2Joy.Contracts/Mapping/Actions/IActionOptionsControl.cs @@ -1,28 +1,28 @@ -using System; - -namespace Key2Joy.Contracts.Mapping.Actions; - -public interface IActionOptionsControl -{ - /// - /// Called to setup the options panel with a action - /// - /// - void Select(object action); - - /// - /// Called when the options panel should modify a resulting action - /// - /// - void Setup(object action); - - /// - /// Called when the mapping is saving and can still be stopped - /// - bool CanMappingSave(object action); - - /// - /// Called when the options on an action change - /// - event EventHandler OptionsChanged; -} +using System; + +namespace Key2Joy.Contracts.Mapping.Actions; + +public interface IActionOptionsControl +{ + /// + /// Called to setup the options panel with a action + /// + /// + void Select(AbstractAction action); + + /// + /// Called when the options panel should modify a resulting action + /// + /// + void Setup(AbstractAction action); + + /// + /// Called when the mapping is saving and can still be stopped + /// + bool CanMappingSave(AbstractAction action); + + /// + /// Called when the options on an action change + /// + event EventHandler OptionsChanged; +} diff --git a/Core/Key2Joy.Contracts/Mapping/Actions/IPluginActionOptionsControl.cs b/Core/Key2Joy.Contracts/Mapping/Actions/IPluginActionOptionsControl.cs new file mode 100644 index 00000000..c1df9f79 --- /dev/null +++ b/Core/Key2Joy.Contracts/Mapping/Actions/IPluginActionOptionsControl.cs @@ -0,0 +1,29 @@ +using System; +using Key2Joy.Contracts.Plugins; + +namespace Key2Joy.Contracts.Mapping.Actions; + +public interface IPluginActionOptionsControl +{ + /// + /// Called to setup the options panel with a action + /// + /// + void Select(PluginAction action); + + /// + /// Called when the options panel should modify a resulting action + /// + /// + void Setup(PluginAction action); + + /// + /// Called when the mapping is saving and can still be stopped + /// + bool CanMappingSave(PluginAction action); + + /// + /// Called when the options on an action change + /// + event EventHandler OptionsChanged; +} diff --git a/Core/Key2Joy.Contracts/Mapping/IWndProcHandler.cs b/Core/Key2Joy.Contracts/Mapping/IWndProcHandler.cs index 800cc8c5..ecd8dc09 100644 --- a/Core/Key2Joy.Contracts/Mapping/IWndProcHandler.cs +++ b/Core/Key2Joy.Contracts/Mapping/IWndProcHandler.cs @@ -1,29 +1,33 @@ -using System; - -namespace Key2Joy.Contracts.Mapping; - -public struct Message -{ - public IntPtr HWnd { get; private set; } - - public int Msg { get; private set; } - - public IntPtr WParam { get; private set; } - - public IntPtr LParam { get; private set; } - - public Message(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam) - { - this.HWnd = hWnd; - this.Msg = msg; - this.WParam = wParam; - this.LParam = lParam; - } -} - -public interface IWndProcHandler -{ - public IntPtr Handle { get; set; } - - public void WndProc(Message message); -} +using System; + +namespace Key2Joy.Contracts.Mapping; + +public struct Message +{ + public IntPtr HWnd { get; private set; } + + public int Msg { get; private set; } + + public IntPtr WParam { get; private set; } + + public IntPtr LParam { get; private set; } + + public Message(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam) + { + this.HWnd = hWnd; + this.Msg = msg; + this.WParam = wParam; + this.LParam = lParam; + } +} + +public interface IWndProcHandler +{ + public IntPtr Handle { get; set; } + + /// + /// Called when a message is sent to the window. + /// + /// + public void WndProc(Message message); +} diff --git a/Core/Key2Joy.Contracts/Mapping/MappingAttribute.cs b/Core/Key2Joy.Contracts/Mapping/MappingAttribute.cs index 43ac2ddc..5c4a0670 100644 --- a/Core/Key2Joy.Contracts/Mapping/MappingAttribute.cs +++ b/Core/Key2Joy.Contracts/Mapping/MappingAttribute.cs @@ -1,28 +1,38 @@ -using System; - -namespace Key2Joy.Contracts.Mapping; - -[AttributeUsage(AttributeTargets.Class)] -public abstract class MappingAttribute : Attribute, IComparable -{ - /// - /// Customizable name format for the action/trigger - /// - public string NameFormat { get; set; } - - /// - /// Description for the action/trigger - /// - public string Description { get; set; } - - /// - /// When this action should be visibile in menu's. - /// - public MappingMenuVisibility Visibility { get; set; } = MappingMenuVisibility.Always; - - public override string ToString() => this.Description; - - public override int GetHashCode() => this.Description.GetHashCode(); - - public int CompareTo(MappingAttribute other) => this.Description.CompareTo(other.Description); -} +using System; + +namespace Key2Joy.Contracts.Mapping; + +[AttributeUsage(AttributeTargets.Class)] +public abstract class MappingAttribute : Attribute, IComparable +{ + /// + /// Customizable name format for the action/trigger + /// + public string NameFormat { get; set; } + + /// + /// Description for the action/trigger + /// + public string Description { get; set; } + + /// + /// When this action should be visibile in menu's. + /// + public MappingMenuVisibility Visibility { get; set; } = MappingMenuVisibility.Always; + + /// + /// Group the aspect should be categorized under. + /// + public string GroupName { get; set; } + + /// + /// Image for the group the aspect should be categorized under. + /// + public string GroupImage { get; set; } + + public override string ToString() => this.Description; + + public override int GetHashCode() => this.Description.GetHashCode(); + + public int CompareTo(MappingAttribute other) => this.Description.CompareTo(other.Description); +} diff --git a/Core/Key2Joy.Contracts/Mapping/Triggers/AbstractTrigger.cs b/Core/Key2Joy.Contracts/Mapping/Triggers/AbstractTrigger.cs index 4d1ff78f..3af224a7 100644 --- a/Core/Key2Joy.Contracts/Mapping/Triggers/AbstractTrigger.cs +++ b/Core/Key2Joy.Contracts/Mapping/Triggers/AbstractTrigger.cs @@ -27,13 +27,6 @@ public AbstractTrigger(string name) /// Singleton trigger listener public abstract AbstractTriggerListener GetTriggerListener(); - /// - /// Must return an input value unique in the profile. Like a Keys combination or an AxisDirection. - /// Will be used to quickly lookup input triggers and their corresponding action - /// - /// - public abstract string GetUniqueKey(); - /// /// Through this the trigger can decide if it wants to execute. By default it calls the /// event and executes if it's not handled. diff --git a/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriber.cs b/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriber.cs index fff343ec..6749bd17 100644 --- a/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriber.cs +++ b/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriber.cs @@ -1,63 +1,63 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace Key2Joy.Contracts.Plugins.Remoting; - -public class RemoteEventSubscriber -{ - internal static readonly TimeSpan MaxHeartbeatInterval = TimeSpan.FromSeconds(10); - - public const string SignalReady = "READY"; - public const string SignalHeartbeat = "HEARTBEAT"; - public const string SignalExit = "EXIT"; - - private static readonly Dictionary Subscriptions = new(); - - public static RemoteEventSubscriberClient ClientInstance { get; private set; } - - /// - /// Exposes a named pipe endpoint corresponding to the unique name for this plugin host - /// - public static RemoteEventSubscriberHost InitHostForPlugin(string portName) => new RemoteEventSubscriberHost(portName); - - public static void InitClient(string portName) - { - ClientInstance = new RemoteEventSubscriberClient(portName); - - try - { - ClientInstance.SendToHost(SignalReady); - } - catch (IOException ex) - { - Output.WriteLine(ex.ToString()); - Console.WriteLine(ex); - } - } - - public static SubscriptionRegistration SubscribeEvent(string eventName, RemoteEventHandlerCallback handler) - { - var subscriptionId = Guid.NewGuid().ToString(); - SubscriptionRegistration subscription = new(eventName, subscriptionId, handler); - - Subscriptions.Add(subscriptionId, subscription); - - return subscription; - } - - public static void HandleInvoke(string subscriptionId) - { - // Find subscription and call related event - if (!TryGetSubscription(subscriptionId, out var fullSubscriptionInfo)) - { - throw new ApplicationException($"Could not find event subscription with id {subscriptionId}!"); - } - - fullSubscriptionInfo.EventHandler?.Invoke(fullSubscriptionInfo.CustomSender, new RemoteEventArgs(fullSubscriptionInfo.Ticket)); - } - - public static void UnsubscribeEvent(string subscriptionId) => Subscriptions.Remove(subscriptionId); - - public static bool TryGetSubscription(string subscriptionId, out SubscriptionRegistration subscription) => Subscriptions.TryGetValue(subscriptionId, out subscription); -} +using System; +using System.Collections.Generic; +using System.IO; + +namespace Key2Joy.Contracts.Plugins.Remoting; + +public class RemoteEventSubscriber +{ + internal static readonly TimeSpan MaxHeartbeatInterval = TimeSpan.FromSeconds(10); + + public const string SignalReady = "READY"; + public const string SignalHeartbeat = "HEARTBEAT"; + public const string SignalExit = "EXIT"; + + private static readonly Dictionary Subscriptions = new(); + + public static RemoteEventSubscriberClient ClientInstance { get; private set; } + + /// + /// Exposes a named pipe endpoint corresponding to the unique name for this plugin host + /// + public static RemoteEventSubscriberHost InitHostForPlugin(string portName) => new RemoteEventSubscriberHost(portName); + + public static void InitClient(string portName) + { + ClientInstance = new RemoteEventSubscriberClient(portName); + + try + { + ClientInstance.SendToHost(SignalReady); + } + catch (IOException ex) + { + Output.WriteLine(ex.ToString()); + Console.WriteLine(ex); + } + } + + public static SubscriptionRegistration SubscribeEvent(string eventName, RemoteEventHandlerCallback handler) + { + var subscriptionId = Guid.NewGuid().ToString(); + SubscriptionRegistration subscription = new(eventName, subscriptionId, handler); + + Subscriptions.Add(subscriptionId, subscription); + + return subscription; + } + + public static void HandleInvoke(string subscriptionId) + { + // Find subscription and call related event + if (!TryGetSubscription(subscriptionId, out var fullSubscriptionInfo)) + { + throw new ApplicationException($"Could not find event subscription with id {subscriptionId}!"); + } + + fullSubscriptionInfo.EventHandler?.Invoke(fullSubscriptionInfo.CustomSender, new RemoteEventArgs(fullSubscriptionInfo.Ticket)); + } + + public static void UnsubscribeEvent(string subscriptionId) => Subscriptions.Remove(subscriptionId); + + public static bool TryGetSubscription(string subscriptionId, out SubscriptionRegistration subscription) => Subscriptions.TryGetValue(subscriptionId, out subscription); +} diff --git a/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriberHost.cs b/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriberHost.cs index cff5214e..5498282a 100644 --- a/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriberHost.cs +++ b/Core/Key2Joy.Contracts/Plugins/Remoting/RemoteEventSubscriberHost.cs @@ -1,140 +1,141 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.IO.Pipes; -using System.Threading.Tasks; -using System.Threading; - -namespace Key2Joy.Contracts.Plugins.Remoting; - -public class RemoteEventSubscriberHost : IDisposable -{ - public event EventHandler Disposing; - - private readonly NamedPipeServerStream pipeStream; - private bool isPipeServerStreamReady = false; - - private readonly CancellationTokenSource pipeCancellation; - private static readonly TimeSpan HeartbeatWithMargin = RemoteEventSubscriber.MaxHeartbeatInterval - TimeSpan.FromSeconds(2); - private static readonly TimeSpan MaxWaitForReady = TimeSpan.FromSeconds(5); - - internal RemoteEventSubscriberHost(string portName) - { - this.pipeStream = new NamedPipeServerStream( - RemotePipe.GetAbsolutePipeName(portName), - PipeDirection.InOut, - 1, - PipeTransmissionMode.Message, - PipeOptions.Asynchronous); - - this.pipeCancellation = new CancellationTokenSource(); - - this.InitBackgroundEventThread(); - this.InitBackgroundHeartbeatThread(); - } - - /// - /// Listens in the background for any incoming messages from the client. - /// - private async void InitBackgroundEventThread() - { - var backgroundThread = Task.Run(() => - { - this.pipeStream.WaitForConnection(); - - StreamReader reader = new(this.pipeStream); - - while (!this.pipeCancellation.IsCancellationRequested) - { - try - { - var messageOrSubscriptionId = RemotePipe.ReadMessage(this.pipeStream); - - if (string.IsNullOrEmpty(messageOrSubscriptionId)) - { - continue; - } - - if (messageOrSubscriptionId == RemoteEventSubscriber.SignalReady) - { - this.isPipeServerStreamReady = true; - continue; - } - else if (messageOrSubscriptionId == RemoteEventSubscriber.SignalExit) - { - this.Dispose(); - return; - } - - RemoteEventSubscriber.HandleInvoke(messageOrSubscriptionId); - } - catch (Exception ex) - { - Output.WriteLine(ex); - Debug.WriteLine($"-------------> Exception: {ex.Message}"); - return; - } - } - }); - - await backgroundThread; - } - - /// - /// Sends heartbeats in the background at the commonly agreed interval. - /// - private async void InitBackgroundHeartbeatThread() - { - var backgroundThread = Task.Run(async () => - { - while (!this.pipeCancellation.IsCancellationRequested) - { - await Task.Delay(HeartbeatWithMargin); - if (!this.isPipeServerStreamReady) - { - continue; - } - - try - { - RemotePipe.WriteMessage(this.pipeStream, RemoteEventSubscriber.SignalHeartbeat); - } - catch (Exception ex) - { - Output.WriteLine(ex); - Debug.WriteLine($"-------------> Exception: {ex.Message}"); - return; - } - } - }); - - await backgroundThread; - } - - /// - /// Stops execution until the event pipe has received the ready signal - /// - public void WaitForEventPipeReady() - { - var waitStart = DateTime.Now; - - while (!this.isPipeServerStreamReady) - { - Task.Delay(10); - - if ((DateTime.Now - waitStart) > MaxWaitForReady) - { - throw new TimeoutException($"WaitForEventPipeReady timed out after {MaxWaitForReady.TotalMilliseconds}ms"); - } - } - } - - public void Dispose() - { - this.Disposing?.Invoke(this, EventArgs.Empty); - - this.pipeStream?.Dispose(); - - this.pipeCancellation?.Cancel(); - } -} +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Threading.Tasks; +using System.Threading; +using Key2Joy.Contracts.Util; + +namespace Key2Joy.Contracts.Plugins.Remoting; + +public class RemoteEventSubscriberHost : IDisposable +{ + public event EventHandler Disposing; + + private readonly NamedPipeServerStream pipeStream; + private bool isPipeServerStreamReady = false; + + private readonly CancellationTokenSource pipeCancellation; + private static readonly TimeSpan HeartbeatWithMargin = RemoteEventSubscriber.MaxHeartbeatInterval - TimeSpan.FromSeconds(2); + private static readonly TimeSpan MaxWaitForReady = TimingHelper.FromSeconds(5); + + internal RemoteEventSubscriberHost(string portName) + { + this.pipeStream = new NamedPipeServerStream( + RemotePipe.GetAbsolutePipeName(portName), + PipeDirection.InOut, + 1, + PipeTransmissionMode.Message, + PipeOptions.Asynchronous); + + this.pipeCancellation = new CancellationTokenSource(); + + this.InitBackgroundEventThread(); + this.InitBackgroundHeartbeatThread(); + } + + /// + /// Listens in the background for any incoming messages from the client. + /// + private async void InitBackgroundEventThread() + { + var backgroundThread = Task.Run(() => + { + this.pipeStream.WaitForConnection(); + + StreamReader reader = new(this.pipeStream); + + while (!this.pipeCancellation.IsCancellationRequested) + { + try + { + var messageOrSubscriptionId = RemotePipe.ReadMessage(this.pipeStream); + + if (string.IsNullOrEmpty(messageOrSubscriptionId)) + { + continue; + } + + if (messageOrSubscriptionId == RemoteEventSubscriber.SignalReady) + { + this.isPipeServerStreamReady = true; + continue; + } + else if (messageOrSubscriptionId == RemoteEventSubscriber.SignalExit) + { + this.Dispose(); + return; + } + + RemoteEventSubscriber.HandleInvoke(messageOrSubscriptionId); + } + catch (Exception ex) + { + Output.WriteLine(ex); + Debug.WriteLine($"-------------> Exception: {ex.Message}"); + return; + } + } + }); + + await backgroundThread; + } + + /// + /// Sends heartbeats in the background at the commonly agreed interval. + /// + private async void InitBackgroundHeartbeatThread() + { + var backgroundThread = Task.Run(async () => + { + while (!this.pipeCancellation.IsCancellationRequested) + { + await Task.Delay(HeartbeatWithMargin); + if (!this.isPipeServerStreamReady) + { + continue; + } + + try + { + RemotePipe.WriteMessage(this.pipeStream, RemoteEventSubscriber.SignalHeartbeat); + } + catch (Exception ex) + { + Output.WriteLine(ex); + Debug.WriteLine($"-------------> Exception: {ex.Message}"); + return; + } + } + }); + + await backgroundThread; + } + + /// + /// Stops execution until the event pipe has received the ready signal + /// + public void WaitForEventPipeReady() + { + var waitStart = DateTime.Now; + + while (!this.isPipeServerStreamReady) + { + Task.Delay(10); + + if ((DateTime.Now - waitStart) > MaxWaitForReady) + { + throw new TimeoutException($"WaitForEventPipeReady timed out after {MaxWaitForReady.TotalMilliseconds}ms"); + } + } + } + + public void Dispose() + { + this.Disposing?.Invoke(this, EventArgs.Empty); + + this.pipeStream?.Dispose(); + + this.pipeCancellation?.Cancel(); + } +} diff --git a/Core/Key2Joy.Contracts/Util/TimingHelper.cs b/Core/Key2Joy.Contracts/Util/TimingHelper.cs new file mode 100644 index 00000000..b11c6eb7 --- /dev/null +++ b/Core/Key2Joy.Contracts/Util/TimingHelper.cs @@ -0,0 +1,18 @@ +using System; + +namespace Key2Joy.Contracts.Util; + +/// +/// Helps get TimeSpans that are a lot longer on GitHub Actions (to prevent test timeouts) +/// +public static class TimingHelper +{ + public static int Modifier + => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true" ? 5 : 1; + + public static TimeSpan FromSeconds(double seconds) + => TimeSpan.FromSeconds(seconds * Modifier); + + public static TimeSpan FromMilliseconds(double milliseconds) + => TimeSpan.FromMilliseconds(milliseconds * Modifier); +} diff --git a/Core/Key2Joy.Core/Config/ConfigState.cs b/Core/Key2Joy.Core/Config/ConfigState.cs index 3929d7ca..300dc988 100644 --- a/Core/Key2Joy.Core/Config/ConfigState.cs +++ b/Core/Key2Joy.Core/Config/ConfigState.cs @@ -22,6 +22,18 @@ public string LastInstallPath private string lastInstallPath; + [EnumConfigControl( + Text = "Style mapped options are grouped by", + EnumType = typeof(ViewMappingGroupType) + )] + public ViewMappingGroupType SelectedViewMappingGroupType + { + get => this.selectedViewMappingGroupType; + set => this.SaveIfInitialized(this.selectedViewMappingGroupType = value); + } + + private ViewMappingGroupType selectedViewMappingGroupType = ViewMappingGroupType.ByAction; + [BooleanConfigControl( Text = "Minimize app when pressing the close button" )] diff --git a/Core/Key2Joy.Core/Config/EnumConfigControlAttribute.cs b/Core/Key2Joy.Core/Config/EnumConfigControlAttribute.cs new file mode 100644 index 00000000..1233f950 --- /dev/null +++ b/Core/Key2Joy.Core/Config/EnumConfigControlAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Key2Joy.Config; + +/// +/// Only applied to +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +public class EnumConfigControlAttribute : ConfigControlAttribute +{ + public Type EnumType { get; set; } +} diff --git a/Core/Key2Joy.Core/Config/ViewMappingGroupType.cs b/Core/Key2Joy.Core/Config/ViewMappingGroupType.cs new file mode 100644 index 00000000..7898d17f --- /dev/null +++ b/Core/Key2Joy.Core/Config/ViewMappingGroupType.cs @@ -0,0 +1,22 @@ +namespace Key2Joy.Config; + +/// +/// The type of grouping when listing mapped options in the UI +/// +public enum ViewMappingGroupType +{ + /// + /// No grouping + /// + None, + + /// + /// Group by action GroupName's + /// + ByAction, + + /// + /// Group by trigger GroupName's + /// + ByTrigger +} diff --git a/Core/Key2Joy.Core/IKey2JoyManager.cs b/Core/Key2Joy.Core/IKey2JoyManager.cs index 79054595..5f022d60 100644 --- a/Core/Key2Joy.Core/IKey2JoyManager.cs +++ b/Core/Key2Joy.Core/IKey2JoyManager.cs @@ -7,6 +7,11 @@ public interface IKey2JoyManager { void CallOnUiThread(Action action); + /// + /// Arms the mapping options so the triggers cause the actions to be executed. + /// + /// + /// Occurs when an illegal configuration can't be started void ArmMappings(MappingProfile profile); bool GetIsArmed(MappingProfile profile = null); diff --git a/Core/Key2Joy.Core/Key2Joy.Core.csproj b/Core/Key2Joy.Core/Key2Joy.Core.csproj index a7ce8b03..48285a81 100644 --- a/Core/Key2Joy.Core/Key2Joy.Core.csproj +++ b/Core/Key2Joy.Core/Key2Joy.Core.csproj @@ -14,7 +14,7 @@ NET48 latest - x86 + AnyCPU True Key2Joy False @@ -36,9 +36,9 @@ - - PreserveNewest - + + Never + diff --git a/Core/Key2Joy.Core/Key2JoyManager.cs b/Core/Key2Joy.Core/Key2JoyManager.cs index a0a41ebd..1ae71d8b 100644 --- a/Core/Key2Joy.Core/Key2JoyManager.cs +++ b/Core/Key2Joy.Core/Key2JoyManager.cs @@ -12,7 +12,8 @@ using Key2Joy.Contracts.Mapping.Triggers; using Key2Joy.Interop; using Key2Joy.Interop.Commands; -using Key2Joy.LowLevelInput.GamePad; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.LowLevelInput.XInput; using Key2Joy.Mapping; using Key2Joy.Mapping.Actions.Logic; using Key2Joy.Mapping.Triggers.Keyboard; @@ -92,7 +93,10 @@ public static void InitSafely(AppCommandRunner commandRunner, Action #pragma warning restore IDE0001 // Simplify Names var gamePadService = new SimulatedGamePadService(); - serviceLocator.Register(gamePadService); + serviceLocator.Register(gamePadService); + + var xInputService = new XInputService(); + serviceLocator.Register(xInputService); var commandRepository = new CommandRepository(); serviceLocator.Register(commandRepository); @@ -177,6 +181,7 @@ public bool GetIsArmed(MappingProfile profile = null) return this.armedProfile == profile; } + /// public void ArmMappings(MappingProfile profile) { this.armedProfile = profile; @@ -186,46 +191,61 @@ public void ArmMappings(MappingProfile profile) var allActions = (IList)profile.MappedOptions.Select(m => m.Action).ToList(); - foreach (var mappedOption in profile.MappedOptions) + var xInputService = ServiceLocator.Current.GetInstance(); + // We must recognize physical devices before any simulated ones are added. + // Otherwise we wont be able to tell the difference. + xInputService.RecognizePhysicalDevices(); + xInputService.StartPolling(); + + try { - if (mappedOption.Trigger == null) + foreach (var mappedOption in profile.MappedOptions) { - continue; - } + if (mappedOption.Trigger == null) + { + continue; + } - var listener = mappedOption.Trigger.GetTriggerListener(); + var listener = mappedOption.Trigger.GetTriggerListener(); - if (!allListeners.Contains(listener)) - { - allListeners.Add(listener); - } + if (!allListeners.Contains(listener)) + { + allListeners.Add(listener); + } - if (listener is IWndProcHandler listenerWndProcHAndler) - { - this.wndProcListeners.Add(listenerWndProcHAndler); - } + if (listener is IWndProcHandler listenerWndProcHAndler) + { + this.wndProcListeners.Add(listenerWndProcHAndler); + } - mappedOption.Action.OnStartListening(listener, ref allActions); - listener.AddMappedOption(mappedOption); - } + mappedOption.Action.OnStartListening(listener, ref allActions); + listener.AddMappedOption(mappedOption); + } - var allListenersForSharing = (IList)allListeners; + var allListenersForSharing = (IList)allListeners; - foreach (var listener in allListeners) - { - if (listener is IWndProcHandler listenerWndProcHAndler) + foreach (var listener in allListeners) { - listenerWndProcHAndler.Handle = this.handleAndInvoker.Handle; + if (listener is IWndProcHandler listenerWndProcHAndler) + { + listenerWndProcHAndler.Handle = this.handleAndInvoker.Handle; + } + + listener.StartListening(ref allListenersForSharing); } - listener.StartListening(ref allListenersForSharing); + StatusChanged?.Invoke(this, new StatusChangedEventArgs + { + IsEnabled = true, + Profile = this.armedProfile + }); } - - StatusChanged?.Invoke(this, new StatusChangedEventArgs + catch (MappingArmingFailedException ex) { - IsEnabled = true, - Profile = this.armedProfile - }); + //cleanup + this.DisarmMappings(); + throw ex; + } } public void DisarmMappings() @@ -257,7 +277,10 @@ public void DisarmMappings() listener.StopListening(); } - var gamePadService = ServiceLocator.Current.GetInstance(); + var xInputService = ServiceLocator.Current.GetInstance(); + xInputService.StopPolling(); + + var gamePadService = ServiceLocator.Current.GetInstance(); gamePadService.EnsureAllUnplugged(); this.armedProfile = null; diff --git a/Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePad.cs b/Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePad.cs deleted file mode 100644 index 5364b5db..00000000 --- a/Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePad.cs +++ /dev/null @@ -1,35 +0,0 @@ -using SimWinInput; - -namespace Key2Joy.LowLevelInput.GamePad; - -public interface IGamePad -{ - int Index { get; } - - void PlugIn(); - - bool GetIsPluggedIn(); - - void Unplug(); - - void Use(GamePadControl control, int holdTimeMS = 50); - - void SetControl(GamePadControl control); - - void ReleaseControl(GamePadControl control); - - /// - /// Get the raw input state from the GamePad - /// - SimulatedGamePadState GetState(); - - /// - /// Resets the GamePad state to the natural at-rest stat - /// - void ResetState(); - - /// - /// Update any changes made to the state to be reflected in the gamepad - /// - void Update(); -} diff --git a/Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePadService.cs b/Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePadService.cs deleted file mode 100644 index 44435c58..00000000 --- a/Core/Key2Joy.Core/LowLevelInput/GamePad/IGamePadService.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Key2Joy.LowLevelInput.GamePad; - -public interface IGamePadService -{ - IGamePad GetGamePad(int gamePadIndex); - - IGamePad[] GetAllGamePads(); - - void Initialize(); - - void ShutDown(); - - void EnsurePluggedIn(int gamePadIndex); - - void EnsureUnplugged(int gamePadIndex); - - void EnsureAllUnplugged(); -} diff --git a/Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePad.cs b/Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePad.cs deleted file mode 100644 index 1d12351f..00000000 --- a/Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePad.cs +++ /dev/null @@ -1,48 +0,0 @@ -using SimWinInput; - -namespace Key2Joy.LowLevelInput.GamePad; - -/// -/// Implementation based on https://github.com/DavidRieman/SimWinInput -/// -public class SimulatedGamePad : IGamePad -{ - public int Index { get; private set; } - private bool isPluggedIn = false; - - public SimulatedGamePad(int index) - => this.Index = index; - - public void PlugIn() - { - this.isPluggedIn = true; - SimGamePad.Instance.PlugIn(this.Index); - } - - public bool GetIsPluggedIn() - => this.isPluggedIn; - - public void Unplug() - { - SimGamePad.Instance.Unplug(this.Index); - this.isPluggedIn = false; - } - - public void Use(GamePadControl control, int holdTimeMS = 50) - => SimGamePad.Instance.Use(control, this.Index, holdTimeMS); - - public void SetControl(GamePadControl control) - => SimGamePad.Instance.SetControl(control, this.Index); - - public void ReleaseControl(GamePadControl control) - => SimGamePad.Instance.ReleaseControl(control, this.Index); - - public SimulatedGamePadState GetState() - => SimGamePad.Instance.State[this.Index]; - - public void ResetState() - => SimGamePad.Instance.State[this.Index].Reset(); - - public void Update() - => SimGamePad.Instance.Update(this.Index); -} diff --git a/Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePadService.cs b/Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePadService.cs deleted file mode 100644 index 164a5191..00000000 --- a/Core/Key2Joy.Core/LowLevelInput/GamePad/SimulatedGamePadService.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Linq; -using SimWinInput; - -namespace Key2Joy.LowLevelInput.GamePad; - -public class SimulatedGamePadService : IGamePadService -{ - private const int MAX_GAMEPADS = 4; - private readonly IGamePad[] gamePads; - - public SimulatedGamePadService(IGamePad[] gamePads = null) - { - if (gamePads != null) - { - this.gamePads = gamePads; - return; - } - - this.gamePads = new IGamePad[MAX_GAMEPADS]; - - for (var index = 0; index < MAX_GAMEPADS; index++) - { - this.gamePads[index] = new SimulatedGamePad(index); - } - } - - public void Initialize() => SimGamePad.Instance.Initialize(); - - public void ShutDown() => SimGamePad.Instance.ShutDown(); - - public IGamePad GetGamePad(int gamePadIndex) - => this.gamePads[gamePadIndex]; - - public IGamePad[] GetAllGamePads() - => this.gamePads; - - public void EnsurePluggedIn(int gamePadIndex) - { - if (gamePadIndex is < 0 or >= MAX_GAMEPADS) - { - throw new ArgumentOutOfRangeException(nameof(gamePadIndex)); - } - - var gamePad = this.gamePads[gamePadIndex]; - - if (gamePad.GetIsPluggedIn()) - { - return; - } - - gamePad.PlugIn(); - } - - public void EnsureUnplugged(int gamePadIndex) - { - if (gamePadIndex is < 0 or >= MAX_GAMEPADS) - { - throw new ArgumentOutOfRangeException(nameof(gamePadIndex)); - } - - var gamePad = this.gamePads[gamePadIndex]; - - if (!gamePad.GetIsPluggedIn()) - { - return; - } - - gamePad.Unplug(); - } - - public void EnsureAllUnplugged() - { - foreach (var gamePad in this.gamePads.Where(gamePad => gamePad.GetIsPluggedIn())) - { - gamePad.Unplug(); - } - } -} diff --git a/Core/Key2Joy.Core/LowLevelInput/GamePadInfo.cs b/Core/Key2Joy.Core/LowLevelInput/GamePadInfo.cs new file mode 100644 index 00000000..f62855cf --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/GamePadInfo.cs @@ -0,0 +1,51 @@ +using System; + +namespace Key2Joy.LowLevelInput; + +public class GamePadActivityOccurredEventArgs : EventArgs +{ } + +public class GamePadInfo : IGamePadInfo +{ + /// + public event EventHandler ActivityOccurred; + + /// + public int Index { get; } + + /// + public string Name { get; } + + public GamePadInfo(int index, string name) + { + this.Index = index; + this.Name = name; + } + + /// + public void OnActivityOccurred() + => this.ActivityOccurred?.Invoke(this, new()); +} + +public interface IGamePadInfo +{ + /// + /// Called when gamepad activity occurs. + /// + event EventHandler ActivityOccurred; + + /// + /// The index of the gamepad + /// + int Index { get; } + + /// + /// The display name for this gamepad + /// + string Name { get; } + + /// + /// Raises the event. + /// + void OnActivityOccurred(); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePad.cs b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePad.cs new file mode 100644 index 00000000..60b23443 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePad.cs @@ -0,0 +1,71 @@ +using SimWinInput; + +namespace Key2Joy.LowLevelInput.SimulatedGamePad; + +/// +/// Represents a simulated gamepad device. +/// +public interface ISimulatedGamePad +{ + /// + /// Gets the index of the simulated gamepad. + /// + int Index { get; } + + /// + /// Plugs in the simulated gamepad. + /// + void PlugIn(); + + /// + /// Checks if the simulated gamepad is currently plugged in. + /// + /// True if the gamepad is plugged in; otherwise, false. + bool GetIsPluggedIn(); + + /// + /// Unplugs the simulated gamepad. + /// + void Unplug(); + + /// + /// Simulates pressing and holding a control on the gamepad. + /// + /// The control to simulate. + /// The duration (in milliseconds) to hold the control (default is 50ms). + void Use(GamePadControl control, int holdTimeMS = 50); + + /// + /// Sets a specific control on the gamepad. + /// + /// The control to set. + void SetControl(GamePadControl control); + + /// + /// Releases a specific control on the gamepad. + /// + /// The control to release. + void ReleaseControl(GamePadControl control); + + /// + /// Get the raw input state from the GamePad + /// + /// The raw input state of the gamepad. + SimulatedGamePadState GetState(); + + /// + /// Resets the GamePad state to the natural at-rest stat + /// + void ResetState(); + + /// + /// Update any changes made to the state to be reflected in the gamepad + /// + void Update(); + + /// + /// Returns the gamepad info on this device + /// + /// + IGamePadInfo GetInfo(); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePadService.cs b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePadService.cs new file mode 100644 index 00000000..910900af --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/ISimulatedGamePadService.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace Key2Joy.LowLevelInput.SimulatedGamePad; + +/// +/// Represents a service for managing simulated gamepad devices. +/// +public interface ISimulatedGamePadService +{ + /// + /// Gets a simulated gamepad by its index. + /// + /// The index of the gamepad to retrieve. + /// An instance of the simulated gamepad. + ISimulatedGamePad GetGamePad(int gamePadIndex); + + /// + /// Retrieves an array of all available simulated gamepad devices. + /// + /// + /// An array of simulated gamepad instances. + ISimulatedGamePad[] GetAllGamePads(bool onlyPluggedIn = true); + + /// + /// Initializes the simulated gamepad service. + /// + void Initialize(); + + /// + /// Shuts down the simulated gamepad service. + /// + void ShutDown(); + + /// + /// Ensures that a specific gamepad is plugged in. + /// + /// The index of the gamepad to ensure is plugged in. + void EnsurePluggedIn(int gamePadIndex); + + /// + /// Ensures that a specific gamepad is unplugged. + /// + /// The index of the gamepad to ensure is unplugged. + void EnsureUnplugged(int gamePadIndex); + + /// + /// Ensures that all simulated gamepads are unplugged. + /// + void EnsureAllUnplugged(); + + /// + /// Gets the active gamepad device indexes. + /// + /// + IList GetActiveDevicesInfo(); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePad.cs b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePad.cs new file mode 100644 index 00000000..e018ae4a --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePad.cs @@ -0,0 +1,87 @@ +using SimWinInput; + +namespace Key2Joy.LowLevelInput.SimulatedGamePad; + +/// +/// Implementation based on https://github.com/DavidRieman/SimWinInput +/// +public class SimulatedGamePad : ISimulatedGamePad +{ + private const string GAMEPAD_NAME = "Simulated"; + + /// + public int Index { get; private set; } + + private bool isPluggedIn = false; + private readonly IGamePadInfo gamePadInfo; + + public SimulatedGamePad(int index) + { + this.Index = index; + this.gamePadInfo = new GamePadInfo(index, GAMEPAD_NAME); + } + + public IGamePadInfo GetInfo() + => this.gamePadInfo; + + /// + public void PlugIn() + { + SimGamePad.Instance.PlugIn(this.Index); + this.isPluggedIn = true; + + // Ensure the state starts reset fixes problem where other (real) gamepad may get button stuck + this.ResetState(); + this.Update(); + } + + /// + public bool GetIsPluggedIn() + => this.isPluggedIn; + + /// + public void Unplug() + { + SimGamePad.Instance.Unplug(this.Index); + this.isPluggedIn = false; + } + + /// + public void Use(GamePadControl control, int holdTimeMS = 50) + { + SimGamePad.Instance.Use(control, this.Index, holdTimeMS); + this.gamePadInfo.OnActivityOccurred(); + } + + /// + public void SetControl(GamePadControl control) + { + SimGamePad.Instance.SetControl(control, this.Index); + this.gamePadInfo.OnActivityOccurred(); + } + + /// + public void ReleaseControl(GamePadControl control) + { + SimGamePad.Instance.ReleaseControl(control, this.Index); + this.gamePadInfo.OnActivityOccurred(); + } + + /// + public SimulatedGamePadState GetState() + => SimGamePad.Instance.State[this.Index]; + + /// + public void ResetState() + { + SimGamePad.Instance.State[this.Index].Reset(); + this.gamePadInfo.OnActivityOccurred(); + } + + /// + public void Update() + { + SimGamePad.Instance.Update(this.Index); + this.gamePadInfo.OnActivityOccurred(); + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadService.cs b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadService.cs new file mode 100644 index 00000000..9f624475 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadService.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CommonServiceLocator; +using Key2Joy.LowLevelInput.XInput; +using SimWinInput; + +namespace Key2Joy.LowLevelInput.SimulatedGamePad; + +/// +public class SimulatedGamePadService : ISimulatedGamePadService +{ + private const int MAX_GAMEPADS = 4; + + private readonly ISimulatedGamePad[] gamePads; + + public SimulatedGamePadService(ISimulatedGamePad[] gamePads = null) + { + if (gamePads != null) + { + this.gamePads = gamePads; + return; + } + + this.gamePads = new ISimulatedGamePad[MAX_GAMEPADS]; + + for (var index = 0; index < MAX_GAMEPADS; index++) + { + this.gamePads[index] = new SimulatedGamePad(index); + } + } + + /// + public void Initialize() + => SimGamePad.Instance.Initialize(); + + /// + public void ShutDown() + => SimGamePad.Instance.ShutDown(); + + /// + public ISimulatedGamePad GetGamePad(int gamePadIndex) + => this.gamePads[gamePadIndex]; + + /// + public ISimulatedGamePad[] GetAllGamePads(bool onlyPluggedIn = true) + { + if (onlyPluggedIn) + { + return this.gamePads.Where(gamePad => gamePad.GetIsPluggedIn()).ToArray(); + } + + return this.gamePads; + } + + /// + public IList GetActiveDevicesInfo() + => this.gamePads + .Where(gamePad => gamePad.GetIsPluggedIn()) + .Select(gamePad => gamePad.GetInfo()).ToList(); + + /// + public void EnsurePluggedIn(int gamePadIndex) + { + if (gamePadIndex is < 0 or >= MAX_GAMEPADS) + { + throw new ArgumentOutOfRangeException(nameof(gamePadIndex)); + } + + var xInputService = ServiceLocator.Current.GetInstance(); + var physicalDeviceIndexes = xInputService.GetActiveDevicesInfo(); + + // If the physical device is active at the index, then we can't use that index + if (physicalDeviceIndexes.FirstOrDefault(info => info.Index == gamePadIndex) is IGamePadInfo info) + { + throw new MappingArmingFailedException( + $"There is a physical gamepad in use at index {gamePadIndex}. Cannot simulate at that index."); + } + + var gamePad = this.gamePads[gamePadIndex]; + + if (gamePad.GetIsPluggedIn()) + { + return; + } + + gamePad.PlugIn(); + } + + /// + public void EnsureUnplugged(int gamePadIndex) + { + if (gamePadIndex is < 0 or >= MAX_GAMEPADS) + { + throw new ArgumentOutOfRangeException(nameof(gamePadIndex)); + } + + var xInputService = ServiceLocator.Current.GetInstance(); + var physicalDeviceIndexes = xInputService.GetActiveDevicesInfo(); + + // If the physical device is active at the index, then we can't use that index + if (physicalDeviceIndexes.FirstOrDefault(info => info.Index == gamePadIndex) is IGamePadInfo info) + { + throw new MappingArmingFailedException( + $"There is a physical gamepad in use at index {gamePadIndex}. Cannot simulate at that index."); + } + + var gamePad = this.gamePads[gamePadIndex]; + + if (!gamePad.GetIsPluggedIn()) + { + return; + } + + gamePad.Unplug(); + } + + /// + public void EnsureAllUnplugged() + { + foreach (var gamePad in this.GetAllGamePads(true)) + { + gamePad.Unplug(); + } + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/SimulatedKeyboard.cs b/Core/Key2Joy.Core/LowLevelInput/SimulatedKeyboard.cs index ce2b884f..f49f7b65 100644 --- a/Core/Key2Joy.Core/LowLevelInput/SimulatedKeyboard.cs +++ b/Core/Key2Joy.Core/LowLevelInput/SimulatedKeyboard.cs @@ -1,748 +1,921 @@ -using System; -using System.Runtime.InteropServices; - -namespace Key2Joy.LowLevelInput; - -// Source: https://stackoverflow.com/a/48967155 -public static class SimulatedKeyboard -{ - public static void PressKey(KeyboardKey scanCode) => Send(scanCode); - - public static void ReleaseKey(KeyboardKey scanCode) => Send(scanCode, KEYEVENTF.KEYUP); - - public static void Send(KeyboardKey scanCode, KEYEVENTF? rawPressState = null) - { - var Inputs = new Simulator.INPUT[1]; - Simulator.INPUT Input = new() - { - type = 1 // 1 = Keyboard Input - }; - Input.U.ki.wScan = scanCode; - - if (rawPressState.HasValue) - { - Input.U.ki.dwFlags = rawPressState.Value | KEYEVENTF.SCANCODE; - } - else - { - Input.U.ki.dwFlags = KEYEVENTF.SCANCODE; - } - - Inputs[0] = Input; - - Simulator.SendInput(1, Inputs, Simulator.INPUT.Size); - } - - [StructLayout(LayoutKind.Sequential)] - public struct KEYBDINPUT - { - public VirtualKeyShort wVk; - public KeyboardKey wScan; - public KEYEVENTF dwFlags; - public int time; - public UIntPtr dwExtraInfo; - } - - [Flags] - public enum KEYEVENTF : uint - { - EXTENDEDKEY = 0x0001, - KEYUP = 0x0002, - SCANCODE = 0x0008, - UNICODE = 0x0004 - } - - public enum VirtualKeyShort : short - { - /// - ///Left mouse button - /// - LBUTTON = 0x01, - /// - ///Right mouse button - /// - RBUTTON = 0x02, - /// - ///Control-break processing - /// - CANCEL = 0x03, - /// - ///Middle mouse button (three-button mouse) - /// - MBUTTON = 0x04, - /// - ///Windows 2000/XP: X1 mouse button - /// - XBUTTON1 = 0x05, - /// - ///Windows 2000/XP: X2 mouse button - /// - XBUTTON2 = 0x06, - /// - ///BACKSPACE key - /// - BACK = 0x08, - /// - ///TAB key - /// - TAB = 0x09, - /// - ///CLEAR key - /// - CLEAR = 0x0C, - /// - ///ENTER key - /// - RETURN = 0x0D, - /// - ///SHIFT key - /// - SHIFT = 0x10, - /// - ///CTRL key - /// - CONTROL = 0x11, - /// - ///ALT key - /// - MENU = 0x12, - /// - ///PAUSE key - /// - PAUSE = 0x13, - /// - ///CAPS LOCK key - /// - CAPITAL = 0x14, - /// - ///Input Method Editor (IME) Kana mode - /// - KANA = 0x15, - /// - ///IME Hangul mode - /// - HANGUL = 0x15, - /// - ///IME Junja mode - /// - JUNJA = 0x17, - /// - ///IME final mode - /// - FINAL = 0x18, - /// - ///IME Hanja mode - /// - HANJA = 0x19, - /// - ///IME Kanji mode - /// - KANJI = 0x19, - /// - ///ESC key - /// - ESCAPE = 0x1B, - /// - ///IME convert - /// - CONVERT = 0x1C, - /// - ///IME nonconvert - /// - NONCONVERT = 0x1D, - /// - ///IME accept - /// - ACCEPT = 0x1E, - /// - ///IME mode change request - /// - MODECHANGE = 0x1F, - /// - ///SPACEBAR - /// - SPACE = 0x20, - /// - ///PAGE UP key - /// - PRIOR = 0x21, - /// - ///PAGE DOWN key - /// - NEXT = 0x22, - /// - ///END key - /// - END = 0x23, - /// - ///HOME key - /// - HOME = 0x24, - /// - ///LEFT ARROW key - /// - LEFT = 0x25, - /// - ///UP ARROW key - /// - UP = 0x26, - /// - ///RIGHT ARROW key - /// - RIGHT = 0x27, - /// - ///DOWN ARROW key - /// - DOWN = 0x28, - /// - ///SELECT key - /// - SELECT = 0x29, - /// - ///PRINT key - /// - PRINT = 0x2A, - /// - ///EXECUTE key - /// - EXECUTE = 0x2B, - /// - ///PRINT SCREEN key - /// - SNAPSHOT = 0x2C, - /// - ///INS key - /// - INSERT = 0x2D, - /// - ///DEL key - /// - DELETE = 0x2E, - /// - ///HELP key - /// - HELP = 0x2F, - /// - ///0 key - /// - KEY_0 = 0x30, - /// - ///1 key - /// - KEY_1 = 0x31, - /// - ///2 key - /// - KEY_2 = 0x32, - /// - ///3 key - /// - KEY_3 = 0x33, - /// - ///4 key - /// - KEY_4 = 0x34, - /// - ///5 key - /// - KEY_5 = 0x35, - /// - ///6 key - /// - KEY_6 = 0x36, - /// - ///7 key - /// - KEY_7 = 0x37, - /// - ///8 key - /// - KEY_8 = 0x38, - /// - ///9 key - /// - KEY_9 = 0x39, - /// - ///A key - /// - KEY_A = 0x41, - /// - ///B key - /// - KEY_B = 0x42, - /// - ///C key - /// - KEY_C = 0x43, - /// - ///D key - /// - KEY_D = 0x44, - /// - ///E key - /// - KEY_E = 0x45, - /// - ///F key - /// - KEY_F = 0x46, - /// - ///G key - /// - KEY_G = 0x47, - /// - ///H key - /// - KEY_H = 0x48, - /// - ///I key - /// - KEY_I = 0x49, - /// - ///J key - /// - KEY_J = 0x4A, - /// - ///K key - /// - KEY_K = 0x4B, - /// - ///L key - /// - KEY_L = 0x4C, - /// - ///M key - /// - KEY_M = 0x4D, - /// - ///N key - /// - KEY_N = 0x4E, - /// - ///O key - /// - KEY_O = 0x4F, - /// - ///P key - /// - KEY_P = 0x50, - /// - ///Q key - /// - KEY_Q = 0x51, - /// - ///R key - /// - KEY_R = 0x52, - /// - ///S key - /// - KEY_S = 0x53, - /// - ///T key - /// - KEY_T = 0x54, - /// - ///U key - /// - KEY_U = 0x55, - /// - ///V key - /// - KEY_V = 0x56, - /// - ///W key - /// - KEY_W = 0x57, - /// - ///X key - /// - KEY_X = 0x58, - /// - ///Y key - /// - KEY_Y = 0x59, - /// - ///Z key - /// - KEY_Z = 0x5A, - /// - ///Left Windows key (Microsoft Natural keyboard) - /// - LWIN = 0x5B, - /// - ///Right Windows key (Natural keyboard) - /// - RWIN = 0x5C, - /// - ///Applications key (Natural keyboard) - /// - APPS = 0x5D, - /// - ///Computer Sleep key - /// - SLEEP = 0x5F, - /// - ///Numeric keypad 0 key - /// - NUMPAD0 = 0x60, - /// - ///Numeric keypad 1 key - /// - NUMPAD1 = 0x61, - /// - ///Numeric keypad 2 key - /// - NUMPAD2 = 0x62, - /// - ///Numeric keypad 3 key - /// - NUMPAD3 = 0x63, - /// - ///Numeric keypad 4 key - /// - NUMPAD4 = 0x64, - /// - ///Numeric keypad 5 key - /// - NUMPAD5 = 0x65, - /// - ///Numeric keypad 6 key - /// - NUMPAD6 = 0x66, - /// - ///Numeric keypad 7 key - /// - NUMPAD7 = 0x67, - /// - ///Numeric keypad 8 key - /// - NUMPAD8 = 0x68, - /// - ///Numeric keypad 9 key - /// - NUMPAD9 = 0x69, - /// - ///Multiply key - /// - MULTIPLY = 0x6A, - /// - ///Add key - /// - ADD = 0x6B, - /// - ///Separator key - /// - SEPARATOR = 0x6C, - /// - ///Subtract key - /// - SUBTRACT = 0x6D, - /// - ///Decimal key - /// - DECIMAL = 0x6E, - /// - ///Divide key - /// - DIVIDE = 0x6F, - /// - ///F1 key - /// - F1 = 0x70, - /// - ///F2 key - /// - F2 = 0x71, - /// - ///F3 key - /// - F3 = 0x72, - /// - ///F4 key - /// - F4 = 0x73, - /// - ///F5 key - /// - F5 = 0x74, - /// - ///F6 key - /// - F6 = 0x75, - /// - ///F7 key - /// - F7 = 0x76, - /// - ///F8 key - /// - F8 = 0x77, - /// - ///F9 key - /// - F9 = 0x78, - /// - ///F10 key - /// - F10 = 0x79, - /// - ///F11 key - /// - F11 = 0x7A, - /// - ///F12 key - /// - F12 = 0x7B, - /// - ///F13 key - /// - F13 = 0x7C, - /// - ///F14 key - /// - F14 = 0x7D, - /// - ///F15 key - /// - F15 = 0x7E, - /// - ///F16 key - /// - F16 = 0x7F, - /// - ///F17 key - /// - F17 = 0x80, - /// - ///F18 key - /// - F18 = 0x81, - /// - ///F19 key - /// - F19 = 0x82, - /// - ///F20 key - /// - F20 = 0x83, - /// - ///F21 key - /// - F21 = 0x84, - /// - ///F22 key, (PPC only) Key used to lock device. - /// - F22 = 0x85, - /// - ///F23 key - /// - F23 = 0x86, - /// - ///F24 key - /// - F24 = 0x87, - /// - ///NUM LOCK key - /// - NUMLOCK = 0x90, - /// - ///SCROLL LOCK key - /// - SCROLL = 0x91, - /// - ///Left SHIFT key - /// - LSHIFT = 0xA0, - /// - ///Right SHIFT key - /// - RSHIFT = 0xA1, - /// - ///Left CONTROL key - /// - LCONTROL = 0xA2, - /// - ///Right CONTROL key - /// - RCONTROL = 0xA3, - /// - ///Left MENU key - /// - LMENU = 0xA4, - /// - ///Right MENU key - /// - RMENU = 0xA5, - /// - ///Windows 2000/XP: Browser Back key - /// - BROWSER_BACK = 0xA6, - /// - ///Windows 2000/XP: Browser Forward key - /// - BROWSER_FORWARD = 0xA7, - /// - ///Windows 2000/XP: Browser Refresh key - /// - BROWSER_REFRESH = 0xA8, - /// - ///Windows 2000/XP: Browser Stop key - /// - BROWSER_STOP = 0xA9, - /// - ///Windows 2000/XP: Browser Search key - /// - BROWSER_SEARCH = 0xAA, - /// - ///Windows 2000/XP: Browser Favorites key - /// - BROWSER_FAVORITES = 0xAB, - /// - ///Windows 2000/XP: Browser Start and Home key - /// - BROWSER_HOME = 0xAC, - /// - ///Windows 2000/XP: Volume Mute key - /// - VOLUME_MUTE = 0xAD, - /// - ///Windows 2000/XP: Volume Down key - /// - VOLUME_DOWN = 0xAE, - /// - ///Windows 2000/XP: Volume Up key - /// - VOLUME_UP = 0xAF, - /// - ///Windows 2000/XP: Next Track key - /// - MEDIA_NEXT_TRACK = 0xB0, - /// - ///Windows 2000/XP: Previous Track key - /// - MEDIA_PREV_TRACK = 0xB1, - /// - ///Windows 2000/XP: Stop Media key - /// - MEDIA_STOP = 0xB2, - /// - ///Windows 2000/XP: Play/Pause Media key - /// - MEDIA_PLAY_PAUSE = 0xB3, - /// - ///Windows 2000/XP: Start Mail key - /// - LAUNCH_MAIL = 0xB4, - /// - ///Windows 2000/XP: Select Media key - /// - LAUNCH_MEDIA_SELECT = 0xB5, - /// - ///Windows 2000/XP: Start Application 1 key - /// - LAUNCH_APP1 = 0xB6, - /// - ///Windows 2000/XP: Start Application 2 key - /// - LAUNCH_APP2 = 0xB7, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_1 = 0xBA, - /// - ///Windows 2000/XP: For any country/region, the '+' key - /// - OEM_PLUS = 0xBB, - /// - ///Windows 2000/XP: For any country/region, the ',' key - /// - OEM_COMMA = 0xBC, - /// - ///Windows 2000/XP: For any country/region, the '-' key - /// - OEM_MINUS = 0xBD, - /// - ///Windows 2000/XP: For any country/region, the '.' key - /// - OEM_PERIOD = 0xBE, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_2 = 0xBF, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_3 = 0xC0, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_4 = 0xDB, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_5 = 0xDC, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_6 = 0xDD, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_7 = 0xDE, - /// - ///Used for miscellaneous characters; it can vary by keyboard. - /// - OEM_8 = 0xDF, - /// - ///Windows 2000/XP: Either the angle bracket key or the backslash key on the RT 102-key keyboard - /// - OEM_102 = 0xE2, - /// - ///Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key - /// - PROCESSKEY = 0xE5, - /// - ///Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. - ///The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, - ///see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP - /// - PACKET = 0xE7, - /// - ///Attn key - /// - ATTN = 0xF6, - /// - ///CrSel key - /// - CRSEL = 0xF7, - /// - ///ExSel key - /// - EXSEL = 0xF8, - /// - ///Erase EOF key - /// - EREOF = 0xF9, - /// - ///Play key - /// - PLAY = 0xFA, - /// - ///Zoom key - /// - ZOOM = 0xFB, - /// - ///Reserved - /// - NONAME = 0xFC, - /// - ///PA1 key - /// - PA1 = 0xFD, - /// - ///Clear key - /// - OEM_CLEAR = 0xFE - } -} +using System; +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput; + +// Source: https://stackoverflow.com/a/48967155 +public static class SimulatedKeyboard +{ + public static void PressKey(KeyboardKey scanCode) + => Send(scanCode); + + public static void ReleaseKey(KeyboardKey scanCode) + => Send(scanCode, KEYEVENTF.KEYUP); + + public static void Send(KeyboardKey scanCode, KEYEVENTF? rawPressState = null) + { + var inputs = new Simulator.INPUT[1]; + Simulator.INPUT input = new() + { + type = 1 // 1 = Keyboard Input + }; + input.U.ki.wScan = scanCode; + + if (rawPressState.HasValue) + { + input.U.ki.dwFlags = rawPressState.Value | KEYEVENTF.SCANCODE; + } + else + { + input.U.ki.dwFlags = KEYEVENTF.SCANCODE; + } + + inputs[0] = input; + + Simulator.SendInput(1, inputs, Simulator.INPUT.Size); + } + + [StructLayout(LayoutKind.Sequential)] + public struct KEYBDINPUT + { + public VirtualKeyShort wVk; + public KeyboardKey wScan; + public KEYEVENTF dwFlags; + public int time; + public UIntPtr dwExtraInfo; + } + + [Flags] + public enum KEYEVENTF : uint + { + EXTENDEDKEY = 0x0001, + KEYUP = 0x0002, + SCANCODE = 0x0008, + UNICODE = 0x0004 + } + + public enum VirtualKeyShort : short + { + /// + ///Left mouse button + /// + LBUTTON = 0x01, + + /// + ///Right mouse button + /// + RBUTTON = 0x02, + + /// + ///Control-break processing + /// + CANCEL = 0x03, + + /// + ///Middle mouse button (three-button mouse) + /// + MBUTTON = 0x04, + + /// + ///Windows 2000/XP: X1 mouse button + /// + XBUTTON1 = 0x05, + + /// + ///Windows 2000/XP: X2 mouse button + /// + XBUTTON2 = 0x06, + + /// + ///BACKSPACE key + /// + BACK = 0x08, + + /// + ///TAB key + /// + TAB = 0x09, + + /// + ///CLEAR key + /// + CLEAR = 0x0C, + + /// + ///ENTER key + /// + RETURN = 0x0D, + + /// + ///SHIFT key + /// + SHIFT = 0x10, + + /// + ///CTRL key + /// + CONTROL = 0x11, + + /// + ///ALT key + /// + MENU = 0x12, + + /// + ///PAUSE key + /// + PAUSE = 0x13, + + /// + ///CAPS LOCK key + /// + CAPITAL = 0x14, + + /// + ///Input Method Editor (IME) Kana mode + /// + KANA = 0x15, + + /// + ///IME Hangul mode + /// + HANGUL = 0x15, + + /// + ///IME Junja mode + /// + JUNJA = 0x17, + + /// + ///IME final mode + /// + FINAL = 0x18, + + /// + ///IME Hanja mode + /// + HANJA = 0x19, + + /// + ///IME Kanji mode + /// + KANJI = 0x19, + + /// + ///ESC key + /// + ESCAPE = 0x1B, + + /// + ///IME convert + /// + CONVERT = 0x1C, + + /// + ///IME nonconvert + /// + NONCONVERT = 0x1D, + + /// + ///IME accept + /// + ACCEPT = 0x1E, + + /// + ///IME mode change request + /// + MODECHANGE = 0x1F, + + /// + ///SPACEBAR + /// + SPACE = 0x20, + + /// + ///PAGE UP key + /// + PRIOR = 0x21, + + /// + ///PAGE DOWN key + /// + NEXT = 0x22, + + /// + ///END key + /// + END = 0x23, + + /// + ///HOME key + /// + HOME = 0x24, + + /// + ///LEFT ARROW key + /// + LEFT = 0x25, + + /// + ///UP ARROW key + /// + UP = 0x26, + + /// + ///RIGHT ARROW key + /// + RIGHT = 0x27, + + /// + ///DOWN ARROW key + /// + DOWN = 0x28, + + /// + ///SELECT key + /// + SELECT = 0x29, + + /// + ///PRINT key + /// + PRINT = 0x2A, + + /// + ///EXECUTE key + /// + EXECUTE = 0x2B, + + /// + ///PRINT SCREEN key + /// + SNAPSHOT = 0x2C, + + /// + ///INS key + /// + INSERT = 0x2D, + + /// + ///DEL key + /// + DELETE = 0x2E, + + /// + ///HELP key + /// + HELP = 0x2F, + + /// + ///0 key + /// + KEY_0 = 0x30, + + /// + ///1 key + /// + KEY_1 = 0x31, + + /// + ///2 key + /// + KEY_2 = 0x32, + + /// + ///3 key + /// + KEY_3 = 0x33, + + /// + ///4 key + /// + KEY_4 = 0x34, + + /// + ///5 key + /// + KEY_5 = 0x35, + + /// + ///6 key + /// + KEY_6 = 0x36, + + /// + ///7 key + /// + KEY_7 = 0x37, + + /// + ///8 key + /// + KEY_8 = 0x38, + + /// + ///9 key + /// + KEY_9 = 0x39, + + /// + ///A key + /// + KEY_A = 0x41, + + /// + ///B key + /// + KEY_B = 0x42, + + /// + ///C key + /// + KEY_C = 0x43, + + /// + ///D key + /// + KEY_D = 0x44, + + /// + ///E key + /// + KEY_E = 0x45, + + /// + ///F key + /// + KEY_F = 0x46, + + /// + ///G key + /// + KEY_G = 0x47, + + /// + ///H key + /// + KEY_H = 0x48, + + /// + ///I key + /// + KEY_I = 0x49, + + /// + ///J key + /// + KEY_J = 0x4A, + + /// + ///K key + /// + KEY_K = 0x4B, + + /// + ///L key + /// + KEY_L = 0x4C, + + /// + ///M key + /// + KEY_M = 0x4D, + + /// + ///N key + /// + KEY_N = 0x4E, + + /// + ///O key + /// + KEY_O = 0x4F, + + /// + ///P key + /// + KEY_P = 0x50, + + /// + ///Q key + /// + KEY_Q = 0x51, + + /// + ///R key + /// + KEY_R = 0x52, + + /// + ///S key + /// + KEY_S = 0x53, + + /// + ///T key + /// + KEY_T = 0x54, + + /// + ///U key + /// + KEY_U = 0x55, + + /// + ///V key + /// + KEY_V = 0x56, + + /// + ///W key + /// + KEY_W = 0x57, + + /// + ///X key + /// + KEY_X = 0x58, + + /// + ///Y key + /// + KEY_Y = 0x59, + + /// + ///Z key + /// + KEY_Z = 0x5A, + + /// + ///Left Windows key (Microsoft Natural keyboard) + /// + LWIN = 0x5B, + + /// + ///Right Windows key (Natural keyboard) + /// + RWIN = 0x5C, + + /// + ///Applications key (Natural keyboard) + /// + APPS = 0x5D, + + /// + ///Computer Sleep key + /// + SLEEP = 0x5F, + + /// + ///Numeric keypad 0 key + /// + NUMPAD0 = 0x60, + + /// + ///Numeric keypad 1 key + /// + NUMPAD1 = 0x61, + + /// + ///Numeric keypad 2 key + /// + NUMPAD2 = 0x62, + + /// + ///Numeric keypad 3 key + /// + NUMPAD3 = 0x63, + + /// + ///Numeric keypad 4 key + /// + NUMPAD4 = 0x64, + + /// + ///Numeric keypad 5 key + /// + NUMPAD5 = 0x65, + + /// + ///Numeric keypad 6 key + /// + NUMPAD6 = 0x66, + + /// + ///Numeric keypad 7 key + /// + NUMPAD7 = 0x67, + + /// + ///Numeric keypad 8 key + /// + NUMPAD8 = 0x68, + + /// + ///Numeric keypad 9 key + /// + NUMPAD9 = 0x69, + + /// + ///Multiply key + /// + MULTIPLY = 0x6A, + + /// + ///Add key + /// + ADD = 0x6B, + + /// + ///Separator key + /// + SEPARATOR = 0x6C, + + /// + ///Subtract key + /// + SUBTRACT = 0x6D, + + /// + ///Decimal key + /// + DECIMAL = 0x6E, + + /// + ///Divide key + /// + DIVIDE = 0x6F, + + /// + ///F1 key + /// + F1 = 0x70, + + /// + ///F2 key + /// + F2 = 0x71, + + /// + ///F3 key + /// + F3 = 0x72, + + /// + ///F4 key + /// + F4 = 0x73, + + /// + ///F5 key + /// + F5 = 0x74, + + /// + ///F6 key + /// + F6 = 0x75, + + /// + ///F7 key + /// + F7 = 0x76, + + /// + ///F8 key + /// + F8 = 0x77, + + /// + ///F9 key + /// + F9 = 0x78, + + /// + ///F10 key + /// + F10 = 0x79, + + /// + ///F11 key + /// + F11 = 0x7A, + + /// + ///F12 key + /// + F12 = 0x7B, + + /// + ///F13 key + /// + F13 = 0x7C, + + /// + ///F14 key + /// + F14 = 0x7D, + + /// + ///F15 key + /// + F15 = 0x7E, + + /// + ///F16 key + /// + F16 = 0x7F, + + /// + ///F17 key + /// + F17 = 0x80, + + /// + ///F18 key + /// + F18 = 0x81, + + /// + ///F19 key + /// + F19 = 0x82, + + /// + ///F20 key + /// + F20 = 0x83, + + /// + ///F21 key + /// + F21 = 0x84, + + /// + ///F22 key, (PPC only) Key used to lock device. + /// + F22 = 0x85, + + /// + ///F23 key + /// + F23 = 0x86, + + /// + ///F24 key + /// + F24 = 0x87, + + /// + ///NUM LOCK key + /// + NUMLOCK = 0x90, + + /// + ///SCROLL LOCK key + /// + SCROLL = 0x91, + + /// + ///Left SHIFT key + /// + LSHIFT = 0xA0, + + /// + ///Right SHIFT key + /// + RSHIFT = 0xA1, + + /// + ///Left CONTROL key + /// + LCONTROL = 0xA2, + + /// + ///Right CONTROL key + /// + RCONTROL = 0xA3, + + /// + ///Left MENU key + /// + LMENU = 0xA4, + + /// + ///Right MENU key + /// + RMENU = 0xA5, + + /// + ///Windows 2000/XP: Browser Back key + /// + BROWSER_BACK = 0xA6, + + /// + ///Windows 2000/XP: Browser Forward key + /// + BROWSER_FORWARD = 0xA7, + + /// + ///Windows 2000/XP: Browser Refresh key + /// + BROWSER_REFRESH = 0xA8, + + /// + ///Windows 2000/XP: Browser Stop key + /// + BROWSER_STOP = 0xA9, + + /// + ///Windows 2000/XP: Browser Search key + /// + BROWSER_SEARCH = 0xAA, + + /// + ///Windows 2000/XP: Browser Favorites key + /// + BROWSER_FAVORITES = 0xAB, + + /// + ///Windows 2000/XP: Browser Start and Home key + /// + BROWSER_HOME = 0xAC, + + /// + ///Windows 2000/XP: Volume Mute key + /// + VOLUME_MUTE = 0xAD, + + /// + ///Windows 2000/XP: Volume Down key + /// + VOLUME_DOWN = 0xAE, + + /// + ///Windows 2000/XP: Volume Up key + /// + VOLUME_UP = 0xAF, + + /// + ///Windows 2000/XP: Next Track key + /// + MEDIA_NEXT_TRACK = 0xB0, + + /// + ///Windows 2000/XP: Previous Track key + /// + MEDIA_PREV_TRACK = 0xB1, + + /// + ///Windows 2000/XP: Stop Media key + /// + MEDIA_STOP = 0xB2, + + /// + ///Windows 2000/XP: Play/Pause Media key + /// + MEDIA_PLAY_PAUSE = 0xB3, + + /// + ///Windows 2000/XP: Start Mail key + /// + LAUNCH_MAIL = 0xB4, + + /// + ///Windows 2000/XP: Select Media key + /// + LAUNCH_MEDIA_SELECT = 0xB5, + + /// + ///Windows 2000/XP: Start Application 1 key + /// + LAUNCH_APP1 = 0xB6, + + /// + ///Windows 2000/XP: Start Application 2 key + /// + LAUNCH_APP2 = 0xB7, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_1 = 0xBA, + + /// + ///Windows 2000/XP: For any country/region, the '+' key + /// + OEM_PLUS = 0xBB, + + /// + ///Windows 2000/XP: For any country/region, the ',' key + /// + OEM_COMMA = 0xBC, + + /// + ///Windows 2000/XP: For any country/region, the '-' key + /// + OEM_MINUS = 0xBD, + + /// + ///Windows 2000/XP: For any country/region, the '.' key + /// + OEM_PERIOD = 0xBE, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_2 = 0xBF, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_3 = 0xC0, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_4 = 0xDB, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_5 = 0xDC, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_6 = 0xDD, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_7 = 0xDE, + + /// + ///Used for miscellaneous characters; it can vary by keyboard. + /// + OEM_8 = 0xDF, + + /// + ///Windows 2000/XP: Either the angle bracket key or the backslash key on the RT 102-key keyboard + /// + OEM_102 = 0xE2, + + /// + ///Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key + /// + PROCESSKEY = 0xE5, + + /// + ///Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. + ///The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, + ///see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP + /// + PACKET = 0xE7, + + /// + ///Attn key + /// + ATTN = 0xF6, + + /// + ///CrSel key + /// + CRSEL = 0xF7, + + /// + ///ExSel key + /// + EXSEL = 0xF8, + + /// + ///Erase EOF key + /// + EREOF = 0xF9, + + /// + ///Play key + /// + PLAY = 0xFA, + + /// + ///Zoom key + /// + ZOOM = 0xFB, + + /// + ///Reserved + /// + NONAME = 0xFC, + + /// + ///PA1 key + /// + PA1 = 0xFD, + + /// + ///Clear key + /// + OEM_CLEAR = 0xFE + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/Simulator.cs b/Core/Key2Joy.Core/LowLevelInput/Simulator.cs index 8c913b83..4d486a79 100644 --- a/Core/Key2Joy.Core/LowLevelInput/Simulator.cs +++ b/Core/Key2Joy.Core/LowLevelInput/Simulator.cs @@ -1,66 +1,62 @@ -using System.Runtime.InteropServices; - -namespace Key2Joy.LowLevelInput; - -public class Simulator -{ - /// - /// Declaration of external SendInput method - /// - [DllImport("user32.dll")] - public static extern uint SendInput( - uint nInputs, - [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, - int cbSize); - - [DllImport("user32.dll")] - private static extern int GetSystemMetrics(SystemMetric smIndex); - - public static int CalculateAbsoluteCoordinateX(int x) => x * 65536 / GetSystemMetrics(SystemMetric.SM_CXSCREEN); - - public static int CalculateAbsoluteCoordinateY(int y) => y * 65536 / GetSystemMetrics(SystemMetric.SM_CYSCREEN); - - public enum GamePadStick - { - Left = 0, - Right = 1, - } - - private enum SystemMetric - { - SM_CXSCREEN = 0, - SM_CYSCREEN = 1, - } - - // Declare the INPUT struct - [StructLayout(LayoutKind.Sequential)] - public struct INPUT - { - public uint type; - public InputUnion U; - public static int Size => Marshal.SizeOf(typeof(INPUT)); - } - - // Declare the InputUnion struct - [StructLayout(LayoutKind.Explicit)] - public struct InputUnion - { - [FieldOffset(0)] - public SimulatedMouse.MOUSEINPUT mi; - [FieldOffset(0)] - public SimulatedKeyboard.KEYBDINPUT ki; - [FieldOffset(0)] - public HARDWAREINPUT hi; - } - - /// - /// Define HARDWAREINPUT struct - /// - [StructLayout(LayoutKind.Sequential)] - public struct HARDWAREINPUT - { - public int uMsg; - public short wParamL; - public short wParamH; - } -} +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput; + +public class Simulator +{ + /// + /// Declaration of external SendInput method + /// + [DllImport("user32.dll")] + public static extern uint SendInput( + uint nInputs, + [MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs, + int cbSize); + + [DllImport("user32.dll")] + private static extern int GetSystemMetrics(SystemMetric smIndex); + + public static int CalculateAbsoluteCoordinateX(int x) => x * 65536 / GetSystemMetrics(SystemMetric.SM_CXSCREEN); + + public static int CalculateAbsoluteCoordinateY(int y) => y * 65536 / GetSystemMetrics(SystemMetric.SM_CYSCREEN); + + private enum SystemMetric + { + SM_CXSCREEN = 0, + SM_CYSCREEN = 1, + } + + // Declare the INPUT struct + [StructLayout(LayoutKind.Sequential)] + public struct INPUT + { + public uint type; + public InputUnion U; + public static int Size => Marshal.SizeOf(typeof(INPUT)); + } + + // Declare the InputUnion struct + [StructLayout(LayoutKind.Explicit)] + public struct InputUnion + { + [FieldOffset(0)] + public SimulatedMouse.MOUSEINPUT mi; + + [FieldOffset(0)] + public SimulatedKeyboard.KEYBDINPUT ki; + + [FieldOffset(0)] + public HARDWAREINPUT hi; + } + + /// + /// Define HARDWAREINPUT struct + /// + [StructLayout(LayoutKind.Sequential)] + public struct HARDWAREINPUT + { + public int uMsg; + public short wParamL; + public short wParamH; + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryDeviceType.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryDeviceType.cs new file mode 100644 index 00000000..4ad505df --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryDeviceType.cs @@ -0,0 +1,17 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the battery device types for XInput Game Controllers. +/// +public enum BatteryDeviceType : byte +{ + /// + /// The device type is a gamepad. + /// + BATTERY_DEVTYPE_GAMEPAD = 0x00, + + /// + /// The device type is a headset. + /// + BATTERY_DEVTYPE_HEADSET = 0x01, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryLevel.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryLevel.cs new file mode 100644 index 00000000..992a7505 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryLevel.cs @@ -0,0 +1,27 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the charge state of the battery for a wireless XInput device. +/// +public enum BatteryLevel : byte +{ + /// + /// The battery is empty. + /// + BATTERY_LEVEL_EMPTY = 0x00, + + /// + /// The battery is low. + /// + BATTERY_LEVEL_LOW = 0x01, + + /// + /// The battery is at a medium charge level. + /// + BATTERY_LEVEL_MEDIUM = 0x02, + + /// + /// The battery is full. + /// + BATTERY_LEVEL_FULL = 0x03, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryTypes.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryTypes.cs new file mode 100644 index 00000000..0eb936f0 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/BatteryTypes.cs @@ -0,0 +1,32 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the type of battery for an XInput device. +/// +public enum BatteryTypes : byte +{ + /// + /// This device is not connected. + /// + BATTERY_TYPE_DISCONNECTED = 0x00, + + /// + /// Wired device, no battery. + /// + BATTERY_TYPE_WIRED = 0x01, + + /// + /// Alkaline battery source. + /// + BATTERY_TYPE_ALKALINE = 0x02, + + /// + /// Nickel Metal Hydride battery source. + /// + BATTERY_TYPE_NIMH = 0x03, + + /// + /// Cannot determine the battery type. + /// + BATTERY_TYPE_UNKNOWN = 0xFF, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityFlags.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityFlags.cs new file mode 100644 index 00000000..aa8d7754 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityFlags.cs @@ -0,0 +1,34 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Flags representing various capabilities of an XInput controller. +/// +public enum CapabilityFlags +{ + /// + /// The device has an integrated voice device. + /// + XINPUT_CAPS_VOICE_SUPPORTED = 0x0004, + + /// + /// The device supports force feedback functionality. Note that these force-feedback features beyond rumble + /// are not currently supported through XINPUT on Windows. + /// + XINPUT_CAPS_FFB_SUPPORTED = 0x0001, + + /// + /// The device is wireless. + /// + XINPUT_CAPS_WIRELESS = 0x0002, + + /// + /// The device supports plug-in modules. Note that plug-in modules like the text input device (TID) + /// are not supported currently through XINPUT on Windows. + /// + XINPUT_CAPS_PMD_SUPPORTED = 0x0008, + + /// + /// The device lacks menu navigation buttons (START, BACK, DPAD). + /// + XINPUT_CAPS_NO_NAVIGATION = 0x0010, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityRequestFlag.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityRequestFlag.cs new file mode 100644 index 00000000..4df65cd4 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/CapabilityRequestFlag.cs @@ -0,0 +1,13 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// requires a flag to specify which device type to retrieve +/// capabilities for. This is the only available flag. +/// +public enum CapabilityRequestFlag : int +{ + /// + /// Limit query to devices of Xbox 360 Controller type. + /// + XINPUT_FLAG_GAMEPAD = 0x00000001, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/DevicePacketReceivedEventArgs.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/DevicePacketReceivedEventArgs.cs new file mode 100644 index 00000000..522447ec --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/DevicePacketReceivedEventArgs.cs @@ -0,0 +1,28 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Provides data for the DevicePacketReceivedEventArgs event. +/// +public class DevicePacketReceivedEventArgs +{ + /// + /// Gets the index of the device that has sent a packet. + /// + public int DeviceIndex { get; } + + /// + /// Gets the state of the device. + /// + public XInputState State { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The index of the device that has sent a packet. + /// The state of the device. + public DevicePacketReceivedEventArgs(int deviceIndex, XInputState state) + { + this.DeviceIndex = deviceIndex; + this.State = state; + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceStateChangedEventArgs.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceStateChangedEventArgs.cs new file mode 100644 index 00000000..4e37d3be --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceStateChangedEventArgs.cs @@ -0,0 +1,30 @@ +using System; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Provides data for the DeviceStateChanged event. +/// +public class DeviceStateChangedEventArgs : EventArgs +{ + /// + /// Gets the index of the device that has changed state. + /// + public int DeviceIndex { get; } + + /// + /// Gets the new state of the device. + /// + public XInputState NewState { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The index of the device that has changed state. + /// The new state of the device. + public DeviceStateChangedEventArgs(int deviceIndex, XInputState newState) + { + this.DeviceIndex = deviceIndex; + this.NewState = newState; + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceSubType.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceSubType.cs new file mode 100644 index 00000000..3aebe2c4 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceSubType.cs @@ -0,0 +1,90 @@ +using System; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Specifies the subtype of an XInput device. +/// +[Flags] +public enum DeviceSubType : byte +{ + /// + /// Unknown controller subtype. + /// + XINPUT_DEVSUBTYPE_UNKNOWN = 0x00, + + /// + /// Gamepad controller subtype. + /// Includes Left and Right Sticks, Left and Right Triggers, Directional Pad, + /// and all standard buttons (A, B, X, Y, START, BACK, LB, RB, LSB, RSB). + /// + XINPUT_DEVSUBTYPE_GAMEPAD = 0x01, + + /// + /// Racing wheel controller subtype. + /// Left Stick X reports the wheel rotation, Right Trigger is the acceleration pedal, + /// and Left Trigger is the brake pedal. Includes Directional Pad and most standard buttons + /// (A, B, X, Y, START, BACK, LB, RB). LSB and RSB are optional. + /// + XINPUT_DEVSUBTYPE_WHEEL = 0x02, + + /// + /// Arcade stick controller subtype. + /// Includes a Digital Stick that reports as a DPAD (up, down, left, right), and most standard buttons + /// (A, B, X, Y, START, BACK). The Left and Right Triggers are implemented as digital buttons and + /// report either 0 or 0xFF. LB, LSB, RB, and RSB are optional. + /// + XINPUT_DEVSUBTYPE_ARCADE_STICK = 0x03, + + /// + /// Flight stick controller subtype. + /// Includes a pitch and roll stick that reports as the Left Stick, a POV Hat which reports as the + /// Right Stick, a rudder (handle twist or rocker) that reports as Left Trigger, and a throttle control + /// as the Right Trigger. Includes support for a primary weapon (A), secondary weapon (B), and other + /// standard buttons (X, Y, START, BACK). LB, LSB, RB, and RSB are optional. + /// + XINPUT_DEVSUBTYPE_FLIGHT_STICK = 0x04, + + /// + /// Dance pad controller subtype. + /// Includes the Directional Pad and standard buttons (A, B, X, Y) on the pad, plus BACK and START. + /// + XINPUT_DEVSUBTYPE_DANCE_PAD = 0x05, + + /// + /// Guitar controller subtype. + /// The strum bar maps to DPAD (up and down), and the frets are assigned to A (green), B (red), + /// Y (yellow), X (blue), and LB (orange). Right Stick Y is associated with a vertical orientation sensor; + /// Right Stick X is the whammy bar. Includes support for BACK, START, DPAD (left, right). + /// Left Trigger (pickup selector), Right Trigger, RB, LSB (fret modifier), RSB are optional. + /// + XINPUT_DEVSUBTYPE_GUITAR = 0x06, + + /// + /// Alternate guitar controller subtype. + /// Supports a larger range of movement for the vertical orientation sensor. + /// + XINPUT_DEVSUBTYPE_GUITAR_ALTERNATE = 0x07, + + /// + /// Drum controller subtype. + /// The drum pads are assigned to buttons: A for green (Floor Tom), B for red (Snare Drum), + /// X for blue (Low Tom), Y for yellow (High Tom), and LB for the pedal (Bass Drum). + /// Includes Directional-Pad, BACK, and START. RB, LSB, and RSB are optional. + /// + XINPUT_DEVSUBTYPE_DRUM_KIT = 0x08, + + /// + /// Bass guitar controller subtype. + /// Identical to Guitar, with the distinct subtype to simplify setup. + /// + XINPUT_DEVSUBTYPE_GUITAR_BASS = 0x0B, + + /// + /// Arcade pad controller subtype. + /// Includes Directional Pad and most standard buttons (A, B, X, Y, START, BACK, LB, RB). + /// The Left and Right Triggers are implemented as digital buttons and report either 0 or 0xFF. + /// Left Stick, Right Stick, LSB, and RSB are optional. + /// + XINPUT_DEVSUBTYPE_ARCADE_PAD = 0x13 +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceType.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceType.cs new file mode 100644 index 00000000..a05afbac --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/DeviceType.cs @@ -0,0 +1,12 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Device type available in XINPUT_CAPABILITIES +/// +public enum DeviceType : byte +{ + /// + /// The device is a game controller. + /// + Gamepad = 0x01, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/GamePadButton.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/GamePadButton.cs new file mode 100644 index 00000000..3a251e3b --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/GamePadButton.cs @@ -0,0 +1,80 @@ +using System; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents a set of flags that indicate the state of various buttons on a device. +/// +[Flags] +public enum GamePadButton : int +{ + /// + /// The Up button on the directional pad. + /// + DPadUp = 0x0001, + + /// + /// The Down button on the directional pad. + /// + DPadDown = 0x0002, + + /// + /// The Left button on the directional pad. + /// + DPadLeft = 0x0004, + + /// + /// The Right button on the directional pad. + /// + DPadRight = 0x0008, + + /// + /// The Start button. + /// + Start = 0x0010, + + /// + /// The Back button. + /// + Back = 0x0020, + + /// + /// The Left Thumbstick button. + /// + LeftThumbstick = 0x0040, + + /// + /// The Right Thumbstick button. + /// + RightThumbstick = 0x0080, + + /// + /// The Left Shoulder button. + /// + LeftShoulder = 0x0100, + + /// + /// The Right Shoulder button. + /// + RightShoulder = 0x0200, + + /// + /// The A button. + /// + A = 0x1000, + + /// + /// The B button. + /// + B = 0x2000, + + /// + /// The X button. + /// + X = 0x4000, + + /// + /// The Y button. + /// + Y = 0x8000, +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/IXInput.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/IXInput.cs new file mode 100644 index 00000000..36a4b9ad --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/IXInput.cs @@ -0,0 +1,60 @@ +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Provides an interface for the XInput functionality. +/// +public interface IXInput +{ + /// + /// Retrieves the state of a controller. + /// + /// Index of the gamer associated with the device. + /// Receives the current state. + /// Returns the result code. + XInputResultCode XInputGetState(int userIndex, ref XInputState inputState); + + /// + /// Sets the vibration state of a controller. + /// + /// Index of the gamer associated with the device. + /// The vibration information to send to the controller. + /// Returns the result code. + XInputResultCode XInputSetState(int userIndex, ref XInputVibration vibrationInfo); + + /// + /// Retrieves the capabilities of a controller. + /// + /// + /// Index of the gamer associated with the device. + /// Input flags that identify the device type. + /// Receives the capabilities. + /// Returns the result code. + XInputResultCode XInputGetCapabilities(int userIndex, CapabilityRequestFlag flags, ref XInputCapabilities capabilities); + + /// + /// Retrieves the capabilities of a controller for the default device type (gamepad). + /// + /// + /// Index of the gamer associated with the device. + /// Receives the capabilities. + /// Returns the result code. + XInputResultCode XInputGetCapabilities(int userIndex, ref XInputCapabilities capabilities); + + /// + /// Retrieves battery information for a controller. + /// + /// Index of the gamer associated with the device. + /// Which device on this user index. + /// Contains the level and types of batteries. + /// Returns the result code. + XInputResultCode XInputGetBatteryInformation(int userIndex, BatteryDeviceType deviceType, ref XInputBatteryInformation batteryInformation); + + /// + /// Retrieves a keystroke event from a controller. + /// + /// Index of the gamer associated with the device. + /// Reserved for future use. + /// Pointer to an XINPUT_KEYSTROKE structure that receives an input event. + /// Returns the result code. + XInputResultCode XInputGetKeystroke(int userIndex, int reserved, ref XInputKeystroke keystrokeData); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/IXInputService.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/IXInputService.cs new file mode 100644 index 00000000..3f0ef032 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/IXInputService.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the interface for interacting with XInput devices. +/// +public interface IXInputService +{ + /// + /// Occurs when the state of a registered device changes. + /// + event EventHandler StateChanged; + + /// + /// Must be called before simulated gamepads are plugged in. + /// This should recognize which of the gamepads are physical and it + /// should register them. + /// + void RecognizePhysicalDevices(); + + /// + /// Starts polling all registered devices for state changes. + /// + void StartPolling(); + + /// + /// Stops polling all registered devices. + /// + void StopPolling(); + + /// + /// Retrieves the current state of the specified device. + /// + /// The index of the device. + /// The current state of the device or null if the device is invalid. + XInputState? GetState(int deviceIndex); + + /// + /// Retrieves the capabilities of the specified device. + /// + /// The index of the device. + /// The capabilities of the device. + XInputCapabilities GetCapabilities(int deviceIndex); + + /// + /// Retrieves battery information for the specified device. + /// + /// The index of the device. + /// Specifies which type of device (e.g. gamepad, headset) on this user index to retrieve information for. + /// Battery information of the specified device. + XInputBatteryInformation GetBatteryInformation(int deviceIndex, BatteryDeviceType deviceType); + + /// + /// Retrieves a keystroke event from the specified device. + /// + /// The index of the device. + /// Keystroke data from the device. + XInputKeystroke GetKeystroke(int deviceIndex); + + /// + /// Vibrates the given device's left and or right motor by the specified intensity. + /// + /// The index of the device + /// Fraction (0-1) indicating left motor intensity + /// Fraction (0-1) indicating right motor intensity + /// How long to vibrate for + void Vibrate(int deviceIndex, double leftMotorSpeedFraction, double rightMotorSpeedFraction, TimeSpan duration); + + /// + /// Stops vibration of the given device. + /// + /// The index of the device + void StopVibration(int deviceIndex); + + /// + /// Gets the active gamepad device indexes. + /// + /// + IList GetActiveDevicesInfo(); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/NativeXInput.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/NativeXInput.cs new file mode 100644 index 00000000..59ed24da --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/NativeXInput.cs @@ -0,0 +1,54 @@ +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Handles XInput functionality by calling native functions. +/// +/// +public class NativeXInput : IXInput +{ + [DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")] + private static extern int InternalXInputGetState(int dwUserIndex, ref XInputState pState); + + [DllImport("xinput1_4.dll", EntryPoint = "XInputSetState")] + private static extern int InternalXInputSetState(int dwUserIndex, ref XInputVibration pVibration); + + [DllImport("xinput1_4.dll", EntryPoint = "XInputGetCapabilities")] + private static extern int InternalXInputGetCapabilities(int dwUserIndex, int dwFlags, ref XInputCapabilities pCapabilities); + + [DllImport("xinput1_4.dll", EntryPoint = "XInputGetBatteryInformation")] + private static extern int InternalXInputGetBatteryInformation(int dwUserIndex, byte devType, ref XInputBatteryInformation pBatteryInformation); + + [DllImport("xinput1_4.dll", EntryPoint = "XInputGetKeystroke")] + private static extern int InternalXInputGetKeystroke(int dwUserIndex, int dwReserved, ref XInputKeystroke pKeystroke); + + /// + public XInputResultCode XInputGetState(int userIndex, ref XInputState inputState) + => XInputResult.FromResult(InternalXInputGetState(userIndex, ref inputState)); + + /// + public XInputResultCode XInputSetState(int userIndex, ref XInputVibration vibrationInfo) + => XInputResult.FromResult(InternalXInputSetState(userIndex, ref vibrationInfo)); + + /// + public XInputResultCode XInputGetCapabilities(int userIndex, CapabilityRequestFlag flags, ref XInputCapabilities capabilities) + => XInputResult.FromResult(InternalXInputGetCapabilities(userIndex, (int)flags, ref capabilities)); + + /// + public XInputResultCode XInputGetBatteryInformation(int userIndex, BatteryDeviceType deviceType, ref XInputBatteryInformation batteryInformation) + => XInputResult.FromResult(InternalXInputGetBatteryInformation(userIndex, (byte)deviceType, ref batteryInformation)); + + /// + public XInputResultCode XInputGetKeystroke(int userIndex, int reserved, ref XInputKeystroke keystrokeData) + => XInputResult.FromResult(InternalXInputGetKeystroke(userIndex, reserved, ref keystrokeData)); + + /// + /// Get capabilities for the specified controller. + /// + /// + /// + /// + public XInputResultCode XInputGetCapabilities(int userIndex, ref XInputCapabilities capabilities) + => XInputResult.FromResult(InternalXInputGetCapabilities(userIndex, (int)CapabilityRequestFlag.XINPUT_FLAG_GAMEPAD, ref capabilities)); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputBatteryInformation.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputBatteryInformation.cs new file mode 100644 index 00000000..5f1c16ce --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputBatteryInformation.cs @@ -0,0 +1,35 @@ +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents battery information for an XInput device, including its type and charge state. +/// +[StructLayout(LayoutKind.Explicit)] +public struct XInputBatteryInformation +{ + /// + /// Gets or sets the type of battery for the XInput device. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(0)] + public byte BatteryType; + + /// + /// Gets or sets the charge state of the battery for the XInput device. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(1)] + public byte BatteryLevel; + + /// + /// Returns a string representation of the XInputBatteryInformation struct. + /// + /// A string containing the battery type and charge level. + public override readonly string ToString() + => string.Format( + "{0} {1}", + (BatteryTypes)this.BatteryType, + (BatteryLevel)this.BatteryLevel + ); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputCapabilities.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputCapabilities.cs new file mode 100644 index 00000000..4a682628 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputCapabilities.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Describes the capabilities of a connected controller. +/// The function returns this. +/// +/// +[StructLayout(LayoutKind.Explicit)] +public struct XInputCapabilities +{ + /// + /// Device type. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(0)] + public byte RawType; + + /// + /// Subtype of the game controller. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(1)] + public byte RawSubType; + + /// + /// Features of the controller. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(2)] + public short Flags; + + /// + /// XINPUT_GAMEPAD structure that describes available controller features + /// and control resolutions. + /// + [FieldOffset(4)] + public XInputGamePad Gamepad; + + /// + /// XINPUT_VIBRATION structure that describes available vibration functionality + /// and resolutions. + /// + [FieldOffset(16)] + public XInputVibration Vibration; + + /// + /// Device type. + /// + public readonly DeviceType Type + => (DeviceType)this.RawType; + + /// + /// Device sub type. + /// + public readonly DeviceSubType SubType + => Enum.IsDefined(typeof(DeviceSubType), this.RawSubType) + ? (DeviceSubType)this.RawSubType + : DeviceSubType.XINPUT_DEVSUBTYPE_GAMEPAD; + + /// + /// The capability flags. + /// + public readonly CapabilityFlags CapabilityFlags + => (CapabilityFlags)this.Flags; +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputGamepad.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputGamepad.cs new file mode 100644 index 00000000..675cb92a --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputGamepad.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Triggers.GamePad; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the state of a controller. +/// +[StructLayout(LayoutKind.Explicit)] +public struct XInputGamePad : IEquatable +{ + public const int ThumbstickValueMin = short.MinValue; + public const int ThumbstickValueMax = short.MaxValue; + public const int TriggerValueMin = 0; + public const int TriggerValueMax = 255; + + /// + /// Can be used as a positive and negative value to filter left thumbstick input. + /// + public const int XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE = 7849; + + /// + /// Can be used as a positive and negative value to filter right thumbstick input. + /// + public const int XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE = 8689; + + /// + /// May be used as the value which bLeftTrigger and bRightTrigger must be greater than to register as pressed. This is optional, but often desirable. Xbox 360 Controller buttons do not manifest crosstalk. + /// + public const int XINPUT_GAMEPAD_TRIGGER_THRESHOLD = 30; + + /// + /// Bitmask of the device digital buttons. A set bit indicates that the corresponding button is pressed. + /// + /// + /// Refer to XINPUT_GAMEPAD_* bitmasks for specific button mappings. + /// Bits that are set but not defined are reserved, and their state is undefined. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(0)] + public short ButtonsBitmask; + + /// + /// The current value of the left trigger analog control. The value is between 0 and 255. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(2)] + public byte LeftTrigger; + + /// + /// The current value of the right trigger analog control. The value is between 0 and 255. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(3)] + public byte RightTrigger; + + /// + /// Left thumbstick x-axis value. Negative values signify down or to the left, + /// positive values signify up or to the right. The value is between -32768 and 32767. + /// A value of 0 is centered. Negative values signify down or to the left. Positive values + /// signify up or to the right. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(4)] + public short LeftThumbX; + + /// + /// Left thumbstick y-axis value. The value is between -32768 and 32767. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(6)] + public short LeftThumbY; + + /// + /// Right thumbstick x-axis value. The value is between -32768 and 32767. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(8)] + public short RightThumbX; + + /// + /// Right thumbstick y-axis value. The value is between -32768 and 32767. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(10)] + public short RightThumbY; + + /// + /// Checks if a specific button or buttons represented by the bitmask are pressed. + /// + /// The bitmask representing the button(s). + /// True if the button(s) are pressed, otherwise false. + public readonly bool IsButtonPressed(GamePadButton buttonFlags) + => (this.ButtonsBitmask & (int)buttonFlags) == (int)buttonFlags; + + /// + /// Checks if a specific button or buttons represented by the bitmask are present. + /// + /// The bitmask representing the button(s). + /// True if the button(s) are present, otherwise false. + public readonly bool IsButtonPresent(GamePadButton buttonFlags) + => (this.ButtonsBitmask & (int)buttonFlags) == (int)buttonFlags; + + /// + /// Returns all pressed buttons in a List. + /// + /// + public readonly IList GetPressedButtonsList() + { + var pressedButtons = new List(); + + foreach (GamePadButton button in Enum.GetValues(typeof(GamePadButton))) + { + if (this.IsButtonPressed(button)) + { + pressedButtons.Add(button); + } + } + + return pressedButtons; + } + + /// + /// Checks if the thumb stick on the given side has moved past a certain threshold (or else the default deadzone) + /// + /// + /// + /// + public readonly bool IsThumbstickMoved(GamePadSide side, ExactAxisDirection? deltaMargin = null) + { + var defaultDeadzone = side == GamePadSide.Left ? XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE : XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + var thumbstickX = side == GamePadSide.Left ? this.LeftThumbX : this.RightThumbX; + var thumbstickY = side == GamePadSide.Left ? this.LeftThumbY : this.RightThumbY; + var deadzoneX = deltaMargin?.X * ThumbstickValueMax ?? defaultDeadzone; + var deadzoneY = deltaMargin?.Y * ThumbstickValueMax ?? defaultDeadzone; + + // We must convert to an int, otherwise the absolute of short -32768 (32768) would fail since it's too big + if (Math.Abs((int)thumbstickX) > deadzoneX || Math.Abs((int)thumbstickY) > deadzoneY) + { + return true; + } + + return false; + } + + /// + /// Checks if the trigger on a certain side has pulled back past a certain threshold (or else the default deadzone) + /// + /// + /// + /// + public readonly bool IsTriggerPulled(GamePadSide side, float? deltaMargin) + { + var deadzone = deltaMargin ?? XINPUT_GAMEPAD_TRIGGER_THRESHOLD; + var trigger = side == GamePadSide.Left ? this.LeftTrigger : this.RightTrigger; + + if (trigger > deadzone) + { + return true; + } + + return false; + } + + /// + /// Returns the stick delta for a given side as an exact axis fraction. + /// + /// + /// + public readonly ExactAxisDirection GetStickDelta(GamePadSide side) + { + if (side == GamePadSide.Left) + { + return new ExactAxisDirection( + (float)this.LeftThumbX / ThumbstickValueMax, + (float)this.LeftThumbY / ThumbstickValueMax); + } + + return new ExactAxisDirection( + (float)this.RightThumbX / ThumbstickValueMax, + (float)this.RightThumbY / ThumbstickValueMax); + } + + /// + /// Returns the trigger delta for a given side as an exact axis fraction. + /// + /// + /// + public readonly float GetTriggerDelta(GamePadSide side) + { + if (side == GamePadSide.Left) + { + return (float)this.LeftTrigger / TriggerValueMax; + } + + return (float)this.RightTrigger / TriggerValueMax; + } + + /// + /// Copies the values from the source gamepad to the current instance. + /// + /// The source gamepad from which values should be copied. + public void Copy(XInputGamePad source) + { + this.LeftThumbX = source.LeftThumbX; + this.LeftThumbY = source.LeftThumbY; + this.RightThumbX = source.RightThumbX; + this.RightThumbY = source.RightThumbY; + this.LeftTrigger = source.LeftTrigger; + this.RightTrigger = source.RightTrigger; + this.ButtonsBitmask = source.ButtonsBitmask; + } + + /// + public readonly bool Equals(XInputGamePad other) + => this.ButtonsBitmask == other.ButtonsBitmask + && this.LeftTrigger == other.LeftTrigger + && this.RightTrigger == other.RightTrigger + && this.LeftThumbX == other.LeftThumbX + && this.LeftThumbY == other.LeftThumbY + && this.RightThumbX == other.RightThumbX + && this.RightThumbY == other.RightThumbY; + + /// + public override readonly bool Equals(object obj) + => obj is XInputGamePad gamepad && this.Equals(gamepad); + + /// + public override int GetHashCode() + { + var hashCode = 235782390; + hashCode = (hashCode * -1521134295) + this.ButtonsBitmask.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.LeftTrigger.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.RightTrigger.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.LeftThumbX.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.LeftThumbY.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.RightThumbX.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.RightThumbY.GetHashCode(); + return hashCode; + } + + public override string ToString() + => $"{nameof(XInputGamePad)} Buttons: {this.ButtonsBitmask}, LeftTrigger: {this.LeftTrigger}, RightTrigger: {this.RightTrigger}, LeftThumb: ({this.LeftThumbX}, {this.LeftThumbY}), RightThumb: ({this.RightThumbX}, {this.RightThumbY})"; +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputKeystroke.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputKeystroke.cs new file mode 100644 index 00000000..63b6f31e --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputKeystroke.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents a structure containing information about a keystroke input event. +/// +[StructLayout(LayoutKind.Explicit)] +public struct XInputKeystroke +{ + /// + /// Virtual-key code of the key, button, or stick movement. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(0)] + public short VirtualKey; + + /// + /// This member is unused and the value is zero. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(2)] + public char Unicode; + + /// + /// Flags that indicate the keyboard state at the time of the input event. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(4)] + public short Flags; + + /// + /// Index of the signed-in gamer associated with the device. + /// + [MarshalAs(UnmanagedType.I2)] + [FieldOffset(5)] + public byte UserIndex; + + /// + /// HID code corresponding to the input. If there is no corresponding HID code, this value is zero. + /// + [MarshalAs(UnmanagedType.I1)] + [FieldOffset(6)] + public byte HidCode; +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputResultCode.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputResultCode.cs new file mode 100644 index 00000000..ac6e35c8 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputResultCode.cs @@ -0,0 +1,40 @@ +using System; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the result codes for XInput operations. +/// +/// Note that mentions: +/// "If the function fails, the return value is an error code defined in WinError.h. The function does not use SetLastError to set the calling thread's last-error code." +/// +/// This may mean that we'll have to add those possible error codes here, or at least the ones we're interested in. +/// +public enum XInputResultCode : int +{ + /// + /// The operation completed successfully. + /// + ERROR_SUCCESS = 0, + + /// + /// The requested resource is empty or not found. + /// + ERROR_EMPTY = 4306, + + /// + /// The XInput device is not connected or available. + /// + ERROR_DEVICE_NOT_CONNECTED = 1167 +} + +/// +/// Helps translate result codes from Native methods +/// +public static class XInputResult +{ + public static XInputResultCode FromResult(int result) + => Enum.IsDefined(typeof(XInputResultCode), result) + ? (XInputResultCode)result + : throw new ArgumentOutOfRangeException(nameof(result), result, $"The result code {result} is not defined in {nameof(XInputResultCode)}."); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputService.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputService.cs new file mode 100644 index 00000000..3151869f --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputService.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Timer = System.Timers.Timer; + +namespace Key2Joy.LowLevelInput.XInput; + +public class XInputService : IXInputService +{ + private const string GAMEPAD_NAME = "Physical"; + + public const int MaxDevices = 4; + private const int UpdateIntervalInMs = 20; + + /// + /// Called when the state of a device changes. + /// + public event EventHandler StateChanged; + + /// + /// Called whenever the device sends a new packet + /// + public event EventHandler PacketReceived; + + private readonly IXInput xInputInstance; + private readonly Dictionary registeredDevices; + private readonly Dictionary vibrationTimers; + private readonly Dictionary lastStates; + private Thread pollingThread; + private bool isPolling; + + public XInputService(IXInput xinputInstance = null) + { + this.xInputInstance = xinputInstance ?? new NativeXInput(); + this.registeredDevices = new(); + this.vibrationTimers = new(); + this.lastStates = new(); + } + + private bool GetIsDeviceConnected(int deviceIndex) + { + var state = new XInputState(); + var resultCode = this.xInputInstance.XInputGetState(deviceIndex, ref state); + + return resultCode == XInputResultCode.ERROR_SUCCESS; + } + + /// + public void RecognizePhysicalDevices() + { + // If we've started polling then what is connected is what is connected. + if (this.isPolling) + { + return; + } + + lock (this.registeredDevices) + { + this.registeredDevices.Clear(); + + for (var i = 0; i < MaxDevices; i++) + { + this.RegisterDevice(i); + } + } + } + + /// + /// Registers a device by its index for monitoring its state. + /// + /// The index of the device to register. + private void RegisterDevice(int deviceIndex) + { + if (!this.GetIsDeviceConnected(deviceIndex)) + { + return; + } + + this.registeredDevices.Add(deviceIndex, new GamePadInfo(deviceIndex, GAMEPAD_NAME)); + } + + /// + public void StartPolling() + { + if (this.isPolling) + { + return; + } + + this.isPolling = true; + + this.pollingThread = new Thread(() => + { + lock (this.registeredDevices) + { + do + { + // Add a delay to avoid hammering the IXInput instance too rapidly + Thread.Sleep(UpdateIntervalInMs); + + foreach (var device in this.registeredDevices.Values) + { + var deviceIndex = device.Index; + var newState = new XInputState(); + var resultCode = this.xInputInstance.XInputGetState(deviceIndex, ref newState); + + if (resultCode != XInputResultCode.ERROR_SUCCESS) + { + continue; + } + + this.PacketReceived?.Invoke(this, new DevicePacketReceivedEventArgs(deviceIndex, newState)); + var hasLastState = this.lastStates.TryGetValue(deviceIndex, out var lastState); + + if (!hasLastState || !lastState.Equals(newState)) + { + if (!hasLastState) + { + this.lastStates.Add(deviceIndex, newState); + } + else + { + this.lastStates[deviceIndex] = newState; + this.StateChanged?.Invoke(this, new DeviceStateChangedEventArgs(deviceIndex, newState)); + device.OnActivityOccurred(); + } + } + } + } while (this.isPolling); + } + }); + + this.pollingThread.Start(); + } + + /// + public void StopPolling() + => this.isPolling = false; + + /// + public XInputState? GetState(int deviceIndex) + { + if (!this.registeredDevices.ContainsKey(deviceIndex)) + { + // Only return the state for devices registered before simulated + // devices were added. XInputGetState also returns those, so we + // need to check it manually. + return null; + } + + var inputState = new XInputState(); + this.xInputInstance.XInputGetState(deviceIndex, ref inputState); + + return inputState; + } + + /// + public XInputCapabilities GetCapabilities(int deviceIndex) + { + var capabilities = new XInputCapabilities(); + + this.xInputInstance.XInputGetCapabilities(deviceIndex, ref capabilities); + + return capabilities; + } + + /// + public XInputBatteryInformation GetBatteryInformation(int deviceIndex, BatteryDeviceType deviceType) + { + var batteryInformation = new XInputBatteryInformation(); + + this.xInputInstance.XInputGetBatteryInformation(deviceIndex, deviceType, ref batteryInformation); + + return batteryInformation; + } + + /// + public XInputKeystroke GetKeystroke(int deviceIndex) + { + var keystrokeData = new XInputKeystroke(); + + this.xInputInstance.XInputGetKeystroke(deviceIndex, 0, ref keystrokeData); + + return keystrokeData; + } + + /// + public void Vibrate(int deviceIndex, double leftMotorSpeedFraction, double rightMotorSpeedFraction, TimeSpan duration) + { + var vibrationInfo = new XInputVibration(leftMotorSpeedFraction, rightMotorSpeedFraction); + this.xInputInstance.XInputSetState(deviceIndex, ref vibrationInfo); + + // If a timer for this device is already running, stop it first + if (this.vibrationTimers.ContainsKey(deviceIndex)) + { + var existingTimer = this.vibrationTimers[deviceIndex]; + existingTimer.Stop(); + existingTimer.Dispose(); + } + + // Create a new timer for this vibration + var timer = new Timer(duration.TotalMilliseconds); + timer.Elapsed += (sender, e) => this.StopVibration(deviceIndex); + timer.AutoReset = false; + timer.Start(); + + this.vibrationTimers[deviceIndex] = timer; + } + + /// + public void StopVibration(int deviceIndex) + { + var vibrationInfo = new XInputVibration(0, 0); + this.xInputInstance.XInputSetState(deviceIndex, ref vibrationInfo); + + if (this.vibrationTimers.ContainsKey(deviceIndex)) + { + var timer = this.vibrationTimers[deviceIndex]; + timer.Stop(); + timer.Dispose(); + this.vibrationTimers.Remove(deviceIndex); + } + } + + /// + public IList GetActiveDevicesInfo() + => this.registeredDevices.Values.ToList(); +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputState.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputState.cs new file mode 100644 index 00000000..66d4d862 --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputState.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents the state of an Xbox 360 controller. +/// +/// +/// The member is incremented only if the status of the controller has changed since the controller was last polled. +/// +[StructLayout(LayoutKind.Explicit)] +public struct XInputState : IEquatable +{ + /// + /// State packet number. Indicates whether there have been any changes in the state of the controller. + /// If the member is the same in sequentially returned XInputState structures, the controller state has not changed. + /// + [FieldOffset(0)] + public int PacketNumber; + + /// + /// XINPUT_GAMEPAD structure containing the current state of an Xbox 360 Controller. + /// + [FieldOffset(4)] + public XInputGamePad Gamepad; + + /// + /// Copies the state from another XInputState object. + /// + /// The source XInputState to copy from. + public void Copy(XInputState source) + { + this.PacketNumber = source.PacketNumber; + this.Gamepad.Copy(source.Gamepad); + } + + /// + public override readonly bool Equals(object obj) + => obj is XInputState state && this.Equals(state); + + /// + public readonly bool Equals(XInputState other) + => this.PacketNumber == other.PacketNumber + && this.Gamepad.Equals(other.Gamepad); + + /// + public override int GetHashCode() + { + var hashCode = -1459879740; + hashCode = (hashCode * -1521134295) + this.PacketNumber.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.Gamepad.GetHashCode(); + return hashCode; + } +} diff --git a/Core/Key2Joy.Core/LowLevelInput/XInput/XInputVibration.cs b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputVibration.cs new file mode 100644 index 00000000..9115019b --- /dev/null +++ b/Core/Key2Joy.Core/LowLevelInput/XInput/XInputVibration.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace Key2Joy.LowLevelInput.XInput; + +/// +/// Represents motor speed levels for the vibration function of a controller. +/// +[StructLayout(LayoutKind.Sequential)] +public struct XInputVibration +{ + private const double MaxMotorSpeed = 65535.0; + + /// + /// Speed of the left motor. Valid values are in the range 0 to 65,535. + /// Zero signifies no motor use; 65,535 signifies 100 percent motor use. + /// + [MarshalAs(UnmanagedType.I2)] + public ushort LeftMotorSpeed; + + /// + /// Speed of the right motor. Valid values are in the range 0 to 65,535. + /// Zero signifies no motor use; 65,535 signifies 100 percent motor use. + /// + [MarshalAs(UnmanagedType.I2)] + public ushort RightMotorSpeed; + + /// + /// Construct a new XInputVibration struct with the specified motor speeds as fractions. + /// + /// + /// + public XInputVibration(double leftMotorSpeedFraction, double rightMotorSpeedFraction) + { + this.LeftMotorSpeed = GetTrueMotorSpeed(leftMotorSpeedFraction); + this.RightMotorSpeed = GetTrueMotorSpeed(rightMotorSpeedFraction); + } + + /// + /// Construct a new XInputVibration struct with the specified motor speeds. + /// + /// + /// + public XInputVibration(ushort leftMotorSpeed, ushort rightMotorSpeed) + { + this.LeftMotorSpeed = leftMotorSpeed; + this.RightMotorSpeed = rightMotorSpeed; + } + + /// + /// Get the true speed for a motor by specifying a fraction of the maximum speed. + /// + /// The speed for a motor as a fraction of the maximum speed. + /// The true speed for a motor. + public static ushort GetTrueMotorSpeed(double motorSpeed) + { + if (motorSpeed is < 0.0 or > 1.0) + { + throw new ArgumentOutOfRangeException(nameof(motorSpeed), "Speed must be in the range 0.0 to 1.0."); + } + + return (ushort)(MaxMotorSpeed * motorSpeed); + } +} diff --git a/Core/Key2Joy.Core/Mapping/Actions/CoreAction.cs b/Core/Key2Joy.Core/Mapping/Actions/CoreAction.cs index 6e963f35..1da3a3d0 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/CoreAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/CoreAction.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Util; using Key2Joy.Plugins; namespace Key2Joy.Mapping.Actions; @@ -36,6 +35,4 @@ public AbstractAction MakeStartedAction(MappingTypeFactory actio action.SetStartData(this.Listener, ref this.OtherActions); return action; } - - public override string ToString() => this.GetNameDisplay(); } diff --git a/Core/Key2Joy.Core/Mapping/Actions/DisabledAction.cs b/Core/Key2Joy.Core/Mapping/Actions/DisabledAction.cs index 2d564d32..8a1380e1 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/DisabledAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/DisabledAction.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.Contracts.Mapping.Triggers; @@ -8,22 +9,30 @@ namespace Key2Joy.Mapping.Actions; [Action( Description = "Disabled Action", NameFormat = DisabledNameFormat, - Visibility = Contracts.Mapping.MappingMenuVisibility.Never + Visibility = MappingMenuVisibility.Never, + GroupName = "Requires Attention", + GroupImage = "cross" )] public class DisabledAction : CoreAction { - private const string DisabledNameFormat = "The action '{0}' was unavailable upon loading Key2Joy. We have replaced it with this placeholder."; + private const string DisabledNameFormat = "The action '{0}' was unavailable upon loading Key2Joy. The error that caused this was: {1}"; public string ActionName { get; set; } public DisabledAction(string name) : base(name) { } + /// public override async Task Execute(AbstractInputBag inputBag = null) { } - public override string GetNameDisplay() => DisabledNameFormat.Replace("{0}", this.ActionName); + /// + public override string GetNameDisplay() + => DisabledNameFormat + .Replace("{0}", this.ActionName) + .Replace("{1}", this.Name); + /// public override bool Equals(object obj) { if (obj is not DisabledAction action) diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadButtonAction.cs similarity index 66% rename from Core/Key2Joy.Core/Mapping/Actions/Input/GamePadAction.cs rename to Core/Key2Joy.Core/Mapping/Actions/Input/GamePadButtonAction.cs index 1bbfebaa..86d167e2 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadButtonAction.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.Contracts.Mapping.Triggers; using Key2Joy.LowLevelInput; -using Key2Joy.LowLevelInput.GamePad; -using Key2Joy.Mapping.Triggers.Mouse; +using Key2Joy.LowLevelInput.SimulatedGamePad; using SimWinInput; namespace Key2Joy.Mapping.Actions.Input; @@ -17,24 +18,28 @@ namespace Key2Joy.Mapping.Actions.Input; GroupName = "GamePad Simulation", GroupImage = "joystick" )] -public class GamePadAction : CoreAction, IPressState +public class GamePadButtonAction : CoreAction, IPressState, IProvideReverseAspect { public GamePadControl Control { get; set; } public PressState PressState { get; set; } public int GamePadIndex { get; set; } - public GamePadAction(string name) + public GamePadButtonAction(string name) : base(name) { } + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); + public static List GetAllButtonActions(PressState pressState) { - var actionFactory = ActionsRepository.GetAction(typeof(GamePadAction)); + var actionFactory = ActionsRepository.GetAction(typeof(GamePadButtonAction)); List actions = new(); foreach (var control in GetAllButtons()) { - var action = (GamePadAction)MakeAction(actionFactory); + var action = (GamePadButtonAction)MakeAction(actionFactory); action.Control = control; action.PressState = pressState; @@ -50,9 +55,29 @@ public static GamePadControl[] GetAllButtons() { var allEnums = Enum.GetValues(typeof(GamePadControl)); - // Skip the first (= None) enumeration value - var buttons = new GamePadControl[allEnums.Length - 1]; - Array.Copy(allEnums, 1, buttons, 0, buttons.Length); + var skip = new GamePadControl[] + { + GamePadControl.None, + + // Handled separately in GamePadTriggerAction + GamePadControl.LeftTrigger, + GamePadControl.RightTrigger, + + // Handled separately in GamePadStickAction + GamePadControl.LeftStickLeft, + GamePadControl.LeftStickRight, + GamePadControl.LeftStickUp, + GamePadControl.LeftStickDown, + GamePadControl.RightStickLeft, + GamePadControl.RightStickRight, + GamePadControl.RightStickUp, + GamePadControl.RightStickDown, + }; + + var buttons = allEnums + .Cast() + .Where(x => !skip.Contains(x)) + .ToArray(); return buttons; } @@ -61,43 +86,10 @@ public override void OnStartListening(AbstractTriggerListener listener, ref ILis { base.OnStartListening(listener, ref otherActions); - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); gamePadService.EnsurePluggedIn(this.GamePadIndex); } - private void HandleMouseMove(IGamePad gamePad, MouseMoveInputBag mouseMoveInputBag) - { - var state = gamePad.GetState(); - - switch (this.Control) - { - case GamePadControl.LeftStickLeft: - case GamePadControl.LeftStickRight: - state.LeftStickX = this.CalculateNewState(mouseMoveInputBag.DeltaX, state.LeftStickX); - break; - - case GamePadControl.LeftStickUp: - case GamePadControl.LeftStickDown: - state.LeftStickY = this.CalculateNewState(mouseMoveInputBag.DeltaY, state.LeftStickY); - break; - - case GamePadControl.RightStickLeft: - case GamePadControl.RightStickRight: - state.RightStickX = this.CalculateNewState(mouseMoveInputBag.DeltaX, state.RightStickX); - break; - - case GamePadControl.RightStickUp: - case GamePadControl.RightStickDown: - state.RightStickY = this.CalculateNewState(mouseMoveInputBag.DeltaY, state.RightStickY); - break; - } - - gamePad.Update(); - } - - private short CalculateNewState(int delta, short currentState) - => (short)((delta + currentState) / 2); - /// /// Input /// Api/Input @@ -127,7 +119,7 @@ public void ExecuteForScript(GamePadControl control, PressState pressState, int this.PressState = pressState; this.GamePadIndex = gamepadIndex; - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); var gamePad = gamePadService.GetGamePad(this.GamePadIndex); if (!gamePad.GetIsPluggedIn()) @@ -148,15 +140,9 @@ public void ExecuteForScript(GamePadControl control, PressState pressState, int public override async Task Execute(AbstractInputBag inputBag = null) { - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); var gamePad = gamePadService.GetGamePad(this.GamePadIndex); - if (inputBag is MouseMoveInputBag mouseMoveInputBag) - { - this.HandleMouseMove(gamePad, mouseMoveInputBag); - return; - } - if (this.PressState == PressState.Press) { gamePad.SetControl(this.Control); @@ -173,7 +159,7 @@ public override string GetNameDisplay() => this.Name.Replace("{0}", this.Control public override bool Equals(object obj) { - if (obj is not GamePadAction action) + if (obj is not GamePadButtonAction action) { return false; } diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadResetAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadResetAction.cs index f7487585..3d23ce57 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadResetAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadResetAction.cs @@ -5,7 +5,7 @@ using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput.GamePad; +using Key2Joy.LowLevelInput.SimulatedGamePad; namespace Key2Joy.Mapping.Actions.Input; @@ -13,7 +13,7 @@ namespace Key2Joy.Mapping.Actions.Input; Description = "GamePad Reset Simulation", Visibility = MappingMenuVisibility.Never, NameFormat = "Reset GamePad #{0}", - GroupName = "GamePad Reset Simulation", + GroupName = "GamePad Simulation", GroupImage = "joystick" )] public class GamePadResetAction : CoreAction @@ -28,7 +28,7 @@ public override void OnStartListening(AbstractTriggerListener listener, ref ILis { base.OnStartListening(listener, ref otherActions); - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); gamePadService.EnsurePluggedIn(this.GamePadIndex); } @@ -57,7 +57,7 @@ public async void ExecuteForScript(int gamepadIndex = 0) { this.GamePadIndex = gamepadIndex; - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); var gamePad = gamePadService.GetGamePad(this.GamePadIndex); if (!gamePad.GetIsPluggedIn()) @@ -72,7 +72,7 @@ public async void ExecuteForScript(int gamepadIndex = 0) public override async Task Execute(AbstractInputBag inputBag = null) { - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); var gamePad = gamePadService.GetGamePad(this.GamePadIndex); var state = gamePad.GetState(); state.Reset(); diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadStickAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadStickAction.cs index 28edb9cc..514e8ccd 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadStickAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadStickAction.cs @@ -5,33 +5,97 @@ using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; -using Key2Joy.LowLevelInput.GamePad; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Mapping.Triggers.GamePad; +using Key2Joy.Mapping.Triggers.Mouse; namespace Key2Joy.Mapping.Actions.Input; [Action( Description = "GamePad Stick Simulation", - Visibility = MappingMenuVisibility.Never, - NameFormat = "Move {0} Stick on GamePad #{3} by {1},{2}", - GroupName = "GamePad Stick Simulation", + NameFormat = "Move {0} Stick on GamePad #{3} by ({1}, {2})", + GroupName = "GamePad Simulation", GroupImage = "joystick" )] -public class GamePadStickAction : CoreAction +public class GamePadStickAction : CoreAction, IProvideReverseAspect, IEquatable { - public Simulator.GamePadStick Stick { get; set; } - public double DeltaX { get; set; } - public double DeltaY { get; set; } + /// + /// This describes what 'a lot of input movement' is, so we can scale + /// the delta and make the scaling numbers feel intuitive. + /// + private const int BIG_DELTA_MOVEMENT = 250; + + private const short MIN_STICK_VALUE = XInputGamePad.ThumbstickValueMin; + private const short MAX_STICK_VALUE = XInputGamePad.ThumbstickValueMax; + + private const float EXACT_SCALE = (MAX_STICK_VALUE - MIN_STICK_VALUE) / 2f; + + /// + /// Which side to simulate + /// + public GamePadSide Side { get; set; } + + /// + /// How much to simulate on the X axis. If null, then the inputbag of the input + /// that triggered this will be used if possible + /// + public short? DeltaX { get; set; } = null; + + /// + /// How much to simulate on the Y axis. If null, then the inputbag of the input + /// that triggered this will be used if possible + /// + public short? DeltaY { get; set; } = null; + + /// + /// If DeltaX is null, this is the scale factor to apply to the inputbag + /// + public float InputScaleX { get; set; } = 1; + + /// + /// If DeltaY is null, this is the scale factor to apply to the inputbag + /// + public float InputScaleY { get; set; } = -1; + + /// + /// After how many milliseconds should the stick be reset to 0,0? + /// + public int ResetAfterIdleTimeInMs { get; set; } = 500; + + /// + /// Which gamepad to simulate + /// public int GamePadIndex { get; set; } + private readonly System.Timers.Timer noInputTimer; + public GamePadStickAction(string name) : base(name) - { } + { + this.noInputTimer = new System.Timers.Timer(); + this.noInputTimer.Elapsed += this.NoInputTimer_Elapsed; + this.noInputTimer.AutoReset = false; + } + /// + public void MakeReverse(AbstractMappingAspect aspect) + { + var reverse = aspect as GamePadStickAction; + + if (this.DeltaX.HasValue + && this.DeltaY.HasValue) + { + reverse.DeltaX = (short?)(this.DeltaX.Value * -1); + reverse.DeltaY = (short?)(this.DeltaY.Value * -1); + } + } + + /// public override void OnStartListening(AbstractTriggerListener listener, ref IList otherActions) { base.OnStartListening(listener, ref otherActions); - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); gamePadService.EnsurePluggedIn(this.GamePadIndex); } @@ -55,30 +119,45 @@ public override void OnStartListening(AbstractTriggerListener listener, ref ILis /// /// The fraction by which to move the stick forward (negative) or backward (positive) /// The fraction by which to move the stick right (positive) or left (negative) - /// Which gamepad stick to move, either GamePadStick.Left (default) or .Right + /// Which gamepad stick to move, either GamePadSide.Left (default) or .Right /// Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 /// GamePad.SimulateMove [ExposesScriptingMethod("GamePad.SimulateMove")] public async void ExecuteForScript( - double deltaX, - double deltaY, - Simulator.GamePadStick stick = Simulator.GamePadStick.Left, + short deltaX, + short deltaY, + GamePadSide side = GamePadSide.Left, int gamepadIndex = 0) { this.DeltaX = deltaX; this.DeltaY = deltaY; - this.Stick = stick; + this.Side = side; this.GamePadIndex = gamepadIndex; - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); gamePadService.EnsurePluggedIn(this.GamePadIndex); await this.Execute(); } + /// + /// Scales InputBag values to a reasonable range for gamepad sticks + /// + /// + /// + /// + private static short Scale(int value, float sensitivity = 1.0f) + { + var scaled = (int)((float)value / BIG_DELTA_MOVEMENT * sensitivity * (MAX_STICK_VALUE - MIN_STICK_VALUE)); + var gamePadStickX = (short)Math.Min(Math.Max(scaled, MIN_STICK_VALUE), MAX_STICK_VALUE); + + return gamePadStickX; + } + + /// public override async Task Execute(AbstractInputBag inputBag = null) { - var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePadService = ServiceLocator.Current.GetInstance(); var gamePad = gamePadService.GetGamePad(this.GamePadIndex); if (!gamePad.GetIsPluggedIn()) @@ -88,37 +167,118 @@ public override async Task Execute(AbstractInputBag inputBag = null) var state = gamePad.GetState(); - var x = (short)(short.MinValue * this.DeltaX); - var y = (short)(short.MinValue * this.DeltaY); + var deltaX = (short)0; + var deltaY = (short)0; + + if (this.DeltaX is not null) + { + deltaX = Scale((short)this.DeltaX, EXACT_SCALE); + } + else if (inputBag is AxisDeltaInputBag axisInputBag) + { + deltaX = Scale(axisInputBag.DeltaX, this.InputScaleX); + } + else if (inputBag is GamePadTriggerInputBag triggerInputBag) + { + // TODO: This is now hard-coded, but I'd love for 'modifiers' to exist in between triggers and actions. Those could (with more fine tuning) be configured by the user. + deltaX = Scale((int)(triggerInputBag.LeftTriggerDelta * XInputGamePad.TriggerValueMax), this.InputScaleX); + } - if (this.Stick == Simulator.GamePadStick.Right) + if (this.DeltaY is not null) { - state.RightStickX = x; - state.RightStickY = y; + deltaY = Scale((int)this.DeltaY, EXACT_SCALE); + } + else if (inputBag is AxisDeltaInputBag axisInputBag) + { + deltaY = Scale(axisInputBag.DeltaY, this.InputScaleY); + } + else if (inputBag is GamePadTriggerInputBag triggerInputBag) + { + deltaY = Scale((int)(triggerInputBag.RightTriggerDelta * XInputGamePad.TriggerValueMax), this.InputScaleY); + } + + if (this.Side == GamePadSide.Left) + { + state.LeftStickX = this.GetStickValue(state.LeftStickX, deltaX); + state.LeftStickY = this.GetStickValue(state.LeftStickY, deltaY); } else { - state.LeftStickX = x; - state.LeftStickY = y; + state.RightStickX = this.GetStickValue(state.RightStickX, deltaX); + state.RightStickY = this.GetStickValue(state.RightStickY, deltaY); + } + + if (state.LeftStickX != 0 + || state.LeftStickY != 0 + || state.RightStickX != 0 + || state.RightStickY != 0 + ) + { + // Reset the timer if there's input + this.noInputTimer.Interval = this.ResetAfterIdleTimeInMs; + this.noInputTimer.Stop(); + this.noInputTimer.Start(); } gamePad.Update(); } - public override string GetNameDisplay() => this.Name.Replace("{0}", this.Stick == Simulator.GamePadStick.Left ? "Left" : "Right") - .Replace("{1}", this.DeltaX.ToString()) - .Replace("{2}", this.DeltaY.ToString()) - .Replace("{3}", this.GamePadIndex.ToString()); - - public override bool Equals(object obj) + /// + /// Resets the stick to the center position if there was no input for a while + /// + /// + /// + private void NoInputTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { - if (obj is not GamePadStickAction action) + var gamePad = ServiceLocator.Current.GetInstance().GetGamePad(this.GamePadIndex); + var state = gamePad.GetState(); + + if (this.Side == GamePadSide.Left) + { + state.LeftStickX = 0; + state.LeftStickY = 0; + } + else { - return false; + state.RightStickX = 0; + state.RightStickY = 0; } - return action.Stick == this.Stick - && action.DeltaX == this.DeltaX - && action.DeltaY == this.DeltaY; + gamePad.Update(); + } + + private short GetStickValue(short stickValue, short delta) + => (short)Math.Min(Math.Max(stickValue + delta, MIN_STICK_VALUE), MAX_STICK_VALUE); + + /// + public override string GetNameDisplay() + => this.Name.Replace("{0}", Enum.GetName(typeof(GamePadSide), this.Side)) + .Replace("{1}", this.DeltaX?.ToString() ?? $"") + .Replace("{2}", this.DeltaY?.ToString() ?? $"") + .Replace("{3}", this.GamePadIndex.ToString()); + + /// + public bool Equals(GamePadStickAction other) + => other is not null + && this.Side == other.Side + && this.DeltaX == other.DeltaX + && this.DeltaY == other.DeltaY + && this.InputScaleX == other.InputScaleX + && this.InputScaleY == other.InputScaleY + && this.GamePadIndex == other.GamePadIndex + && this.ResetAfterIdleTimeInMs == other.ResetAfterIdleTimeInMs; + + /// + public override int GetHashCode() + { + var hashCode = -2085055944; + hashCode = (hashCode * -1521134295) + this.Side.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.DeltaX.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.DeltaY.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.InputScaleX.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.InputScaleY.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.GamePadIndex.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.ResetAfterIdleTimeInMs.GetHashCode(); + return hashCode; } } diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadTriggerAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadTriggerAction.cs new file mode 100644 index 00000000..836a8c08 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/GamePadTriggerAction.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Mapping.Triggers.GamePad; +using Key2Joy.Mapping.Triggers.Mouse; + +namespace Key2Joy.Mapping.Actions.Input; + +[Action( + Description = "GamePad Trigger Simulation", + NameFormat = "Move {0} Trigger on GamePad #{3} by {1}", + GroupName = "GamePad Simulation", + GroupImage = "joystick" +)] +public class GamePadTriggerAction : CoreAction, IProvideReverseAspect, IEquatable +{ + private const byte MIN_TRIGGER_VALUE = XInputGamePad.TriggerValueMin; + private const byte MAX_TRIGGER_VALUE = XInputGamePad.TriggerValueMax; + + private const float EXACT_SCALE = (MAX_TRIGGER_VALUE - MIN_TRIGGER_VALUE) / 2f; + + /// + /// Which side to simulate + /// + public GamePadSide Side { get; set; } + + /// + /// How much to simulate the trigger being pulled back as a fraction from 0 - 1. + /// If null, then the inputbag of the input that triggered this will be used if possible. + /// + public float? Delta { get; set; } = null; + + /// + /// If Delta is null, this is the scale factor to apply to the inputbag + /// + public float InputScale { get; set; } = 1; + + /// + /// Which gamepad to simulate + /// + public int GamePadIndex { get; set; } + + public GamePadTriggerAction(string name) : base(name) + { } + + /// + public void MakeReverse(AbstractMappingAspect aspect) + { + var reverse = aspect as GamePadTriggerAction; + + if (this.Delta.HasValue) + { + reverse.Delta = this.Delta.Value * -1; + } + } + + /// + public override void OnStartListening(AbstractTriggerListener listener, ref IList otherActions) + { + base.OnStartListening(listener, ref otherActions); + + var gamePadService = ServiceLocator.Current.GetInstance(); + gamePadService.EnsurePluggedIn(this.GamePadIndex); + } + + /// + /// Input + /// Api/Input + /// + /// + /// Simulate pulling back a gamepad trigger + /// + /// + /// Pulls the left gamepad trigger halfway back, then resets after 500ms + /// + /// + /// + /// + /// The fraction by which to pull the trigger back (between 0 and 1) + /// Which gamepad trigger to pull, either GamePadSide.Left (default) or .Right + /// Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 + /// GamePad.SimulateTrigger + [ExposesScriptingMethod("GamePad.SimulateTrigger")] + public async void ExecuteForScript( + float delta, + GamePadSide side = GamePadSide.Left, + int gamepadIndex = 0) + { + this.Delta = delta; + this.Side = side; + this.GamePadIndex = gamepadIndex; + + var gamePadService = ServiceLocator.Current.GetInstance(); + gamePadService.EnsurePluggedIn(this.GamePadIndex); + + await this.Execute(); + } + + /// + /// Scales InputBag values to a reasonable range for gamepad triggers + /// + /// + /// + /// + private static byte Scale(float value, float sensitivity = 1.0f) + { + var scaled = (int)(value * sensitivity * (MAX_TRIGGER_VALUE - MIN_TRIGGER_VALUE)); + var gamePadTriggerX = (byte)Math.Min(Math.Max(scaled, MIN_TRIGGER_VALUE), MAX_TRIGGER_VALUE); + + return gamePadTriggerX; + } + + /// + public override async Task Execute(AbstractInputBag inputBag = null) + { + var gamePadService = ServiceLocator.Current.GetInstance(); + var gamePad = gamePadService.GetGamePad(this.GamePadIndex); + + if (!gamePad.GetIsPluggedIn()) + { + gamePad.PlugIn(); + } + + var state = gamePad.GetState(); + var deltaX = (byte)0; + var deltaY = (byte)0; + + if (this.Delta is not null) + { + deltaX = Scale((float)this.Delta, EXACT_SCALE); + deltaY = Scale((float)this.Delta, EXACT_SCALE); + } + else if (inputBag is AxisDeltaInputBag axisInputBag) + { + deltaX = Scale(axisInputBag.DeltaX, this.InputScale); + deltaY = Scale(axisInputBag.DeltaY, this.InputScale); + } + else if (inputBag is GamePadTriggerInputBag triggerInputBag) + { + // TODO: This is now hard-coded, but I'd love for 'modifiers' to exist in between triggers and actions. Those could (with more fine tuning) be configured by the user. + if (this.Side == GamePadSide.Left) + { + deltaX = Scale(triggerInputBag.LeftTriggerDelta, this.InputScale); + } + else + { + deltaX = Scale(triggerInputBag.RightTriggerDelta, this.InputScale); + } + } + + if (this.Side == GamePadSide.Left) + { + state.LeftTrigger = Math.Max(deltaX, deltaY); + } + else + { + state.RightTrigger = Math.Max(deltaX, deltaY); + } + + gamePad.Update(); + } + + /// + public override string GetNameDisplay() + => this.Name.Replace("{0}", Enum.GetName(typeof(GamePadSide), this.Side)) + .Replace("{1}", this.Delta?.ToString() ?? $"") + .Replace("{3}", this.GamePadIndex.ToString()); + + /// + public bool Equals(GamePadTriggerAction other) + => other is not null + && this.Side == other.Side + && this.Delta == other.Delta + && this.InputScale == other.InputScale + && this.GamePadIndex == other.GamePadIndex; + + /// + public override int GetHashCode() + { + var hashCode = 1651596494; + hashCode = (hashCode * -1521134295) + this.Side.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.Delta.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.InputScale.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.GamePadIndex.GetHashCode(); + return hashCode; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/KeyboardAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/KeyboardAction.cs index 255eb785..e4e8854a 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Input/KeyboardAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/KeyboardAction.cs @@ -1,116 +1,121 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Actions.Input; - -[Action( - Description = "Keyboard Simulation", - NameFormat = "{1} {0} on Keyboard", - GroupName = "Keyboard Simulation", - GroupImage = "keyboard" -)] -public class KeyboardAction : CoreAction, IPressState -{ - public KeyboardKey Key { get; set; } - public PressState PressState { get; set; } - - public KeyboardAction(string name) - : base(name) - { - } - - public static KeyboardKey[] GetAllKeys() - { - var allEnums = Enum.GetValues(typeof(KeyboardKey)); - List keys = new(); - - // Skip the enumerations that are zero - foreach (var keyEnum in allEnums) - { - if ((short)keyEnum == 0) - { - continue; - } - - keys.Add((KeyboardKey)keyEnum); - } - - return keys.ToArray(); - } - - public static List GetAllButtonActions(PressState pressState) - { - var actionFactory = ActionsRepository.GetAction(typeof(KeyboardAction)); - - List actions = new(); - foreach (var key in GetAllKeys()) - { - var action = (KeyboardAction)MakeAction(actionFactory); - action.Key = key; - action.PressState = pressState; - - actions.Add(new MappedOption - { - Action = action - }); - } - return actions; - } - - /// - /// Input - /// Api/Input - /// - /// - /// Simulate pressing or releasing (or both) keyboard keys. - /// - /// Key to simulate - /// Action to simulate - /// Keyboard.Simulate - [ExposesScriptingMethod("Keyboard.Simulate")] - public async void ExecuteForScript(KeyboardKey key, PressState pressState) - { - this.Key = key; - this.PressState = pressState; - - if (this.PressState == PressState.Press) - { - SimulatedKeyboard.PressKey(this.Key); - } - - if (this.PressState == PressState.Release) - { - SimulatedKeyboard.ReleaseKey(this.Key); - } - } - - public override async Task Execute(AbstractInputBag inputBag = null) - { - if (this.PressState == PressState.Press) - { - SimulatedKeyboard.PressKey(this.Key); - } - else if (this.PressState == PressState.Release) - { - SimulatedKeyboard.ReleaseKey(this.Key); - } - } - - public override string GetNameDisplay() => this.Name.Replace("{0}", Enum.GetName(typeof(KeyboardKey), this.Key)) - .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); - - public override bool Equals(object obj) - { - if (obj is not KeyboardAction action) - { - return false; - } - - return action.Key == this.Key - && action.PressState == this.PressState; - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Actions.Input; + +[Action( + Description = "Keyboard Simulation", + NameFormat = "{1} {0} on Keyboard", + GroupName = "Keyboard Simulation", + GroupImage = "keyboard" +)] +public class KeyboardAction : CoreAction, IPressState, IProvideReverseAspect +{ + public KeyboardKey Key { get; set; } + public PressState PressState { get; set; } + + public KeyboardAction(string name) + : base(name) + { + } + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); + + public static KeyboardKey[] GetAllKeys() + { + var allEnums = Enum.GetValues(typeof(KeyboardKey)); + List keys = new(); + + // Skip the enumerations that are zero + foreach (var keyEnum in allEnums) + { + if ((short)keyEnum == 0) + { + continue; + } + + keys.Add((KeyboardKey)keyEnum); + } + + return keys.ToArray(); + } + + public static List GetAllButtonActions(PressState pressState) + { + var actionFactory = ActionsRepository.GetAction(typeof(KeyboardAction)); + + List actions = new(); + foreach (var key in GetAllKeys()) + { + var action = (KeyboardAction)MakeAction(actionFactory); + action.Key = key; + action.PressState = pressState; + + actions.Add(new MappedOption + { + Action = action + }); + } + return actions; + } + + /// + /// Input + /// Api/Input + /// + /// + /// Simulate pressing or releasing (or both) keyboard keys. + /// + /// Key to simulate + /// Action to simulate + /// Keyboard.Simulate + [ExposesScriptingMethod("Keyboard.Simulate")] + public async void ExecuteForScript(KeyboardKey key, PressState pressState) + { + this.Key = key; + this.PressState = pressState; + + if (this.PressState == PressState.Press) + { + SimulatedKeyboard.PressKey(this.Key); + } + + if (this.PressState == PressState.Release) + { + SimulatedKeyboard.ReleaseKey(this.Key); + } + } + + public override async Task Execute(AbstractInputBag inputBag = null) + { + if (this.PressState == PressState.Press) + { + SimulatedKeyboard.PressKey(this.Key); + } + else if (this.PressState == PressState.Release) + { + SimulatedKeyboard.ReleaseKey(this.Key); + } + } + + public override string GetNameDisplay() => this.Name.Replace("{0}", Enum.GetName(typeof(KeyboardKey), this.Key)) + .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); + + public override bool Equals(object obj) + { + if (obj is not KeyboardAction action) + { + return false; + } + + return action.Key == this.Key + && action.PressState == this.PressState; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/MouseButtonAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/MouseButtonAction.cs index 767a35e3..622b1132 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Input/MouseButtonAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/MouseButtonAction.cs @@ -1,91 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Actions.Input; - -[Action( - Description = "Mouse Button Simulation", - Visibility = MappingMenuVisibility.Never, - NameFormat = "{1} {0} on Mouse", - GroupName = "Mouse Button Simulation", - GroupImage = "mouse" -)] -public class MouseButtonAction : CoreAction, IPressState -{ - public Mouse.Buttons Button { get; set; } - public PressState PressState { get; set; } - - public MouseButtonAction(string name) - : base(name) - { - } - - public static Mouse.Buttons[] GetAllButtons() - { - var allEnums = Enum.GetValues(typeof(Mouse.Buttons)); - List buttons = new(); - - // Skip the enumerations that are zero - foreach (var buttonEnum in allEnums) - { - if ((short)buttonEnum == 0) - { - continue; - } - - buttons.Add((Mouse.Buttons)buttonEnum); - } - - return buttons.ToArray(); - } - - /// - /// Input - /// Api/Input - /// - /// - /// Simulate pressing or releasing (or both) mouse buttons. - /// - /// Button to simulate - /// Action to simulate - /// Mouse.Simulate - [ExposesScriptingMethod("Mouse.Simulate")] - public async void ExecuteForScript(Mouse.Buttons button, PressState pressState) - { - this.Button = button; - this.PressState = pressState; - - await this.Execute(); - } - - public override async Task Execute(AbstractInputBag inputBag = null) - { - if (this.PressState == PressState.Press) - { - SimulatedMouse.PressButton(this.Button); - } - else if (this.PressState == PressState.Release) - { - SimulatedMouse.ReleaseButton(this.Button); - } - } - - public override string GetNameDisplay() => this.Name.Replace("{0}", Enum.GetName(typeof(Mouse.Buttons), this.Button)) - .Replace("{1}", Enum.GetName(typeof(Mouse.Buttons), this.PressState)); - - public override bool Equals(object obj) - { - if (obj is not MouseButtonAction action) - { - return false; - } - - return action.Button == this.Button - && action.PressState == this.PressState; - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Actions.Input; + +[Action( + Description = "Mouse Button Simulation", + Visibility = MappingMenuVisibility.Never, + NameFormat = "{1} {0} on Mouse", + GroupName = "Mouse Simulation", + GroupImage = "mouse" +)] +public class MouseButtonAction : CoreAction, IPressState, IProvideReverseAspect +{ + public Mouse.Buttons Button { get; set; } + public PressState PressState { get; set; } + + public MouseButtonAction(string name) + : base(name) + { + } + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); + + public static Mouse.Buttons[] GetAllButtons() + { + var allEnums = Enum.GetValues(typeof(Mouse.Buttons)); + List buttons = new(); + + // Skip the enumerations that are zero + foreach (var buttonEnum in allEnums) + { + if ((short)buttonEnum == 0) + { + continue; + } + + buttons.Add((Mouse.Buttons)buttonEnum); + } + + return buttons.ToArray(); + } + + /// + /// Input + /// Api/Input + /// + /// + /// Simulate pressing or releasing (or both) mouse buttons. + /// + /// Button to simulate + /// Action to simulate + /// Mouse.Simulate + [ExposesScriptingMethod("Mouse.Simulate")] + public async void ExecuteForScript(Mouse.Buttons button, PressState pressState) + { + this.Button = button; + this.PressState = pressState; + + await this.Execute(); + } + + public override async Task Execute(AbstractInputBag inputBag = null) + { + if (this.PressState == PressState.Press) + { + SimulatedMouse.PressButton(this.Button); + } + else if (this.PressState == PressState.Release) + { + SimulatedMouse.ReleaseButton(this.Button); + } + } + + public override string GetNameDisplay() => this.Name.Replace("{0}", Enum.GetName(typeof(Mouse.Buttons), this.Button)) + .Replace("{1}", Enum.GetName(typeof(Mouse.Buttons), this.PressState)); + + public override bool Equals(object obj) + { + if (obj is not MouseButtonAction action) + { + return false; + } + + return action.Button == this.Button + && action.PressState == this.PressState; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Actions/Input/MouseMoveAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Input/MouseMoveAction.cs index 3df3458f..06d96a87 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Input/MouseMoveAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Input/MouseMoveAction.cs @@ -1,82 +1,91 @@ -using System; -using System.Threading.Tasks; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Actions.Input; - -[Action( - Description = "Mouse Move Simulation", - Visibility = MappingMenuVisibility.Never, - NameFormat = "Move {0} {1},{2} on Mouse", - GroupName = "Mouse Move Simulation", - GroupImage = "mouse" -)] -public class MouseMoveAction : CoreAction -{ - public Mouse.MoveType MoveType { get; set; } - public int X { get; set; } - public int Y { get; set; } - - public MouseMoveAction(string name) - : base(name) - { - } - - /// - /// Input - /// Api/Input - /// - /// - /// Simulate moving the mouse - /// - /// - /// Nudges the cursor 100 pixels to the left from where it is now. - /// - /// - /// - /// - /// - /// Moves the cursor to an absolute position on the screen. - /// - /// - /// - /// - /// X coordinate to move by/to - /// Y coordinate to move by/to - /// Whether to move relative to the current cursor position (default) or to an absolute position on screen - /// Mouse.SimulateMove - [ExposesScriptingMethod("Mouse.SimulateMove")] - public async void ExecuteForScript(int x, int y, Mouse.MoveType moveType = Mouse.MoveType.Relative) - { - this.X = x; - this.Y = y; - this.MoveType = moveType; - - await this.Execute(); - } - - public override async Task Execute(AbstractInputBag inputBag = null) => SimulatedMouse.Move(this.X, this.Y, this.MoveType); - - public override string GetNameDisplay() => this.Name.Replace("{0}", this.MoveType == Mouse.MoveType.Absolute ? "To" : "By") - .Replace("{1}", this.X.ToString()) - .Replace("{2}", this.Y.ToString()); - - public override bool Equals(object obj) - { - if (obj is not MouseMoveAction action) - { - return false; - } - - return action.MoveType == this.MoveType - && action.X == this.X - && action.Y == this.Y; - } -} +using System; +using System.Threading.Tasks; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Actions.Input; + +[Action( + Description = "Mouse Move Simulation", + Visibility = MappingMenuVisibility.Never, + NameFormat = "Move {0} {1},{2} on Mouse", + GroupName = "Mouse Simulation", + GroupImage = "mouse" +)] +public class MouseMoveAction : CoreAction, IProvideReverseAspect +{ + public Mouse.MoveType MoveType { get; set; } + public int X { get; set; } + public int Y { get; set; } + + public MouseMoveAction(string name) + : base(name) + { + } + + /// + public void MakeReverse(AbstractMappingAspect aspect) + { + var reverse = aspect as MouseMoveAction; + + reverse.X = this.X * -1; + reverse.Y = this.Y * -1; + } + + /// + /// Input + /// Api/Input + /// + /// + /// Simulate moving the mouse + /// + /// + /// Nudges the cursor 100 pixels to the left from where it is now. + /// + /// + /// + /// + /// + /// Moves the cursor to an absolute position on the screen. + /// + /// + /// + /// + /// X coordinate to move by/to + /// Y coordinate to move by/to + /// Whether to move relative to the current cursor position (default) or to an absolute position on screen + /// Mouse.SimulateMove + [ExposesScriptingMethod("Mouse.SimulateMove")] + public async void ExecuteForScript(int x, int y, Mouse.MoveType moveType = Mouse.MoveType.Relative) + { + this.X = x; + this.Y = y; + this.MoveType = moveType; + + await this.Execute(); + } + + public override async Task Execute(AbstractInputBag inputBag = null) => SimulatedMouse.Move(this.X, this.Y, this.MoveType); + + public override string GetNameDisplay() => this.Name.Replace("{0}", this.MoveType == Mouse.MoveType.Absolute ? "To" : "By") + .Replace("{1}", this.X.ToString()) + .Replace("{2}", this.Y.ToString()); + + public override bool Equals(object obj) + { + if (obj is not MouseMoveAction action) + { + return false; + } + + return action.MoveType == this.MoveType + && action.X == this.X + && action.Y == this.Y; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Actions/Scripting/BaseScriptAction.cs b/Core/Key2Joy.Core/Mapping/Actions/Scripting/BaseScriptAction.cs index 95e3230a..fa710946 100644 --- a/Core/Key2Joy.Core/Mapping/Actions/Scripting/BaseScriptAction.cs +++ b/Core/Key2Joy.Core/Mapping/Actions/Scripting/BaseScriptAction.cs @@ -60,7 +60,7 @@ public virtual void Print(params object[] args) public override string GetNameDisplay() { - var truncatedScript = this.Script.Ellipsize(50); + var truncatedScript = this.Script; return this.Name.Replace("{0}", truncatedScript); } diff --git a/Core/Key2Joy.Core/Mapping/AxisDirection.cs b/Core/Key2Joy.Core/Mapping/AxisDirection.cs index 7c669276..2594ba8e 100644 --- a/Core/Key2Joy.Core/Mapping/AxisDirection.cs +++ b/Core/Key2Joy.Core/Mapping/AxisDirection.cs @@ -1,12 +1,13 @@ -namespace Key2Joy.Mapping; - -public enum AxisDirection -{ - None = 0x000000, - - // This is purposefully a lot higher than the highest values in System.Windows.Forms.Keys (so they wont conflict and we can mix them both) - Forward = 0xFFFF00, - Right = 0xFFFF01, - Backward = 0xFFFF02, - Left = 0xFFFF03 -} +namespace Key2Joy.Mapping; + +public enum AxisDirection +{ + Any = 0x000000, + + // This is purposefully a lot higher than the highest values in System.Windows.Forms.Keys (so they wont conflict and we can mix them both) + Forward = 0xFFFF00, + + Right = 0xFFFF01, + Backward = 0xFFFF02, + Left = 0xFFFF03 +} diff --git a/Core/Key2Joy.Core/Mapping/ExactAxisDirection.cs b/Core/Key2Joy.Core/Mapping/ExactAxisDirection.cs new file mode 100644 index 00000000..91252bfd --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/ExactAxisDirection.cs @@ -0,0 +1,51 @@ +using System; + +namespace Key2Joy.Mapping; + +/// +/// More detailed specification of direction than . +/// +public struct ExactAxisDirection : IEquatable +{ + /// + /// A fraction from -1 to 1, where -1 is left and 1 is right. + /// + public float X { get; set; } + + /// + /// A fraction from -1 to 1, where -1 is up and 1 is down. + /// + public float Y { get; set; } + + /// + /// Creates a new instance of . + /// + /// + /// + public ExactAxisDirection(float x, float y) + { + this.X = x; + this.Y = y; + } + + /// + public override bool Equals(object obj) + => obj is ExactAxisDirection direction && this.Equals(direction); + + /// + public readonly bool Equals(ExactAxisDirection other) + => this.X == other.X && this.Y == other.Y; + + /// + public override readonly int GetHashCode() + { + var hashCode = 1861411795; + hashCode = hashCode * -1521134295 + this.X.GetHashCode(); + hashCode = hashCode * -1521134295 + this.Y.GetHashCode(); + return hashCode; + } + + /// + public override readonly string ToString() + => $"({this.X}, {this.Y})"; +} diff --git a/Core/Key2Joy.Core/Mapping/IPressState.cs b/Core/Key2Joy.Core/Mapping/IPressState.cs index 6935e067..a42b3fb5 100644 --- a/Core/Key2Joy.Core/Mapping/IPressState.cs +++ b/Core/Key2Joy.Core/Mapping/IPressState.cs @@ -1,8 +1,11 @@ -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping; - -public interface IPressState -{ - PressState PressState { get; set; } -} +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping; + +public interface IPressState +{ + /// + /// Which press state this is + /// + PressState PressState { get; set; } +} diff --git a/Core/Key2Joy.Core/Mapping/IProvideReverseAspect.cs b/Core/Key2Joy.Core/Mapping/IProvideReverseAspect.cs new file mode 100644 index 00000000..948b58ec --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/IProvideReverseAspect.cs @@ -0,0 +1,20 @@ +using Key2Joy.Contracts.Mapping; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping; + +public interface IProvideReverseAspect +{ + /// + /// Makes the given aspect the reverse of the current instance + /// + /// + /// + void MakeReverse(AbstractMappingAspect aspect); +} + +public static class CommonReverseAspect +{ + public static void MakeReversePressState(AbstractMappingAspect current, AbstractMappingAspect aspect) + => (aspect as IPressState).PressState = (current as IPressState).PressState == PressState.Press ? PressState.Release : PressState.Press; +} diff --git a/Core/Key2Joy.Core/Mapping/JsonMappingAspectConverter.cs b/Core/Key2Joy.Core/Mapping/JsonMappingAspectConverter.cs index acd687f6..7bbc5c9d 100644 --- a/Core/Key2Joy.Core/Mapping/JsonMappingAspectConverter.cs +++ b/Core/Key2Joy.Core/Mapping/JsonMappingAspectConverter.cs @@ -1,168 +1,204 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Serialization; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.Mapping.Actions; -using Key2Joy.Mapping.Triggers; -using Key2Joy.Plugins; - -namespace Key2Joy.Mapping; - -internal struct JsonMappingAspectWithType -{ - [JsonPropertyName("$type")] - public string FullTypeName { get; set; } - - public MappingAspectOptions Options { get; set; } - - public JsonMappingAspectWithType() - { } -} - -internal class JsonMappingAspectConverter : JsonConverter where T : AbstractMappingAspect -{ - private readonly IDictionary allowedTypes; - - public JsonMappingAspectConverter() - { - this.allowedTypes = new Dictionary(); - - foreach (var actionFactory in ActionsRepository.GetAllActions().Select(x => x.Value)) - { - this.allowedTypes.Add(actionFactory.FullTypeName, actionFactory); - } - - foreach (var triggerFactory in TriggersRepository.GetAllTriggers().Select(x => x.Value)) - { - this.allowedTypes.Add(triggerFactory.FullTypeName, triggerFactory); - } - } - - /// - /// Prevent recursion by not including this converter in child (de)serializations - /// - /// - /// - private JsonSerializerOptions GetOptionsWithoutSelf(JsonSerializerOptions options) - { - JsonSerializerOptions newOptions = new(options); - //var thisConverter = newOptions.Converters.SingleOrDefault(c => c is JsonMappingAspectConverter); - - //if (thisConverter != null) - //{ - // newOptions.Converters.Remove(thisConverter); - //} - - return newOptions; - } - - private AbstractMappingAspect ParseJson(Type typeToConvert, JsonDocument json, JsonSerializerOptions options) - { - MappingAspectOptions mappingAspectOptions = new(); - var typeProperty = json.RootElement.GetProperty("$type"); - var type = MappingTypeHelper.EnsureSimpleTypeName(typeProperty.GetString()); - - if (!this.allowedTypes.TryGetValue(type, out var factory)) - { - if (typeToConvert == typeof(AbstractAction)) - { - factory = new MappingTypeFactory( - typeof(DisabledAction).FullName, - typeof(DisabledAction).GetCustomAttribute()); - mappingAspectOptions.Add(nameof(DisabledAction.ActionName), type); - } - else if (typeToConvert == typeof(AbstractTrigger)) - { - factory = new MappingTypeFactory( - typeof(DisabledTrigger).FullName, - typeof(DisabledTrigger).GetCustomAttribute()); - mappingAspectOptions.Add(nameof(DisabledTrigger.TriggerName), type); - } - else - { - throw new NotSupportedException($"Type {type} is not supported"); - } - } - - var aspectRootProperty = json.RootElement.GetProperty( - options.PropertyNamingPolicy.ConvertName( - nameof(JsonMappingAspectWithType.Options) - ) - ); - - foreach (var property in aspectRootProperty.EnumerateObject()) - { - if (property.Value.ValueKind == JsonValueKind.Object) - { - throw new NotImplementedException($"The value kind {property.Value.ValueKind} is not yet supported."); - } - else if (property.Value.ValueKind == JsonValueKind.String) - { - mappingAspectOptions.Add(property.Name, property.Value.GetString()); - } - else if (property.Value.ValueKind == JsonValueKind.Number) - { - mappingAspectOptions.Add(property.Name, property.Value.GetInt32()); - } - else if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False) - { - mappingAspectOptions.Add(property.Name, property.Value.GetBoolean()); - } - else if (property.Value.ValueKind == JsonValueKind.Null) - { - mappingAspectOptions.Add(property.Name, null); - } - else if (property.Value.ValueKind == JsonValueKind.Array) - { - List list = new(); - - foreach (var item in property.Value.EnumerateArray()) - { - var rawJson = item.GetRawText(); - var document = JsonDocument.Parse(rawJson); - list.Add(this.ParseJson(typeToConvert, document, options)); - } - - mappingAspectOptions.Add(property.Name, list); - } - else - { - throw new NotImplementedException($"The value kind {property.Value.ValueKind} is not supported."); - } - } - - var name = mappingAspectOptions[nameof(AbstractMappingAspect.Name)] as string; - //options.Remove("name"); - - var mappingAspect = factory.CreateInstance(new object[] - { - name, - }); - mappingAspect.LoadOptions(mappingAspectOptions); - - return (T)mappingAspect; - } - - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var json = JsonDocument.ParseValue(ref reader); - - return (T)this.ParseJson(typeToConvert, json, options); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - var realTypeName = MappingTypeHelper.GetTypeFullName(this.allowedTypes, value); - - JsonSerializer.Serialize(writer, new JsonMappingAspectWithType - { - Options = value.SaveOptions(), - FullTypeName = realTypeName, - }, this.GetOptionsWithoutSelf(options)); - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Mapping.Actions; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Plugins; + +namespace Key2Joy.Mapping; + +internal struct JsonMappingAspectWithType +{ + [JsonPropertyName("$type")] + public string FullTypeName { get; set; } + + public MappingAspectOptions Options { get; set; } + + public JsonMappingAspectWithType() + { } +} + +internal class JsonMappingAspectConverter : JsonConverter where T : AbstractMappingAspect +{ + private readonly IDictionary allowedTypes; + + public JsonMappingAspectConverter() + { + this.allowedTypes = new Dictionary(); + + foreach (var actionFactory in ActionsRepository.GetAllActions().Select(x => x.Value)) + { + this.allowedTypes.Add(actionFactory.FullTypeName, actionFactory); + } + + foreach (var triggerFactory in TriggersRepository.GetAllTriggers().Select(x => x.Value)) + { + this.allowedTypes.Add(triggerFactory.FullTypeName, triggerFactory); + } + } + + /// + /// Prevent recursion by not including this converter in child (de)serializations + /// + /// + /// + private JsonSerializerOptions GetOptionsWithoutSelf(JsonSerializerOptions options) + { + JsonSerializerOptions newOptions = new(options); + //var thisConverter = newOptions.Converters.SingleOrDefault(c => c is JsonMappingAspectConverter); + + //if (thisConverter != null) + //{ + // newOptions.Converters.Remove(thisConverter); + //} + + return newOptions; + } + + private AbstractMappingAspect ParseJson(Type typeToConvert, JsonDocument json, JsonSerializerOptions options) + { + MappingAspectOptions mappingAspectOptions = new(); + var typeProperty = json.RootElement.GetProperty("$type"); + var type = MappingTypeHelper.EnsureSimpleTypeName(typeProperty.GetString()); + MappingTypeFactory failingFactory; + + if (typeToConvert == typeof(AbstractAction)) + { + failingFactory = new MappingTypeFactory( + typeof(DisabledAction).FullName, + typeof(DisabledAction).GetCustomAttribute()); + } + else if (typeToConvert == typeof(AbstractTrigger)) + { + failingFactory = new MappingTypeFactory( + typeof(DisabledTrigger).FullName, + typeof(DisabledTrigger).GetCustomAttribute()); + } + else + { + throw new NotSupportedException($"Type {type} is not supported for {json.RootElement}."); + } + + if (!this.allowedTypes.TryGetValue(type, out var factory)) + { + if (typeToConvert == typeof(AbstractAction)) + { + mappingAspectOptions.Add(nameof(DisabledAction.ActionName), type); + } + else if (typeToConvert == typeof(AbstractTrigger)) + { + mappingAspectOptions.Add(nameof(DisabledTrigger.TriggerName), type); + } + + factory = failingFactory; + } + + AbstractMappingAspect mappingAspect; + + try + { + var aspectRootProperty = json.RootElement.GetProperty( + options.PropertyNamingPolicy.ConvertName( + nameof(JsonMappingAspectWithType.Options) + ) + ); + + foreach (var property in aspectRootProperty.EnumerateObject()) + { + if (property.Value.ValueKind == JsonValueKind.Object) + { + throw new NotImplementedException($"The value kind {property.Value.ValueKind} is not yet supported."); + } + else if (property.Value.ValueKind == JsonValueKind.String) + { + mappingAspectOptions.Add(property.Name, property.Value.GetString()); + } + else if (property.Value.ValueKind == JsonValueKind.Number) + { + mappingAspectOptions.Add(property.Name, property.Value.GetDouble()); + } + else if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False) + { + mappingAspectOptions.Add(property.Name, property.Value.GetBoolean()); + } + else if (property.Value.ValueKind == JsonValueKind.Null) + { + mappingAspectOptions.Add(property.Name, null); + } + else if (property.Value.ValueKind == JsonValueKind.Array) + { + List list = new(); + + foreach (var item in property.Value.EnumerateArray()) + { + var rawJson = item.GetRawText(); + var document = JsonDocument.Parse(rawJson); + list.Add(this.ParseJson(typeToConvert, document, options)); + } + + mappingAspectOptions.Add(property.Name, list); + } + else + { + throw new NotImplementedException($"The value kind {property.Value.ValueKind} is not supported for {json.RootElement}."); + } + } + + var name = mappingAspectOptions[nameof(AbstractMappingAspect.Name)] as string; + mappingAspect = factory.CreateInstance(new object[] + { + name, + }); + mappingAspect.LoadOptions(mappingAspectOptions); + } + catch (Exception ex) + { + if (typeToConvert == typeof(AbstractAction)) + { + if (!mappingAspectOptions.ContainsKey(nameof(DisabledAction.ActionName))) + { + mappingAspectOptions.Add(nameof(DisabledAction.ActionName), type); + } + } + else if (typeToConvert == typeof(AbstractTrigger)) + { + if (!mappingAspectOptions.ContainsKey(nameof(DisabledTrigger.TriggerName))) + { + mappingAspectOptions.Add(nameof(DisabledTrigger.TriggerName), type); + } + } + mappingAspectOptions.Remove(nameof(AbstractMappingAspect.Name)); // Ensure we put the error in name + mappingAspect = failingFactory.CreateInstance(new object[] + { + ex.Message, + }); + mappingAspect.LoadOptions(mappingAspectOptions); + } + + return (T)mappingAspect; + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var json = JsonDocument.ParseValue(ref reader); + + return (T)this.ParseJson(typeToConvert, json, options); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var realTypeName = MappingTypeHelper.GetTypeFullName(this.allowedTypes, value); + + JsonSerializer.Serialize(writer, new JsonMappingAspectWithType + { + Options = value.SaveOptions(), + FullTypeName = realTypeName, + }, this.GetOptionsWithoutSelf(options)); + } +} diff --git a/Core/Key2Joy.Core/Mapping/MappedOption.cs b/Core/Key2Joy.Core/Mapping/MappedOption.cs index 0716d9cd..4a5d7fdb 100644 --- a/Core/Key2Joy.Core/Mapping/MappedOption.cs +++ b/Core/Key2Joy.Core/Mapping/MappedOption.cs @@ -1,47 +1,128 @@ +using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; namespace Key2Joy.Mapping; public class MappedOption : AbstractMappedOption { - public override object Clone() => new MappedOption() + [JsonIgnore] + public bool IsChild => this.ParentGuid != null; + + [JsonIgnore] + public MappedOption Parent { get; set; } + + [JsonIgnore] + public IList Children { get; set; } = new List(); + + public MappedOption() + : base() + => this.Guid = Guid.NewGuid(); + + public MappedOption(Guid guid) + : base() + => this.Guid = guid; + + public void SetParent(MappedOption parent) + { + if (parent == null) + { + this.Parent.Children.Remove(this); + this.ParentGuid = null; + this.Parent = null; + + return; + } + + this.ParentGuid = parent.Guid; + this.Parent = parent; + parent.Children.Add(this); + } + + public bool IsChildOf(MappedOption parent) + => this.ParentGuid != null && this.ParentGuid == parent.Guid; + + public void Initialize(IList allMappedOptions) + { + this.Children = new List(); + + foreach (var mappedOption in allMappedOptions) + { + if (mappedOption.ParentGuid.Equals(this.Guid)) + { + this.Children.Add(mappedOption); + mappedOption.Parent = this; + } + } + } + + public override string ToString() + => $"[{this.Guid}] Trigger: {this.Trigger} -> Action: {this.Action}"; + + /// + public override object Clone() => new MappedOption(this.Guid) { Trigger = this.Trigger != null ? (AbstractTrigger)this.Trigger.Clone() : null, Action = (AbstractAction)this.Action.Clone(), + ParentGuid = this.ParentGuid, }; - public static List GenerateOppositePressStateMappings(List mappings) + /// + /// Goes through all provided mappings and asks them to provide the reverse for their + /// action and trigger. If no is implemented, a + /// copy of the current mapping is returned. + /// + /// + /// + public static List GenerateReverseMappings(List mappings) { List newOptions = new(); - foreach (var pressVariant in mappings) + foreach (var mapping in mappings) { - var actionCopy = (AbstractAction)pressVariant.Action.Clone(); - var triggerCopy = (AbstractTrigger)pressVariant.Trigger.Clone(); + newOptions.Add(GenerateReverseMapping(mapping)); + } - if (actionCopy is IPressState actionWithPressState) - { - actionWithPressState.PressState = actionWithPressState.PressState == PressState.Press ? PressState.Release : PressState.Press; - } + return newOptions; + } - if (triggerCopy is IPressState triggerWithPressState) - { - triggerWithPressState.PressState = triggerWithPressState.PressState == PressState.Press ? PressState.Release : PressState.Press; - } + /// + /// Asks the provided mappings for a variant with reverse action and trigger. + /// If no is implemented, a copy of the + /// current mapping is returned. + /// + /// + /// Optionally dont set the parent, useful to get a reverse that wont be saved. + /// + public static MappedOption GenerateReverseMapping(MappedOption mapping, bool dontSetParent = false) + { + var actionCopy = (AbstractAction)mapping.Action.Clone(); + var triggerCopy = (AbstractTrigger)mapping.Trigger.Clone(); - MappedOption variantOption = new() - { - Action = actionCopy, - Trigger = triggerCopy, - }; + if (mapping.Action is IProvideReverseAspect action) + { + action.MakeReverse(actionCopy); + } - newOptions.Add(variantOption); + if (mapping.Trigger is IProvideReverseAspect trigger) + { + trigger.MakeReverse(triggerCopy); } - return newOptions; + MappedOption variantOption = new() + { + Action = actionCopy, + Trigger = triggerCopy, + }; + + if (!dontSetParent) + { + variantOption.SetParent(mapping); + } + + return variantOption; } } diff --git a/Core/Key2Joy.Core/Mapping/MappingProfile.cs b/Core/Key2Joy.Core/Mapping/MappingProfile.cs index 6efc6089..49e2e7bd 100644 --- a/Core/Key2Joy.Core/Mapping/MappingProfile.cs +++ b/Core/Key2Joy.Core/Mapping/MappingProfile.cs @@ -25,7 +25,7 @@ public class MappingProfile public const string BACKUP_EXTENSION = ".bak"; public const string SAVE_DIR = "Profiles"; - public BindingList MappedOptions { get; set; } = new BindingList(); + public BindingList MappedOptions { get; set; } = new(); public string Name { get; set; } public int Version { get; set; } = NO_VERSION; // Version is set on save @@ -51,6 +51,11 @@ public MappingProfile(string name, BindingList mappedOptions = nul { this.MappedOptions.Add((MappedOption)mappedOption.Clone()); } + + foreach (var mappedOption in this.MappedOptions) + { + mappedOption.Initialize(this.MappedOptions); + } } } @@ -64,7 +69,8 @@ public void AddMappingRange(IEnumerable mappedOptions) } } - public void RemoveMapping(MappedOption mappedOption) => this.MappedOptions.Remove(mappedOption); + public void RemoveMapping(MappedOption mappedOption) + => this.MappedOptions.Remove(mappedOption); public bool TryGetMappedOption(AbstractTrigger trigger, out MappedOption mappedOption) { @@ -121,7 +127,7 @@ public static void ExtractDefaultIfNotExists() using (FileStream file = new(defaultPath, FileMode.Create, FileAccess.Write)) using (BinaryWriter writer = new(file)) { - writer.Write(Properties.Resources.default_profile_k2j); + writer.Write(GetDefaultProfileContents()); } var configState = ServiceLocator.Current @@ -133,6 +139,9 @@ public static void ExtractDefaultIfNotExists() } } + public static byte[] GetDefaultProfileContents() + => Properties.Resources.default_profile_k2j; + public static string ResolveProfilePath(string filePath) { if (!File.Exists(filePath)) diff --git a/Core/Key2Joy.Core/Mapping/Triggers/CoreTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/CoreTriggerListener.cs index 7e318148..ebbb0997 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/CoreTriggerListener.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/CoreTriggerListener.cs @@ -41,15 +41,7 @@ public override void StopListening() protected virtual void Stop() => this.IsActive = false; - /// - /// Subclasses MUST call this to have their actions executed. - /// - /// Even when they know no actions are listening, they should call this. This - /// lets events provide other mapped options to be injected. - /// - /// - /// - /// + /// protected override bool DoExecuteTrigger( IList mappedOptions, AbstractInputBag inputBag, diff --git a/Core/Key2Joy.Core/Mapping/Triggers/DisabledTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/DisabledTrigger.cs index a4dce52d..fd0ca089 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/DisabledTrigger.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/DisabledTrigger.cs @@ -1,3 +1,4 @@ +using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Triggers; namespace Key2Joy.Mapping.Triggers; @@ -5,11 +6,13 @@ namespace Key2Joy.Mapping.Triggers; [Trigger( Description = "Disabled Trigger", NameFormat = DisabledNameFormat, - Visibility = Contracts.Mapping.MappingMenuVisibility.Never + Visibility = MappingMenuVisibility.Never, + GroupName = "Requires Attention", + GroupImage = "cross" )] public class DisabledTrigger : CoreTrigger { - private const string DisabledNameFormat = "The trigger '{0}' was unavailable upon loading Key2Joy. We have replaced it with this placeholder."; + private const string DisabledNameFormat = "The trigger '{0}' was unavailable upon loading Key2Joy. The error that caused this was: {1}"; public string TriggerName { get; set; } public DisabledTrigger(string name) @@ -18,8 +21,13 @@ public DisabledTrigger(string name) public override AbstractTriggerListener GetTriggerListener() => DisabledTriggerListener.Instance; - public override string GetUniqueKey() => $"DISABLED_{this.TriggerName}"; + /// + public override string GetNameDisplay() + => DisabledNameFormat + .Replace("{0}", this.TriggerName) + .Replace("{1}", this.Name); + /// public override bool Equals(object obj) { if (obj is not DisabledTrigger trigger) diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonInputBag.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonInputBag.cs new file mode 100644 index 00000000..f7db0644 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonInputBag.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.XInput; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +public class GamePadButtonInputBag : AbstractInputBag +{ + /// + /// Buttons that were pressed since the last update. + /// + public IList PressedButtons { get; set; } + + /// + /// Buttons that were released since the last update. + /// + public IList ReleasedButtons { get; set; } + + /// + /// The raw state of the gamepad. + /// + public XInputGamePad State { get; set; } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTrigger.cs new file mode 100644 index 00000000..3ae92c44 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTrigger.cs @@ -0,0 +1,72 @@ +using System; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; +using Key2Joy.LowLevelInput.XInput; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +[Trigger( + Description = "GamePad Button Event", + GroupName = "GamePad Triggers", + GroupImage = "joystick" +)] +public class GamePadButtonTrigger : CoreTrigger, IPressState, IProvideReverseAspect, IReturnInputHash, IEquatable +{ + public const string PREFIX_UNIQUE = nameof(GamePadButtonTrigger); + + public GamePadButton Button { get; set; } + + public PressState PressState { get; set; } + + [JsonConstructor] + public GamePadButtonTrigger(string name) + : base(name) + { } + + public override AbstractTriggerListener GetTriggerListener() + => GamePadButtonTriggerListener.Instance; + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); + + public static int GetInputHashFor(GamePadButton button) + => button.GetHashCode(); + + public int GetInputHash() + => GetInputHashFor(this.Button); + + // Keep Press and Release together while sorting + public override int CompareTo(AbstractMappingAspect other) + { + if (other == null || other is not GamePadButtonTrigger otherGamePadButtonTrigger) + { + return base.CompareTo(other); + } + + return $"{this.Button}#{(int)this.PressState}" + .CompareTo($"{otherGamePadButtonTrigger.Button}#{(int)otherGamePadButtonTrigger.PressState}"); + } + + public override bool Equals(object obj) + { + if (obj is not GamePadButtonTrigger other) + { + return false; + } + + return this.Equals(other); + } + + public bool Equals(GamePadButtonTrigger other) => this.Button == other.Button + && this.PressState == other.PressState; + + public override string GetNameDisplay() + { + var format = "(gamepad) {1} {0}"; + return format.Replace("{0}", this.Button.ToString()) + .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTriggerListener.cs new file mode 100644 index 00000000..31c81b40 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadButtonTriggerListener.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.XInput; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +public class GamePadButtonTriggerListener : PressReleaseTriggerListener +{ + private static GamePadButtonTriggerListener instance; + + public static GamePadButtonTriggerListener Instance + { + get + { + instance ??= new GamePadButtonTriggerListener(); + + return instance; + } + } + + private readonly IXInputService xInputService; + private readonly Dictionary currentKeysDown = new(); + + private GamePadButtonTriggerListener() + { + this.xInputService = ServiceLocator.Current.GetInstance(); + this.currentKeysDown = new(); + } + + public bool GetKeyDown(GamePadButton key) + => this.currentKeysDown.ContainsKey(key); + + /// + protected override void Start() + { + this.xInputService.StateChanged += this.XInputService_StateChanged; + this.currentKeysDown.Clear(); + + base.Start(); + } + + /// + protected override void Stop() + { + this.xInputService.StateChanged -= this.XInputService_StateChanged; + instance = null; + this.currentKeysDown.Clear(); + + base.Stop(); + } + + /// + public override bool GetIsTriggered(AbstractTrigger trigger) + { + if (trigger is not GamePadButtonTrigger gamePadButtonTrigger) + { + return false; + } + + return this.currentKeysDown.ContainsKey(gamePadButtonTrigger.Button); + } + + /// + /// This method is called when the state of a gamepad has changed. + /// We'll use it to find in the lookup which mapped options are triggered based on + /// the triggered device index and margins (using ). + /// + /// + /// + private void XInputService_StateChanged(object sender, DeviceStateChangedEventArgs e) + { + var state = e.NewState; + var mappedOptions = new List(); + + GamePadButtonInputBag inputBag = new() + { + PressedButtons = state.Gamepad.GetPressedButtonsList(), + State = state.Gamepad, + }; + + // Finds all mapped options that are triggered by newly pressed buttons + foreach (var button in inputBag.PressedButtons) + { + if (this.currentKeysDown.ContainsKey(button)) + { + continue; // Prevent firing multiple times for a single button press + } + + this.currentKeysDown.Add(button, true); + + var hash = GamePadButtonTrigger.GetInputHashFor(button); + + if (this.LookupPress.TryGetValue(hash, out var dictionaryMappedOptions)) + { + mappedOptions.AddRange(dictionaryMappedOptions); + } + } + + // Go through all buttons that are down, but not pressed anymore + // and find all mapped options that are triggered by them + var releasedButtons = new List(); + + foreach (var button in this.currentKeysDown.Keys) + { + if (inputBag.PressedButtons.Contains(button)) + { + continue; + } + + releasedButtons.Add(button); + + var hash = GamePadButtonTrigger.GetInputHashFor(button); + + if (this.LookupRelease.TryGetValue(hash, out var dictionaryMappedOptions)) + { + mappedOptions.AddRange(dictionaryMappedOptions); + } + } + + inputBag.ReleasedButtons = releasedButtons; + + if (this.DoExecuteTrigger( + mappedOptions, + inputBag, + this.GetIsTriggered) + ) + { + // TODO: Override default behaviour (if possible, if not then we should let the user know) + } + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadSide.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadSide.cs new file mode 100644 index 00000000..1097aab4 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadSide.cs @@ -0,0 +1,7 @@ +namespace Key2Joy.Mapping.Triggers.GamePad; + +public enum GamePadSide +{ + Left = 0, + Right = 1, +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickInputBag.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickInputBag.cs new file mode 100644 index 00000000..2e0e3ceb --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickInputBag.cs @@ -0,0 +1,16 @@ +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +public class GamePadStickInputBag : AbstractInputBag +{ + /// + /// The raw data from the gamepad for the left stick. + /// + public ExactAxisDirection LeftStickDelta { get; set; } + + /// + /// The raw data from the gamepad for the right stick. + /// + public ExactAxisDirection RightStickDelta { get; set; } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTrigger.cs new file mode 100644 index 00000000..274c1af9 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTrigger.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +[Trigger( + Description = "GamePad Stick Move Event", + GroupName = "GamePad Triggers", + GroupImage = "joystick" +)] +public class GamePadStickTrigger : CoreTrigger, IReturnInputHash, IEquatable +{ + public const string PREFIX_UNIQUE = nameof(GamePadStickTrigger); + + /// + /// Which gamepad index activates this trigger? + /// + public int GamePadIndex { get; set; } + + /// + /// Which stick activates this trigger? + /// + public GamePadSide StickSide { get; set; } + + /// + /// With what margin should the stick be moved to trigger? + /// If null then this trigger will be fired on any move (taking into account the default deadzone). + /// + public ExactAxisDirection? DeltaMargin { get; set; } = null; + + [JsonConstructor] + public GamePadStickTrigger(string name) + : base(name) + { } + + /// + public override AbstractTriggerListener GetTriggerListener() => GamePadStickTriggerListener.Instance; + + /// + public int GetInputHash() + => this.GetHashCode(); + + /// + public bool Equals(GamePadStickTrigger other) + => other is not null + && this.StickSide == other.StickSide + && EqualityComparer.Default.Equals(this.DeltaMargin, other.DeltaMargin); + + /// + public override bool Equals(object obj) + { + if (obj is not GamePadStickTrigger other) + { + return false; + } + + return this.Equals(other); + } + + /// + public override string GetNameDisplay() + { + var axis = this.DeltaMargin != null ? Enum.GetName(typeof(ExactAxisDirection), this.DeltaMargin) : "Any"; + return $"(gamepad) Move #{this.GamePadIndex} {this.StickSide} Stick {axis}"; + } + + /// + public override int GetHashCode() + { + var hashCode = -1723086675; + hashCode = (hashCode * -1521134295) + this.GamePadIndex.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.StickSide.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.DeltaMargin.GetHashCode(); + return hashCode; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTriggerListener.cs new file mode 100644 index 00000000..07a7fd97 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadStickTriggerListener.cs @@ -0,0 +1,134 @@ +using System.Collections.Generic; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.XInput; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +public class GamePadStickLookup : Dictionary> +{ } + +public class GamePadStickTriggerListener : CoreTriggerListener +{ + public static GamePadStickTriggerListener instance; + + public static GamePadStickTriggerListener Instance + { + get + { + instance ??= new GamePadStickTriggerListener(); + + return instance; + } + } + + private readonly Dictionary stickAxisLookups; + private readonly IXInputService xInputService; + + private GamePadStickTriggerListener() + { + this.xInputService = ServiceLocator.Current.GetInstance(); + + this.stickAxisLookups = new() + { + [GamePadSide.Left] = new GamePadStickLookup(), + [GamePadSide.Right] = new GamePadStickLookup() + }; + } + + /// + protected override void Start() + { + this.xInputService.StateChanged += this.XInputService_StateChanged; + + base.Start(); + } + + /// + protected override void Stop() + { + this.xInputService.StateChanged -= this.XInputService_StateChanged; + instance = null; + + base.Stop(); + } + + /// + public override void AddMappedOption(AbstractMappedOption mappedOption) + { + var trigger = mappedOption.Trigger as GamePadStickTrigger; + var lookup = this.stickAxisLookups[trigger.StickSide]; + + if (!lookup.TryGetValue(trigger.GetInputHash(), out var mappedOptions)) + { + lookup.Add(trigger.GetInputHash(), mappedOptions = new List()); + } + + mappedOptions.Add(mappedOption); + } + + /// + public override bool GetIsTriggered(AbstractTrigger trigger) + { + if (trigger is not GamePadStickTrigger gamePadStickTrigger) + { + return false; + } + + var state = this.xInputService.GetState(gamePadStickTrigger.GamePadIndex); + + if (state is null) + { + return false; + } + + return state.Value.Gamepad.IsThumbstickMoved(gamePadStickTrigger.StickSide, gamePadStickTrigger.DeltaMargin); + } + + /// + /// This method is called when the state of a gamepad has changed. + /// We'll use it to find in the lookup which mapped options are triggered based on + /// the triggered device index and margins (using ). + /// + /// + /// + private void XInputService_StateChanged(object sender, DeviceStateChangedEventArgs e) + { + var state = e.NewState; + + List mappedOptions = new(); + + foreach (var stickAxisLookupKvp in this.stickAxisLookups) + { + var sideToLookup = stickAxisLookupKvp.Key; + var lookup = stickAxisLookupKvp.Value; + + foreach (var lookupKvp in lookup) + { + var triggerHash = lookupKvp.Key; + var lookupMappedOptions = lookupKvp.Value; + + foreach (var mappedOption in lookupMappedOptions) + { + if (this.GetIsTriggered(mappedOption.Trigger)) + { + mappedOptions.AddRange(lookupMappedOptions); + } + } + } + } + + GamePadStickInputBag inputBag = new() + { + LeftStickDelta = state.Gamepad.GetStickDelta(GamePadSide.Left), + RightStickDelta = state.Gamepad.GetStickDelta(GamePadSide.Right), + }; + + this.DoExecuteTrigger( + mappedOptions, + inputBag, + this.GetIsTriggered + ); + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerInputBag.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerInputBag.cs new file mode 100644 index 00000000..9924d64e --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerInputBag.cs @@ -0,0 +1,16 @@ +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +public class GamePadTriggerInputBag : AbstractInputBag +{ + /// + /// How much the left trigger was pulled back. + /// + public float LeftTriggerDelta { get; set; } + + /// + /// How much the right trigger was pulled back. + /// + public float RightTriggerDelta { get; set; } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTrigger.cs new file mode 100644 index 00000000..167b9ef4 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTrigger.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +[Trigger( + Description = "GamePad Trigger Pull Event", + GroupName = "GamePad Triggers", + GroupImage = "joystick" +)] +public class GamePadTriggerTrigger : CoreTrigger, IReturnInputHash, IEquatable +{ + public const string PREFIX_UNIQUE = nameof(GamePadTriggerTrigger); + + /// + /// Which gamepad index activates this trigger? + /// + public int GamePadIndex { get; set; } + + /// + /// Which stick activates this trigger? + /// + public GamePadSide TriggerSide { get; set; } + + /// + /// With what margin should the trigger be pulled back to activate? + /// If null then this trigger will be fired on any move (taking into account the default deadzone). + /// + public float? DeltaMargin { get; set; } = null; + + /// + /// Used by the listener to check if it should fire even if 0. It fires once when the trigger was + /// pulled before, but has no become zero. + /// + [JsonIgnore] + public bool WasPulled { get; private set; } + + [JsonConstructor] + public GamePadTriggerTrigger(string name) + : base(name) + { } + + /// + public override AbstractTriggerListener GetTriggerListener() + => GamePadTriggerTriggerListener.Instance; + + /// + public override void DoActivate(AbstractInputBag inputBag, bool executed = false) + { + base.DoActivate(inputBag, executed); + + var triggerInputBag = inputBag as GamePadTriggerInputBag; + + if (this.TriggerSide == GamePadSide.Left) + { + this.WasPulled = triggerInputBag.LeftTriggerDelta > 0; + } + else + { + this.WasPulled = triggerInputBag.RightTriggerDelta > 0; + } + } + + /// + public int GetInputHash() + => this.GetHashCode(); + + /// + public bool Equals(GamePadTriggerTrigger other) + => other is not null + && this.TriggerSide == other.TriggerSide + && EqualityComparer.Default.Equals(this.DeltaMargin, other.DeltaMargin); + + /// + public override bool Equals(object obj) + { + if (obj is not GamePadTriggerTrigger other) + { + return false; + } + + return this.Equals(other); + } + + /// + public override string GetNameDisplay() + { + var margin = this.DeltaMargin != null ? this.DeltaMargin.Value.ToString() : "Any Amount"; + return $"(gamepad) Pull #{this.GamePadIndex} {this.TriggerSide} Trigger by {margin}"; + } + + /// + public override int GetHashCode() + { + var hashCode = -890627829; + hashCode = (hashCode * -1521134295) + this.GamePadIndex.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.TriggerSide.GetHashCode(); + hashCode = (hashCode * -1521134295) + this.DeltaMargin.GetHashCode(); + return hashCode; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTriggerListener.cs new file mode 100644 index 00000000..cbd5ce95 --- /dev/null +++ b/Core/Key2Joy.Core/Mapping/Triggers/GamePad/GamePadTriggerTriggerListener.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.XInput; + +namespace Key2Joy.Mapping.Triggers.GamePad; + +public class GamePadTriggerTriggerListener : CoreTriggerListener +{ + public static GamePadTriggerTriggerListener instance; + + public static GamePadTriggerTriggerListener Instance + { + get + { + instance ??= new GamePadTriggerTriggerListener(); + + return instance; + } + } + + private readonly Dictionary> triggerLookup; + private readonly IXInputService xInputService; + + private GamePadTriggerTriggerListener() + { + this.xInputService = ServiceLocator.Current.GetInstance(); + + this.triggerLookup = new() + { + [GamePadSide.Left] = new List(), + [GamePadSide.Right] = new List() + }; + } + + /// + protected override void Start() + { + this.xInputService.StateChanged += this.XInputService_StateChanged; + + base.Start(); + } + + /// + protected override void Stop() + { + this.xInputService.StateChanged -= this.XInputService_StateChanged; + instance = null; + + base.Stop(); + } + + /// + public override void AddMappedOption(AbstractMappedOption mappedOption) + { + var trigger = mappedOption.Trigger as GamePadTriggerTrigger; + var lookup = this.triggerLookup[trigger.TriggerSide]; + + lookup.Add(mappedOption); + } + + /// + public override bool GetIsTriggered(AbstractTrigger trigger) + { + if (trigger is not GamePadTriggerTrigger gamePadTriggerTrigger) + { + return false; + } + + var state = this.xInputService.GetState(gamePadTriggerTrigger.GamePadIndex); + + if (state is null) + { + // Ignore simulated gamepads + return false; + } + + var isPulled = state.Value.Gamepad.IsTriggerPulled(gamePadTriggerTrigger.TriggerSide, gamePadTriggerTrigger.DeltaMargin); + + if (isPulled) + { + return true; + } + + if (gamePadTriggerTrigger.WasPulled) + { + return true; + } + + return false; + } + + /// + /// This method is called when the state of a gamepad has changed. + /// We'll use it to find in the lookup which mapped options are triggered based on + /// the triggered device index and margins (using ). + /// + /// + /// + private void XInputService_StateChanged(object sender, DeviceStateChangedEventArgs e) + { + var state = e.NewState; + + List mappedOptions = new(); + + foreach (var stickAxisLookupKvp in this.triggerLookup) + { + var sideToLookup = stickAxisLookupKvp.Key; + var lookup = stickAxisLookupKvp.Value; + + foreach (var mappedOption in lookup) + { + if (this.GetIsTriggered(mappedOption.Trigger)) + { + mappedOptions.Add(mappedOption); + } + } + } + + GamePadTriggerInputBag inputBag = new() + { + LeftTriggerDelta = state.Gamepad.GetTriggerDelta(GamePadSide.Left), + RightTriggerDelta = state.Gamepad.GetTriggerDelta(GamePadSide.Right), + }; + + this.DoExecuteTrigger( + mappedOptions, + inputBag, + this.GetIsTriggered + ); + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/ITriggerOptionsControl.cs b/Core/Key2Joy.Core/Mapping/Triggers/ITriggerOptionsControl.cs index 4582cf1f..77c4aec2 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/ITriggerOptionsControl.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/ITriggerOptionsControl.cs @@ -1,24 +1,29 @@ -using System; -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Mapping.Triggers; - -public interface ITriggerOptionsControl -{ - /// - /// Called to setup the options panel with a trigger - /// - /// - void Select(AbstractTrigger trigger); - - /// - /// Called when the options panel should modify a resulting trigger - /// - /// - void Setup(AbstractTrigger trigger); - - /// - /// Called when the options on a trigger change - /// - event EventHandler OptionsChanged; -} +using System; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers; + +public interface ITriggerOptionsControl +{ + /// + /// Called to setup the options panel with a trigger + /// + /// + void Select(AbstractTrigger trigger); + + /// + /// Called when the options panel should modify a resulting trigger + /// + /// + void Setup(AbstractTrigger trigger); + + /// + /// Called when the mapping is saving and can still be stopped + /// + bool CanMappingSave(AbstractTrigger trigger); + + /// + /// Called when the options on a trigger change + /// + event EventHandler OptionsChanged; +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTrigger.cs index 62823327..77bab314 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTrigger.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTrigger.cs @@ -1,75 +1,79 @@ -using System; -using System.Text.Json.Serialization; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Triggers.Keyboard; - -[Trigger( - Description = "Keyboard Event" -)] -public class KeyboardTrigger : CoreTrigger, IPressState, IReturnInputHash, IEquatable -{ - public const string PREFIX_UNIQUE = nameof(KeyboardTrigger); - - public Keys Keys { get; set; } - - public PressState PressState { get; set; } - - [JsonConstructor] - public KeyboardTrigger(string name) - : base(name) - { } - - public override AbstractTriggerListener GetTriggerListener() => KeyboardTriggerListener.Instance; - - public static int GetInputHashFor(Keys keys) => (int)keys; - - public int GetInputHash() => GetInputHashFor(this.Keys); - - public override string GetUniqueKey() => $"{PREFIX_UNIQUE}_{this.Keys}"; - - // Keep Press and Release together while sorting - public override int CompareTo(AbstractMappingAspect other) - { - if (other == null || other is not KeyboardTrigger otherKeyboardTrigger) - { - return base.CompareTo(other); - } - - return $"{this.Keys}#{(int)this.PressState}" - .CompareTo($"{otherKeyboardTrigger.Keys}#{(int)otherKeyboardTrigger.PressState}"); - } - - public override bool Equals(object obj) - { - if (obj is not KeyboardTrigger other) - { - return false; - } - - return this.Equals(other); - } - - public bool Equals(KeyboardTrigger other) => this.Keys == other.Keys - && this.PressState == other.PressState; - - public override string ToString() - { - var format = "(keyboard) {1} {0}"; - return format.Replace("{0}", this.Keys.ToString()) - .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); - } - - public KeyboardState GetKeyboardState() - { - if (this.PressState == PressState.Press) - { - return KeyboardState.KeyDown; - } - - return KeyboardState.KeyUp; - } -} +using System; +using System.Text.Json.Serialization; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Triggers.Keyboard; + +[Trigger( + Description = "Keyboard Event", + GroupName = "Keyboard Triggers", + GroupImage = "keyboard" +)] +public class KeyboardTrigger : CoreTrigger, IPressState, IProvideReverseAspect, IReturnInputHash, IEquatable +{ + public const string PREFIX_UNIQUE = nameof(KeyboardTrigger); + + public Keys Keys { get; set; } + + public PressState PressState { get; set; } + + [JsonConstructor] + public KeyboardTrigger(string name) + : base(name) + { } + + public override AbstractTriggerListener GetTriggerListener() => KeyboardTriggerListener.Instance; + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); + + public static int GetInputHashFor(Keys keys) => (int)keys; + + public int GetInputHash() => GetInputHashFor(this.Keys); + + // Keep Press and Release together while sorting + public override int CompareTo(AbstractMappingAspect other) + { + if (other == null || other is not KeyboardTrigger otherKeyboardTrigger) + { + return base.CompareTo(other); + } + + return $"{this.Keys}#{(int)this.PressState}" + .CompareTo($"{otherKeyboardTrigger.Keys}#{(int)otherKeyboardTrigger.PressState}"); + } + + public override bool Equals(object obj) + { + if (obj is not KeyboardTrigger other) + { + return false; + } + + return this.Equals(other); + } + + public bool Equals(KeyboardTrigger other) => this.Keys == other.Keys + && this.PressState == other.PressState; + + public override string GetNameDisplay() + { + var format = "(keyboard) {1} {0}"; + return format.Replace("{0}", this.Keys.ToString()) + .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); + } + + public KeyboardState GetKeyboardState() + { + if (this.PressState == PressState.Press) + { + return KeyboardState.KeyDown; + } + + return KeyboardState.KeyUp; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTriggerListener.cs index fd092ac5..87d748ad 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTriggerListener.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Keyboard/KeyboardTriggerListener.cs @@ -22,31 +22,34 @@ public static KeyboardTriggerListener Instance private GlobalInputHook globalKeyboardHook; private readonly VirtualKeyConverter virtualKeyConverter = new(); - private readonly Dictionary currentKeysDown = new(); + private readonly Dictionary currentKeysPressed = new(); - public bool GetKeyDown(Keys key) => this.currentKeysDown.ContainsKey(key); + public bool GetKeyDown(Keys key) => this.currentKeysPressed.ContainsKey(key); + /// protected override void Start() { // This captures global keyboard input and blocks default behaviour by setting e.Handled this.globalKeyboardHook = new GlobalInputHook(); this.globalKeyboardHook.KeyboardInputEvent += this.OnKeyInputEvent; - this.currentKeysDown.Clear(); + this.currentKeysPressed.Clear(); base.Start(); } + /// protected override void Stop() { instance = null; this.globalKeyboardHook.KeyboardInputEvent -= this.OnKeyInputEvent; this.globalKeyboardHook.Dispose(); this.globalKeyboardHook = null; - this.currentKeysDown.Clear(); + this.currentKeysPressed.Clear(); base.Stop(); } + /// public override bool GetIsTriggered(AbstractTrigger trigger) { if (trigger is not KeyboardTrigger keyboardTrigger) @@ -54,7 +57,7 @@ public override bool GetIsTriggered(AbstractTrigger trigger) return false; } - return this.currentKeysDown.ContainsKey(keyboardTrigger.Keys); + return this.currentKeysPressed.ContainsKey(keyboardTrigger.Keys); } private void OnKeyInputEvent(object sender, GlobalKeyboardHookEventArgs e) @@ -70,24 +73,24 @@ private void OnKeyInputEvent(object sender, GlobalKeyboardHookEventArgs e) if (e.KeyboardState == KeyboardState.KeyDown) { - dictionary = this.lookupDown; + dictionary = this.LookupPress; - if (this.currentKeysDown.ContainsKey(keys)) + if (this.currentKeysPressed.ContainsKey(keys)) { return; // Prevent firing multiple times for a single key press } else { - this.currentKeysDown.Add(keys, true); + this.currentKeysPressed.Add(keys, true); } } else { - dictionary = this.lookupRelease; + dictionary = this.LookupRelease; - if (this.currentKeysDown.ContainsKey(keys)) + if (this.currentKeysPressed.ContainsKey(keys)) { - this.currentKeysDown.Remove(keys); + this.currentKeysPressed.Remove(keys); } } diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTrigger.cs index 45724bf1..dc6194dc 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTrigger.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTrigger.cs @@ -1,42 +1,43 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Mapping.Triggers.Logic; - -[Trigger( - Description = "Multiple Triggers Combined", - Visibility = MappingMenuVisibility.OnlyTopLevel -)] -public class CombinedTrigger : CoreTrigger, IEquatable -{ - public const string PREFIX_UNIQUE = nameof(CombinedTrigger); - - public List Triggers { get; set; } - - [JsonConstructor] - public CombinedTrigger(string name) - : base(name) - { } - - public override AbstractTriggerListener GetTriggerListener() => CombinedTriggerListener.Instance; - - public override string GetUniqueKey() => this.ToString(); - - public override bool Equals(object obj) - { - if (obj is not CombinedTrigger other) - { - return false; - } - - return this.Equals(other); - } - - public bool Equals(CombinedTrigger other) => this.Triggers.SequenceEqual(other.Triggers); - - public override string ToString() => string.Join(" + ", this.Triggers); -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.Logic; + +[Trigger( + Description = "Multiple Triggers Combined", + Visibility = MappingMenuVisibility.OnlyTopLevel, + GroupName = "Logic Triggers", + GroupImage = "application_xp_terminal" +)] +public class CombinedTrigger : CoreTrigger, IEquatable +{ + public const string PREFIX_UNIQUE = nameof(CombinedTrigger); + + public List Triggers { get; set; } + + [JsonConstructor] + public CombinedTrigger(string name) + : base(name) + { } + + public override AbstractTriggerListener GetTriggerListener() => CombinedTriggerListener.Instance; + + public override bool Equals(object obj) + { + if (obj is not CombinedTrigger other) + { + return false; + } + + return this.Equals(other); + } + + public bool Equals(CombinedTrigger other) => this.Triggers.SequenceEqual(other.Triggers); + + public override string GetNameDisplay() + => "(combined) " + string.Join(" + ", this.Triggers); +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTriggerListener.cs index 356b9bf3..03fd33d7 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTriggerListener.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Logic/CombinedTriggerListener.cs @@ -1,161 +1,165 @@ -using System.Collections.Generic; -using System.Linq; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Mapping.Triggers.Logic; - -public class CombinedTriggerListener : CoreTriggerListener -{ - public static CombinedTriggerListener instance; - public static CombinedTriggerListener Instance - { - get - { - instance ??= new CombinedTriggerListener(); - - return instance; - } - } - - protected IDictionary> lookup = new Dictionary>(); - private IDictionary optionsToExecute; - - public override void AddMappedOption(AbstractMappedOption mappedOption) - { - var trigger = mappedOption.Trigger as CombinedTrigger; - - if (!this.lookup.TryGetValue(trigger, out var mappedOptions)) - { - this.lookup.Add(trigger, mappedOptions = new List()); - } - - foreach (var realTrigger in trigger.Triggers) - { - // Disable these options from ever executing since we will do that manually in Listener_TriggerActivated - realTrigger.Executing += (s, e) => e.Handled = true; - } - - mappedOptions.Add(mappedOption); - } - - public override bool GetIsTriggered(AbstractTrigger trigger) => false; - - protected override void Start() - { - base.Start(); - - // Only listen to listeners that we have triggers for - foreach (var listener in this.allListeners) - { - var foundListener = false; - - foreach (var combinedTrigger in this.lookup.Keys) - { - foreach (var trigger in combinedTrigger.Triggers) - { - if (trigger.GetTriggerListener() == listener) - { - foundListener = true; - break; - } - } - - if (foundListener) - { - break; - } - } - - if (foundListener) - { - listener.TriggerActivating += this.Listener_TriggerActivating; - listener.TriggerActivated += this.Listener_TriggerActivated; - } - } - } - - private void Listener_TriggerActivating(object sender, TriggerActivatingEventArgs e) - { - this.optionsToExecute = new Dictionary(); - - foreach (var combinedTrigger in this.lookup.Keys) - { - var found = false; - - foreach (var trigger in combinedTrigger.Triggers) - { - if (e.GetIsMappedOptionCandidate(trigger)) - { - foreach (var mappedOption in this.lookup[combinedTrigger]) - { - this.optionsToExecute.Add(new MappedOption() - { - Trigger = trigger, - Action = mappedOption.Action - }, combinedTrigger); - } - found = true; - break; - } - } - - if (found) - { - break; - } - } - - foreach (var key in this.optionsToExecute.Keys) - { - e.MappedOptionCandidates.Add(key); - } - } - - private void Listener_TriggerActivated(object sender, TriggerActivatedEventArgs e) - { - foreach (var mappedOption in e.MappedOptions) - { - // Skip every mapped option that we didn't add as candidates ourselves - if (!this.optionsToExecute.TryGetValue(mappedOption, out var combinedTrigger)) - { - continue; - } - - this.optionsToExecute.Remove(mappedOption); - - // Check if all triggers for this mapped option are matched. Only then Executes the actions. - var allTriggered = true; - - foreach (var trigger in combinedTrigger.Triggers) - { - if (!trigger.GetTriggerListener().GetIsTriggered(trigger)) - { - allTriggered = false; - break; - } - } - - if (allTriggered) - { - CombinedInputBag inputBag = new() - { - InputBags = combinedTrigger.Triggers.Select(t => t.LastInputBag).ToList() - }; - - // TODO: Test: If we don't provide a filter, will we get an infinite loop for events that try to add options to CombinedTriggerListener? - this.DoExecuteTrigger( - this.lookup[combinedTrigger], - inputBag); - - break; - } - } - } - - protected override void Stop() - { - instance = null; - base.Stop(); - } -} +using System.Collections.Generic; +using System.Linq; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.Logic; + +public class CombinedTriggerListener : CoreTriggerListener +{ + public static CombinedTriggerListener instance; + + public static CombinedTriggerListener Instance + { + get + { + instance ??= new CombinedTriggerListener(); + + return instance; + } + } + + protected IDictionary> lookup = new Dictionary>(); + private IDictionary optionsToExecute; + + public override void AddMappedOption(AbstractMappedOption mappedOption) + { + var trigger = mappedOption.Trigger as CombinedTrigger; + + if (!this.lookup.TryGetValue(trigger, out var mappedOptions)) + { + this.lookup.Add(trigger, mappedOptions = new List()); + } + + foreach (var realTrigger in trigger.Triggers) + { + // Disable these options from ever executing since we will do that manually in Listener_TriggerActivated + realTrigger.Executing += (s, e) => e.Handled = true; + } + + mappedOptions.Add(mappedOption); + } + + /// + public override bool GetIsTriggered(AbstractTrigger trigger) => false; + + /// + protected override void Start() + { + base.Start(); + + // Only listen to listeners that we have triggers for + foreach (var listener in this.allListeners) + { + var foundListener = false; + + foreach (var combinedTrigger in this.lookup.Keys) + { + foreach (var trigger in combinedTrigger.Triggers) + { + if (trigger.GetTriggerListener() == listener) + { + foundListener = true; + break; + } + } + + if (foundListener) + { + break; + } + } + + if (foundListener) + { + listener.TriggerActivating += this.Listener_TriggerActivating; + listener.TriggerActivated += this.Listener_TriggerActivated; + } + } + } + + /// + protected override void Stop() + { + instance = null; + base.Stop(); + } + + private void Listener_TriggerActivating(object sender, TriggerActivatingEventArgs e) + { + this.optionsToExecute = new Dictionary(); + + foreach (var combinedTrigger in this.lookup.Keys) + { + var found = false; + + foreach (var trigger in combinedTrigger.Triggers) + { + if (e.GetIsMappedOptionCandidate(trigger)) + { + foreach (var mappedOption in this.lookup[combinedTrigger]) + { + this.optionsToExecute.Add(new MappedOption() + { + Trigger = trigger, + Action = mappedOption.Action + }, combinedTrigger); + } + found = true; + break; + } + } + + if (found) + { + break; + } + } + + foreach (var key in this.optionsToExecute.Keys) + { + e.MappedOptionCandidates.Add(key); + } + } + + private void Listener_TriggerActivated(object sender, TriggerActivatedEventArgs e) + { + foreach (var mappedOption in e.MappedOptions) + { + // Skip every mapped option that we didn't add as candidates ourselves + if (!this.optionsToExecute.TryGetValue(mappedOption, out var combinedTrigger)) + { + continue; + } + + this.optionsToExecute.Remove(mappedOption); + + // Check if all triggers for this mapped option are matched. Only then Executes the actions. + var allTriggered = true; + + foreach (var trigger in combinedTrigger.Triggers) + { + if (!trigger.GetTriggerListener().GetIsTriggered(trigger)) + { + allTriggered = false; + break; + } + } + + if (allTriggered) + { + CombinedInputBag inputBag = new() + { + InputBags = combinedTrigger.Triggers.Select(t => t.LastInputBag).ToList() + }; + + // TODO: Test: If we don't provide a filter, will we get an infinite loop for events that try to add options to CombinedTriggerListener? + this.DoExecuteTrigger( + this.lookup[combinedTrigger], + inputBag); + + break; + } + } + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveInputBag.cs b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/AxisDeltaInputBag.cs similarity index 54% rename from Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveInputBag.cs rename to Core/Key2Joy.Core/Mapping/Triggers/Mouse/AxisDeltaInputBag.cs index a93b6b2b..586d5e9f 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveInputBag.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/AxisDeltaInputBag.cs @@ -1,9 +1,9 @@ -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Mapping.Triggers.Mouse; - -public class MouseMoveInputBag : AbstractInputBag -{ - public int DeltaX { get; set; } - public int DeltaY { get; set; } -} +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.Mouse; + +public class AxisDeltaInputBag : AbstractInputBag +{ + public int DeltaX { get; set; } + public int DeltaY { get; set; } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTrigger.cs index 61c47ac0..0314cd4c 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTrigger.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTrigger.cs @@ -1,63 +1,74 @@ -using System; -using System.Text.Json.Serialization; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Triggers.Mouse; - -[Trigger( - Description = "Mouse Button Event" -)] -public class MouseButtonTrigger : CoreTrigger, IPressState, IReturnInputHash, IEquatable -{ - public const string PREFIX_UNIQUE = nameof(MouseButtonTrigger); - - public LowLevelInput.Mouse.Buttons MouseButtons { get; set; } - public PressState PressState { get; set; } - - [JsonConstructor] - public MouseButtonTrigger(string name) - : base(name) - { } - - public override AbstractTriggerListener GetTriggerListener() => MouseButtonTriggerListener.Instance; - - public override string GetUniqueKey() => $"{PREFIX_UNIQUE}_{this.MouseButtons}"; - - public static int GetInputHashFor(LowLevelInput.Mouse.Buttons mouseButtons) => (int)mouseButtons; - - public int GetInputHash() => GetInputHashFor(this.MouseButtons); - - // Keep Press and Release together while sorting - public override int CompareTo(AbstractMappingAspect other) - { - if (other == null || other is not MouseButtonTrigger otherMouseTrigger) - { - return base.CompareTo(other); - } - - return $"{this.MouseButtons}#{(int)this.PressState}" - .CompareTo($"{otherMouseTrigger.MouseButtons}#{(int)otherMouseTrigger.PressState}"); - } - - public override bool Equals(object obj) - { - if (obj is not MouseButtonTrigger other) - { - return false; - } - - return this.Equals(other); - } - - public bool Equals(MouseButtonTrigger other) => this.MouseButtons == other.MouseButtons - && this.PressState == other.PressState; - - public override string ToString() - { - var format = "(mouse) {1} {0}"; - return format.Replace("{0}", this.MouseButtons.ToString()) - .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); - } -} +using System; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Triggers.Mouse; + +[Trigger( + Description = "Mouse Button Event", + GroupName = "Mouse Triggers", + GroupImage = "mouse" +)] +public class MouseButtonTrigger : CoreTrigger, IPressState, IProvideReverseAspect, IReturnInputHash, IEquatable +{ + public const string PREFIX_UNIQUE = nameof(MouseButtonTrigger); + + public LowLevelInput.Mouse.Buttons MouseButtons { get; set; } + public PressState PressState { get; set; } + + [JsonConstructor] + public MouseButtonTrigger(string name) + : base(name) + { } + + /// + public override AbstractTriggerListener GetTriggerListener() => MouseButtonTriggerListener.Instance; + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); + + /// + public static int GetInputHashFor(LowLevelInput.Mouse.Buttons mouseButtons) => (int)mouseButtons; + + /// + public int GetInputHash() => GetInputHashFor(this.MouseButtons); + + /// + public override int CompareTo(AbstractMappingAspect other) + { + if (other == null || other is not MouseButtonTrigger otherMouseTrigger) + { + return base.CompareTo(other); + } + + return $"{this.MouseButtons}#{(int)this.PressState}" + .CompareTo($"{otherMouseTrigger.MouseButtons}#{(int)otherMouseTrigger.PressState}"); + } + + /// + public override bool Equals(object obj) + { + if (obj is not MouseButtonTrigger other) + { + return false; + } + + return this.Equals(other); + } + + /// + public bool Equals(MouseButtonTrigger other) + => this.MouseButtons == other.MouseButtons + && this.PressState == other.PressState; + + /// + public override string GetNameDisplay() + { + var format = "(mouse) {1} {0}"; + return format.Replace("{0}", this.MouseButtons.ToString()) + .Replace("{1}", Enum.GetName(typeof(PressState), this.PressState)); + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTriggerListener.cs index fb587327..53593cec 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTriggerListener.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseButtonTriggerListener.cs @@ -1,115 +1,119 @@ -using System.Collections.Generic; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Triggers.Mouse; - -public class MouseButtonTriggerListener : PressReleaseTriggerListener -{ - public static MouseButtonTriggerListener instance; - public static MouseButtonTriggerListener Instance - { - get - { - instance ??= new MouseButtonTriggerListener(); - - return instance; - } - } - - private GlobalInputHook globalMouseButtonHook; - private readonly Dictionary currentButtonsDown = new(); - - public bool GetButtonsDown(LowLevelInput.Mouse.Buttons buttons) => this.currentButtonsDown.ContainsKey(buttons); - - protected override void Start() - { - // This captures global mouse input and blocks default behaviour by setting e.Handled - this.globalMouseButtonHook = new GlobalInputHook(); - this.globalMouseButtonHook.MouseInputEvent += this.OnMouseButtonInputEvent; - - base.Start(); - } - - protected override void Stop() - { - instance = null; - this.globalMouseButtonHook.MouseInputEvent -= this.OnMouseButtonInputEvent; - this.globalMouseButtonHook.Dispose(); - this.globalMouseButtonHook = null; - - base.Stop(); - } - - public override bool GetIsTriggered(AbstractTrigger trigger) - { - if (trigger is not MouseButtonTrigger mouseButtonTrigger) - { - return false; - } - - return this.currentButtonsDown.ContainsKey(mouseButtonTrigger.MouseButtons); - } - - private void OnMouseButtonInputEvent(object sender, GlobalMouseHookEventArgs e) - { - if (!this.IsActive) - { - return; - } - - // Mouse movement is handled through WndProc and TryOverrideMouseMoveInput in MouseMoveTriggerListener - if (e.MouseState == MouseState.Move) - { - return; - } - - var buttons = LowLevelInput.Mouse.ButtonsFromEvent(e, out var isDown); - var dictionary = this.lookupRelease; - - if (isDown) - { - dictionary = this.lookupDown; - - if (this.currentButtonsDown.ContainsKey(buttons)) - { - return; // Prevent firing multiple times for a single key press - } - else - { - this.currentButtonsDown.Add(buttons, true); - } - } - else - { - if (this.currentButtonsDown.ContainsKey(buttons)) - { - this.currentButtonsDown.Remove(buttons); - } - } - - MouseButtonInputBag inputBag = new() - { - State = e.MouseState, - IsDown = isDown, - LastX = e.RawData.Position.X, - LastY = e.RawData.Position.Y, - }; - - var hash = MouseButtonTrigger.GetInputHashFor(buttons); - dictionary.TryGetValue(hash, out var mappedOptions); - - if (this.DoExecuteTrigger( - mappedOptions, - inputBag, - trigger => - { - var mouseTrigger = trigger as MouseButtonTrigger; - return mouseTrigger.GetInputHash() == hash - && mouseTrigger.MouseButtons == buttons; - })) - { - e.Handled = true; - } - } -} +using System.Collections.Generic; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Triggers.Mouse; + +public class MouseButtonTriggerListener : PressReleaseTriggerListener +{ + public static MouseButtonTriggerListener instance; + + public static MouseButtonTriggerListener Instance + { + get + { + instance ??= new MouseButtonTriggerListener(); + + return instance; + } + } + + private GlobalInputHook globalMouseButtonHook; + private readonly Dictionary currentButtonsDown = new(); + + public bool GetButtonsDown(LowLevelInput.Mouse.Buttons buttons) => this.currentButtonsDown.ContainsKey(buttons); + + /// + protected override void Start() + { + // This captures global mouse input and blocks default behaviour by setting e.Handled + this.globalMouseButtonHook = new GlobalInputHook(); + this.globalMouseButtonHook.MouseInputEvent += this.OnMouseButtonInputEvent; + + base.Start(); + } + + /// + protected override void Stop() + { + instance = null; + this.globalMouseButtonHook.MouseInputEvent -= this.OnMouseButtonInputEvent; + this.globalMouseButtonHook.Dispose(); + this.globalMouseButtonHook = null; + + base.Stop(); + } + + /// + public override bool GetIsTriggered(AbstractTrigger trigger) + { + if (trigger is not MouseButtonTrigger mouseButtonTrigger) + { + return false; + } + + return this.currentButtonsDown.ContainsKey(mouseButtonTrigger.MouseButtons); + } + + private void OnMouseButtonInputEvent(object sender, GlobalMouseHookEventArgs e) + { + if (!this.IsActive) + { + return; + } + + // Mouse movement is handled through WndProc and TryOverrideMouseMoveInput in MouseMoveTriggerListener + if (e.MouseState == MouseState.Move) + { + return; + } + + var buttons = LowLevelInput.Mouse.ButtonsFromEvent(e, out var isDown); + var dictionary = this.LookupRelease; + + if (isDown) + { + dictionary = this.LookupPress; + + if (this.currentButtonsDown.ContainsKey(buttons)) + { + return; // Prevent firing multiple times for a single key press + } + else + { + this.currentButtonsDown.Add(buttons, true); + } + } + else + { + if (this.currentButtonsDown.ContainsKey(buttons)) + { + this.currentButtonsDown.Remove(buttons); + } + } + + MouseButtonInputBag inputBag = new() + { + State = e.MouseState, + IsDown = isDown, + LastX = e.RawData.Position.X, + LastY = e.RawData.Position.Y, + }; + + var hash = MouseButtonTrigger.GetInputHashFor(buttons); + dictionary.TryGetValue(hash, out var mappedOptions); + + if (this.DoExecuteTrigger( + mappedOptions, + inputBag, + trigger => + { + var mouseTrigger = trigger as MouseButtonTrigger; + return mouseTrigger.GetInputHash() == hash + && mouseTrigger.MouseButtons == buttons; + })) + { + e.Handled = true; + } + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTrigger.cs b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTrigger.cs index 2a35a01b..ba75ca1e 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTrigger.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTrigger.cs @@ -1,44 +1,50 @@ -using System; -using System.Text.Json.Serialization; -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Mapping.Triggers.Mouse; - -[Trigger( - Description = "Mouse Move Event" -)] -public class MouseMoveTrigger : CoreTrigger, IReturnInputHash -{ - public const string PREFIX_UNIQUE = nameof(MouseButtonTrigger); - - public AxisDirection AxisBinding { get; set; } - - [JsonConstructor] - public MouseMoveTrigger(string name) - : base(name) - { } - - public override AbstractTriggerListener GetTriggerListener() => MouseMoveTriggerListener.Instance; - - public override string GetUniqueKey() => $"{PREFIX_UNIQUE}_{this.AxisBinding}"; - - public static int GetInputHashFor(AxisDirection axisBinding) => (int)axisBinding; - - public int GetInputHash() => GetInputHashFor(this.AxisBinding); - - public override bool Equals(object obj) - { - if (obj is not MouseMoveTrigger other) - { - return false; - } - - return this.AxisBinding == other.AxisBinding; - } - - public override string ToString() - { - var axis = Enum.GetName(typeof(AxisDirection), this.AxisBinding); - return $"(mouse) Move {axis}"; - } -} +using System; +using System.Text.Json.Serialization; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Mapping.Triggers.Mouse; + +[Trigger( + Description = "Mouse Move Event", + GroupName = "Mouse Triggers", + GroupImage = "mouse" +)] +public class MouseMoveTrigger : CoreTrigger, IReturnInputHash +{ + public const string PREFIX_UNIQUE = nameof(MouseMoveTrigger); + + /// + /// The direction that the mouse must move in order to trigger this action. + /// + public AxisDirection AxisBinding { get; set; } + + [JsonConstructor] + public MouseMoveTrigger(string name) + : base(name) + { } + + public override AbstractTriggerListener GetTriggerListener() + => MouseMoveTriggerListener.Instance; + + public static int GetInputHashFor(AxisDirection axisBinding) + => axisBinding.GetHashCode(); + + public int GetInputHash() + => GetInputHashFor(this.AxisBinding); + + public override bool Equals(object obj) + { + if (obj is not MouseMoveTrigger other) + { + return false; + } + + return this.AxisBinding == other.AxisBinding; + } + + public override string GetNameDisplay() + { + var axis = Enum.GetName(typeof(AxisDirection), this.AxisBinding); + return $"(mouse) Move {axis}"; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTriggerListener.cs index 2e7cd5c1..bd3cf1cf 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTriggerListener.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/Mouse/MouseMoveTriggerListener.cs @@ -1,165 +1,170 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using Key2Joy.Contracts; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; -using Linearstar.Windows.RawInput; - -namespace Key2Joy.Mapping.Triggers.Mouse; - -public class MouseMoveTriggerListener : CoreTriggerListener, IWndProcHandler -{ - public IntPtr Handle { get; set; } - - public static MouseMoveTriggerListener instance; - - public static MouseMoveTriggerListener Instance - { - get - { - instance ??= new MouseMoveTriggerListener(); - - return instance; - } - } - - private static readonly TimeSpan IS_MOVING_TOLERANCE = TimeSpan.FromMilliseconds(10); - private const double SENSITIVITY = 0.05; - private const int WM_INPUT = 0x00FF; - - private readonly Dictionary> lookupAxis; - - private List lastDirectionHashes; - private DateTime lastMoveTime; - - private MouseMoveTriggerListener() => this.lookupAxis = new Dictionary>(); - - protected override void Start() - { - RawInputDevice.RegisterDevice(HidUsageAndPage.Mouse, RawInputDeviceFlags.InputSink, this.Handle); - - base.Start(); - } - - protected override void Stop() - { - instance = null; - - base.Stop(); - } - - public override void AddMappedOption(AbstractMappedOption mappedOption) - { - var trigger = mappedOption.Trigger as MouseMoveTrigger; - - if (!this.lookupAxis.TryGetValue(trigger.GetInputHash(), out var mappedOptions)) - { - this.lookupAxis.Add(trigger.GetInputHash(), mappedOptions = new List()); - } - - mappedOptions.Add(mappedOption); - } - - public override bool GetIsTriggered(AbstractTrigger trigger) - { - if (trigger is not MouseMoveTrigger mouseMoveTrigger) - { - return false; - } - - return DateTime.Now - this.lastMoveTime < IS_MOVING_TOLERANCE - && this.lastDirectionHashes.Contains(mouseMoveTrigger.GetInputHash()); - } - - public void WndProc(Message m) - { - if (!this.IsActive) - { - return; - } - - if (m.Msg != WM_INPUT) - { - return; - } - - try - { - var data = RawInputData.FromHandle(m.LParam); - - if (data is not RawInputMouseData mouse) - { - return; - } - - if (this.TryOverrideMouseMoveInput(mouse.Mouse.LastX, mouse.Mouse.LastY)) - { - return; - } - } - catch (Linearstar.Windows.RawInput.Native.Win32ErrorException ex) - { - Output.WriteLine(ex); - // This exception seems to occur accross AppDomain boundary, when clicking on a MessageBox OK button - Debug.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace); - } - } - - private bool TryOverrideMouseMoveInput(int lastX, int lastY) - { - var deltaX = (short)Math.Min(Math.Max(lastX * short.MaxValue * SENSITIVITY, short.MinValue), short.MaxValue); - var deltaY = (short)-Math.Min(Math.Max(lastY * short.MaxValue * SENSITIVITY, short.MinValue), short.MaxValue); - - List mappedOptions = new(); - List directionHashes = new(); - Dictionary> directionChecks = new() - { - { - MouseMoveTrigger.GetInputHashFor(AxisDirection.Right), - () => deltaX > 0 - }, - { - MouseMoveTrigger.GetInputHashFor(AxisDirection.Left), - () => deltaX < 0 - }, - { - MouseMoveTrigger.GetInputHashFor(AxisDirection.Forward), - () => deltaY > 0 - }, - { - MouseMoveTrigger.GetInputHashFor(AxisDirection.Backward), - () => deltaY < 0 - }, - }; - - foreach (var directionCheck in directionChecks) - { - if (directionCheck.Value.Invoke()) - { - if (this.lookupAxis.TryGetValue(directionCheck.Key, out var matchedOptions)) - { - mappedOptions.AddRange(matchedOptions); - directionHashes.Add(directionCheck.Key); - } - } - } - - MouseMoveInputBag inputBag = new() - { - DeltaX = deltaX, - DeltaY = deltaY, - }; - - this.DoExecuteTrigger( - mappedOptions, - inputBag, - trigger => directionHashes.Contains((trigger as IReturnInputHash).GetInputHash()) - ); - - this.lastDirectionHashes = directionHashes; - this.lastMoveTime = DateTime.Now; - - return true; - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Key2Joy.Contracts; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Linearstar.Windows.RawInput; + +namespace Key2Joy.Mapping.Triggers.Mouse; + +public class MouseMoveTriggerListener : CoreTriggerListener, IWndProcHandler +{ + public IntPtr Handle { get; set; } + + public static MouseMoveTriggerListener instance; + + public static MouseMoveTriggerListener Instance + { + get + { + instance ??= new MouseMoveTriggerListener(); + + return instance; + } + } + + private static readonly TimeSpan IS_MOVING_TOLERANCE = TimeSpan.FromMilliseconds(10); + private const int WM_INPUT = 0x00FF; + + private readonly Dictionary> lookupAxis; + + private List lastDirectionHashes; + private DateTime lastMoveTime; + + private MouseMoveTriggerListener() => this.lookupAxis = new Dictionary>(); + + /// + protected override void Start() + { + RawInputDevice.RegisterDevice(HidUsageAndPage.Mouse, RawInputDeviceFlags.InputSink, this.Handle); + + base.Start(); + } + + /// + protected override void Stop() + { + instance = null; + + base.Stop(); + } + + /// + public override void AddMappedOption(AbstractMappedOption mappedOption) + { + var trigger = mappedOption.Trigger as MouseMoveTrigger; + + if (!this.lookupAxis.TryGetValue(trigger.GetInputHash(), out var mappedOptions)) + { + this.lookupAxis.Add(trigger.GetInputHash(), mappedOptions = new List()); + } + + mappedOptions.Add(mappedOption); + } + + /// + public override bool GetIsTriggered(AbstractTrigger trigger) + { + if (trigger is not MouseMoveTrigger mouseMoveTrigger) + { + return false; + } + + return DateTime.Now - this.lastMoveTime < IS_MOVING_TOLERANCE + && this.lastDirectionHashes.Contains(mouseMoveTrigger.GetInputHash()); + } + + /// + public void WndProc(Message m) + { + if (!this.IsActive) + { + return; + } + + if (m.Msg != WM_INPUT) + { + return; + } + + try + { + var data = RawInputData.FromHandle(m.LParam); + + if (data is not RawInputMouseData mouse) + { + return; + } + + if (this.TryOverrideMouseMoveInput(mouse.Mouse.LastX, mouse.Mouse.LastY)) + { + return; + } + } + catch (Linearstar.Windows.RawInput.Native.Win32ErrorException ex) + { + Output.WriteLine(ex); + // This exception seems to occur accross AppDomain boundary, when clicking on a MessageBox OK button + Debug.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace); + } + } + + private bool TryOverrideMouseMoveInput(int deltaX, int deltaY) + { + List mappedOptions = new(); + List directionHashes = new(); + Dictionary> directionChecks = new() + { + { + MouseMoveTrigger.GetInputHashFor(AxisDirection.Any), + () => deltaX != 0 || deltaY != 0 + }, + { + MouseMoveTrigger.GetInputHashFor(AxisDirection.Right), + () => deltaX > 0 + }, + { + MouseMoveTrigger.GetInputHashFor(AxisDirection.Left), + () => deltaX < 0 + }, + { + MouseMoveTrigger.GetInputHashFor(AxisDirection.Forward), + () => deltaY > 0 + }, + { + MouseMoveTrigger.GetInputHashFor(AxisDirection.Backward), + () => deltaY < 0 + }, + }; + + foreach (var directionCheck in directionChecks) + { + if (directionCheck.Value.Invoke()) + { + if (this.lookupAxis.TryGetValue(directionCheck.Key, out var matchedOptions)) + { + mappedOptions.AddRange(matchedOptions); + directionHashes.Add(directionCheck.Key); + } + } + } + + AxisDeltaInputBag inputBag = new() + { + DeltaX = deltaX, + DeltaY = deltaY, + }; + + this.DoExecuteTrigger( + mappedOptions, + inputBag, + trigger => directionHashes.Contains((trigger as IReturnInputHash).GetInputHash()) + ); + + this.lastDirectionHashes = directionHashes; + this.lastMoveTime = DateTime.Now; + + return true; + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/PressReleaseTriggerListener.cs b/Core/Key2Joy.Core/Mapping/Triggers/PressReleaseTriggerListener.cs index 5bddddc4..d289f794 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/PressReleaseTriggerListener.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/PressReleaseTriggerListener.cs @@ -1,46 +1,58 @@ -using System.Collections.Generic; -using Key2Joy.Contracts.Mapping; -using Key2Joy.LowLevelInput; - -namespace Key2Joy.Mapping.Triggers; - -public abstract class PressReleaseTriggerListener : CoreTriggerListener - where TTrigger : class, IPressState, IReturnInputHash -{ - protected Dictionary> lookupDown; - protected Dictionary> lookupRelease; - - protected PressReleaseTriggerListener() - { - this.lookupDown = new Dictionary>(); - this.lookupRelease = new Dictionary>(); - } - - public override void AddMappedOption(AbstractMappedOption mappedOption) - { - var trigger = mappedOption.Trigger as TTrigger; - var dictionary = (Dictionary>)null; - - if (trigger.PressState == PressState.Press) - { - dictionary = this.lookupDown; - } - - if (trigger.PressState == PressState.Release) - { - dictionary = this.lookupRelease; - } - - if (dictionary == null) - { - return; - } - - if (!dictionary.TryGetValue(trigger.GetInputHash(), out var mappedOptions)) - { - dictionary.Add(trigger.GetInputHash(), mappedOptions = new List()); - } - - mappedOptions.Add(mappedOption); - } -} +using System.Collections.Generic; +using Key2Joy.Contracts.Mapping; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Mapping.Triggers; + +/// +/// Base class for trigger listeners that listen for press and release events. +/// +/// +public abstract class PressReleaseTriggerListener : CoreTriggerListener + where TTrigger : class, IPressState, IReturnInputHash +{ + /// + /// Quick lookup by hash for mappings bound to pressing input down + /// + protected Dictionary> LookupPress; + + /// + /// Quick lookup by hash for mappings bound to releasing input + /// + protected Dictionary> LookupRelease; + + protected PressReleaseTriggerListener() + { + this.LookupPress = new Dictionary>(); + this.LookupRelease = new Dictionary>(); + } + + /// + public override void AddMappedOption(AbstractMappedOption mappedOption) + { + var trigger = mappedOption.Trigger as TTrigger; + var dictionary = (Dictionary>)null; + + if (trigger.PressState == PressState.Press) + { + dictionary = this.LookupPress; + } + + if (trigger.PressState == PressState.Release) + { + dictionary = this.LookupRelease; + } + + if (dictionary == null) + { + return; + } + + if (!dictionary.TryGetValue(trigger.GetInputHash(), out var mappedOptions)) + { + dictionary.Add(trigger.GetInputHash(), mappedOptions = new List()); + } + + mappedOptions.Add(mappedOption); + } +} diff --git a/Core/Key2Joy.Core/Mapping/Triggers/TriggersRepository.cs b/Core/Key2Joy.Core/Mapping/Triggers/TriggersRepository.cs index b01054e9..2949f936 100644 --- a/Core/Key2Joy.Core/Mapping/Triggers/TriggersRepository.cs +++ b/Core/Key2Joy.Core/Mapping/Triggers/TriggersRepository.cs @@ -1,77 +1,91 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.Plugins; - -namespace Key2Joy.Mapping.Triggers; - -public class TriggersRepository -{ - private static Dictionary> triggers; - - /// - /// Loads all triggers in the assembly, optionally merging it with additional trigger types. - /// - /// - public static void Buffer(IReadOnlyList> additionalTriggerTypeFactories = null) - { - triggers = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => t.GetCustomAttribute(typeof(TriggerAttribute), false) != null) - .ToDictionary( - t => t.FullName, - t => new MappingTypeFactory(t.FullName, t.GetCustomAttribute()) - ); - - if (additionalTriggerTypeFactories == null) - { - return; - } - - foreach (var triggerFactory in additionalTriggerTypeFactories) - { - if (triggers.ContainsKey(triggerFactory.FullTypeName)) - { - Console.WriteLine("Trigger {0} already exists in the trigger buffer. Overwriting.", triggerFactory.FullTypeName); - } - - triggers.Add(triggerFactory.FullTypeName, triggerFactory); - } - } - - /// - /// Gets all trigger types and their attribute annotations - /// - /// - /// - public static Dictionary> GetAllTriggers() => triggers; - - /// - /// Gets all trigger types and their attribute annotations depending on the specified visibility - /// - /// - /// - public static SortedDictionary> GetAllTriggers(bool forTopLevel) => new SortedDictionary>( - triggers - .Where(kvp => - { - if (kvp.Value.Attribute is not TriggerAttribute triggerAttribute - || triggerAttribute.Visibility == MappingMenuVisibility.Never) - { - return false; - } - - if (forTopLevel) - { - return triggerAttribute.Visibility is MappingMenuVisibility.Always - or MappingMenuVisibility.OnlyTopLevel; - } - - return triggerAttribute.Visibility is MappingMenuVisibility.Always or MappingMenuVisibility.UnlessTopLevel; - }) - .ToDictionary(t => t.Value.Attribute as TriggerAttribute, t => t.Value) - ); -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Jint; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Plugins; + +namespace Key2Joy.Mapping.Triggers; + +public class TriggersRepository +{ + private static Dictionary> triggers; + + /// + /// Loads all triggers in the assembly, optionally merging it with additional trigger types. + /// + /// + public static void Buffer(IReadOnlyList> additionalTriggerTypeFactories = null) + { + triggers = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.GetCustomAttribute(typeof(TriggerAttribute), false) != null) + .ToDictionary( + t => t.FullName, + t => new MappingTypeFactory(t.FullName, t.GetCustomAttribute()) + ); + + if (additionalTriggerTypeFactories == null) + { + return; + } + + foreach (var triggerFactory in additionalTriggerTypeFactories) + { + if (triggers.ContainsKey(triggerFactory.FullTypeName)) + { + Console.WriteLine("Trigger {0} already exists in the trigger buffer. Overwriting.", triggerFactory.FullTypeName); + } + + triggers.Add(triggerFactory.FullTypeName, triggerFactory); + } + } + + /// + /// Gets all trigger types and their attribute annotations + /// + /// + /// + public static Dictionary> GetAllTriggers() => triggers; + + /// + /// Gets all trigger types and their attribute annotations depending on the specified visibility + /// + /// + /// + public static SortedDictionary> GetAllTriggers(bool forTopLevel) => new SortedDictionary>( + triggers + .Where(kvp => + { + if (kvp.Value.Attribute is not TriggerAttribute triggerAttribute + || triggerAttribute.Visibility == MappingMenuVisibility.Never) + { + return false; + } + + if (forTopLevel) + { + return triggerAttribute.Visibility is MappingMenuVisibility.Always + or MappingMenuVisibility.OnlyTopLevel; + } + + return triggerAttribute.Visibility is MappingMenuVisibility.Always or MappingMenuVisibility.UnlessTopLevel; + }) + .ToDictionary(t => t.Value.Attribute as TriggerAttribute, t => t.Value) + ); + + /// + /// Gets the attribute for the provided trigger + /// + /// + /// + public static TriggerAttribute GetAttributeForTrigger(AbstractTrigger trigger) + { + var realTypeName = MappingTypeHelper.GetTypeFullName(triggers, trigger); + realTypeName = MappingTypeHelper.EnsureSimpleTypeName(realTypeName); + return triggers[realTypeName].Attribute as TriggerAttribute; + } +} diff --git a/Core/Key2Joy.Core/MappingArmingFailedException.cs b/Core/Key2Joy.Core/MappingArmingFailedException.cs new file mode 100644 index 00000000..9ad978cb --- /dev/null +++ b/Core/Key2Joy.Core/MappingArmingFailedException.cs @@ -0,0 +1,10 @@ +using System; + +namespace Key2Joy; + +public class MappingArmingFailedException : Exception +{ + public MappingArmingFailedException(string message) : base(message) + { + } +} diff --git a/Core/Key2Joy.Core/Plugins/ElementHostProxy.cs b/Core/Key2Joy.Core/Plugins/ElementHostProxy.cs index 31ead988..83e0b661 100644 --- a/Core/Key2Joy.Core/Plugins/ElementHostProxy.cs +++ b/Core/Key2Joy.Core/Plugins/ElementHostProxy.cs @@ -1,43 +1,43 @@ -using System; -using System.Windows; -using System.Windows.Forms.Integration; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.PluginHost; - -namespace Key2Joy.Plugins; - -public class ElementHostProxy : ElementHost, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - private readonly NativeHandleContractInsulator contract; - - public ElementHostProxy(FrameworkElement child, NativeHandleContractInsulator contract) - : base() - { - this.Child = child; - this.contract = contract; - } - - public void InvokeOptionsChanged() => OptionsChanged?.Invoke(this, EventArgs.Empty); - - public bool CanMappingSave(object action) - { - var realAction = ((PluginActionProxy)action).GetRealObject(); - return (bool)this.contract.RemoteInvokeUI(nameof(CanMappingSave), new object[] { realAction }); - } - - public void Select(object action) - { - var realAction = ((PluginActionProxy)action).GetRealObject(); - this.contract.RemoteInvokeUI(nameof(Select), new object[] { realAction }); - } - - public void Setup(object action) - { - var realAction = ((PluginActionProxy)action).GetRealObject(); - this.contract.RemoteInvokeUI(nameof(Setup), new object[] { realAction }); - } - - public int GetDesiredHeight() => (int)this.contract.RemoteInvokeUI(nameof(GetDesiredHeight), new object[] { }); -} +using System; +using System.Windows; +using System.Windows.Forms.Integration; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.PluginHost; + +namespace Key2Joy.Plugins; + +public class ElementHostProxy : ElementHost, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + private readonly NativeHandleContractInsulator contract; + + public ElementHostProxy(FrameworkElement child, NativeHandleContractInsulator contract) + : base() + { + this.Child = child; + this.contract = contract; + } + + public void InvokeOptionsChanged() => OptionsChanged?.Invoke(this, EventArgs.Empty); + + public bool CanMappingSave(AbstractAction action) + { + var realAction = ((PluginActionProxy)action).GetRealObject(); + return (bool)this.contract.RemoteInvokeUI(nameof(CanMappingSave), new object[] { realAction }); + } + + public void Select(AbstractAction action) + { + var realAction = ((PluginActionProxy)action).GetRealObject(); + this.contract.RemoteInvokeUI(nameof(Select), new object[] { realAction }); + } + + public void Setup(AbstractAction action) + { + var realAction = ((PluginActionProxy)action).GetRealObject(); + this.contract.RemoteInvokeUI(nameof(Setup), new object[] { realAction }); + } + + public int GetDesiredHeight() => (int)this.contract.RemoteInvokeUI(nameof(GetDesiredHeight), new object[] { }); +} diff --git a/Core/Key2Joy.Core/Plugins/PluginSet.cs b/Core/Key2Joy.Core/Plugins/PluginSet.cs index 3bb012e8..a60582d2 100644 --- a/Core/Key2Joy.Core/Plugins/PluginSet.cs +++ b/Core/Key2Joy.Core/Plugins/PluginSet.cs @@ -11,7 +11,7 @@ using Key2Joy.Mapping; using Key2Joy.Mapping.Actions; using Key2Joy.Mapping.Triggers; -using static System.Windows.Forms.AxHost; +using Key2Joy.Mapping.Triggers.GamePad; namespace Key2Joy.Plugins; @@ -19,7 +19,7 @@ namespace Key2Joy.Plugins; [ExposesEnumeration(typeof(LowLevelInput.Mouse.Buttons))] [ExposesEnumeration(typeof(SimWinInput.GamePadControl))] [ExposesEnumeration(typeof(LowLevelInput.PressState))] -[ExposesEnumeration(typeof(LowLevelInput.Simulator.GamePadStick))] +[ExposesEnumeration(typeof(GamePadSide))] [ExposesEnumeration(typeof(Mapping.Actions.Logic.AppCommand))] [ExposesEnumeration(typeof(LowLevelInput.KeyboardKey))] public class PluginSet : IDisposable diff --git a/Core/Key2Joy.Core/Plugins/PluginTriggerProxy.cs b/Core/Key2Joy.Core/Plugins/PluginTriggerProxy.cs index 37ec4283..c5e38f75 100644 --- a/Core/Key2Joy.Core/Plugins/PluginTriggerProxy.cs +++ b/Core/Key2Joy.Core/Plugins/PluginTriggerProxy.cs @@ -1,20 +1,18 @@ -using System; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.Contracts.Plugins; -using Key2Joy.Mapping.Triggers; - -namespace Key2Joy.Plugins; - -public class PluginTriggerProxy : CoreTrigger, IGetRealObject -{ - private readonly PluginTriggerInsulator source; - - public PluginTriggerProxy(string name, PluginTriggerInsulator source) - : base(name) => this.source = source; - - public PluginTrigger GetRealObject() => this.source.PluginTrigger; - - public override AbstractTriggerListener GetTriggerListener() => throw new NotImplementedException(); - - public override string GetUniqueKey() => throw new NotImplementedException(); -} +using System; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Contracts.Plugins; +using Key2Joy.Mapping.Triggers; + +namespace Key2Joy.Plugins; + +public class PluginTriggerProxy : CoreTrigger, IGetRealObject +{ + private readonly PluginTriggerInsulator source; + + public PluginTriggerProxy(string name, PluginTriggerInsulator source) + : base(name) => this.source = source; + + public PluginTrigger GetRealObject() => this.source.PluginTrigger; + + public override AbstractTriggerListener GetTriggerListener() => throw new NotImplementedException(); +} diff --git a/Core/Key2Joy.Core/Util/TypeExtensions.cs b/Core/Key2Joy.Core/Util/TypeExtensions.cs index 9e95342b..4b336c91 100644 --- a/Core/Key2Joy.Core/Util/TypeExtensions.cs +++ b/Core/Key2Joy.Core/Util/TypeExtensions.cs @@ -62,4 +62,176 @@ public static Array CopyArrayToNewType(this object[] originalArray, Type element // Delegate.CreateDelegate(converterDelegateType, typeof(Convert).GetMethod("ChangeType", new[] { typeof(object), typeof(Type) })) // }); } + + /// + /// Gets the minimum value for a numeric type + /// + /// + /// + public static object GetNumericMinValue(this Type type) + { + type = Nullable.GetUnderlyingType(type) ?? type; + + if (type == typeof(int)) + { + return int.MinValue; + } + else if (type == typeof(uint)) + { + return uint.MinValue; + } + else if (type == typeof(long)) + { + return long.MinValue; + } + else if (type == typeof(ulong)) + { + return ulong.MinValue; + } + else if (type == typeof(short)) + { + return short.MinValue; + } + else if (type == typeof(ushort)) + { + return ushort.MinValue; + } + else if (type == typeof(byte)) + { + return byte.MinValue; + } + else if (type == typeof(sbyte)) + { + return sbyte.MinValue; + } + else if (type == typeof(float)) + { + return float.MinValue; + } + else if (type == typeof(double)) + { + return double.MinValue; + } + else if (type == typeof(decimal)) + { + return decimal.MinValue; + } + else + { + throw new ArgumentException($"Type {type} is not a numeric type"); + } + } + + /// + /// Gets the maximum value for a numeric type + /// + /// + /// + public static object GetNumericMaxValue(this Type type) + { + type = Nullable.GetUnderlyingType(type) ?? type; + + if (type == typeof(int)) + { + return int.MaxValue; + } + else if (type == typeof(uint)) + { + return uint.MaxValue; + } + else if (type == typeof(long)) + { + return long.MaxValue; + } + else if (type == typeof(ulong)) + { + return ulong.MaxValue; + } + else if (type == typeof(short)) + { + return short.MaxValue; + } + else if (type == typeof(ushort)) + { + return ushort.MaxValue; + } + else if (type == typeof(byte)) + { + return byte.MaxValue; + } + else if (type == typeof(sbyte)) + { + return sbyte.MaxValue; + } + else if (type == typeof(float)) + { + return float.MaxValue; + } + else if (type == typeof(double)) + { + return double.MaxValue; + } + else if (type == typeof(decimal)) + { + return decimal.MaxValue; + } + else + { + throw new ArgumentException($"Type {type} is not a numeric type"); + } + } + + /// + /// + /// + /// + /// + public static decimal ToDecimalSafe(object input) + { + if (input is decimal dec) + { + return dec; + } + + if (input is double d) + { + if (d > (double)decimal.MaxValue) + { + return decimal.MaxValue; + } + else if (d < (double)decimal.MinValue) + { + return decimal.MinValue; + } + else + { + return (decimal)d; + } + } + + if (input is float f) + { + if (f > (float)decimal.MaxValue) + { + return decimal.MaxValue; + } + else if (f < (float)decimal.MinValue) + { + return decimal.MinValue; + } + else + { + return (decimal)f; + } + } + + try + { + return Convert.ToDecimal(input); + } + catch (OverflowException) + { + return decimal.MaxValue; + } + } } diff --git a/Core/Key2Joy.Core/default-profile.k2j.json b/Core/Key2Joy.Core/default-profile.k2j.json index 062053f5..facc0265 100644 --- a/Core/Key2Joy.Core/default-profile.k2j.json +++ b/Core/Key2Joy.Core/default-profile.k2j.json @@ -1,947 +1,931 @@ -{ - "mappedOptions": [ - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadUp", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Up", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadDown", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Down", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadLeft", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Left", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadRight", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Right", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Start", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F1", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Back", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F2", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickClick", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "LControlKey", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickClick", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "RControlKey", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftShoulder", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Q", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightShoulder", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "E", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "A", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "B", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Z", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "X", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "R", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Y", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Y", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftTrigger", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D1", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightTrigger", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D2", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickLeft", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "A", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickRight", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickUp", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "W", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickDown", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "S", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Logic.AppCommandAction", - "options": { - "Command": "Abort", - "Name": "Run App Command \u0027{0}\u0027" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Escape", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Scripting.LuaScriptAction", - "options": { - "Script": "print(\u0022Shift \u002B A was pressed\u0022)", - "IsScriptPath": false, - "Name": "Lua Script: {0}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Logic.CombinedTrigger", - "options": { - "Triggers": [ - { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "A", - "PressState": "Press", - "Name": null - } - }, - { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "LShiftKey", - "PressState": "Press", - "Name": null - } - } - ], - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Logic.SequenceAction", - "options": { - "Name": "Run Sequence: {0}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Forward", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Logic.AppCommandAction", - "options": { - "Command": "Abort", - "Name": "Run App Command \u0027{0}\u0027" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Logic.CombinedTrigger", - "options": { - "Triggers": [ - { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D6", - "PressState": "Press", - "Name": null - } - } - ], - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickUp", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "W", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickRight", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickLeft", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "A", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickDown", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "S", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickUp", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Forward", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickDown", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Backward", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickLeft", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Left", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickRight", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Right", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadUp", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Up", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadRight", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Right", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadDown", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Down", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadLeft", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Left", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftTrigger", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D1", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadRight", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Right", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Start", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F1", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Back", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F2", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickClick", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "LControlKey", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickClick", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "RControlKey", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftShoulder", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Q", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightShoulder", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "E", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "A", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "B", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Z", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "X", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "R", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Y", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Y", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickDown", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Backward", - "Name": null - } - } - } - ], - "name": "Default Profile", - "version": 6 -} +{ + "mappedOptions": [ + { + "guid": "11f92b2f-80a3-4c81-b9b4-036bd9564490", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadUp", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Up", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "5004e0b6-b428-44ef-8497-af957a79300c", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadDown", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Down", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "068128bd-da62-41e5-8e31-59863de65228", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadLeft", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Left", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "5b5c6cfc-128e-4573-b0aa-0724e142cd45", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadRight", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Right", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "907eb5b8-d6af-4443-b845-cdc78b46e43c", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "Start", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "F1", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "8b917498-be59-4471-a55c-0d3840897464", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "Back", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "F2", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "940dfd49-80c5-4fd9-a406-36da0afee1ef", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "LeftStickClick", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "LControlKey", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "0610c96c-ddcc-49e5-a488-ea6c98610fd9", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "RightStickClick", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "RControlKey", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "1a8d8280-7e65-4906-9104-f942daf6f369", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "LeftShoulder", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Q", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "04cbb0f6-d354-457c-931d-3e5d9a5e94ac", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "RightShoulder", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "E", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "72e2af12-8836-43c7-a3c2-e869637ae606", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "A", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "F", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "54dfe1b9-ff7d-4ab4-b81f-7a426ac746b5", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "B", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Z", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "8169725e-73e8-44a8-aa89-63e239b6836b", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "X", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "R", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "e5cc8372-9152-4cb9-8fe5-77d43550b0f0", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "Y", + "PressState": "Press", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Y", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "3641e034-4163-46d4-a723-d43cff39ac7c", + "action": { + "$type": "Key2Joy.Mapping.Actions.Logic.AppCommandAction", + "options": { + "Command": "Abort", + "Name": "Run App Command \u0027{0}\u0027" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Escape", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "5ab87e26-2bb8-45dd-897c-c78e11dc8e66", + "action": { + "$type": "Key2Joy.Mapping.Actions.Scripting.LuaScriptAction", + "options": { + "Script": "print(\u0022Shift \u002B A was pressed\u0022)", + "IsScriptPath": false, + "Name": "Lua Script: {0}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Logic.CombinedTrigger", + "options": { + "Triggers": [ + { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "A", + "PressState": "Press", + "Name": null + } + }, + { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "LShiftKey", + "PressState": "Press", + "Name": null + } + } + ], + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "8539d6ba-4225-4a89-8400-97ea009fb915", + "action": { + "$type": "Key2Joy.Mapping.Actions.Logic.AppCommandAction", + "options": { + "Command": "Abort", + "Name": "Run App Command \u0027{0}\u0027" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Logic.CombinedTrigger", + "options": { + "Triggers": [ + { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "LShiftKey", + "PressState": "Press", + "Name": null + } + }, + { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "D6", + "PressState": "Press", + "Name": null + } + } + ], + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "cb5f73d2-067d-43e8-b679-64c9ac6af992", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadUp", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Up", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "11f92b2f-80a3-4c81-b9b4-036bd9564490" + }, + { + "guid": "8b024b42-87dc-40db-9760-ef1ee665f343", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadDown", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Down", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "5004e0b6-b428-44ef-8497-af957a79300c" + }, + { + "guid": "52c2e928-fcc3-4c6c-9203-88079d38014f", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadLeft", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Left", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "068128bd-da62-41e5-8e31-59863de65228" + }, + { + "guid": "a150028a-b45b-4278-9b23-4f995f62c9eb", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "DPadRight", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Right", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "5b5c6cfc-128e-4573-b0aa-0724e142cd45" + }, + { + "guid": "48de3144-66d9-4b33-8ea4-65fd7395ca9b", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "Start", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "F1", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "907eb5b8-d6af-4443-b845-cdc78b46e43c" + }, + { + "guid": "0dfd6ae2-36fe-4c1e-923d-8c7e16949ab8", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "Back", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "F2", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "8b917498-be59-4471-a55c-0d3840897464" + }, + { + "guid": "75928ea2-66c5-49c0-bee8-213d86627748", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "LeftStickClick", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "LControlKey", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "940dfd49-80c5-4fd9-a406-36da0afee1ef" + }, + { + "guid": "5de7c673-c54d-4089-9000-32ef68967b09", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "RightStickClick", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "RControlKey", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "0610c96c-ddcc-49e5-a488-ea6c98610fd9" + }, + { + "guid": "10beec27-3f94-4a51-87db-10846a9fdaca", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "LeftShoulder", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Q", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "1a8d8280-7e65-4906-9104-f942daf6f369" + }, + { + "guid": "c67a2dd5-2a20-45dc-999c-85325d9c46d4", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "RightShoulder", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "E", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "04cbb0f6-d354-457c-931d-3e5d9a5e94ac" + }, + { + "guid": "80160d50-eda6-4348-9161-d403dfe7d73e", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "A", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "F", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "72e2af12-8836-43c7-a3c2-e869637ae606" + }, + { + "guid": "206cf073-56a9-47a3-9c9a-0b7a263b9cc7", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "B", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Z", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "54dfe1b9-ff7d-4ab4-b81f-7a426ac746b5" + }, + { + "guid": "589797e4-d865-4c57-98cf-ffc3b32fc8d3", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "X", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "R", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "8169725e-73e8-44a8-aa89-63e239b6836b" + }, + { + "guid": "0781a731-ef5d-4667-84af-938dc0ef3ec5", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadButtonAction", + "options": { + "Control": "Y", + "PressState": "Release", + "GamePadIndex": 2, + "Name": "{1} {0} on GamePad #{2}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "Y", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "e5cc8372-9152-4cb9-8fe5-77d43550b0f0" + }, + { + "guid": "672b6b5d-a659-4506-b9ca-00049b12f802", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Right", + "DeltaX": null, + "DeltaY": null, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 25, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", + "options": { + "AxisBinding": "Any", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "6511ff92-30a0-4ddf-b7f4-f0e3816ee958", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": 0, + "DeltaY": 1, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "W", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "687492ad-dba8-4bcd-b358-fe055b393891", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": 0, + "DeltaY": -1, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "W", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "6511ff92-30a0-4ddf-b7f4-f0e3816ee958" + }, + { + "guid": "f5e4f57b-d58e-4bf2-8ad5-646ff6cd429f", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": -1, + "DeltaY": 0, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "A", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "ee8c1c9c-fd83-4d50-af64-32547dd1fc2a", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": 1, + "DeltaY": 0, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "D", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "be8a1e36-5680-4505-90d3-e4df8cd09a72", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": 0, + "DeltaY": -1, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "S", + "PressState": "Press", + "Name": null + } + }, + "parentGuid": null + }, + { + "guid": "794c1280-61c6-4764-ade9-bccfb6cdfd89", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": 1, + "DeltaY": 0, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "A", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "f5e4f57b-d58e-4bf2-8ad5-646ff6cd429f" + }, + { + "guid": "ed259a6b-0ce2-4f2c-bc92-d327de6c95ff", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": -1, + "DeltaY": 0, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "D", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "ee8c1c9c-fd83-4d50-af64-32547dd1fc2a" + }, + { + "guid": "16e15394-5f7b-4fa3-be5b-05516dd8d89d", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadStickAction", + "options": { + "Side": "Left", + "DeltaX": 0, + "DeltaY": 1, + "InputScaleX": 1, + "InputScaleY": -1, + "ResetAfterIdleTimeInMs": 2147483647, + "GamePadIndex": 2, + "Name": "Move {0} Stick on GamePad #{3} by ({1}, {2})" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", + "options": { + "Keys": "S", + "PressState": "Release", + "Name": null + } + }, + "parentGuid": "be8a1e36-5680-4505-90d3-e4df8cd09a72" + }, + { + "guid": "62a59d46-ce21-4591-9404-e6501c5fc360", + "action": { + "$type": "Key2Joy.Mapping.Actions.Input.GamePadTriggerAction", + "options": { + "Side": "Left", + "Delta": null, + "InputScale": 1, + "GamePadIndex": 2, + "Name": "Move {0} Trigger on GamePad #{3} by {1}" + } + }, + "trigger": { + "$type": "Key2Joy.Mapping.Triggers.GamePad.GamePadTriggerTrigger", + "options": { + "GamePadIndex": 0, + "TriggerSide": "Left", + "DeltaMargin": null, + "Name": null + } + }, + "parentGuid": null + } + ], + "name": "Default Profile", + "version": 6 +} diff --git a/Core/Key2Joy.PluginHost/Key2Joy.PluginHost.csproj b/Core/Key2Joy.PluginHost/Key2Joy.PluginHost.csproj index ce351b77..44cd6624 100644 --- a/Core/Key2Joy.PluginHost/Key2Joy.PluginHost.csproj +++ b/Core/Key2Joy.PluginHost/Key2Joy.PluginHost.csproj @@ -15,7 +15,7 @@ NET48 latest Always - x86 + AnyCPU true {28FD937C-CCA7-4E85-A29B-D723E8390E51} Exe diff --git a/Docs/Scripting.md b/Docs/Scripting.md index 36e74223..c208b141 100644 --- a/Docs/Scripting.md +++ b/Docs/Scripting.md @@ -59,23 +59,24 @@ implementations: this project uses [NLua](https://github.com/NLua/NLua) and end ) ``` -![image](https://user-images.githubusercontent.com/2738114/177006114-1ffafa7e-2f94-43d4-bddc-1bcca7c51344.png) +![Screenshot of Key2Joy showing the mapping form being configured with a keyboard trigger and Lua action](screenshot-scripting.png) -2. In Key2Joy click *Create New Mapping* -3. Choose the trigger *Keyboard Event* -4. Press the "F"-key on your keyboard -5. Select *Release* from the dropdown. This ensures the script will only run +2. In Key2Joy click **Create New Mapping** *(Button marked A in the screenshot)* +3. Choose the trigger **Keyboard Event** *(Section B in the screenshot)* +4. Click the marked area and press the "F"-key on your keyboard +5. Select **Release** from the dropdown. This ensures the script will only run once when the F-key is released. -6. Choose the action to run when releasing the F-key: *Lua Script Action* -7. Untick *Direct Input*, so we can select the `test.lua` script. -8. Browse to the `test.lua` file +6. For the action we'll choose: **Lua Script Action** *(Section C in the + screenshot)* +7. Uncheck **Direct Input** so we can select the `test.lua` script we created + earlier. +8. Click **Browse**, navigate to the `test.lua` file and select it. 9. Save the mapping. -Now when you enable the mappings *(Enable checkbox in the top right of +Now when you enable the mappings *(Check the `Arm Mappings` checkbox in the top right of Key2Joy)* you can run that Lua script by pressing and releasing the F-key on your keyboard. - ## Some Script Examples You can find more examples in the [📃 Scripting API Reference](Index.md). diff --git a/Docs/screenshot-scripting.png b/Docs/screenshot-scripting.png new file mode 100644 index 0000000000000000000000000000000000000000..ef8682a7b8bd155b1170aab4b1a4915adee01d29 GIT binary patch literal 66671 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

Xm?bS<9Jm@ZOEKURvZOo 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.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

q+-ShjGgG0dj z%4U(46Ox1KA*APa#L06{;tqh4Jk*?m-94tQdl{yZ)loA&L~)Wo zhc~mJnEvoqgE=J7is2@eitRNbq2O#XPxgB_cd_k30p`Sl$^r5$uD?rUMw++gox$5$ zbN2(An4K%&%_d;|@s^v^(9A!tsEgAAt>6q{=7%4$`GVI}_#P&x{PPR)38%X|J@YDb&(v( zUQ+_f2-d$}>4xyze?I;Ft3MC^sm%Z17qQw`u@`?CI)u#M(~f7o0{p2eX()b|Hx2wh D;lHu3 literal 0 HcmV?d00001 diff --git a/Docs/screenshot.png b/Docs/screenshot.png index e8f07b69b59b15ee963067a3a92e75a54e294e86..c45b064870435c52f19a7894675b8b94bd42e89c 100644 GIT binary patch literal 87790 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 N|1X6Jn{@yH literal 62124 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 latest Graphics\Icons\app-icons.ico - x86 + AnyCPU true Exe Key2Joy.Cmd.Program diff --git a/Key2Joy.Gui/ConfigForm.Designer.cs b/Key2Joy.Gui/ConfigForm.Designer.cs index 537408a4..0a68fe8c 100644 --- a/Key2Joy.Gui/ConfigForm.Designer.cs +++ b/Key2Joy.Gui/ConfigForm.Designer.cs @@ -1,74 +1,90 @@ -namespace Key2Joy.Gui -{ - partial class ConfigForm - { - ///

- /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.pnlConfigurations = new System.Windows.Forms.Panel(); - this.btnSave = new System.Windows.Forms.Button(); - this.SuspendLayout(); - // - // pnlConfigurations - // - this.pnlConfigurations.AutoScroll = true; - this.pnlConfigurations.Dock = System.Windows.Forms.DockStyle.Fill; - this.pnlConfigurations.Location = new System.Drawing.Point(0, 0); - this.pnlConfigurations.Name = "pnlConfigurations"; - this.pnlConfigurations.Size = new System.Drawing.Size(453, 196); - this.pnlConfigurations.TabIndex = 0; - // - // btnSave - // - this.btnSave.Dock = System.Windows.Forms.DockStyle.Bottom; - this.btnSave.Location = new System.Drawing.Point(0, 196); - this.btnSave.Name = "btnSave"; - this.btnSave.Size = new System.Drawing.Size(453, 35); - this.btnSave.TabIndex = 0; - this.btnSave.Text = "Save"; - this.btnSave.UseVisualStyleBackColor = true; - this.btnSave.Click += new System.EventHandler(this.BtnSave_Click); - // - // ConfigForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(453, 231); - this.Controls.Add(this.pnlConfigurations); - this.Controls.Add(this.btnSave); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.Name = "ConfigForm"; - this.Text = "Key2Joy User Configurations"; - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.Panel pnlConfigurations; - private System.Windows.Forms.Button btnSave; - } +namespace Key2Joy.Gui +{ + partial class ConfigForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.pnlConfigurations = new System.Windows.Forms.Panel(); + this.btnSave = new System.Windows.Forms.Button(); + this.pnlSave = new System.Windows.Forms.Panel(); + this.pnlSave.SuspendLayout(); + this.SuspendLayout(); + // + // pnlConfigurations + // + this.pnlConfigurations.AutoSize = true; + this.pnlConfigurations.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlConfigurations.Location = new System.Drawing.Point(0, 0); + this.pnlConfigurations.Name = "pnlConfigurations"; + this.pnlConfigurations.Size = new System.Drawing.Size(453, 0); + this.pnlConfigurations.TabIndex = 0; + // + // btnSave + // + this.btnSave.Dock = System.Windows.Forms.DockStyle.Bottom; + this.btnSave.Location = new System.Drawing.Point(0, 6); + this.btnSave.Name = "btnSave"; + this.btnSave.Size = new System.Drawing.Size(453, 35); + this.btnSave.TabIndex = 0; + this.btnSave.Text = "Save"; + this.btnSave.UseVisualStyleBackColor = true; + this.btnSave.Click += new System.EventHandler(this.BtnSave_Click); + // + // pnlSave + // + this.pnlSave.Controls.Add(this.btnSave); + this.pnlSave.Dock = System.Windows.Forms.DockStyle.Bottom; + this.pnlSave.Location = new System.Drawing.Point(0, 75); + this.pnlSave.Name = "pnlSave"; + this.pnlSave.Size = new System.Drawing.Size(453, 41); + this.pnlSave.TabIndex = 1; + // + // ConfigForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoScroll = true; + this.AutoSize = true; + this.ClientSize = new System.Drawing.Size(453, 116); + this.Controls.Add(this.pnlConfigurations); + this.Controls.Add(this.pnlSave); + this.MaximumSize = new System.Drawing.Size(469, 600); + this.Name = "ConfigForm"; + this.Text = "Key2Joy User Configurations"; + this.pnlSave.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Panel pnlConfigurations; + private System.Windows.Forms.Button btnSave; + private System.Windows.Forms.Panel pnlSave; + } } \ No newline at end of file diff --git a/Key2Joy.Gui/ConfigForm.cs b/Key2Joy.Gui/ConfigForm.cs index d96d0570..612417ca 100644 --- a/Key2Joy.Gui/ConfigForm.cs +++ b/Key2Joy.Gui/ConfigForm.cs @@ -30,15 +30,17 @@ public ConfigForm() var value = property.GetValue(this.configState); var control = this.MakeControl(attribute, value, controlParent); control.Tag = kvp; - control.Dock = DockStyle.Top; + control.Dock = DockStyle.Bottom; controlParent.AutoSize = true; controlParent.Padding = new Padding(10, 10, 10, 0); controlParent.Controls.Add(control); this.pnlConfigurations.Controls.Add(controlParent); - controlParent.Dock = DockStyle.Top; + controlParent.Dock = DockStyle.Bottom; } + + this.PerformLayout(); } private void BtnSave_Click(object sender, EventArgs e) @@ -69,6 +71,17 @@ private void BtnSave_Click(object sender, EventArgs e) private Control MakeControl(ConfigControlAttribute attribute, object value, Panel controlParent) { + void CreateLabel() + { + Label label = new() + { + AutoSize = true, + Dock = DockStyle.Bottom, + Text = $"{attribute.Text}: " + }; + controlParent.Controls.Add(label); + } + switch (attribute) { case BooleanConfigControlAttribute booleanConfigControlAttribute: @@ -83,14 +96,7 @@ private Control MakeControl(ConfigControlAttribute attribute, object value, Pane } case NumericConfigControlAttribute numericConfigControlAttribute: { - Label label = new() - { - AutoSize = true, - Dock = DockStyle.Top, - Text = $"{this.Text}: " - }; - controlParent.Controls.Add(label); - + CreateLabel(); NumericUpDown control = new() { Minimum = (decimal)numericConfigControlAttribute.Minimum, @@ -102,14 +108,7 @@ private Control MakeControl(ConfigControlAttribute attribute, object value, Pane } case TextConfigControlAttribute textConfigControlAttribute: { - Label label = new() - { - AutoSize = true, - Dock = DockStyle.Top, - Text = $"{this.Text}: " - }; - controlParent.Controls.Add(label); - + CreateLabel(); TextBox control = new() { Text = value.ToString(), @@ -118,6 +117,25 @@ private Control MakeControl(ConfigControlAttribute attribute, object value, Pane return control; } + case EnumConfigControlAttribute enumConfigControlAttribute: + { + CreateLabel(); + var enumValues = Enum.GetValues(enumConfigControlAttribute.EnumType); + var selected = Enum.Parse(enumConfigControlAttribute.EnumType, value.ToString()); + ComboBox control = new() + { + DropDownStyle = ComboBoxStyle.DropDownList, + }; + + foreach (var enumValue in enumValues) + { + control.Items.Add(enumValue); + } + + control.SelectedIndex = Array.IndexOf(enumValues, selected); + + return control; + } default: break; @@ -131,23 +149,25 @@ private object GetControlValue(ConfigControlAttribute attribute, Control control switch (attribute) { case BooleanConfigControlAttribute: - { var checkbox = (CheckBox)control; return checkbox.Checked; } case NumericConfigControlAttribute: - { var numeric = (NumericUpDown)control; return numeric.Value; } case TextConfigControlAttribute: - { var textbox = (TextBox)control; return textbox.Text; } + case EnumConfigControlAttribute: + { + var combobox = (ComboBox)control; + return combobox.SelectedItem; + } default: break; diff --git a/Key2Joy.Gui/ConfigForm.resx b/Key2Joy.Gui/ConfigForm.resx index 1af7de15..29dcb1b3 100644 --- a/Key2Joy.Gui/ConfigForm.resx +++ b/Key2Joy.Gui/ConfigForm.resx @@ -1,120 +1,120 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/Key2Joy.Gui/DeviceControl.Designer.cs b/Key2Joy.Gui/DeviceControl.Designer.cs new file mode 100644 index 00000000..c56ce076 --- /dev/null +++ b/Key2Joy.Gui/DeviceControl.Designer.cs @@ -0,0 +1,104 @@ +namespace Key2Joy.Gui; + +partial class DeviceControl +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.picImage = new System.Windows.Forms.PictureBox(); + this.lblDevice = new System.Windows.Forms.Label(); + this.lblIndex = new System.Windows.Forms.Label(); + this.pnlDevice = new System.Windows.Forms.Panel(); + ((System.ComponentModel.ISupportInitialize)(this.picImage)).BeginInit(); + this.pnlDevice.SuspendLayout(); + this.SuspendLayout(); + // + // picImage + // + this.picImage.Dock = System.Windows.Forms.DockStyle.Fill; + this.picImage.Image = global::Key2Joy.Gui.Properties.Resources.disconnect; + this.picImage.Location = new System.Drawing.Point(8, 26); + this.picImage.Name = "picImage"; + this.picImage.Size = new System.Drawing.Size(77, 49); + this.picImage.SizeMode = System.Windows.Forms.PictureBoxSizeMode.CenterImage; + this.picImage.TabIndex = 0; + this.picImage.TabStop = false; + // + // lblDevice + // + this.lblDevice.Dock = System.Windows.Forms.DockStyle.Bottom; + this.lblDevice.Font = new System.Drawing.Font("Arial", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblDevice.Location = new System.Drawing.Point(8, 75); + this.lblDevice.Name = "lblDevice"; + this.lblDevice.Size = new System.Drawing.Size(77, 18); + this.lblDevice.TabIndex = 1; + this.lblDevice.Text = "Device Name"; + this.lblDevice.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // lblIndex + // + this.lblIndex.Dock = System.Windows.Forms.DockStyle.Top; + this.lblIndex.Font = new System.Drawing.Font("Arial", 9F, System.Drawing.FontStyle.Bold); + this.lblIndex.Location = new System.Drawing.Point(8, 8); + this.lblIndex.Name = "lblIndex"; + this.lblIndex.Size = new System.Drawing.Size(77, 18); + this.lblIndex.TabIndex = 2; + this.lblIndex.Text = "?"; + this.lblIndex.TextAlign = System.Drawing.ContentAlignment.TopCenter; + // + // pnlDevice + // + this.pnlDevice.Controls.Add(this.picImage); + this.pnlDevice.Controls.Add(this.lblIndex); + this.pnlDevice.Controls.Add(this.lblDevice); + this.pnlDevice.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlDevice.Location = new System.Drawing.Point(0, 0); + this.pnlDevice.Name = "pnlDevice"; + this.pnlDevice.Padding = new System.Windows.Forms.Padding(8); + this.pnlDevice.Size = new System.Drawing.Size(93, 101); + this.pnlDevice.TabIndex = 3; + // + // DeviceControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.Transparent; + this.Controls.Add(this.pnlDevice); + this.Name = "DeviceControl"; + this.Size = new System.Drawing.Size(93, 101); + ((System.ComponentModel.ISupportInitialize)(this.picImage)).EndInit(); + this.pnlDevice.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.PictureBox picImage; + private System.Windows.Forms.Label lblDevice; + private System.Windows.Forms.Label lblIndex; + private System.Windows.Forms.Panel pnlDevice; +} diff --git a/Key2Joy.Gui/DeviceControl.cs b/Key2Joy.Gui/DeviceControl.cs new file mode 100644 index 00000000..73ba18b7 --- /dev/null +++ b/Key2Joy.Gui/DeviceControl.cs @@ -0,0 +1,96 @@ +using System; +using System.Drawing; +using System.Windows.Forms; +using Key2Joy.LowLevelInput; + +namespace Key2Joy.Gui; + +public partial class DeviceControl : UserControl +{ + private const int FADE_DURATION = 2; + private const int BORDER_WIDTH = 2; + private DateTime lastActivityOccurred = DateTime.Now; + private readonly Timer fadeTimer = new(); + private readonly IGamePadInfo device; + + private DeviceControl() + { + this.InitializeComponent(); + + this.fadeTimer.Interval = 50; + this.fadeTimer.Tick += this.FadeTimer_Tick; + this.fadeTimer.Start(); + + this.pnlDevice.Paint += this.DeviceControl_Paint; + this.Layout += this.DeviceControl_Layout; + } + + public DeviceControl(IGamePadInfo device) + : this() + { + this.device = device; + this.lblIndex.Text = $"#{device.Index}"; + this.lblDevice.Text = device.Name; + //this.picImage.Image = null; + device.ActivityOccurred += this.Device_ActivityOccurred; + } + + private void DeviceControl_Layout(object sender, LayoutEventArgs e) + { + if (this.Height == this.Width) + { + return; + } + + this.Height = this.Width; + } + + private void Device_ActivityOccurred(object sender, GamePadActivityOccurredEventArgs e) + { + this.lastActivityOccurred = DateTime.Now; + + this.Invoke((MethodInvoker)delegate + { + this.fadeTimer.Stop(); + this.fadeTimer.Start(); + this.Invalidate(); + }); + } + + private void FadeTimer_Tick(object sender, EventArgs e) + { + if ((DateTime.Now - this.lastActivityOccurred).TotalSeconds <= 2) + { + this.Invalidate(); + } + else + { + this.fadeTimer.Stop(); + } + } + + private void DeviceControl_Paint(object sender, PaintEventArgs e) + { + var owner = (Control)sender; + var g = e.Graphics; + + var elapsed = (DateTime.Now - this.lastActivityOccurred).TotalSeconds; + + var currentColor = this.InterpolateColors(Color.White, Color.Gold, elapsed / FADE_DURATION); + using var brush = new SolidBrush(currentColor); + g.FillRectangle(brush, BORDER_WIDTH, BORDER_WIDTH, owner.Width - (BORDER_WIDTH * 2), owner.Height - (BORDER_WIDTH * 2)); + } + + private Color InterpolateColors(Color start, Color end, double ratio) + { + var r = (int)((start.R * (1 - ratio)) + (end.R * ratio)); + var g = (int)((start.G * (1 - ratio)) + (end.G * ratio)); + var b = (int)((start.B * (1 - ratio)) + (end.B * ratio)); + + return Color.FromArgb( + Math.Max(r, 0), + Math.Max(g, 0), + Math.Max(b, 0) + ); + } +} diff --git a/Key2Joy.Gui/DeviceControl.resx b/Key2Joy.Gui/DeviceControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/DeviceControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/DeviceListControl.Designer.cs b/Key2Joy.Gui/DeviceListControl.Designer.cs new file mode 100644 index 00000000..2646c3af --- /dev/null +++ b/Key2Joy.Gui/DeviceListControl.Designer.cs @@ -0,0 +1,103 @@ +namespace Key2Joy.Gui; + +partial class DeviceListControl +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.pnlDevices = new System.Windows.Forms.Panel(); + this.pnlHeader = new System.Windows.Forms.Panel(); + this.lblDevices = new System.Windows.Forms.Label(); + this.btnRefresh = new System.Windows.Forms.Button(); + this.pnlHeader.SuspendLayout(); + this.SuspendLayout(); + // + // pnlDevices + // + this.pnlDevices.AutoSize = true; + this.pnlDevices.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDevices.Location = new System.Drawing.Point(0, 71); + this.pnlDevices.Name = "pnlDevices"; + this.pnlDevices.Size = new System.Drawing.Size(84, 0); + this.pnlDevices.TabIndex = 0; + // + // pnlHeader + // + this.pnlHeader.BackColor = System.Drawing.Color.Black; + this.pnlHeader.Controls.Add(this.lblDevices); + this.pnlHeader.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlHeader.Location = new System.Drawing.Point(0, 0); + this.pnlHeader.Name = "pnlHeader"; + this.pnlHeader.Size = new System.Drawing.Size(84, 44); + this.pnlHeader.TabIndex = 1; + // + // lblDevices + // + this.lblDevices.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblDevices.Font = new System.Drawing.Font("Arial", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblDevices.ForeColor = System.Drawing.Color.White; + this.lblDevices.Location = new System.Drawing.Point(0, 0); + this.lblDevices.Name = "lblDevices"; + this.lblDevices.Size = new System.Drawing.Size(84, 44); + this.lblDevices.TabIndex = 0; + this.lblDevices.Text = "Connected Devices"; + this.lblDevices.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // btnRefresh + // + this.btnRefresh.Dock = System.Windows.Forms.DockStyle.Top; + this.btnRefresh.Location = new System.Drawing.Point(0, 44); + this.btnRefresh.Name = "btnRefresh"; + this.btnRefresh.Size = new System.Drawing.Size(84, 27); + this.btnRefresh.TabIndex = 2; + this.btnRefresh.Text = "Refresh"; + this.btnRefresh.UseVisualStyleBackColor = true; + this.btnRefresh.Click += new System.EventHandler(this.BtnRefresh_Click); + // + // DeviceListControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoScroll = true; + this.BackColor = System.Drawing.Color.Gold; + this.Controls.Add(this.pnlDevices); + this.Controls.Add(this.btnRefresh); + this.Controls.Add(this.pnlHeader); + this.Name = "DeviceListControl"; + this.Size = new System.Drawing.Size(84, 443); + this.pnlHeader.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Panel pnlDevices; + private System.Windows.Forms.Panel pnlHeader; + private System.Windows.Forms.Label lblDevices; + private System.Windows.Forms.Button btnRefresh; +} diff --git a/Key2Joy.Gui/DeviceListControl.cs b/Key2Joy.Gui/DeviceListControl.cs new file mode 100644 index 00000000..205af0c9 --- /dev/null +++ b/Key2Joy.Gui/DeviceListControl.cs @@ -0,0 +1,74 @@ +using System.Drawing; +using System.Windows.Forms; +using CommonServiceLocator; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.LowLevelInput.XInput; + +namespace Key2Joy.Gui; + +public partial class DeviceListControl : UserControl +{ + public DeviceListControl() + { + this.InitializeComponent(); + + if (System.Diagnostics.Process.GetCurrentProcess().ProcessName == "devenv") + { + return; // The designer can't handle the code below. + } + + this.RefreshDevices(); + } + + public void RefreshDevices() + { + this.pnlDevices.Controls.Clear(); + + this.RefreshSimulatedDevices(); + this.RefreshPhysicalDevices(); + + if (this.pnlDevices.Controls.Count == 0) + { + this.pnlDevices.Controls.Add(new Label() + { + Text = "No physical or simulated devices found. Try arming the mappings.", + Font = new Font("Arial", 8, FontStyle.Italic), + Padding = new Padding(5), + Dock = DockStyle.Top, + Height = 100, + TextAlign = ContentAlignment.MiddleCenter, + }); + } + } + + private void AddDeviceControl(DeviceControl control) + { + control.Dock = DockStyle.Top; + this.pnlDevices.Controls.Add(control); + } + + private void RefreshPhysicalDevices() + { + var xInputService = ServiceLocator.Current.GetInstance(); + xInputService.RecognizePhysicalDevices(); + var deviceIndexes = xInputService.GetActiveDevicesInfo(); + + foreach (var device in deviceIndexes) + { + this.AddDeviceControl(new DeviceControl(device)); + } + } + + private void RefreshSimulatedDevices() + { + var gamePadService = ServiceLocator.Current.GetInstance(); + var simulatedGamePads = gamePadService.GetActiveDevicesInfo(); + + foreach (var gamePad in simulatedGamePads) + { + this.AddDeviceControl(new DeviceControl(gamePad)); + } + } + + private void BtnRefresh_Click(object sender, System.EventArgs e) => this.RefreshDevices(); +} diff --git a/Key2Joy.Gui/DeviceListControl.resx b/Key2Joy.Gui/DeviceListControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/DeviceListControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Graphics/Icons/cross.png b/Key2Joy.Gui/Graphics/Icons/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..1514d51a3cf1b67e1c5b9ada36f1fd474e2d214a GIT binary patch literal 655 zcmV;A0&x9_P)uEoyT++I zn$b9r%cFfhHe2K68PkBu*@^<$y+7xQ$wJ~;c5aBx$R=xq*41Wo zhwQus_VOgm0hughj}MhOvs#{>Vg09Y8WxjWUJY5YW zJ?&8eG!59Cz=|E%Ns@013KLWOLV)CObIIj_5{>{#k%TEAMs_GbdDV`x-iYsGH z#=Z{USAQA>NY(}X7=3{K8#(); + var gamePadService = ServiceLocator.Current.GetInstance(); gamePadService.Initialize(); MainForm mainForm = new(this.shouldStartMinimized); diff --git a/Key2Joy.Gui/Key2Joy.Gui.csproj b/Key2Joy.Gui/Key2Joy.Gui.csproj index 813056ff..bbd3f215 100644 --- a/Key2Joy.Gui/Key2Joy.Gui.csproj +++ b/Key2Joy.Gui/Key2Joy.Gui.csproj @@ -20,7 +20,7 @@ NET48 latest Graphics\Icons\app-icons.ico - x86 + AnyCPU true false false @@ -125,6 +125,21 @@ + + UserControl + + + UserControl + + + UserControl + + + UserControl + + + UserControl + True True diff --git a/Key2Joy.Gui/MainForm.Designer.cs b/Key2Joy.Gui/MainForm.Designer.cs index 0d685f93..e61974a0 100644 --- a/Key2Joy.Gui/MainForm.Designer.cs +++ b/Key2Joy.Gui/MainForm.Designer.cs @@ -41,7 +41,7 @@ private void InitializeComponent() this.pnlProfileManagement = new System.Windows.Forms.Panel(); this.txtProfileName = new System.Windows.Forms.TextBox(); this.lblProfileName = new System.Windows.Forms.Label(); - this.chkEnabled = new System.Windows.Forms.CheckBox(); + this.chkArmed = new System.Windows.Forms.CheckBox(); this.menMainMenu = new System.Windows.Forms.MenuStrip(); this.fileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.newProfileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -53,14 +53,16 @@ private void InitializeComponent() this.closeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.exitProgramToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.viewToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.createNewMappingToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.viewScriptOutputToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.viewLogFileToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.viewEventViewerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator(); this.pluginsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.managePluginsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.openPluginsFolderToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.createNewMappingToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); this.fillProfileWithToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.allGamePadJoystickActionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.pressToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -89,22 +91,30 @@ private void InitializeComponent() this.pnlMainMenu = new System.Windows.Forms.Panel(); this.lblStatusInactive = new System.Windows.Forms.Label(); this.lblStatusActive = new System.Windows.Forms.Label(); + this.pnlNotificationsParent = new System.Windows.Forms.Panel(); + this.splitContainer = new System.Windows.Forms.SplitContainer(); + this.deviceListControl = new Key2Joy.Gui.DeviceListControl(); + this.groupMappingsByToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); ((System.ComponentModel.ISupportInitialize)(this.olvMappings)).BeginInit(); this.pnlActionManagement.SuspendLayout(); this.pnlFiltering.SuspendLayout(); this.pnlProfileManagement.SuspendLayout(); this.menMainMenu.SuspendLayout(); this.pnlMainMenu.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); + this.splitContainer.Panel1.SuspendLayout(); + this.splitContainer.Panel2.SuspendLayout(); + this.splitContainer.SuspendLayout(); this.SuspendLayout(); // // olvMappings // - this.olvMappings.AllColumns.Add(this.olvColumnTrigger); this.olvMappings.AllColumns.Add(this.olvColumnAction); + this.olvMappings.AllColumns.Add(this.olvColumnTrigger); this.olvMappings.CellEditUseWholeCell = false; this.olvMappings.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.olvColumnTrigger, - this.olvColumnAction}); + this.olvColumnAction, + this.olvColumnTrigger}); this.olvMappings.Cursor = System.Windows.Forms.Cursors.Default; this.olvMappings.Dock = System.Windows.Forms.DockStyle.Fill; this.olvMappings.EmptyListMsg = "There are no mappings, or a search filter is applied that matched no mappings."; @@ -115,7 +125,7 @@ private void InitializeComponent() this.olvMappings.Location = new System.Drawing.Point(0, 53); this.olvMappings.Name = "olvMappings"; this.olvMappings.RowHeight = 25; - this.olvMappings.Size = new System.Drawing.Size(684, 467); + this.olvMappings.Size = new System.Drawing.Size(683, 467); this.olvMappings.TabIndex = 84; this.olvMappings.UseCellFormatEvents = true; this.olvMappings.UseCompatibleStateImageBehavior = false; @@ -124,21 +134,18 @@ private void InitializeComponent() this.olvMappings.UseTranslucentHotItem = true; this.olvMappings.UseTranslucentSelection = true; this.olvMappings.View = System.Windows.Forms.View.Details; - this.olvMappings.AboutToCreateGroups += new System.EventHandler(this.OlvMappings_AboutToCreateGroups); - this.olvMappings.CellClick += new System.EventHandler(this.OlvMappings_CellClick); - this.olvMappings.CellRightClick += new System.EventHandler(this.OlvMappings_CellRightClick); - this.olvMappings.FormatCell += new System.EventHandler(this.OlvMappings_FormatCell); - this.olvMappings.KeyUp += new System.Windows.Forms.KeyEventHandler(this.OlvMappings_KeyUp); // // olvColumnAction // this.olvColumnAction.AspectName = "Action"; + this.olvColumnAction.DisplayIndex = 1; this.olvColumnAction.Text = "Action"; this.olvColumnAction.UseInitialLetterForGroup = true; // // olvColumnTrigger // this.olvColumnTrigger.AspectName = "Trigger"; + this.olvColumnTrigger.DisplayIndex = 0; this.olvColumnTrigger.Groupable = false; this.olvColumnTrigger.Text = "Trigger"; // @@ -150,7 +157,7 @@ private void InitializeComponent() this.pnlActionManagement.Location = new System.Drawing.Point(0, 520); this.pnlActionManagement.Name = "pnlActionManagement"; this.pnlActionManagement.Padding = new System.Windows.Forms.Padding(5); - this.pnlActionManagement.Size = new System.Drawing.Size(684, 41); + this.pnlActionManagement.Size = new System.Drawing.Size(683, 41); this.pnlActionManagement.TabIndex = 0; // // pnlFiltering @@ -187,7 +194,7 @@ private void InitializeComponent() // btnCreateMapping // this.btnCreateMapping.Dock = System.Windows.Forms.DockStyle.Right; - this.btnCreateMapping.Location = new System.Drawing.Point(530, 5); + this.btnCreateMapping.Location = new System.Drawing.Point(529, 5); this.btnCreateMapping.Name = "btnCreateMapping"; this.btnCreateMapping.Size = new System.Drawing.Size(149, 31); this.btnCreateMapping.TabIndex = 0; @@ -204,7 +211,7 @@ private void InitializeComponent() this.pnlProfileManagement.Location = new System.Drawing.Point(0, 23); this.pnlProfileManagement.Name = "pnlProfileManagement"; this.pnlProfileManagement.Padding = new System.Windows.Forms.Padding(5); - this.pnlProfileManagement.Size = new System.Drawing.Size(684, 30); + this.pnlProfileManagement.Size = new System.Drawing.Size(683, 30); this.pnlProfileManagement.TabIndex = 82; // // txtProfileName @@ -212,7 +219,7 @@ private void InitializeComponent() this.txtProfileName.Dock = System.Windows.Forms.DockStyle.Fill; this.txtProfileName.Location = new System.Drawing.Point(82, 5); this.txtProfileName.Name = "txtProfileName"; - this.txtProfileName.Size = new System.Drawing.Size(597, 20); + this.txtProfileName.Size = new System.Drawing.Size(596, 20); this.txtProfileName.TabIndex = 85; this.txtProfileName.TextChanged += new System.EventHandler(this.TxtProfileName_TextChanged); // @@ -227,17 +234,17 @@ private void InitializeComponent() this.lblProfileName.Text = "Profile Name:"; this.lblProfileName.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // - // chkEnabled + // chkArmed // - this.chkEnabled.AutoSize = true; - this.chkEnabled.Dock = System.Windows.Forms.DockStyle.Right; - this.chkEnabled.Location = new System.Drawing.Point(424, 0); - this.chkEnabled.Name = "chkEnabled"; - this.chkEnabled.Size = new System.Drawing.Size(59, 23); - this.chkEnabled.TabIndex = 81; - this.chkEnabled.Text = "Enable"; - this.chkEnabled.UseVisualStyleBackColor = true; - this.chkEnabled.CheckedChanged += new System.EventHandler(this.ChkEnabled_CheckedChanged); + this.chkArmed.AutoSize = true; + this.chkArmed.Dock = System.Windows.Forms.DockStyle.Right; + this.chkArmed.Location = new System.Drawing.Point(389, 0); + this.chkArmed.Name = "chkArmed"; + this.chkArmed.Size = new System.Drawing.Size(93, 23); + this.chkArmed.TabIndex = 81; + this.chkArmed.Text = "Arm Mappings"; + this.chkArmed.UseVisualStyleBackColor = true; + this.chkArmed.CheckedChanged += new System.EventHandler(this.ChkEnabled_CheckedChanged); // // menMainMenu // @@ -248,7 +255,7 @@ private void InitializeComponent() this.helpToolStripMenuItem}); this.menMainMenu.Location = new System.Drawing.Point(0, 0); this.menMainMenu.Name = "menMainMenu"; - this.menMainMenu.Size = new System.Drawing.Size(424, 24); + this.menMainMenu.Size = new System.Drawing.Size(389, 24); this.menMainMenu.TabIndex = 81; this.menMainMenu.Text = "menuStrip1"; // @@ -323,27 +330,21 @@ private void InitializeComponent() // viewToolStripMenuItem // this.viewToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.createNewMappingToolStripMenuItem, + this.groupMappingsByToolStripMenuItem, this.viewScriptOutputToolStripMenuItem, + this.toolStripSeparator5, this.pluginsToolStripMenuItem}); this.viewToolStripMenuItem.Name = "viewToolStripMenuItem"; this.viewToolStripMenuItem.Size = new System.Drawing.Size(44, 20); this.viewToolStripMenuItem.Text = "View"; // - // createNewMappingToolStripMenuItem - // - this.createNewMappingToolStripMenuItem.Name = "createNewMappingToolStripMenuItem"; - this.createNewMappingToolStripMenuItem.Size = new System.Drawing.Size(186, 22); - this.createNewMappingToolStripMenuItem.Text = "Create New Mapping"; - this.createNewMappingToolStripMenuItem.Click += new System.EventHandler(this.CreateNewMappingToolStripMenuItem_Click); - // // viewScriptOutputToolStripMenuItem // this.viewScriptOutputToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.viewLogFileToolStripMenuItem, this.viewEventViewerToolStripMenuItem}); this.viewScriptOutputToolStripMenuItem.Name = "viewScriptOutputToolStripMenuItem"; - this.viewScriptOutputToolStripMenuItem.Size = new System.Drawing.Size(186, 22); + this.viewScriptOutputToolStripMenuItem.Size = new System.Drawing.Size(188, 22); this.viewScriptOutputToolStripMenuItem.Text = "View Script Output"; // // viewLogFileToolStripMenuItem @@ -360,6 +361,11 @@ private void InitializeComponent() this.viewEventViewerToolStripMenuItem.Text = "View Event Viewer"; this.viewEventViewerToolStripMenuItem.Click += new System.EventHandler(this.ViewEventViewerToolStripMenuItem_Click); // + // toolStripSeparator5 + // + this.toolStripSeparator5.Name = "toolStripSeparator5"; + this.toolStripSeparator5.Size = new System.Drawing.Size(185, 6); + // // pluginsToolStripMenuItem // this.pluginsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -367,7 +373,7 @@ private void InitializeComponent() this.openPluginsFolderToolStripMenuItem}); this.pluginsToolStripMenuItem.Image = global::Key2Joy.Gui.Properties.Resources.plugin; this.pluginsToolStripMenuItem.Name = "pluginsToolStripMenuItem"; - this.pluginsToolStripMenuItem.Size = new System.Drawing.Size(186, 22); + this.pluginsToolStripMenuItem.Size = new System.Drawing.Size(188, 22); this.pluginsToolStripMenuItem.Text = "Plugins"; // // managePluginsToolStripMenuItem @@ -387,6 +393,8 @@ private void InitializeComponent() // toolsToolStripMenuItem // this.toolsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.createNewMappingToolStripMenuItem, + this.toolStripSeparator6, this.fillProfileWithToolStripMenuItem, this.testMappingsToolStripMenuItem, this.withSelectedToolStripMenuItem, @@ -396,13 +404,25 @@ private void InitializeComponent() this.toolsToolStripMenuItem.Size = new System.Drawing.Size(46, 20); this.toolsToolStripMenuItem.Text = "Tools"; // + // createNewMappingToolStripMenuItem + // + this.createNewMappingToolStripMenuItem.Name = "createNewMappingToolStripMenuItem"; + this.createNewMappingToolStripMenuItem.Size = new System.Drawing.Size(186, 22); + this.createNewMappingToolStripMenuItem.Text = "Create New Mapping"; + this.createNewMappingToolStripMenuItem.Click += new System.EventHandler(this.CreateNewMappingToolStripMenuItem_Click); + // + // toolStripSeparator6 + // + this.toolStripSeparator6.Name = "toolStripSeparator6"; + this.toolStripSeparator6.Size = new System.Drawing.Size(183, 6); + // // fillProfileWithToolStripMenuItem // this.fillProfileWithToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.allGamePadJoystickActionsToolStripMenuItem, this.allKeyboardActionsToolStripMenuItem}); this.fillProfileWithToolStripMenuItem.Name = "fillProfileWithToolStripMenuItem"; - this.fillProfileWithToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.fillProfileWithToolStripMenuItem.Size = new System.Drawing.Size(186, 22); this.fillProfileWithToolStripMenuItem.Text = "Fill Profile With..."; // // allGamePadJoystickActionsToolStripMenuItem @@ -474,7 +494,7 @@ private void InitializeComponent() this.testKeyboardToolStripMenuItem, this.testMouseToolStripMenuItem}); this.testMappingsToolStripMenuItem.Name = "testMappingsToolStripMenuItem"; - this.testMappingsToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.testMappingsToolStripMenuItem.Size = new System.Drawing.Size(186, 22); this.testMappingsToolStripMenuItem.Text = "Test Mappings"; // // testGamePadJoystickToolStripMenuItem @@ -519,26 +539,26 @@ private void InitializeComponent() this.withSelectedToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.generateOppositePressStateMappingsToolStripMenuItem}); this.withSelectedToolStripMenuItem.Name = "withSelectedToolStripMenuItem"; - this.withSelectedToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.withSelectedToolStripMenuItem.Size = new System.Drawing.Size(186, 22); this.withSelectedToolStripMenuItem.Text = "With Selected..."; // // generateOppositePressStateMappingsToolStripMenuItem // this.generateOppositePressStateMappingsToolStripMenuItem.Name = "generateOppositePressStateMappingsToolStripMenuItem"; - this.generateOppositePressStateMappingsToolStripMenuItem.Size = new System.Drawing.Size(287, 22); - this.generateOppositePressStateMappingsToolStripMenuItem.Text = "Generate Opposite Press State Mappings"; - this.generateOppositePressStateMappingsToolStripMenuItem.Click += new System.EventHandler(this.GenerateOppositePressStateMappingsToolStripMenuItem_Click); + this.generateOppositePressStateMappingsToolStripMenuItem.Size = new System.Drawing.Size(220, 22); + this.generateOppositePressStateMappingsToolStripMenuItem.Text = "Generate Reverse Mappings"; + this.generateOppositePressStateMappingsToolStripMenuItem.Click += new System.EventHandler(this.GenerateReverseMappingsToolStripMenuItem_Click); // // toolStripSeparator4 // this.toolStripSeparator4.Name = "toolStripSeparator4"; - this.toolStripSeparator4.Size = new System.Drawing.Size(176, 6); + this.toolStripSeparator4.Size = new System.Drawing.Size(183, 6); // // userConfigurationsToolStripMenuItem // this.userConfigurationsToolStripMenuItem.Image = global::Key2Joy.Gui.Properties.Resources.cog; this.userConfigurationsToolStripMenuItem.Name = "userConfigurationsToolStripMenuItem"; - this.userConfigurationsToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.userConfigurationsToolStripMenuItem.Size = new System.Drawing.Size(186, 22); this.userConfigurationsToolStripMenuItem.Text = "User Configurations"; this.userConfigurationsToolStripMenuItem.Click += new System.EventHandler(this.UserConfigurationsToolStripMenuItem_Click); // @@ -590,13 +610,13 @@ private void InitializeComponent() // pnlMainMenu // this.pnlMainMenu.Controls.Add(this.menMainMenu); - this.pnlMainMenu.Controls.Add(this.chkEnabled); + this.pnlMainMenu.Controls.Add(this.chkArmed); this.pnlMainMenu.Controls.Add(this.lblStatusInactive); this.pnlMainMenu.Controls.Add(this.lblStatusActive); this.pnlMainMenu.Dock = System.Windows.Forms.DockStyle.Top; this.pnlMainMenu.Location = new System.Drawing.Point(0, 0); this.pnlMainMenu.Name = "pnlMainMenu"; - this.pnlMainMenu.Size = new System.Drawing.Size(684, 23); + this.pnlMainMenu.Size = new System.Drawing.Size(683, 23); this.pnlMainMenu.TabIndex = 85; // // lblStatusInactive @@ -604,38 +624,84 @@ private void InitializeComponent() this.lblStatusInactive.BackColor = System.Drawing.Color.IndianRed; this.lblStatusInactive.Dock = System.Windows.Forms.DockStyle.Right; this.lblStatusInactive.ForeColor = System.Drawing.SystemColors.ButtonHighlight; - this.lblStatusInactive.Location = new System.Drawing.Point(483, 0); + this.lblStatusInactive.Location = new System.Drawing.Point(482, 0); this.lblStatusInactive.Name = "lblStatusInactive"; this.lblStatusInactive.Size = new System.Drawing.Size(109, 23); this.lblStatusInactive.TabIndex = 82; - this.lblStatusInactive.Text = "(Mappings not active)"; + this.lblStatusInactive.Text = "(Mappings not armed)"; this.lblStatusInactive.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // lblStatusActive // this.lblStatusActive.BackColor = System.Drawing.Color.LawnGreen; this.lblStatusActive.Dock = System.Windows.Forms.DockStyle.Right; - this.lblStatusActive.Location = new System.Drawing.Point(592, 0); + this.lblStatusActive.Location = new System.Drawing.Point(591, 0); this.lblStatusActive.Name = "lblStatusActive"; this.lblStatusActive.Size = new System.Drawing.Size(92, 23); this.lblStatusActive.TabIndex = 83; - this.lblStatusActive.Text = "(Mappings active)"; + this.lblStatusActive.Text = "(Mappings armed)"; this.lblStatusActive.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // + // pnlNotificationsParent + // + this.pnlNotificationsParent.AutoSize = true; + this.pnlNotificationsParent.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlNotificationsParent.Location = new System.Drawing.Point(0, 53); + this.pnlNotificationsParent.Name = "pnlNotificationsParent"; + this.pnlNotificationsParent.Size = new System.Drawing.Size(683, 0); + this.pnlNotificationsParent.TabIndex = 89; + // + // splitContainer + // + this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel2; + this.splitContainer.Location = new System.Drawing.Point(0, 0); + this.splitContainer.Name = "splitContainer"; + // + // splitContainer.Panel1 + // + this.splitContainer.Panel1.Controls.Add(this.olvMappings); + this.splitContainer.Panel1.Controls.Add(this.pnlActionManagement); + this.splitContainer.Panel1.Controls.Add(this.pnlNotificationsParent); + this.splitContainer.Panel1.Controls.Add(this.pnlProfileManagement); + this.splitContainer.Panel1.Controls.Add(this.pnlMainMenu); + this.splitContainer.Panel1MinSize = 650; + // + // splitContainer.Panel2 + // + this.splitContainer.Panel2.Controls.Add(this.deviceListControl); + this.splitContainer.Panel2MinSize = 80; + this.splitContainer.Size = new System.Drawing.Size(784, 561); + this.splitContainer.SplitterDistance = 683; + this.splitContainer.TabIndex = 90; + // + // deviceListControl + // + this.deviceListControl.AutoScroll = true; + this.deviceListControl.BackColor = System.Drawing.Color.Gold; + this.deviceListControl.Dock = System.Windows.Forms.DockStyle.Fill; + this.deviceListControl.Location = new System.Drawing.Point(0, 0); + this.deviceListControl.Name = "deviceListControl"; + this.deviceListControl.Size = new System.Drawing.Size(97, 561); + this.deviceListControl.TabIndex = 0; + // + // groupMappingsByToolStripMenuItem + // + this.groupMappingsByToolStripMenuItem.Name = "groupMappingsByToolStripMenuItem"; + this.groupMappingsByToolStripMenuItem.Size = new System.Drawing.Size(188, 22); + this.groupMappingsByToolStripMenuItem.Text = "Group Mappings By..."; + // // MainForm // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoSize = true; - this.ClientSize = new System.Drawing.Size(684, 561); - this.Controls.Add(this.olvMappings); - this.Controls.Add(this.pnlActionManagement); - this.Controls.Add(this.pnlProfileManagement); - this.Controls.Add(this.pnlMainMenu); + this.ClientSize = new System.Drawing.Size(784, 561); + this.Controls.Add(this.splitContainer); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.KeyPreview = true; this.MaximizeBox = false; - this.MinimumSize = new System.Drawing.Size(600, 500); + this.MinimumSize = new System.Drawing.Size(800, 500); this.Name = "MainForm"; this.Text = "Key2Joy - Alpha Version"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); @@ -651,12 +717,17 @@ private void InitializeComponent() this.menMainMenu.PerformLayout(); this.pnlMainMenu.ResumeLayout(false); this.pnlMainMenu.PerformLayout(); + this.splitContainer.Panel1.ResumeLayout(false); + this.splitContainer.Panel1.PerformLayout(); + this.splitContainer.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit(); + this.splitContainer.ResumeLayout(false); this.ResumeLayout(false); } #endregion - private System.Windows.Forms.CheckBox chkEnabled; + private System.Windows.Forms.CheckBox chkArmed; private System.Windows.Forms.Panel pnlActionManagement; private System.Windows.Forms.Panel pnlProfileManagement; private System.Windows.Forms.TextBox txtProfileName; @@ -700,7 +771,6 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripSeparator toolStripSeparator4; private System.Windows.Forms.ToolStripMenuItem userConfigurationsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem viewToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem createNewMappingToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem viewScriptOutputToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem viewLogFileToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem viewEventViewerToolStripMenuItem; @@ -715,6 +785,13 @@ private void InitializeComponent() private System.Windows.Forms.TextBox txtFilter; private System.Windows.Forms.Panel pnlFiltering; private System.Windows.Forms.Label txtFilterLabel; + private System.Windows.Forms.Panel pnlNotificationsParent; + private System.Windows.Forms.SplitContainer splitContainer; + private DeviceListControl deviceListControl; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator5; + private System.Windows.Forms.ToolStripMenuItem createNewMappingToolStripMenuItem; + private System.Windows.Forms.ToolStripSeparator toolStripSeparator6; + private System.Windows.Forms.ToolStripMenuItem groupMappingsByToolStripMenuItem; } } diff --git a/Key2Joy.Gui/MainForm.cs b/Key2Joy.Gui/MainForm.cs index 98fe20ed..d53d17ed 100644 --- a/Key2Joy.Gui/MainForm.cs +++ b/Key2Joy.Gui/MainForm.cs @@ -4,13 +4,15 @@ using System.Drawing; using System.IO; using System.Linq; +using System.Media; using System.Windows.Forms; using BrightIdeasSoftware; +using CommandLine; using CommonServiceLocator; using Key2Joy.Config; using Key2Joy.Contracts; using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Util; using Key2Joy.Gui.Properties; using Key2Joy.Gui.Util; using Key2Joy.LowLevelInput; @@ -42,8 +44,63 @@ public MainForm(bool shouldStartMinimized = false) this.SetupNotificationIndicator(); this.PopulateGroupImages(); this.RegisterListViewEvents(); + this.ConfigureTriggerColumn(); - this.RefreshColumnWidths(); + this.ConfigureActionColumn(); + this.ConfigureTooltips(); + } + + private void RefreshMappingGroupMenu() + { + var menu = this.groupMappingsByToolStripMenuItem.DropDown; + var groupTypes = Enum.GetValues(typeof(ViewMappingGroupType)); + var configManager = ServiceLocator.Current.GetInstance(); + var current = configManager.GetConfigState().SelectedViewMappingGroupType; + + menu.Items.Clear(); + + foreach (var groupType in groupTypes) + { + var item = new ToolStripMenuItem(groupType.ToString()); + item.Click += (s, e) => + { + var selected = (ViewMappingGroupType)groupType; + configManager.GetConfigState().SelectedViewMappingGroupType = selected; + this.RefreshMappingsAfterGroupChange(); + }; + + if (current == (ViewMappingGroupType)groupType) + { + item.Checked = true; + } + + menu.Items.Add(item); + } + } + + /// + /// Shows a notification banner at the top of the app. + /// + /// + private void ShowNotification(NotificationBannerControl banner) + { + banner.Dock = DockStyle.Top; + this.pnlNotificationsParent.Controls.Add(banner); + this.pnlNotificationsParent.PerformLayout(); + } + + /// + /// Refresh the listed mappings, their sorting and formatting. + /// Call this after making a change to the mapped options. + /// + private void RefreshMappings() + => this.olvMappings.SetObjects(this.selectedProfile.MappedOptions); + + private void RefreshMappingsAfterGroupChange() + { + this.RefreshMappings(); + this.RefreshMappingGroupMenu(); + this.FindAndDetachParentChildDifferentGroups(); } private void ApplyMinimizedStateIfNeeded(bool shouldMinimize) @@ -53,7 +110,7 @@ private void ApplyMinimizedStateIfNeeded(bool shouldMinimize) } private void ConfigureStatusLabels() - => this.lblStatusActive.Visible = this.chkEnabled.Checked; + => this.lblStatusActive.Visible = this.chkArmed.Checked; private void SetupNotificationIndicator() { @@ -89,7 +146,15 @@ private void RegisterListViewEvents() { this.olvColumnAction.GroupKeyGetter += this.OlvMappings_GroupKeyGetter; this.olvColumnAction.GroupKeyToTitleConverter += this.OlvMappings_GroupKeyToTitleConverter; + this.olvMappings.BeforeCreatingGroups += this.OlvMappings_BeforeCreatingGroups; + this.olvMappings.AboutToCreateGroups += this.OlvMappings_AboutToCreateGroups; + + this.olvMappings.CellClick += this.OlvMappings_CellClick; + this.olvMappings.CellRightClick += this.OlvMappings_CellRightClick; + this.olvMappings.FormatRow += this.OlvMappings_FormatRow; + this.olvMappings.FormatCell += this.OlvMappings_FormatCell; + this.olvMappings.KeyUp += this.OlvMappings_KeyUp; } private void ConfigureTriggerColumn() @@ -102,7 +167,47 @@ private void ConfigureTriggerColumn() return "(no trigger mapped)"; } - return trigger.ToString(); + return trigger.GetNameDisplay().Ellipsize(64); + }; + + private void ConfigureActionColumn() + => this.olvColumnAction.AspectToStringConverter = delegate (object obj) + { + var action = obj as CoreAction; + + if (action == null) + { + return "(no action mapped)"; + } + + return action.GetNameDisplay().Ellipsize(64); + }; + + private void ConfigureTooltips() + => this.olvMappings.CellToolTipShowing += (s, e) => + { + if (e.Model is not MappedOption mappedOption) + { + return; + } + + var action = mappedOption.Action; + var trigger = mappedOption.Trigger; + var toolTipText = string.Empty; + + if (action.GetNameDisplay() != action.GetNameDisplay().Ellipsize(64)) + { + toolTipText += $"Action: {action.GetNameDisplay()}\n"; + } + else if (trigger.GetNameDisplay() != trigger.GetNameDisplay().Ellipsize(64)) + { + toolTipText += $"Trigger: {trigger.GetNameDisplay()}\n"; + } + + if (!string.IsNullOrEmpty(toolTipText)) + { + e.Text = toolTipText; + } }; private void RefreshColumnWidths() @@ -116,9 +221,10 @@ private void SetSelectedProfile(MappingProfile profile) this.selectedProfile = profile; this.configState.LastLoadedProfile = profile.FilePath; - this.olvMappings.SetObjects(profile.MappedOptions); + this.RefreshMappings(); this.olvMappings.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent); this.olvMappings.Sort(this.olvColumnTrigger, SortOrder.Ascending); + this.RefreshMappingsAfterGroupChange(); this.UpdateSelectedProfileName(); } @@ -127,9 +233,9 @@ private void SetSelectedProfile(MappingProfile profile) private void SetStatusView(bool isEnabled) { - this.chkEnabled.CheckedChanged -= this.ChkEnabled_CheckedChanged; - this.chkEnabled.Checked = isEnabled; - this.chkEnabled.CheckedChanged += this.ChkEnabled_CheckedChanged; + this.chkArmed.CheckedChanged -= this.ChkEnabled_CheckedChanged; + this.chkArmed.Checked = isEnabled; + this.chkArmed.CheckedChanged += this.ChkEnabled_CheckedChanged; this.lblStatusActive.Visible = isEnabled; this.lblStatusInactive.Visible = !isEnabled; @@ -146,7 +252,7 @@ private MappingProfile CreateNewProfile(string nameSuffix = default) private void EditMappedOption(MappedOption existingMappedOption = null) { - this.chkEnabled.Checked = false; + this.chkArmed.Checked = false; MappingForm mappingForm = new(existingMappedOption); var result = mappingForm.ShowDialog(); @@ -155,29 +261,57 @@ private void EditMappedOption(MappedOption existingMappedOption = null) return; } - var mappedOption = mappingForm.MappedOption; - if (existingMappedOption == null) { - this.selectedProfile.AddMapping(mappedOption); + this.selectedProfile.AddMapping(mappingForm.MappedOption); + } + + if (mappingForm.MappedOptionReverse != null) + { + this.selectedProfile.AddMapping(mappingForm.MappedOptionReverse); } this.selectedProfile.Save(); + this.RefreshMappings(); + } - if (existingMappedOption == null) + private void RemoveMappings(IList mappedOptions) + { + var children = mappedOptions.SelectMany(x => x.Children).ToList(); + + if (children.Any()) { - this.olvMappings.AddObject(mappedOption); + var introText = mappedOptions.Count == 1 ? "This mapped option has" : "These mapped options have a total of"; + var shouldRemove = MessageBox.Show( + $"{introText} {children.Count} child mapping{(children.Count != 1 ? "s" : "")}. Do you want to remove them as well?", + "Remove child mappings?", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question) == DialogResult.Yes; + + if (shouldRemove) + { + foreach (var child in children) + { + this.selectedProfile.RemoveMapping(child); + } + } + else + { + // Otherwise remove their parent + foreach (var child in children) + { + child.SetParent(null); + } + } } - else + + foreach (var mappedOption in mappedOptions) { - this.olvMappings.UpdateObject(mappedOption); + this.selectedProfile.RemoveMapping(mappedOption); } - } - private void RemoveMapping(MappedOption mappedOption) - { - this.selectedProfile.RemoveMapping(mappedOption); - this.olvMappings.RemoveObject(mappedOption); + this.selectedProfile.Save(); + this.RefreshMappings(); } private void RemoveSelectedMappings() @@ -197,12 +331,118 @@ private void RemoveSelectedMappings() } } - foreach (OLVListItem listItem in this.olvMappings.SelectedItems) + var mappedOptions = new List(); + + foreach (var item in this.olvMappings.SelectedObjects) + { + mappedOptions.Add(item as MappedOption); + } + + this.RemoveMappings(mappedOptions); + this.selectedProfile.Save(); + } + + private void MakeMappingParentless(MappedOption childOption) + { + childOption.SetParent(null); + this.selectedProfile.Save(); + this.RefreshMappings(); + } + + /// + /// Gets the group of the ObjectListView by it's row object. + /// + /// + /// + private ListViewGroup GetByItem(object rowObject) + { + foreach (var item in this.olvMappings.Items) + { + var listViewItem = item as OLVListItem; + + if (listViewItem.RowObject == rowObject) + { + return listViewItem.Group; + } + } + + return null; + } + + private void ChooseNewParent(MappedOption child, MappedOption targetParent) + { + var childGroup = this.GetByItem(child); + var parentGroup = this.GetByItem(targetParent); + + if (childGroup != parentGroup) { - this.RemoveMapping((MappedOption)listItem.RowObject); + MessageBox.Show( + $"The child is in the '{childGroup.Header}' group and the parent is in the '{parentGroup.Header}' group. Cannot parent between groups. Consider disabling grouping in the configuration", + "Cannot parent across groups", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + return; } + child.SetParent(targetParent); this.selectedProfile.Save(); + this.RefreshMappings(); + SystemSounds.Beep.Play(); + + return; + } + + /// + /// Call this after changing the group mode, so that parents and children do not + /// exist across groups. (not supported by the ) + /// + private void FindAndDetachParentChildDifferentGroups() + { + var changedMappings = new List(); + + foreach (var mappedOption in this.selectedProfile.MappedOptions) + { + if (mappedOption.Parent != null) + { + var childGroup = this.GetByItem(mappedOption); + var parentGroup = this.GetByItem(mappedOption.Parent); + + if (childGroup != parentGroup) + { + changedMappings.Add(mappedOption); + } + } + } + + if (changedMappings.Count > 0) + { + var mappingSummaryList = changedMappings.Select(x => $"- {x.ToString().Ellipsize(200)}").ToList(); + var mappingSummary = string.Join(Environment.NewLine, mappingSummaryList); + + this.RefreshMappings(); + var plural = changedMappings.Count > 1 ? "s" : ""; + var pluralWas = changedMappings.Count > 1 ? "were" : "was"; + var result = MessageBox.Show( + $"Found {changedMappings.Count} parent/child mapping{plural} that {pluralWas} in different groups:\n{mappingSummary}\n\nThis can happen if you change grouping or if an invalid profile is loaded. To prevent weird sorting behaviour the mapping{plural} {pluralWas} detached from their parent.\n\nYou can still restore to the previous setup by switching to a compatible grouping type ('None' always works).\n\nSelect 'Cancel' if you want to switch to the grouping type 'None', or 'OK' to save the profile with the detached mapping{plural}.", + $"Parent/child mapping{plural} detached", + MessageBoxButtons.OKCancel, + MessageBoxIcon.Warning + ); + + if (result == DialogResult.Cancel) + { + var configManager = ServiceLocator.Current.GetInstance(); + configManager.GetConfigState().SelectedViewMappingGroupType = ViewMappingGroupType.None; + this.RefreshMappingsAfterGroupChange(); + return; + } + + foreach (var mappedOption in changedMappings) + { + mappedOption.SetParent(null); + } + } } public bool RunAppCommand(AppCommand command) @@ -212,7 +452,7 @@ public bool RunAppCommand(AppCommand command) case AppCommand.Abort: this.BeginInvoke(new MethodInvoker(delegate { - this.chkEnabled.Checked = false; + this.chkArmed.Checked = false; })); return true; @@ -247,6 +487,8 @@ private void MainForm_Load(object sender, EventArgs e) this.SetSelectedProfile(ev.Profile); } }; + + this.RefreshColumnWidths(); } private void BtnCreateMapping_Click(object sender, EventArgs e) @@ -274,7 +516,7 @@ private void OlvMappings_CellClick(object sender, CellClickEventArgs e) this.EditMappedOption(mappedOption); } - private CachedMappingGroup GetGroupOrCreateInCache(ActionAttribute attribute) + private CachedMappingGroup GetGroupOrCreateInCache(MappingAttribute attribute) { var uniqueId = attribute.GroupName + attribute.GroupImage; @@ -293,11 +535,30 @@ private CachedMappingGroup GetGroupOrCreateInCache(ActionAttribute attribute) private object OlvMappings_GroupKeyGetter(object rowObject) { var option = (AbstractMappedOption)rowObject; - var actionAttribute = ActionsRepository.GetAttributeForAction(option.Action); - if (actionAttribute != null) + MappingAttribute attribute = null; + + var configManager = ServiceLocator.Current.GetInstance(); + var mappingGroupType = configManager.GetConfigState().SelectedViewMappingGroupType; + + switch (mappingGroupType) + { + case ViewMappingGroupType.ByAction: + attribute = ActionsRepository.GetAttributeForAction(option.Action); + break; + + case ViewMappingGroupType.ByTrigger: + attribute = TriggersRepository.GetAttributeForTrigger(option.Trigger); + break; + + case ViewMappingGroupType.None: + default: + break; + } + + if (attribute != null) { - return this.GetGroupOrCreateInCache(actionAttribute); + return this.GetGroupOrCreateInCache(attribute); } return null; @@ -318,7 +579,10 @@ private string OlvMappings_GroupKeyToTitleConverter(object groupKey) private void OlvMappings_BeforeCreatingGroups(object sender, CreateGroupsEventArgs e) { e.Parameters.GroupComparer = new MappingGroupComparer(); - e.Parameters.ItemComparer = new MappingGroupItemComparer(e.Parameters.PrimarySort, e.Parameters.PrimarySortOrder); + e.Parameters.ItemComparer = new MappingGroupItemComparer( + e.Parameters.PrimarySort, + e.Parameters.PrimarySortOrder + ); } private void OlvMappings_AboutToCreateGroups(object sender, CreateGroupsEventArgs e) @@ -337,29 +601,29 @@ private void OlvMappings_AboutToCreateGroups(object sender, CreateGroupsEventArg private void OlvMappings_CellRightClick(object sender, CellRightClickEventArgs e) { - ContextMenuStrip menu = new(); - - var addItem = menu.Items.Add("Add New Mapping"); - addItem.Click += (s, _) => this.EditMappedOption(); - - var selectedCount = this.olvMappings.SelectedItems.Count; - - if (selectedCount > 1) + var builder = new MappingContextMenuBuilder(this.olvMappings.SelectedItems); + builder.SelectEditMapping += (s, e) => this.EditMappedOption(e.MappedOption); + builder.SelectMakeMappingParentless += (s, e) => this.MakeMappingParentless(e.MappedOption); + builder.SelectChooseNewParent += (s, e) => this.ChooseNewParent(e.MappedOption, e.NewParent); + builder.SelectMultiEditMapping += this.Builder_SelectMultiEditMapping; + builder.SelectRemoveMappings += (s, e) => { - var removeItems = menu.Items.Add($"Remove {selectedCount} Mappings"); - removeItems.Click += (s, _) => - { - this.RemoveSelectedMappings(); - this.selectedProfile.Save(); - }; - } - else if (e.Model is MappedOption mappedOption) + this.RemoveSelectedMappings(); + this.selectedProfile.Save(); + }; + e.MenuStrip = builder.Build(); + } + + private void Builder_SelectMultiEditMapping(object sender, SelectMultiEditMappingEventArgs e) + { + // Apply, save and refresh + foreach (var mappingAspect in e.MappingAspects) { - var removeItem = menu.Items.Add("Remove Mapping"); - removeItem.Click += (s, _) => this.RemoveMapping(mappedOption); + e.Property.SetValue(mappingAspect, e.Value); } - e.MenuStrip = menu; + this.selectedProfile.Save(); + this.RefreshMappings(); } private void OlvMappings_KeyUp(object sender, KeyEventArgs e) @@ -372,6 +636,25 @@ private void OlvMappings_KeyUp(object sender, KeyEventArgs e) this.RemoveSelectedMappings(); } + private void OlvMappings_FormatRow(object sender, FormatRowEventArgs e) + { + if (e.Model is not MappedOption mappedOption) + { + return; + } + + if (mappedOption.IsChild) + { + e.Item.CellPadding = new Rectangle( + (e.ListView.CellPadding?.Left ?? 0) + 10, + e.ListView.CellPadding?.Top ?? 0, + e.ListView.CellPadding?.Right ?? 0, + e.ListView.CellPadding?.Bottom ?? 0 + ); + e.Item.ForeColor = Color.Gray; + } + } + private void OlvMappings_FormatCell(object sender, FormatCellEventArgs e) { if (e.CellValue != null) @@ -385,18 +668,34 @@ private void OlvMappings_FormatCell(object sender, FormatCellEventArgs e) private void ChkEnabled_CheckedChanged(object sender, EventArgs e) { - var isEnabled = this.chkEnabled.Checked; + var isArmed = this.chkArmed.Checked; - this.SetStatusView(isEnabled); + this.SetStatusView(isArmed); - if (isEnabled) + if (isArmed) { - Key2JoyManager.Instance.ArmMappings(this.selectedProfile); + try + { + Key2JoyManager.Instance.ArmMappings(this.selectedProfile); + } + catch (MappingArmingFailedException ex) + { + this.chkArmed.Checked = false; + MessageBox.Show( + this, + ex.Message, + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } } else { Key2JoyManager.Instance.DisarmMappings(); } + + this.deviceListControl.RefreshDevices(); } private void TxtProfileName_TextChanged(object sender, EventArgs e) @@ -450,7 +749,8 @@ private void LoadProfileToolStripMenuItem_Click(object sender, EventArgs e) this.SetSelectedProfile(profile); } - private void SaveProfileToolStripMenuItem_Click(object sender, EventArgs e) => MessageBox.Show("When you make changes to a profile, changes are automatically saved. This button is only here to explain that feature to you.", "Profile already saved!", MessageBoxButtons.OK, MessageBoxIcon.Information); + private void SaveProfileToolStripMenuItem_Click(object sender, EventArgs e) + => MessageBox.Show("When you make changes to a profile, changes are automatically saved. This button is only here to explain that feature to you.", "Profile already saved!", MessageBoxButtons.OK, MessageBoxIcon.Information); private void OpenProfileFolderToolStripMenuItem_Click(object sender, EventArgs e) { @@ -471,28 +771,28 @@ private void OpenProfileFolderToolStripMenuItem_Click(object sender, EventArgs e private void GamePadPressAndReleaseToolStripMenuItem_Click(object sender, EventArgs e) { List range = new(); - range.AddRange(GamePadAction.GetAllButtonActions(PressState.Press)); - range.AddRange(GamePadAction.GetAllButtonActions(PressState.Release)); + range.AddRange(GamePadButtonAction.GetAllButtonActions(PressState.Press)); + range.AddRange(GamePadButtonAction.GetAllButtonActions(PressState.Release)); this.selectedProfile.AddMappingRange(range); this.selectedProfile.Save(); - this.olvMappings.AddObjects(range); + this.RefreshMappings(); } private void GamePadPressToolStripMenuItem_Click(object sender, EventArgs e) { - var range = GamePadAction.GetAllButtonActions(PressState.Press); + var range = GamePadButtonAction.GetAllButtonActions(PressState.Press); this.selectedProfile.AddMappingRange(range); this.selectedProfile.Save(); - this.olvMappings.AddObjects(range); + this.RefreshMappings(); } private void GamePadReleaseToolStripMenuItem_Click(object sender, EventArgs e) { - var range = GamePadAction.GetAllButtonActions(PressState.Release); + var range = GamePadButtonAction.GetAllButtonActions(PressState.Release); this.selectedProfile.AddMappingRange(range); this.selectedProfile.Save(); - this.olvMappings.AddObjects(range); + this.RefreshMappings(); } private void KeyboardPressAndReleaseToolStripMenuItem_Click(object sender, EventArgs e) @@ -503,7 +803,7 @@ private void KeyboardPressAndReleaseToolStripMenuItem_Click(object sender, Event this.selectedProfile.AddMappingRange(range); this.selectedProfile.Save(); - this.olvMappings.AddObjects(range); + this.RefreshMappings(); } private void KeyboardPressToolStripMenuItem_Click(object sender, EventArgs e) @@ -511,7 +811,7 @@ private void KeyboardPressToolStripMenuItem_Click(object sender, EventArgs e) var range = KeyboardAction.GetAllButtonActions(PressState.Press); this.selectedProfile.AddMappingRange(range); this.selectedProfile.Save(); - this.olvMappings.AddObjects(range); + this.RefreshMappings(); } private void KeyboardReleaseToolStripMenuItem_Click(object sender, EventArgs e) @@ -519,14 +819,20 @@ private void KeyboardReleaseToolStripMenuItem_Click(object sender, EventArgs e) var range = KeyboardAction.GetAllButtonActions(PressState.Release); this.selectedProfile.AddMappingRange(range); this.selectedProfile.Save(); - this.olvMappings.AddObjects(range); + this.RefreshMappings(); } private void TestKeyboardToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://devicetests.com/keyboard-tester"); private void TestMouseToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://devicetests.com/mouse-test"); - private void UserConfigurationsToolStripMenuItem_Click(object sender, EventArgs e) => new ConfigForm().ShowDialog(); + private void UserConfigurationsToolStripMenuItem_Click(object sender, EventArgs e) + { + new ConfigForm().ShowDialog(); + + // Refresh the mapppings in case the user modified a group config + this.RefreshMappingsAfterGroupChange(); + } private void ReportAProblemToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://github.com/luttje/Key2Joy/issues"); @@ -566,7 +872,7 @@ private void ViewLogFileToolStripMenuItem_Click(object sender, EventArgs e) private void ManagePluginsToolStripMenuItem_Click(object sender, EventArgs e) => new PluginsForm().ShowDialog(); - private void GenerateOppositePressStateMappingsToolStripMenuItem_Click(object sender, EventArgs e) + private void GenerateReverseMappingsToolStripMenuItem_Click(object sender, EventArgs e) { var selectedCount = this.olvMappings.SelectedItems.Count; @@ -577,8 +883,9 @@ private void GenerateOppositePressStateMappingsToolStripMenuItem_Click(object se if (selectedCount > 1 && DialogUtilities.Confirm( - $"Are you sure you want to create opposite press state mappings for all {selectedCount} selected mappings? New 'Release' mappings will be created for each 'Press' and vice versa.", - $"Generate {selectedCount} opposite press state mappings" + $"Are you sure you want to create reverse mappings for all {selectedCount} selected mappings? Each type of action and trigger will configure their own useful reverse if possible.\n\n" + + $"An example of an reverse mapping is how new 'Release' mappings will be created for each 'Press' and vice versa.", + $"Generate {selectedCount} reverse mappings" ) == DialogResult.No) { return; @@ -589,7 +896,7 @@ private void GenerateOppositePressStateMappingsToolStripMenuItem_Click(object se .Select(item => (MappedOption)item.RowObject) .ToList(); - var newOptions = MappedOption.GenerateOppositePressStateMappings(selectedMappings); + var newOptions = MappedOption.GenerateReverseMappings(selectedMappings); foreach (var option in newOptions) { @@ -597,7 +904,7 @@ private void GenerateOppositePressStateMappingsToolStripMenuItem_Click(object se } this.selectedProfile.Save(); - this.olvMappings.AddObjects(newOptions); + this.RefreshMappings(); } private void TxtFilter_TextChanged(object sender, EventArgs e) diff --git a/Key2Joy.Gui/Mapping/Actions/ActionControl.cs b/Key2Joy.Gui/Mapping/Actions/ActionControl.cs index 3a5d7641..c47dd2c5 100644 --- a/Key2Joy.Gui/Mapping/Actions/ActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/ActionControl.cs @@ -1,146 +1,151 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Mapping; -using Key2Joy.Mapping.Actions; -using Key2Joy.Plugins; - -namespace Key2Joy.Gui.Mapping; - -public partial class ActionControl : UserControl -{ - public AbstractAction Action { get; private set; } - - public event Action ActionChanged; - - public bool IsTopLevel { get; set; } - - private bool isLoaded = false; - private IActionOptionsControl options; - private AbstractAction selectedAction = null; - - public ActionControl() => this.InitializeComponent(); - - private void BuildAction() - { - if (this.cmbAction.SelectedItem == null) - { - ActionChanged?.Invoke(null); - return; - } - - var selected = (ImageComboBoxItem>>)this.cmbAction.SelectedItem; - var selectedTypeFactory = selected.ItemValue.Value; - - if (this.Action == null || this.Action.GetType().FullName != selectedTypeFactory.FullTypeName) - { - this.Action = CoreAction.MakeAction(selectedTypeFactory); - } - - this.options?.Setup(this.Action); - - ActionChanged?.Invoke(this.Action); - } - - public bool CanMappingSave(AbstractMappedOption mappedOption) - { - if (this.options != null) - { - return this.options.CanMappingSave(mappedOption.Action); - } - - return false; - } - - public void SelectAction(AbstractAction action) - { - if (action is DisabledAction) - { - action = null; - } - - this.selectedAction = action; - - if (!this.isLoaded) - { - return; - } - - var selected = this.cmbAction.Items.Cast>>>(); - var actionFullTypeName = MappingTypeHelper.GetTypeFullName( - ActionsRepository.GetAllActions(), - action - ); - actionFullTypeName = MappingTypeHelper.EnsureSimpleTypeName(actionFullTypeName); - - var selectedType = selected.FirstOrDefault(x => x.ItemValue.Value.FullTypeName == actionFullTypeName); - this.cmbAction.SelectedItem = selectedType; - } - - private void LoadActions() - { - var actionTypeFactories = ActionsRepository.GetAllActions(this.IsTopLevel); - - foreach (var keyValuePair in actionTypeFactories) - { - var mappingControlFactory = MappingControlRepository.GetMappingControlFactory(keyValuePair.Value.FullTypeName); - - if (mappingControlFactory == null) - { - continue; - } - - var customImage = mappingControlFactory.ImageResourceName; - var image = Program.ResourceBitmapFromName(customImage ?? "error"); - ImageComboBoxItem>> item = new(keyValuePair, new Bitmap(image), "Key"); - - this.cmbAction.Items.Add(item); - } - - this.cmbAction.SelectedIndex = -1; - - this.isLoaded = true; - - if (this.selectedAction != null) - { - this.SelectAction(this.selectedAction); - } - } - - private void CmbAction_SelectedIndexChanged(object sender, EventArgs e) - { - if (!this.isLoaded) - { - return; - } - - var options = MappingForm.BuildOptionsForComboBox(this.cmbAction, this.pnlActionOptions); - - if (options != null) - { - this.options = options as IActionOptionsControl; - - if (this.options != null) - { - if (this.selectedAction != null) - { - this.options.Select(this.selectedAction); - } - - this.options.OptionsChanged += (s, _) => this.BuildAction(); - } - } - - this.BuildAction(); - - this.selectedAction = null; - this.PerformLayout(); - } - - private void ActionControl_Load(object sender, EventArgs e) => this.LoadActions(); -} +using System; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Actions; +using Key2Joy.Plugins; + +namespace Key2Joy.Gui.Mapping; + +public partial class ActionControl : UserControl +{ + public AbstractAction Action { get; private set; } + + public event EventHandler ActionChanged; + + public bool IsTopLevel { get; set; } + + private bool isLoaded = false; + private IActionOptionsControl options; + private AbstractAction selectedAction = null; + + public ActionControl() => this.InitializeComponent(); + + private void BuildAction() + { + if (this.cmbAction.SelectedItem == null) + { + ActionChanged?.Invoke(this, new(null)); + return; + } + + var selected = (ImageComboBoxItem>>)this.cmbAction.SelectedItem; + var selectedTypeFactory = selected.ItemValue.Value; + + if (this.Action == null || this.Action.GetType().FullName != selectedTypeFactory.FullTypeName) + { + this.Action = CoreAction.MakeAction(selectedTypeFactory); + } + + this.options?.Setup(this.Action); + + ActionChanged?.Invoke(this, new(this.Action)); + } + + public bool CanMappingSave(AbstractMappedOption mappedOption) + { + if (this.options != null) + { + return this.options.CanMappingSave(mappedOption.Action); + } + + return false; + } + + public void SelectAction(AbstractAction action) + { + if (action is DisabledAction) + { + action = null; + } + + this.selectedAction = action; + + if (!this.isLoaded) + { + return; + } + + var selected = this.cmbAction.Items.Cast>>>(); + var actionFullTypeName = MappingTypeHelper.GetTypeFullName( + ActionsRepository.GetAllActions(), + action + ); + actionFullTypeName = MappingTypeHelper.EnsureSimpleTypeName(actionFullTypeName); + + var selectedType = selected.FirstOrDefault(x => x.ItemValue.Value.FullTypeName == actionFullTypeName); + this.cmbAction.SelectedItem = selectedType; + } + + private void LoadActions() + { + if (System.Diagnostics.Process.GetCurrentProcess().ProcessName == "devenv") + { + return; // The designer can't handle the code below. + } + + var actionTypeFactories = ActionsRepository.GetAllActions(this.IsTopLevel); + + foreach (var keyValuePair in actionTypeFactories) + { + var mappingControlFactory = MappingControlRepository.GetMappingControlFactory(keyValuePair.Value.FullTypeName); + + if (mappingControlFactory == null) + { + continue; + } + + var customImage = mappingControlFactory.ImageResourceName; + var image = Program.ResourceBitmapFromName(customImage ?? "error"); + ImageComboBoxItem>> item = new(keyValuePair, new Bitmap(image), "Key"); + + this.cmbAction.Items.Add(item); + } + + this.cmbAction.SelectedIndex = -1; + + this.isLoaded = true; + + if (this.selectedAction != null) + { + this.SelectAction(this.selectedAction); + } + } + + private void CmbAction_SelectedIndexChanged(object sender, EventArgs e) + { + if (!this.isLoaded) + { + return; + } + + var options = MappingForm.BuildOptionsForComboBox(this.cmbAction, this.pnlActionOptions); + + if (options != null) + { + this.options = options as IActionOptionsControl; + + if (this.options != null) + { + if (this.selectedAction != null) + { + this.options.Select(this.selectedAction); + } + + this.options.OptionsChanged += (s, _) => this.BuildAction(); + } + } + + this.BuildAction(); + + this.selectedAction = null; + this.PerformLayout(); + } + + private void ActionControl_Load(object sender, EventArgs e) => this.LoadActions(); +} diff --git a/Key2Joy.Gui/Mapping/Actions/ActionPluginHostControl.cs b/Key2Joy.Gui/Mapping/Actions/ActionPluginHostControl.cs index 6a867f2a..6186dc20 100644 --- a/Key2Joy.Gui/Mapping/Actions/ActionPluginHostControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/ActionPluginHostControl.cs @@ -1,38 +1,38 @@ -using System; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Plugins; - -namespace Key2Joy.Gui.Mapping; - -public partial class ActionPluginHostControl : UserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - private readonly IActionOptionsControl pluginControlWithOptions; - - public ActionPluginHostControl() => this.InitializeComponent(); - - public ActionPluginHostControl(ElementHostProxy pluginUserControl) - : this() - { - this.pluginControlWithOptions = pluginUserControl; - - this.Padding = new Padding(0, 5, 0, 5); - var desiredHeight = pluginUserControl.GetDesiredHeight() + this.Padding.Vertical; - this.Height = desiredHeight + this.Padding.Vertical; - - this.Controls.Add(pluginUserControl); - pluginUserControl.Dock = DockStyle.Fill; - pluginUserControl.PerformLayout(); - - ActionOptionsChangeListener listener = new(this.pluginControlWithOptions); - listener.OptionsChanged += (s, e) => OptionsChanged?.Invoke(s, e); - } - - public bool CanMappingSave(object action) => this.pluginControlWithOptions.CanMappingSave(action); - - public void Select(object action) => this.pluginControlWithOptions.Select(action); - - public void Setup(object action) => this.pluginControlWithOptions.Setup(action); -} +using System; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Plugins; + +namespace Key2Joy.Gui.Mapping; + +public partial class ActionPluginHostControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + private readonly IActionOptionsControl pluginControlWithOptions; + + public ActionPluginHostControl() => this.InitializeComponent(); + + public ActionPluginHostControl(ElementHostProxy pluginUserControl) + : this() + { + this.pluginControlWithOptions = pluginUserControl; + + this.Padding = new Padding(0, 5, 0, 5); + var desiredHeight = pluginUserControl.GetDesiredHeight() + this.Padding.Vertical; + this.Height = desiredHeight + this.Padding.Vertical; + + this.Controls.Add(pluginUserControl); + pluginUserControl.Dock = DockStyle.Fill; + pluginUserControl.PerformLayout(); + + ActionOptionsChangeListener listener = new(this.pluginControlWithOptions); + listener.OptionsChanged += (s, e) => OptionsChanged?.Invoke(s, e); + } + + public bool CanMappingSave(AbstractAction action) => this.pluginControlWithOptions.CanMappingSave(action); + + public void Select(AbstractAction action) => this.pluginControlWithOptions.Select(action); + + public void Setup(AbstractAction action) => this.pluginControlWithOptions.Setup(action); +} diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.cs index fdadf477..f6f64e75 100644 --- a/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.cs @@ -5,13 +5,13 @@ using Key2Joy.Contracts.Mapping; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.LowLevelInput; -using Key2Joy.LowLevelInput.GamePad; +using Key2Joy.LowLevelInput.SimulatedGamePad; using Key2Joy.Mapping.Actions.Input; namespace Key2Joy.Gui.Mapping; [MappingControl( - ForType = typeof(GamePadAction), + ForType = typeof(GamePadButtonAction), ImageResourceName = "joystick" )] public partial class GamePadActionControl : UserControl, IActionOptionsControl @@ -22,11 +22,11 @@ public GamePadActionControl() { this.InitializeComponent(); - var gamePadService = ServiceLocator.Current.GetInstance(); - var allGamePads = gamePadService.GetAllGamePads(); + var gamePadService = ServiceLocator.Current.GetInstance(); + var allGamePads = gamePadService.GetAllGamePads(false); var allGamePadIndices = allGamePads.Select(gp => gp.Index).ToArray(); - this.cmbGamePad.DataSource = GamePadAction.GetAllButtons(); + this.cmbGamePad.DataSource = GamePadButtonAction.GetAllButtons(); this.cmbPressState.DataSource = PressStates.ALL; this.cmbPressState.SelectedIndex = 0; @@ -34,25 +34,30 @@ public GamePadActionControl() this.cmbGamePadIndex.SelectedIndex = 0; } - public void Select(object action) + public void Select(AbstractAction action) { - var thisAction = (GamePadAction)action; + var thisAction = (GamePadButtonAction)action; this.cmbGamePad.SelectedItem = thisAction.Control; this.cmbPressState.SelectedItem = thisAction.PressState; this.cmbGamePadIndex.SelectedItem = thisAction.GamePadIndex; } - public void Setup(object action) + public void Setup(AbstractAction action) { - var thisAction = (GamePadAction)action; + var thisAction = (GamePadButtonAction)action; thisAction.Control = (SimWinInput.GamePadControl)this.cmbGamePad.SelectedItem; thisAction.PressState = (PressState)this.cmbPressState.SelectedItem; thisAction.GamePadIndex = (int)this.cmbGamePadIndex.SelectedItem; } - public bool CanMappingSave(object action) => true; + public bool CanMappingSave(AbstractAction action) + { + var thisAction = (GamePadButtonAction)action; + + return thisAction.Control != SimWinInput.GamePadControl.None; + } private void CmbGamePad_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.resx b/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.resx index 1af7de15..29dcb1b3 100644 --- a/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.resx +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadActionControl.resx @@ -1,120 +1,120 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.Designer.cs b/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.Designer.cs new file mode 100644 index 00000000..399d3e1c --- /dev/null +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.Designer.cs @@ -0,0 +1,348 @@ +namespace Key2Joy.Gui.Mapping +{ + partial class GamePadStickActionControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.cmbSide = new System.Windows.Forms.ComboBox(); + this.lblInfo = new System.Windows.Forms.Label(); + this.cmbGamePadIndex = new System.Windows.Forms.ComboBox(); + this.lblInfoSide = new System.Windows.Forms.Label(); + this.pnlGamePad = new System.Windows.Forms.Panel(); + this.pnlSide = new System.Windows.Forms.Panel(); + this.pnlDelta = new System.Windows.Forms.Panel(); + this.pnlDeltaConfig = new System.Windows.Forms.Panel(); + this.nudExactY = new System.Windows.Forms.NumericUpDown(); + this.lblExactY = new System.Windows.Forms.Label(); + this.nudExactX = new System.Windows.Forms.NumericUpDown(); + this.lblExactX = new System.Windows.Forms.Label(); + this.pnlTriggerInputScale = new System.Windows.Forms.Panel(); + this.nudTriggerInputScaleY = new System.Windows.Forms.NumericUpDown(); + this.lblTriggerInputScaleY = new System.Windows.Forms.Label(); + this.nudTriggerInputScaleX = new System.Windows.Forms.NumericUpDown(); + this.lblTriggerInputScaleX = new System.Windows.Forms.Label(); + this.chkDeltaFromInput = new System.Windows.Forms.CheckBox(); + this.pnlReset = new System.Windows.Forms.Panel(); + this.nudResetAfterMs = new System.Windows.Forms.NumericUpDown(); + this.lblReset = new System.Windows.Forms.Label(); + this.pnlGamePad.SuspendLayout(); + this.pnlSide.SuspendLayout(); + this.pnlDelta.SuspendLayout(); + this.pnlDeltaConfig.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudExactY)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.nudExactX)).BeginInit(); + this.pnlTriggerInputScale.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudTriggerInputScaleY)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.nudTriggerInputScaleX)).BeginInit(); + this.pnlReset.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudResetAfterMs)).BeginInit(); + this.SuspendLayout(); + // + // cmbSide + // + this.cmbSide.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbSide.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbSide.FormattingEnabled = true; + this.cmbSide.Location = new System.Drawing.Point(44, 5); + this.cmbSide.Name = "cmbSide"; + this.cmbSide.Size = new System.Drawing.Size(368, 21); + this.cmbSide.TabIndex = 9; + this.cmbSide.SelectedIndexChanged += new System.EventHandler(this.CmbGamePad_SelectedIndexChanged); + // + // lblInfo + // + this.lblInfo.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfo.Location = new System.Drawing.Point(0, 0); + this.lblInfo.Name = "lblInfo"; + this.lblInfo.Size = new System.Drawing.Size(65, 24); + this.lblInfo.TabIndex = 10; + this.lblInfo.Text = "GamePad #"; + this.lblInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // cmbGamePadIndex + // + this.cmbGamePadIndex.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbGamePadIndex.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbGamePadIndex.FormattingEnabled = true; + this.cmbGamePadIndex.Location = new System.Drawing.Point(65, 0); + this.cmbGamePadIndex.Name = "cmbGamePadIndex"; + this.cmbGamePadIndex.Size = new System.Drawing.Size(347, 21); + this.cmbGamePadIndex.TabIndex = 13; + this.cmbGamePadIndex.SelectedIndexChanged += new System.EventHandler(this.CmbGamePadIndex_SelectedIndexChanged); + // + // lblInfoSide + // + this.lblInfoSide.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoSide.Location = new System.Drawing.Point(0, 5); + this.lblInfoSide.Name = "lblInfoSide"; + this.lblInfoSide.Size = new System.Drawing.Size(44, 17); + this.lblInfoSide.TabIndex = 14; + this.lblInfoSide.Text = "Stick:"; + this.lblInfoSide.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlGamePad + // + this.pnlGamePad.Controls.Add(this.cmbGamePadIndex); + this.pnlGamePad.Controls.Add(this.lblInfo); + this.pnlGamePad.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlGamePad.Location = new System.Drawing.Point(5, 5); + this.pnlGamePad.Name = "pnlGamePad"; + this.pnlGamePad.Size = new System.Drawing.Size(412, 24); + this.pnlGamePad.TabIndex = 15; + // + // pnlSide + // + this.pnlSide.Controls.Add(this.cmbSide); + this.pnlSide.Controls.Add(this.lblInfoSide); + this.pnlSide.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlSide.Location = new System.Drawing.Point(5, 29); + this.pnlSide.Name = "pnlSide"; + this.pnlSide.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.pnlSide.Size = new System.Drawing.Size(412, 27); + this.pnlSide.TabIndex = 16; + // + // pnlDelta + // + this.pnlDelta.Controls.Add(this.pnlDeltaConfig); + this.pnlDelta.Controls.Add(this.pnlTriggerInputScale); + this.pnlDelta.Controls.Add(this.chkDeltaFromInput); + this.pnlDelta.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDelta.Location = new System.Drawing.Point(5, 56); + this.pnlDelta.Name = "pnlDelta"; + this.pnlDelta.Size = new System.Drawing.Size(412, 74); + this.pnlDelta.TabIndex = 17; + // + // pnlDeltaConfig + // + this.pnlDeltaConfig.Controls.Add(this.nudExactY); + this.pnlDeltaConfig.Controls.Add(this.lblExactY); + this.pnlDeltaConfig.Controls.Add(this.nudExactX); + this.pnlDeltaConfig.Controls.Add(this.lblExactX); + this.pnlDeltaConfig.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlDeltaConfig.Location = new System.Drawing.Point(0, 47); + this.pnlDeltaConfig.Name = "pnlDeltaConfig"; + this.pnlDeltaConfig.Padding = new System.Windows.Forms.Padding(0, 5, 0, 0); + this.pnlDeltaConfig.Size = new System.Drawing.Size(412, 27); + this.pnlDeltaConfig.TabIndex = 18; + // + // nudExactY + // + this.nudExactY.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudExactY.Location = new System.Drawing.Point(241, 5); + this.nudExactY.Name = "nudExactY"; + this.nudExactY.Size = new System.Drawing.Size(171, 20); + this.nudExactY.TabIndex = 18; + this.nudExactY.ValueChanged += new System.EventHandler(this.NudExactDeltaY_ValueChanged); + // + // lblExactY + // + this.lblExactY.Dock = System.Windows.Forms.DockStyle.Left; + this.lblExactY.Location = new System.Drawing.Point(214, 5); + this.lblExactY.Name = "lblExactY"; + this.lblExactY.Size = new System.Drawing.Size(27, 22); + this.lblExactY.TabIndex = 17; + this.lblExactY.Text = "Y:"; + this.lblExactY.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // nudExactX + // + this.nudExactX.Dock = System.Windows.Forms.DockStyle.Left; + this.nudExactX.Location = new System.Drawing.Point(65, 5); + this.nudExactX.Name = "nudExactX"; + this.nudExactX.Size = new System.Drawing.Size(149, 20); + this.nudExactX.TabIndex = 16; + this.nudExactX.ValueChanged += new System.EventHandler(this.NudExactDeltaX_ValueChanged); + // + // lblExactX + // + this.lblExactX.Dock = System.Windows.Forms.DockStyle.Left; + this.lblExactX.Location = new System.Drawing.Point(0, 5); + this.lblExactX.Name = "lblExactX"; + this.lblExactX.Size = new System.Drawing.Size(65, 22); + this.lblExactX.TabIndex = 15; + this.lblExactX.Text = "Exact X:"; + this.lblExactX.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlTriggerInputScale + // + this.pnlTriggerInputScale.Controls.Add(this.nudTriggerInputScaleY); + this.pnlTriggerInputScale.Controls.Add(this.lblTriggerInputScaleY); + this.pnlTriggerInputScale.Controls.Add(this.nudTriggerInputScaleX); + this.pnlTriggerInputScale.Controls.Add(this.lblTriggerInputScaleX); + this.pnlTriggerInputScale.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlTriggerInputScale.Location = new System.Drawing.Point(0, 27); + this.pnlTriggerInputScale.Name = "pnlTriggerInputScale"; + this.pnlTriggerInputScale.Size = new System.Drawing.Size(412, 20); + this.pnlTriggerInputScale.TabIndex = 20; + // + // nudTriggerInputScaleY + // + this.nudTriggerInputScaleY.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudTriggerInputScaleY.Location = new System.Drawing.Point(283, 0); + this.nudTriggerInputScaleY.Minimum = new decimal(new int[] { + 100, + 0, + 0, + -2147483648}); + this.nudTriggerInputScaleY.Name = "nudTriggerInputScaleY"; + this.nudTriggerInputScaleY.Size = new System.Drawing.Size(129, 20); + this.nudTriggerInputScaleY.TabIndex = 19; + this.nudTriggerInputScaleY.Value = new decimal(new int[] { + 1, + 0, + 0, + -2147483648}); + this.nudTriggerInputScaleY.ValueChanged += new System.EventHandler(this.NudTriggerInputScaleY_ValueChanged); + // + // lblTriggerInputScaleY + // + this.lblTriggerInputScaleY.Dock = System.Windows.Forms.DockStyle.Left; + this.lblTriggerInputScaleY.Location = new System.Drawing.Point(247, 0); + this.lblTriggerInputScaleY.Name = "lblTriggerInputScaleY"; + this.lblTriggerInputScaleY.Size = new System.Drawing.Size(36, 20); + this.lblTriggerInputScaleY.TabIndex = 18; + this.lblTriggerInputScaleY.Text = "Y:"; + this.lblTriggerInputScaleY.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // nudTriggerInputScaleX + // + this.nudTriggerInputScaleX.Dock = System.Windows.Forms.DockStyle.Left; + this.nudTriggerInputScaleX.Location = new System.Drawing.Point(142, 0); + this.nudTriggerInputScaleX.Name = "nudTriggerInputScaleX"; + this.nudTriggerInputScaleX.Size = new System.Drawing.Size(105, 20); + this.nudTriggerInputScaleX.TabIndex = 17; + this.nudTriggerInputScaleX.Value = new decimal(new int[] { + 1, + 0, + 0, + 0}); + this.nudTriggerInputScaleX.ValueChanged += new System.EventHandler(this.NudTriggerInputScaleX_ValueChanged); + // + // lblTriggerInputScaleX + // + this.lblTriggerInputScaleX.Dock = System.Windows.Forms.DockStyle.Left; + this.lblTriggerInputScaleX.Location = new System.Drawing.Point(0, 0); + this.lblTriggerInputScaleX.Name = "lblTriggerInputScaleX"; + this.lblTriggerInputScaleX.Size = new System.Drawing.Size(142, 20); + this.lblTriggerInputScaleX.TabIndex = 16; + this.lblTriggerInputScaleX.Text = "Trigger Input Delta Scale X:"; + this.lblTriggerInputScaleX.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // chkDeltaFromInput + // + this.chkDeltaFromInput.AutoSize = true; + this.chkDeltaFromInput.Dock = System.Windows.Forms.DockStyle.Top; + this.chkDeltaFromInput.Location = new System.Drawing.Point(0, 0); + this.chkDeltaFromInput.Name = "chkDeltaFromInput"; + this.chkDeltaFromInput.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.chkDeltaFromInput.Size = new System.Drawing.Size(412, 27); + this.chkDeltaFromInput.TabIndex = 19; + this.chkDeltaFromInput.Text = "Use delta from trigger input (e.g: amount of pixels the cursor moved)"; + this.chkDeltaFromInput.UseVisualStyleBackColor = true; + this.chkDeltaFromInput.CheckedChanged += new System.EventHandler(this.ChkDeltaFromInput_CheckedChanged); + // + // pnlReset + // + this.pnlReset.Controls.Add(this.nudResetAfterMs); + this.pnlReset.Controls.Add(this.lblReset); + this.pnlReset.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlReset.Location = new System.Drawing.Point(5, 130); + this.pnlReset.Name = "pnlReset"; + this.pnlReset.Size = new System.Drawing.Size(412, 22); + this.pnlReset.TabIndex = 21; + // + // nudResetAfterMs + // + this.nudResetAfterMs.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudResetAfterMs.Location = new System.Drawing.Point(191, 0); + this.nudResetAfterMs.Name = "nudResetAfterMs"; + this.nudResetAfterMs.Size = new System.Drawing.Size(221, 20); + this.nudResetAfterMs.TabIndex = 19; + this.nudResetAfterMs.ValueChanged += new System.EventHandler(this.NudResetAfterMs_ValueChanged); + // + // lblReset + // + this.lblReset.Dock = System.Windows.Forms.DockStyle.Left; + this.lblReset.Location = new System.Drawing.Point(0, 0); + this.lblReset.Name = "lblReset"; + this.lblReset.Size = new System.Drawing.Size(191, 22); + this.lblReset.TabIndex = 18; + this.lblReset.Text = "Reset stick to 0, 0 after (milliseconds):"; + this.lblReset.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // GamePadStickActionControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.pnlReset); + this.Controls.Add(this.pnlDelta); + this.Controls.Add(this.pnlSide); + this.Controls.Add(this.pnlGamePad); + this.Name = "GamePadStickActionControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(422, 154); + this.pnlGamePad.ResumeLayout(false); + this.pnlSide.ResumeLayout(false); + this.pnlDelta.ResumeLayout(false); + this.pnlDelta.PerformLayout(); + this.pnlDeltaConfig.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudExactY)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.nudExactX)).EndInit(); + this.pnlTriggerInputScale.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudTriggerInputScaleY)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.nudTriggerInputScaleX)).EndInit(); + this.pnlReset.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudResetAfterMs)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.ComboBox cmbSide; + private System.Windows.Forms.Label lblInfo; + private System.Windows.Forms.ComboBox cmbGamePadIndex; + private System.Windows.Forms.Label lblInfoSide; + private System.Windows.Forms.Panel pnlGamePad; + private System.Windows.Forms.Panel pnlSide; + private System.Windows.Forms.Panel pnlDelta; + private System.Windows.Forms.Panel pnlDeltaConfig; + private System.Windows.Forms.CheckBox chkDeltaFromInput; + private System.Windows.Forms.NumericUpDown nudExactX; + private System.Windows.Forms.Label lblExactX; + private System.Windows.Forms.Label lblExactY; + private System.Windows.Forms.NumericUpDown nudExactY; + private System.Windows.Forms.Panel pnlTriggerInputScale; + private System.Windows.Forms.NumericUpDown nudTriggerInputScaleX; + private System.Windows.Forms.Label lblTriggerInputScaleX; + private System.Windows.Forms.Label lblTriggerInputScaleY; + private System.Windows.Forms.NumericUpDown nudTriggerInputScaleY; + private System.Windows.Forms.Panel pnlReset; + private System.Windows.Forms.NumericUpDown nudResetAfterMs; + private System.Windows.Forms.Label lblReset; + } +} diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.cs new file mode 100644 index 00000000..0c2e603c --- /dev/null +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.Mapping.Actions.Input; +using Key2Joy.Mapping.Triggers.GamePad; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(GamePadStickAction), + ImageResourceName = "joystick" +)] +public partial class GamePadStickActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public GamePadStickActionControl() + { + this.InitializeComponent(); + + var gamePadService = ServiceLocator.Current.GetInstance(); + var allGamePads = gamePadService.GetAllGamePads(false); + var allGamePadIndices = allGamePads.Select(gp => gp.Index).ToArray(); + + this.cmbSide.DataSource = Enum.GetValues(typeof(GamePadSide)); + + this.nudTriggerInputScaleX.Minimum = this.nudTriggerInputScaleY.Minimum = short.MinValue; + this.nudTriggerInputScaleX.Maximum = this.nudTriggerInputScaleY.Maximum = short.MaxValue; + this.nudTriggerInputScaleX.DecimalPlaces = this.nudTriggerInputScaleY.DecimalPlaces = 4; + + this.nudExactX.DecimalPlaces = this.nudExactY.DecimalPlaces = 2; + + this.nudExactX.Minimum = this.nudExactY.Minimum = short.MinValue; + this.nudExactX.Maximum = this.nudExactY.Maximum = short.MaxValue; + + this.cmbGamePadIndex.DataSource = allGamePadIndices; + this.cmbGamePadIndex.SelectedIndex = 0; + + this.nudResetAfterMs.Minimum = 0; + this.nudResetAfterMs.Maximum = int.MaxValue; + this.nudResetAfterMs.Value = this.nudResetAfterMs.Maximum; + + this.chkDeltaFromInput.Checked = true; + } + + public void Select(AbstractAction action) + { + var thisAction = (GamePadStickAction)action; + + this.chkDeltaFromInput.Checked = thisAction.DeltaX == null || thisAction.DeltaY == null; + this.nudTriggerInputScaleX.Value = (decimal)thisAction.InputScaleX; + this.nudTriggerInputScaleY.Value = (decimal)thisAction.InputScaleY; + this.cmbSide.SelectedItem = thisAction.Side; + this.nudExactX.Value = thisAction.DeltaX ?? 0; + this.nudExactY.Value = thisAction.DeltaY ?? 0; + this.nudResetAfterMs.Value = thisAction.ResetAfterIdleTimeInMs; + this.cmbGamePadIndex.SelectedItem = thisAction.GamePadIndex; + } + + public void Setup(AbstractAction action) + { + var thisAction = (GamePadStickAction)action; + + var deltaFromInput = this.chkDeltaFromInput.Checked; + + thisAction.InputScaleX = (float)this.nudTriggerInputScaleX.Value; + thisAction.InputScaleY = (float)this.nudTriggerInputScaleY.Value; + thisAction.Side = (GamePadSide)this.cmbSide.SelectedItem; + thisAction.DeltaX = deltaFromInput ? null : (short)this.nudExactX.Value; + thisAction.DeltaY = deltaFromInput ? null : (short)this.nudExactY.Value; + thisAction.ResetAfterIdleTimeInMs = (int)this.nudResetAfterMs.Value; + thisAction.GamePadIndex = (int)this.cmbGamePadIndex.SelectedItem; + } + + private void MakeChildrenEnabled(Control parent, bool enabled) + { + foreach (Control child in parent.Controls) + { + child.Enabled = enabled; + } + } + + private void UpdateDeltaFromTrigger() + { + this.MakeChildrenEnabled(this.pnlTriggerInputScale, this.chkDeltaFromInput.Checked); + this.MakeChildrenEnabled(this.pnlDeltaConfig, !this.chkDeltaFromInput.Checked); + } + + private void ChkDeltaFromInput_CheckedChanged(object sender, EventArgs e) + { + OptionsChanged?.Invoke(this, EventArgs.Empty); + this.UpdateDeltaFromTrigger(); + } + + public bool CanMappingSave(AbstractAction action) => true; + + private void CmbGamePad_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void CmbPressState_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void CmbGamePadIndex_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudExactDeltaX_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudExactDeltaY_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudTriggerInputScaleX_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudTriggerInputScaleY_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudResetAfterMs_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.resx b/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadStickActionControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.Designer.cs b/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.Designer.cs new file mode 100644 index 00000000..7d5007e8 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.Designer.cs @@ -0,0 +1,249 @@ +namespace Key2Joy.Gui.Mapping +{ + partial class GamePadTriggerActionControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.cmbSide = new System.Windows.Forms.ComboBox(); + this.lblInfo = new System.Windows.Forms.Label(); + this.cmbGamePadIndex = new System.Windows.Forms.ComboBox(); + this.lblInfoSide = new System.Windows.Forms.Label(); + this.pnlGamePad = new System.Windows.Forms.Panel(); + this.pnlSide = new System.Windows.Forms.Panel(); + this.pnlDelta = new System.Windows.Forms.Panel(); + this.pnlDeltaConfig = new System.Windows.Forms.Panel(); + this.nudExact = new System.Windows.Forms.NumericUpDown(); + this.lblExact = new System.Windows.Forms.Label(); + this.pnlTriggerInputScale = new System.Windows.Forms.Panel(); + this.nudTriggerInputScale = new System.Windows.Forms.NumericUpDown(); + this.lblTriggerInputScale = new System.Windows.Forms.Label(); + this.chkDeltaFromInput = new System.Windows.Forms.CheckBox(); + this.pnlGamePad.SuspendLayout(); + this.pnlSide.SuspendLayout(); + this.pnlDelta.SuspendLayout(); + this.pnlDeltaConfig.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudExact)).BeginInit(); + this.pnlTriggerInputScale.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudTriggerInputScale)).BeginInit(); + this.SuspendLayout(); + // + // cmbSide + // + this.cmbSide.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbSide.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbSide.FormattingEnabled = true; + this.cmbSide.Location = new System.Drawing.Point(44, 5); + this.cmbSide.Name = "cmbSide"; + this.cmbSide.Size = new System.Drawing.Size(368, 21); + this.cmbSide.TabIndex = 9; + this.cmbSide.SelectedIndexChanged += new System.EventHandler(this.CmbGamePad_SelectedIndexChanged); + // + // lblInfo + // + this.lblInfo.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfo.Location = new System.Drawing.Point(0, 0); + this.lblInfo.Name = "lblInfo"; + this.lblInfo.Size = new System.Drawing.Size(65, 24); + this.lblInfo.TabIndex = 10; + this.lblInfo.Text = "GamePad #"; + this.lblInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // cmbGamePadIndex + // + this.cmbGamePadIndex.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbGamePadIndex.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbGamePadIndex.FormattingEnabled = true; + this.cmbGamePadIndex.Location = new System.Drawing.Point(65, 0); + this.cmbGamePadIndex.Name = "cmbGamePadIndex"; + this.cmbGamePadIndex.Size = new System.Drawing.Size(347, 21); + this.cmbGamePadIndex.TabIndex = 13; + this.cmbGamePadIndex.SelectedIndexChanged += new System.EventHandler(this.CmbGamePadIndex_SelectedIndexChanged); + // + // lblInfoSide + // + this.lblInfoSide.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoSide.Location = new System.Drawing.Point(0, 5); + this.lblInfoSide.Name = "lblInfoSide"; + this.lblInfoSide.Size = new System.Drawing.Size(44, 17); + this.lblInfoSide.TabIndex = 14; + this.lblInfoSide.Text = "Trigger:"; + this.lblInfoSide.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlGamePad + // + this.pnlGamePad.Controls.Add(this.cmbGamePadIndex); + this.pnlGamePad.Controls.Add(this.lblInfo); + this.pnlGamePad.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlGamePad.Location = new System.Drawing.Point(5, 5); + this.pnlGamePad.Name = "pnlGamePad"; + this.pnlGamePad.Size = new System.Drawing.Size(412, 24); + this.pnlGamePad.TabIndex = 15; + // + // pnlSide + // + this.pnlSide.Controls.Add(this.cmbSide); + this.pnlSide.Controls.Add(this.lblInfoSide); + this.pnlSide.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlSide.Location = new System.Drawing.Point(5, 29); + this.pnlSide.Name = "pnlSide"; + this.pnlSide.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.pnlSide.Size = new System.Drawing.Size(412, 27); + this.pnlSide.TabIndex = 16; + // + // pnlDelta + // + this.pnlDelta.Controls.Add(this.pnlDeltaConfig); + this.pnlDelta.Controls.Add(this.pnlTriggerInputScale); + this.pnlDelta.Controls.Add(this.chkDeltaFromInput); + this.pnlDelta.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDelta.Location = new System.Drawing.Point(5, 56); + this.pnlDelta.Name = "pnlDelta"; + this.pnlDelta.Size = new System.Drawing.Size(412, 74); + this.pnlDelta.TabIndex = 17; + // + // pnlDeltaConfig + // + this.pnlDeltaConfig.Controls.Add(this.nudExact); + this.pnlDeltaConfig.Controls.Add(this.lblExact); + this.pnlDeltaConfig.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlDeltaConfig.Location = new System.Drawing.Point(0, 47); + this.pnlDeltaConfig.Name = "pnlDeltaConfig"; + this.pnlDeltaConfig.Padding = new System.Windows.Forms.Padding(0, 5, 0, 0); + this.pnlDeltaConfig.Size = new System.Drawing.Size(412, 27); + this.pnlDeltaConfig.TabIndex = 18; + // + // nudExact + // + this.nudExact.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudExact.Location = new System.Drawing.Point(44, 5); + this.nudExact.Name = "nudExact"; + this.nudExact.Size = new System.Drawing.Size(368, 20); + this.nudExact.TabIndex = 18; + this.nudExact.ValueChanged += new System.EventHandler(this.NudExactDelta_ValueChanged); + // + // lblExact + // + this.lblExact.Dock = System.Windows.Forms.DockStyle.Left; + this.lblExact.Location = new System.Drawing.Point(0, 5); + this.lblExact.Name = "lblExact"; + this.lblExact.Size = new System.Drawing.Size(44, 22); + this.lblExact.TabIndex = 15; + this.lblExact.Text = "Exact:"; + this.lblExact.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlTriggerInputScale + // + this.pnlTriggerInputScale.Controls.Add(this.nudTriggerInputScale); + this.pnlTriggerInputScale.Controls.Add(this.lblTriggerInputScale); + this.pnlTriggerInputScale.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlTriggerInputScale.Location = new System.Drawing.Point(0, 27); + this.pnlTriggerInputScale.Name = "pnlTriggerInputScale"; + this.pnlTriggerInputScale.Size = new System.Drawing.Size(412, 20); + this.pnlTriggerInputScale.TabIndex = 20; + // + // nudTriggerInputScale + // + this.nudTriggerInputScale.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudTriggerInputScale.Location = new System.Drawing.Point(136, 0); + this.nudTriggerInputScale.Minimum = new decimal(new int[] { + 100, + 0, + 0, + -2147483648}); + this.nudTriggerInputScale.Name = "nudTriggerInputScale"; + this.nudTriggerInputScale.Size = new System.Drawing.Size(276, 20); + this.nudTriggerInputScale.TabIndex = 19; + this.nudTriggerInputScale.Value = new decimal(new int[] { + 1, + 0, + 0, + 65536}); + this.nudTriggerInputScale.ValueChanged += new System.EventHandler(this.NudTriggerInputScale_ValueChanged); + // + // lblTriggerInputScale + // + this.lblTriggerInputScale.Dock = System.Windows.Forms.DockStyle.Left; + this.lblTriggerInputScale.Location = new System.Drawing.Point(0, 0); + this.lblTriggerInputScale.Name = "lblTriggerInputScale"; + this.lblTriggerInputScale.Size = new System.Drawing.Size(136, 20); + this.lblTriggerInputScale.TabIndex = 16; + this.lblTriggerInputScale.Text = "Trigger Input Delta Scale:"; + this.lblTriggerInputScale.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // chkDeltaFromInput + // + this.chkDeltaFromInput.AutoSize = true; + this.chkDeltaFromInput.Dock = System.Windows.Forms.DockStyle.Top; + this.chkDeltaFromInput.Location = new System.Drawing.Point(0, 0); + this.chkDeltaFromInput.Name = "chkDeltaFromInput"; + this.chkDeltaFromInput.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.chkDeltaFromInput.Size = new System.Drawing.Size(412, 27); + this.chkDeltaFromInput.TabIndex = 19; + this.chkDeltaFromInput.Text = "Use delta from trigger input (e.g: amount of pixels the cursor moved)"; + this.chkDeltaFromInput.UseVisualStyleBackColor = true; + this.chkDeltaFromInput.CheckedChanged += new System.EventHandler(this.ChkDeltaFromInput_CheckedChanged); + // + // GamePadTriggerActionControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.pnlDelta); + this.Controls.Add(this.pnlSide); + this.Controls.Add(this.pnlGamePad); + this.Name = "GamePadTriggerActionControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(422, 132); + this.pnlGamePad.ResumeLayout(false); + this.pnlSide.ResumeLayout(false); + this.pnlDelta.ResumeLayout(false); + this.pnlDelta.PerformLayout(); + this.pnlDeltaConfig.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudExact)).EndInit(); + this.pnlTriggerInputScale.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudTriggerInputScale)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.ComboBox cmbSide; + private System.Windows.Forms.Label lblInfo; + private System.Windows.Forms.ComboBox cmbGamePadIndex; + private System.Windows.Forms.Label lblInfoSide; + private System.Windows.Forms.Panel pnlGamePad; + private System.Windows.Forms.Panel pnlSide; + private System.Windows.Forms.Panel pnlDelta; + private System.Windows.Forms.Panel pnlDeltaConfig; + private System.Windows.Forms.CheckBox chkDeltaFromInput; + private System.Windows.Forms.Label lblExact; + private System.Windows.Forms.NumericUpDown nudExact; + private System.Windows.Forms.Panel pnlTriggerInputScale; + private System.Windows.Forms.Label lblTriggerInputScale; + private System.Windows.Forms.NumericUpDown nudTriggerInputScale; + } +} diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.cs new file mode 100644 index 00000000..343e9bc1 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.Mapping.Actions.Input; +using Key2Joy.Mapping.Triggers.GamePad; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(GamePadTriggerAction), + ImageResourceName = "joystick" +)] +public partial class GamePadTriggerActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public GamePadTriggerActionControl() + { + this.InitializeComponent(); + + var gamePadService = ServiceLocator.Current.GetInstance(); + var allGamePads = gamePadService.GetAllGamePads(false); + var allGamePadIndices = allGamePads.Select(gp => gp.Index).ToArray(); + + this.cmbSide.DataSource = Enum.GetValues(typeof(GamePadSide)); + + this.nudTriggerInputScale.Minimum = short.MinValue; + this.nudTriggerInputScale.Maximum = short.MaxValue; + this.nudTriggerInputScale.DecimalPlaces = 4; + + this.nudExact.DecimalPlaces = 2; + this.nudExact.Minimum = short.MinValue; + this.nudExact.Maximum = short.MaxValue; + + this.cmbGamePadIndex.DataSource = allGamePadIndices; + this.cmbGamePadIndex.SelectedIndex = 0; + + this.chkDeltaFromInput.Checked = true; + } + + public void Select(AbstractAction action) + { + var thisAction = (GamePadTriggerAction)action; + + this.chkDeltaFromInput.Checked = thisAction.Delta == null; + this.nudTriggerInputScale.Value = (decimal)thisAction.InputScale; + this.cmbSide.SelectedItem = thisAction.Side; + this.nudExact.Value = (decimal)(thisAction.Delta ?? 0); + this.cmbGamePadIndex.SelectedItem = thisAction.GamePadIndex; + } + + public void Setup(AbstractAction action) + { + var thisAction = (GamePadTriggerAction)action; + + var deltaFromInput = this.chkDeltaFromInput.Checked; + + thisAction.InputScale = (float)this.nudTriggerInputScale.Value; + thisAction.Side = (GamePadSide)this.cmbSide.SelectedItem; + thisAction.Delta = deltaFromInput ? null : (float)this.nudExact.Value; + thisAction.GamePadIndex = (int)this.cmbGamePadIndex.SelectedItem; + } + + private void MakeChildrenEnabled(Control parent, bool enabled) + { + foreach (Control child in parent.Controls) + { + child.Enabled = enabled; + } + } + + private void UpdateDeltaFromTrigger() + { + this.MakeChildrenEnabled(this.pnlTriggerInputScale, this.chkDeltaFromInput.Checked); + this.MakeChildrenEnabled(this.pnlDeltaConfig, !this.chkDeltaFromInput.Checked); + } + + private void ChkDeltaFromInput_CheckedChanged(object sender, EventArgs e) + { + OptionsChanged?.Invoke(this, EventArgs.Empty); + this.UpdateDeltaFromTrigger(); + } + + public bool CanMappingSave(AbstractAction action) => true; + + private void CmbGamePad_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void CmbGamePadIndex_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudExactDelta_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudTriggerInputScale_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.resx b/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Actions/Input/GamePadTriggerActionControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Mapping/Actions/Input/KeyboardActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Input/KeyboardActionControl.cs index 5ffc8e91..e81c2f56 100644 --- a/Key2Joy.Gui/Mapping/Actions/Input/KeyboardActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/Input/KeyboardActionControl.cs @@ -1,48 +1,48 @@ -using System; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.LowLevelInput; -using Key2Joy.Mapping.Actions.Input; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForType = typeof(KeyboardAction), - ImageResourceName = "keyboard" -)] -public partial class KeyboardActionControl : UserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - public KeyboardActionControl() - { - this.InitializeComponent(); - - this.cmbKeyboard.DataSource = KeyboardAction.GetAllKeys(); - this.cmbPressState.DataSource = PressStates.ALL; - this.cmbPressState.SelectedIndex = 0; - } - - public void Select(object action) - { - var thisAction = (KeyboardAction)action; - - this.cmbKeyboard.SelectedItem = thisAction.Key; - this.cmbPressState.SelectedItem = thisAction.PressState; - } - - public void Setup(object action) - { - var thisAction = (KeyboardAction)action; - - thisAction.Key = (KeyboardKey)this.cmbKeyboard.SelectedItem; - thisAction.PressState = (PressState)this.cmbPressState.SelectedItem; - } - - public bool CanMappingSave(object action) => true; - - private void CmbKeyboard_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); - - private void CmbPressState_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); -} +using System; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.LowLevelInput; +using Key2Joy.Mapping.Actions.Input; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(KeyboardAction), + ImageResourceName = "keyboard" +)] +public partial class KeyboardActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public KeyboardActionControl() + { + this.InitializeComponent(); + + this.cmbKeyboard.DataSource = KeyboardAction.GetAllKeys(); + this.cmbPressState.DataSource = PressStates.ALL; + this.cmbPressState.SelectedIndex = 0; + } + + public void Select(AbstractAction action) + { + var thisAction = (KeyboardAction)action; + + this.cmbKeyboard.SelectedItem = thisAction.Key; + this.cmbPressState.SelectedItem = thisAction.PressState; + } + + public void Setup(AbstractAction action) + { + var thisAction = (KeyboardAction)action; + + thisAction.Key = (KeyboardKey)this.cmbKeyboard.SelectedItem; + thisAction.PressState = (PressState)this.cmbPressState.SelectedItem; + } + + public bool CanMappingSave(AbstractAction action) => true; + + private void CmbKeyboard_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void CmbPressState_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Actions/Logic/AppCommandActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Logic/AppCommandActionControl.cs index a3fcc0e3..e54fd4cf 100644 --- a/Key2Joy.Gui/Mapping/Actions/Logic/AppCommandActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/Logic/AppCommandActionControl.cs @@ -1,48 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Mapping.Actions.Logic; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForType = typeof(AppCommandAction), - ImageResourceName = "application_xp_terminal" -)] -public partial class AppCommandActionControl : UserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - public AppCommandActionControl() - { - this.InitializeComponent(); - - List appCommands = new(); - - foreach (AppCommand command in Enum.GetValues(typeof(AppCommand))) - { - appCommands.Add(command); - } - - this.cmbAppCommand.DataSource = appCommands; - } - - public void Select(object action) - { - var thisAction = (AppCommandAction)action; - - this.cmbAppCommand.SelectedItem = thisAction.Command; - } - - public void Setup(object action) - { - var thisAction = (AppCommandAction)action; - - thisAction.Command = (AppCommand)this.cmbAppCommand.SelectedItem; - } - public bool CanMappingSave(object action) => true; - - private void CmbAppCommand_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); -} +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Mapping.Actions.Logic; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(AppCommandAction), + ImageResourceName = "application_xp_terminal" +)] +public partial class AppCommandActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public AppCommandActionControl() + { + this.InitializeComponent(); + + List appCommands = new(); + + foreach (AppCommand command in Enum.GetValues(typeof(AppCommand))) + { + appCommands.Add(command); + } + + this.cmbAppCommand.DataSource = appCommands; + } + + public void Select(AbstractAction action) + { + var thisAction = (AppCommandAction)action; + + this.cmbAppCommand.SelectedItem = thisAction.Command; + } + + public void Setup(AbstractAction action) + { + var thisAction = (AppCommandAction)action; + + thisAction.Command = (AppCommand)this.cmbAppCommand.SelectedItem; + } + public bool CanMappingSave(AbstractAction action) => true; + + private void CmbAppCommand_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.Designer.cs b/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.Designer.cs index 6b248f28..f13c9ac1 100644 --- a/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.Designer.cs +++ b/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.Designer.cs @@ -1,198 +1,198 @@ -using Key2Joy.Contracts.Mapping.Actions; - -namespace Key2Joy.Gui.Mapping -{ - partial class SequenceActionControl - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.lblInfo = new System.Windows.Forms.Label(); - this.lstActions = new System.Windows.Forms.ListBox(); - this.pnlActions = new System.Windows.Forms.Panel(); - this.btnRemove = new System.Windows.Forms.Button(); - this.pnlPadding = new System.Windows.Forms.Panel(); - this.grpSequenceActionOptions = new System.Windows.Forms.GroupBox(); - this.actionControl = new Key2Joy.Gui.Mapping.ActionControl(); - this.pnlActionOptions = new System.Windows.Forms.Panel(); - this.pnlPadding2 = new System.Windows.Forms.Panel(); - this.btnAdd = new System.Windows.Forms.Button(); - this.pnlActions.SuspendLayout(); - this.pnlPadding.SuspendLayout(); - this.grpSequenceActionOptions.SuspendLayout(); - this.pnlPadding2.SuspendLayout(); - this.SuspendLayout(); - // - // lblInfo - // - this.lblInfo.Dock = System.Windows.Forms.DockStyle.Top; - this.lblInfo.Location = new System.Drawing.Point(5, 5); - this.lblInfo.Name = "lblInfo"; - this.lblInfo.Size = new System.Drawing.Size(339, 22); - this.lblInfo.TabIndex = 13; - this.lblInfo.Text = "Add a sequence of actions to perform:"; - this.lblInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - // - // lstActions - // - this.lstActions.Dock = System.Windows.Forms.DockStyle.Top; - this.lstActions.FormattingEnabled = true; - this.lstActions.Location = new System.Drawing.Point(5, 27); - this.lstActions.Name = "lstActions"; - this.lstActions.Size = new System.Drawing.Size(339, 82); - this.lstActions.TabIndex = 14; - this.lstActions.SelectedIndexChanged += new System.EventHandler(this.LstActions_SelectedIndexChanged); - // - // pnlActions - // - this.pnlActions.Controls.Add(this.btnRemove); - this.pnlActions.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlActions.Location = new System.Drawing.Point(5, 109); - this.pnlActions.Name = "pnlActions"; - this.pnlActions.Size = new System.Drawing.Size(339, 29); - this.pnlActions.TabIndex = 17; - // - // btnRemove - // - this.btnRemove.Dock = System.Windows.Forms.DockStyle.Fill; - this.btnRemove.Enabled = false; - this.btnRemove.Location = new System.Drawing.Point(0, 0); - this.btnRemove.Name = "btnRemove"; - this.btnRemove.Size = new System.Drawing.Size(339, 29); - this.btnRemove.TabIndex = 18; - this.btnRemove.Text = "Remove"; - this.btnRemove.UseVisualStyleBackColor = true; - this.btnRemove.Click += new System.EventHandler(this.BtnRemove_Click); - // - // pnlPadding - // - this.pnlPadding.AutoSize = true; - this.pnlPadding.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.pnlPadding.Controls.Add(this.grpSequenceActionOptions); - this.pnlPadding.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlPadding.Location = new System.Drawing.Point(5, 138); - this.pnlPadding.Name = "pnlPadding"; - this.pnlPadding.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); - this.pnlPadding.Size = new System.Drawing.Size(339, 61); - this.pnlPadding.TabIndex = 18; - // - // grpSequenceActionOptions - // - this.grpSequenceActionOptions.AutoSize = true; - this.grpSequenceActionOptions.Controls.Add(this.actionControl); - this.grpSequenceActionOptions.Controls.Add(this.pnlActionOptions); - this.grpSequenceActionOptions.Dock = System.Windows.Forms.DockStyle.Top; - this.grpSequenceActionOptions.Location = new System.Drawing.Point(0, 5); - this.grpSequenceActionOptions.Name = "grpSequenceActionOptions"; - this.grpSequenceActionOptions.Size = new System.Drawing.Size(339, 51); - this.grpSequenceActionOptions.TabIndex = 16; - this.grpSequenceActionOptions.TabStop = false; - this.grpSequenceActionOptions.Text = "Action Options"; - // - // actionControl - // - this.actionControl.AutoSize = true; - this.actionControl.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.actionControl.Dock = System.Windows.Forms.DockStyle.Top; - this.actionControl.IsTopLevel = false; - this.actionControl.Location = new System.Drawing.Point(3, 16); - this.actionControl.MinimumSize = new System.Drawing.Size(300, 32); - this.actionControl.Name = "actionControl"; - this.actionControl.Padding = new System.Windows.Forms.Padding(5); - this.actionControl.Size = new System.Drawing.Size(333, 32); - this.actionControl.TabIndex = 1; - this.actionControl.ActionChanged += new System.Action(this.ActionControl_ActionChanged); - // - // pnlActionOptions - // - this.pnlActionOptions.AutoSize = true; - this.pnlActionOptions.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.pnlActionOptions.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlActionOptions.Location = new System.Drawing.Point(3, 16); - this.pnlActionOptions.Name = "pnlActionOptions"; - this.pnlActionOptions.Size = new System.Drawing.Size(333, 0); - this.pnlActionOptions.TabIndex = 0; - // - // pnlPadding2 - // - this.pnlPadding2.Controls.Add(this.btnAdd); - this.pnlPadding2.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlPadding2.Location = new System.Drawing.Point(5, 199); - this.pnlPadding2.Name = "pnlPadding2"; - this.pnlPadding2.Size = new System.Drawing.Size(339, 29); - this.pnlPadding2.TabIndex = 19; - // - // btnAdd - // - this.btnAdd.Dock = System.Windows.Forms.DockStyle.Fill; - this.btnAdd.Enabled = false; - this.btnAdd.Location = new System.Drawing.Point(0, 0); - this.btnAdd.Name = "btnAdd"; - this.btnAdd.Size = new System.Drawing.Size(339, 29); - this.btnAdd.TabIndex = 17; - this.btnAdd.Text = "Add"; - this.btnAdd.UseVisualStyleBackColor = true; - this.btnAdd.Click += new System.EventHandler(this.BtnAdd_Click); - // - // SequenceActionControl - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.AutoSize = true; - this.Controls.Add(this.pnlPadding2); - this.Controls.Add(this.pnlPadding); - this.Controls.Add(this.pnlActions); - this.Controls.Add(this.lstActions); - this.Controls.Add(this.lblInfo); - this.MinimumSize = new System.Drawing.Size(256, 64); - this.Name = "SequenceActionControl"; - this.Padding = new System.Windows.Forms.Padding(5); - this.Size = new System.Drawing.Size(349, 233); - this.pnlActions.ResumeLayout(false); - this.pnlPadding.ResumeLayout(false); - this.pnlPadding.PerformLayout(); - this.grpSequenceActionOptions.ResumeLayout(false); - this.grpSequenceActionOptions.PerformLayout(); - this.pnlPadding2.ResumeLayout(false); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - - private System.Windows.Forms.Label lblInfo; - private System.Windows.Forms.ListBox lstActions; - private System.Windows.Forms.Panel pnlActions; - private System.Windows.Forms.Button btnRemove; - private System.Windows.Forms.Panel pnlPadding; - private System.Windows.Forms.GroupBox grpSequenceActionOptions; - private System.Windows.Forms.Panel pnlActionOptions; - private ActionControl actionControl; - private System.Windows.Forms.Panel pnlPadding2; - private System.Windows.Forms.Button btnAdd; - } -} +using Key2Joy.Contracts.Mapping.Actions; + +namespace Key2Joy.Gui.Mapping +{ + partial class SequenceActionControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lblInfo = new System.Windows.Forms.Label(); + this.lstActions = new System.Windows.Forms.ListBox(); + this.pnlActions = new System.Windows.Forms.Panel(); + this.btnRemove = new System.Windows.Forms.Button(); + this.pnlPadding = new System.Windows.Forms.Panel(); + this.grpSequenceActionOptions = new System.Windows.Forms.GroupBox(); + this.actionControl = new Key2Joy.Gui.Mapping.ActionControl(); + this.pnlActionOptions = new System.Windows.Forms.Panel(); + this.pnlPadding2 = new System.Windows.Forms.Panel(); + this.btnAdd = new System.Windows.Forms.Button(); + this.pnlActions.SuspendLayout(); + this.pnlPadding.SuspendLayout(); + this.grpSequenceActionOptions.SuspendLayout(); + this.pnlPadding2.SuspendLayout(); + this.SuspendLayout(); + // + // lblInfo + // + this.lblInfo.Dock = System.Windows.Forms.DockStyle.Top; + this.lblInfo.Location = new System.Drawing.Point(5, 5); + this.lblInfo.Name = "lblInfo"; + this.lblInfo.Size = new System.Drawing.Size(339, 22); + this.lblInfo.TabIndex = 13; + this.lblInfo.Text = "Add a sequence of actions to perform:"; + this.lblInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // lstActions + // + this.lstActions.Dock = System.Windows.Forms.DockStyle.Top; + this.lstActions.FormattingEnabled = true; + this.lstActions.Location = new System.Drawing.Point(5, 27); + this.lstActions.Name = "lstActions"; + this.lstActions.Size = new System.Drawing.Size(339, 82); + this.lstActions.TabIndex = 14; + this.lstActions.SelectedIndexChanged += new System.EventHandler(this.LstActions_SelectedIndexChanged); + // + // pnlActions + // + this.pnlActions.Controls.Add(this.btnRemove); + this.pnlActions.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlActions.Location = new System.Drawing.Point(5, 109); + this.pnlActions.Name = "pnlActions"; + this.pnlActions.Size = new System.Drawing.Size(339, 29); + this.pnlActions.TabIndex = 17; + // + // btnRemove + // + this.btnRemove.Dock = System.Windows.Forms.DockStyle.Fill; + this.btnRemove.Enabled = false; + this.btnRemove.Location = new System.Drawing.Point(0, 0); + this.btnRemove.Name = "btnRemove"; + this.btnRemove.Size = new System.Drawing.Size(339, 29); + this.btnRemove.TabIndex = 18; + this.btnRemove.Text = "Remove"; + this.btnRemove.UseVisualStyleBackColor = true; + this.btnRemove.Click += new System.EventHandler(this.BtnRemove_Click); + // + // pnlPadding + // + this.pnlPadding.AutoSize = true; + this.pnlPadding.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.pnlPadding.Controls.Add(this.grpSequenceActionOptions); + this.pnlPadding.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlPadding.Location = new System.Drawing.Point(5, 138); + this.pnlPadding.Name = "pnlPadding"; + this.pnlPadding.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.pnlPadding.Size = new System.Drawing.Size(339, 61); + this.pnlPadding.TabIndex = 18; + // + // grpSequenceActionOptions + // + this.grpSequenceActionOptions.AutoSize = true; + this.grpSequenceActionOptions.Controls.Add(this.actionControl); + this.grpSequenceActionOptions.Controls.Add(this.pnlActionOptions); + this.grpSequenceActionOptions.Dock = System.Windows.Forms.DockStyle.Top; + this.grpSequenceActionOptions.Location = new System.Drawing.Point(0, 5); + this.grpSequenceActionOptions.Name = "grpSequenceActionOptions"; + this.grpSequenceActionOptions.Size = new System.Drawing.Size(339, 51); + this.grpSequenceActionOptions.TabIndex = 16; + this.grpSequenceActionOptions.TabStop = false; + this.grpSequenceActionOptions.Text = "Action Options"; + // + // actionControl + // + this.actionControl.AutoSize = true; + this.actionControl.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.actionControl.Dock = System.Windows.Forms.DockStyle.Top; + this.actionControl.IsTopLevel = false; + this.actionControl.Location = new System.Drawing.Point(3, 16); + this.actionControl.MinimumSize = new System.Drawing.Size(300, 32); + this.actionControl.Name = "actionControl"; + this.actionControl.Padding = new System.Windows.Forms.Padding(5); + this.actionControl.Size = new System.Drawing.Size(333, 32); + this.actionControl.TabIndex = 1; + this.actionControl.ActionChanged += new System.EventHandler(this.ActionControl_ActionChanged); + // + // pnlActionOptions + // + this.pnlActionOptions.AutoSize = true; + this.pnlActionOptions.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.pnlActionOptions.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlActionOptions.Location = new System.Drawing.Point(3, 16); + this.pnlActionOptions.Name = "pnlActionOptions"; + this.pnlActionOptions.Size = new System.Drawing.Size(333, 0); + this.pnlActionOptions.TabIndex = 0; + // + // pnlPadding2 + // + this.pnlPadding2.Controls.Add(this.btnAdd); + this.pnlPadding2.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlPadding2.Location = new System.Drawing.Point(5, 199); + this.pnlPadding2.Name = "pnlPadding2"; + this.pnlPadding2.Size = new System.Drawing.Size(339, 29); + this.pnlPadding2.TabIndex = 19; + // + // btnAdd + // + this.btnAdd.Dock = System.Windows.Forms.DockStyle.Fill; + this.btnAdd.Enabled = false; + this.btnAdd.Location = new System.Drawing.Point(0, 0); + this.btnAdd.Name = "btnAdd"; + this.btnAdd.Size = new System.Drawing.Size(339, 29); + this.btnAdd.TabIndex = 17; + this.btnAdd.Text = "Add"; + this.btnAdd.UseVisualStyleBackColor = true; + this.btnAdd.Click += new System.EventHandler(this.BtnAdd_Click); + // + // SequenceActionControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSize = true; + this.Controls.Add(this.pnlPadding2); + this.Controls.Add(this.pnlPadding); + this.Controls.Add(this.pnlActions); + this.Controls.Add(this.lstActions); + this.Controls.Add(this.lblInfo); + this.MinimumSize = new System.Drawing.Size(256, 64); + this.Name = "SequenceActionControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(349, 233); + this.pnlActions.ResumeLayout(false); + this.pnlPadding.ResumeLayout(false); + this.pnlPadding.PerformLayout(); + this.grpSequenceActionOptions.ResumeLayout(false); + this.grpSequenceActionOptions.PerformLayout(); + this.pnlPadding2.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label lblInfo; + private System.Windows.Forms.ListBox lstActions; + private System.Windows.Forms.Panel pnlActions; + private System.Windows.Forms.Button btnRemove; + private System.Windows.Forms.Panel pnlPadding; + private System.Windows.Forms.GroupBox grpSequenceActionOptions; + private System.Windows.Forms.Panel pnlActionOptions; + private ActionControl actionControl; + private System.Windows.Forms.Panel pnlPadding2; + private System.Windows.Forms.Button btnAdd; + } +} diff --git a/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.cs index b76010ad..bb1d9c1e 100644 --- a/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/Logic/SequenceActionControl.cs @@ -1,89 +1,89 @@ -using System; -using System.Collections.Generic; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Mapping.Actions.Logic; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForType = typeof(SequenceAction), - ImageResourceName = "text_list_numbers" -)] -public partial class SequenceActionControl : UserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - private readonly List childActions; - - public SequenceActionControl() - { - this.InitializeComponent(); - - this.childActions = new List(); - } - - public void Select(object action) - { - var thisAction = (SequenceAction)action; - - foreach (var childAction in thisAction.ChildActions) - { - // Clone so we don't modify the action in a profile - this.AddChildAction((AbstractAction)childAction.Clone()); - } - } - - public void Setup(object action) - { - var thisAction = (SequenceAction)action; - thisAction.ChildActions.Clear(); - - foreach (var childAction in this.childActions) - { - thisAction.ChildActions.Add(childAction); - } - } - - public bool CanMappingSave(object action) => true; - - private void AddChildAction(AbstractAction action) - { - this.childActions.Add(action); - this.lstActions.Items.Add(action); - } - - private void RemoveChildAction(AbstractAction action) - { - var index = this.lstActions.Items.IndexOf(action); - - this.childActions.Remove(action); - this.lstActions.Items.Remove(action); - - if (this.lstActions.Items.Count > 0) - { - if (index >= this.lstActions.Items.Count) - { - index = this.lstActions.Items.Count - 1; - } - this.lstActions.SelectedIndex = index; - } - } - - private void BtnAdd_Click(object sender, EventArgs e) - { - this.AddChildAction((AbstractAction)this.actionControl.Action.Clone()); - OptionsChanged?.Invoke(this, EventArgs.Empty); - } - - private void BtnRemove_Click(object sender, EventArgs e) - { - this.RemoveChildAction((AbstractAction)this.lstActions.SelectedItem); - OptionsChanged?.Invoke(this, EventArgs.Empty); - } - - private void LstActions_SelectedIndexChanged(object sender, EventArgs e) => this.btnRemove.Enabled = this.lstActions.SelectedIndex > -1; - - private void ActionControl_ActionChanged(AbstractAction action) => this.btnAdd.Enabled = action != null; -} +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Mapping.Actions.Logic; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(SequenceAction), + ImageResourceName = "text_list_numbers" +)] +public partial class SequenceActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + private readonly List childActions; + + public SequenceActionControl() + { + this.InitializeComponent(); + + this.childActions = new List(); + } + + public void Select(AbstractAction action) + { + var thisAction = (SequenceAction)action; + + foreach (var childAction in thisAction.ChildActions) + { + // Clone so we don't modify the action in a profile + this.AddChildAction((AbstractAction)childAction.Clone()); + } + } + + public void Setup(AbstractAction action) + { + var thisAction = (SequenceAction)action; + thisAction.ChildActions.Clear(); + + foreach (var childAction in this.childActions) + { + thisAction.ChildActions.Add(childAction); + } + } + + public bool CanMappingSave(AbstractAction action) => true; + + private void AddChildAction(AbstractAction action) + { + this.childActions.Add(action); + this.lstActions.Items.Add(action); + } + + private void RemoveChildAction(AbstractAction action) + { + var index = this.lstActions.Items.IndexOf(action); + + this.childActions.Remove(action); + this.lstActions.Items.Remove(action); + + if (this.lstActions.Items.Count > 0) + { + if (index >= this.lstActions.Items.Count) + { + index = this.lstActions.Items.Count - 1; + } + this.lstActions.SelectedIndex = index; + } + } + + private void BtnAdd_Click(object sender, EventArgs e) + { + this.AddChildAction((AbstractAction)this.actionControl.Action.Clone()); + OptionsChanged?.Invoke(this, EventArgs.Empty); + } + + private void BtnRemove_Click(object sender, EventArgs e) + { + this.RemoveChildAction((AbstractAction)this.lstActions.SelectedItem); + OptionsChanged?.Invoke(this, EventArgs.Empty); + } + + private void LstActions_SelectedIndexChanged(object sender, EventArgs e) => this.btnRemove.Enabled = this.lstActions.SelectedIndex > -1; + + private void ActionControl_ActionChanged(object sender, ActionChangedEventArgs e) => this.btnAdd.Enabled = e.Action != null; +} diff --git a/Key2Joy.Gui/Mapping/Actions/Logic/WaitActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Logic/WaitActionControl.cs index 9ad9b6bb..3790dbfe 100644 --- a/Key2Joy.Gui/Mapping/Actions/Logic/WaitActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/Logic/WaitActionControl.cs @@ -1,41 +1,41 @@ -using System; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Mapping.Actions.Logic; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForType = typeof(WaitAction), - ImageResourceName = "clock" -)] -public partial class WaitActionControl : UserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - public WaitActionControl() - { - this.InitializeComponent(); - - this.nudWaitTimeInMs.Maximum = decimal.MaxValue; - } - - public void Select(object action) - { - var thisAction = (WaitAction)action; - - this.nudWaitTimeInMs.Value = (decimal)thisAction.WaitTime.TotalMilliseconds; - } - - public void Setup(object action) - { - var thisAction = (WaitAction)action; - - thisAction.WaitTime = TimeSpan.FromMilliseconds((double)this.nudWaitTimeInMs.Value); - } - - public bool CanMappingSave(object action) => true; - - private void NudWaitTimeInMs_ValueChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); -} +using System; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Mapping.Actions.Logic; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(WaitAction), + ImageResourceName = "clock" +)] +public partial class WaitActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public WaitActionControl() + { + this.InitializeComponent(); + + this.nudWaitTimeInMs.Maximum = decimal.MaxValue; + } + + public void Select(AbstractAction action) + { + var thisAction = (WaitAction)action; + + this.nudWaitTimeInMs.Value = (decimal)thisAction.WaitTime.TotalMilliseconds; + } + + public void Setup(AbstractAction action) + { + var thisAction = (WaitAction)action; + + thisAction.WaitTime = TimeSpan.FromMilliseconds((double)this.nudWaitTimeInMs.Value); + } + + public bool CanMappingSave(AbstractAction action) => true; + + private void NudWaitTimeInMs_ValueChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Actions/Scripting/ScriptActionControl.cs b/Key2Joy.Gui/Mapping/Actions/Scripting/ScriptActionControl.cs index 9c82427d..2384544e 100644 --- a/Key2Joy.Gui/Mapping/Actions/Scripting/ScriptActionControl.cs +++ b/Key2Joy.Gui/Mapping/Actions/Scripting/ScriptActionControl.cs @@ -1,141 +1,141 @@ -using System; -using System.IO; -using System.Windows.Forms; -using Esprima; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Mapping.Actions.Scripting; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForTypes = new[] - { - typeof(JavascriptAction), - typeof(LuaScriptAction), - }, - ImageResourceName = "script_code" -)] -public partial class ScriptActionControl : UserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - public ScriptActionControl() - { - this.InitializeComponent(); - - this.pnlFileInput.Visible = !this.chkDirectInput.Checked; - } - - public void Select(object action) - { - var thisAction = (BaseScriptAction)action; - - var mappingType = action.GetType(); - string languageName; - - // If it's LuaScriptAction or JavascriptAction change the typename to ScriptAction - if (mappingType == typeof(LuaScriptAction)) - { - languageName = "Lua"; - } - else if (mappingType == typeof(JavascriptAction)) - { - languageName = "Javascript"; - } - else - { - languageName = "Unknown Language"; - } - - this.lblInfo.Text = $"{languageName} Script:"; - - this.txtScript.Text = this.txtFilePath.Text = thisAction.Script; - this.chkDirectInput.Checked = !thisAction.IsScriptPath; - this.pnlFileInput.Visible = !this.chkDirectInput.Checked; - } - - public void Setup(object action) - { - var thisAction = (BaseScriptAction)action; - - thisAction.IsScriptPath = !this.chkDirectInput.Checked; - - if (thisAction.IsScriptPath) - { - thisAction.Script = this.txtFilePath.Text; - return; - } - - thisAction.Script = this.txtScript.Text; - } - - public bool CanMappingSave(object action) - { - var thisAction = (BaseScriptAction)action; - var mappingType = action.GetType(); - - if (mappingType == typeof(LuaScriptAction)) - { - // TODO: Parse Lua and check for errors - } - else if (mappingType == typeof(JavascriptAction)) - { - // Since we only get a ParserError somewhere inside Esprima.dll, we check for errors here - var parser = new JavaScriptParser(); - - try - { - parser.ParseScript(thisAction.Script); - } - catch (ParserException ex) - { - MessageBox.Show(ex.Message, "Script Error", MessageBoxButtons.OK, MessageBoxIcon.Error); - return false; - } - } - - return MessageBox.Show( - "Scripts can click and type like you do and therefor impersonate you. " - + "Scripts could cause harm to your pc, you or else. " - + "For that reason you should only run scripts that you trust!" - + "\n\nDo you trust this script?", - "Warning:! Scripts can be dangerous!", - MessageBoxButtons.YesNoCancel, - MessageBoxIcon.Warning) == DialogResult.Yes; - } - - private void TxtScript_TextChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); - - private void TxtFilePath_TextChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); - - private void ChkDirectInput_CheckedChanged(object sender, EventArgs e) - { - this.txtScript.Visible = this.chkDirectInput.Checked; - this.pnlFileInput.Visible = !this.chkDirectInput.Checked; - OptionsChanged?.Invoke(this, EventArgs.Empty); - this.PerformLayout(); - } - - private void BtnBrowseFile_Click(object sender, EventArgs e) - { - OpenFileDialog filePicker = new(); - - if (filePicker.ShowDialog() != DialogResult.OK) - { - return; - } - - var file = filePicker.FileName; - try - { - File.ReadAllText(file); - this.txtFilePath.Text = file; - OptionsChanged?.Invoke(this, EventArgs.Empty); - } - catch (IOException) - { - MessageBox.Show($"This file could not be loaded as a script.", "Invalid script file!", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } -} +using System; +using System.IO; +using System.Windows.Forms; +using Esprima; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Mapping.Actions.Scripting; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForTypes = new[] + { + typeof(JavascriptAction), + typeof(LuaScriptAction), + }, + ImageResourceName = "script_code" +)] +public partial class ScriptActionControl : UserControl, IActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public ScriptActionControl() + { + this.InitializeComponent(); + + this.pnlFileInput.Visible = !this.chkDirectInput.Checked; + } + + public void Select(AbstractAction action) + { + var thisAction = (BaseScriptAction)action; + + var mappingType = action.GetType(); + string languageName; + + // If it's LuaScriptAction or JavascriptAction change the typename to ScriptAction + if (mappingType == typeof(LuaScriptAction)) + { + languageName = "Lua"; + } + else if (mappingType == typeof(JavascriptAction)) + { + languageName = "Javascript"; + } + else + { + languageName = "Unknown Language"; + } + + this.lblInfo.Text = $"{languageName} Script:"; + + this.txtScript.Text = this.txtFilePath.Text = thisAction.Script; + this.chkDirectInput.Checked = !thisAction.IsScriptPath; + this.pnlFileInput.Visible = !this.chkDirectInput.Checked; + } + + public void Setup(AbstractAction action) + { + var thisAction = (BaseScriptAction)action; + + thisAction.IsScriptPath = !this.chkDirectInput.Checked; + + if (thisAction.IsScriptPath) + { + thisAction.Script = this.txtFilePath.Text; + return; + } + + thisAction.Script = this.txtScript.Text; + } + + public bool CanMappingSave(AbstractAction action) + { + var thisAction = (BaseScriptAction)action; + var mappingType = action.GetType(); + + if (mappingType == typeof(LuaScriptAction)) + { + // TODO: Parse Lua and check for errors + } + else if (mappingType == typeof(JavascriptAction)) + { + // Since we only get a ParserError somewhere inside Esprima.dll, we check for errors here + var parser = new JavaScriptParser(); + + try + { + parser.ParseScript(thisAction.Script); + } + catch (ParserException ex) + { + MessageBox.Show(ex.Message, "Script Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return false; + } + } + + return MessageBox.Show( + "Scripts can click and type like you do and therefor impersonate you. " + + "Scripts could cause harm to your pc, you or else. " + + "For that reason you should only run scripts that you trust!" + + "\n\nDo you trust this script?", + "Warning:! Scripts can be dangerous!", + MessageBoxButtons.YesNoCancel, + MessageBoxIcon.Warning) == DialogResult.Yes; + } + + private void TxtScript_TextChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void TxtFilePath_TextChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void ChkDirectInput_CheckedChanged(object sender, EventArgs e) + { + this.txtScript.Visible = this.chkDirectInput.Checked; + this.pnlFileInput.Visible = !this.chkDirectInput.Checked; + OptionsChanged?.Invoke(this, EventArgs.Empty); + this.PerformLayout(); + } + + private void BtnBrowseFile_Click(object sender, EventArgs e) + { + OpenFileDialog filePicker = new(); + + if (filePicker.ShowDialog() != DialogResult.OK) + { + return; + } + + var file = filePicker.FileName; + try + { + File.ReadAllText(file); + this.txtFilePath.Text = file; + OptionsChanged?.Invoke(this, EventArgs.Empty); + } + catch (IOException) + { + MessageBox.Show($"This file could not be loaded as a script.", "Invalid script file!", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.Designer.cs b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.Designer.cs new file mode 100644 index 00000000..0b48da2f --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.Designer.cs @@ -0,0 +1,161 @@ +namespace Key2Joy.Gui.Mapping +{ + partial class GamePadButtonTriggerControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.txtButtonBind = new System.Windows.Forms.TextBox(); + this.lblInfo = new System.Windows.Forms.Label(); + this.cmbPressState = new System.Windows.Forms.ComboBox(); + this.pnlGamePadIndex = new System.Windows.Forms.Panel(); + this.nudGamePadIndex = new System.Windows.Forms.NumericUpDown(); + this.lblInfoIndex = new System.Windows.Forms.Label(); + this.pnlButton = new System.Windows.Forms.Panel(); + this.lblLastGamePadLabel = new System.Windows.Forms.Label(); + this.pnlGamePadIndex.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudGamePadIndex)).BeginInit(); + this.pnlButton.SuspendLayout(); + this.SuspendLayout(); + // + // txtButtonBind + // + this.txtButtonBind.Dock = System.Windows.Forms.DockStyle.Fill; + this.txtButtonBind.Location = new System.Drawing.Point(43, 5); + this.txtButtonBind.Name = "txtButtonBind"; + this.txtButtonBind.ReadOnly = true; + this.txtButtonBind.Size = new System.Drawing.Size(249, 20); + this.txtButtonBind.TabIndex = 7; + this.txtButtonBind.Text = "(click here, then press any button to select it as the trigger)"; + this.txtButtonBind.MouseUp += new System.Windows.Forms.MouseEventHandler(this.TxtKeyBind_MouseUp); + // + // lblInfo + // + this.lblInfo.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfo.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfo.Location = new System.Drawing.Point(0, 5); + this.lblInfo.Name = "lblInfo"; + this.lblInfo.Size = new System.Drawing.Size(43, 16); + this.lblInfo.TabIndex = 10; + this.lblInfo.Text = "Button:"; + this.lblInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // cmbPressState + // + this.cmbPressState.Dock = System.Windows.Forms.DockStyle.Right; + this.cmbPressState.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbPressState.FormattingEnabled = true; + this.cmbPressState.Location = new System.Drawing.Point(292, 5); + this.cmbPressState.Name = "cmbPressState"; + this.cmbPressState.Size = new System.Drawing.Size(74, 21); + this.cmbPressState.TabIndex = 11; + this.cmbPressState.SelectedIndexChanged += new System.EventHandler(this.CmbPressedState_SelectedIndexChanged); + // + // pnlGamePadIndex + // + this.pnlGamePadIndex.Controls.Add(this.nudGamePadIndex); + this.pnlGamePadIndex.Controls.Add(this.lblInfoIndex); + this.pnlGamePadIndex.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlGamePadIndex.Location = new System.Drawing.Point(5, 5); + this.pnlGamePadIndex.Name = "pnlGamePadIndex"; + this.pnlGamePadIndex.Size = new System.Drawing.Size(366, 20); + this.pnlGamePadIndex.TabIndex = 12; + // + // nudGamePadIndex + // + this.nudGamePadIndex.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudGamePadIndex.Location = new System.Drawing.Point(69, 0); + this.nudGamePadIndex.Name = "nudGamePadIndex"; + this.nudGamePadIndex.Size = new System.Drawing.Size(297, 20); + this.nudGamePadIndex.TabIndex = 10; + this.nudGamePadIndex.ValueChanged += new System.EventHandler(this.NudGamePadIndex_ValueChanged); + // + // lblInfoIndex + // + this.lblInfoIndex.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoIndex.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoIndex.Location = new System.Drawing.Point(0, 0); + this.lblInfoIndex.Name = "lblInfoIndex"; + this.lblInfoIndex.Size = new System.Drawing.Size(69, 20); + this.lblInfoIndex.TabIndex = 9; + this.lblInfoIndex.Text = "GamePad #:"; + this.lblInfoIndex.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlButton + // + this.pnlButton.Controls.Add(this.txtButtonBind); + this.pnlButton.Controls.Add(this.cmbPressState); + this.pnlButton.Controls.Add(this.lblInfo); + this.pnlButton.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlButton.Location = new System.Drawing.Point(5, 25); + this.pnlButton.Name = "pnlButton"; + this.pnlButton.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.pnlButton.Size = new System.Drawing.Size(366, 26); + this.pnlButton.TabIndex = 14; + // + // lblLastGamePadLabel + // + this.lblLastGamePadLabel.Dock = System.Windows.Forms.DockStyle.Top; + this.lblLastGamePadLabel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Italic, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblLastGamePadLabel.ForeColor = System.Drawing.SystemColors.GrayText; + this.lblLastGamePadLabel.Location = new System.Drawing.Point(5, 51); + this.lblLastGamePadLabel.Name = "lblLastGamePadLabel"; + this.lblLastGamePadLabel.Size = new System.Drawing.Size(366, 15); + this.lblLastGamePadLabel.TabIndex = 15; + this.lblLastGamePadLabel.Text = "Last GamePad used was #: "; + // + // GamePadButtonTriggerControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.Control; + this.Controls.Add(this.lblLastGamePadLabel); + this.Controls.Add(this.pnlButton); + this.Controls.Add(this.pnlGamePadIndex); + this.ForeColor = System.Drawing.SystemColors.ControlText; + this.Name = "GamePadButtonTriggerControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(376, 71); + this.pnlGamePadIndex.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudGamePadIndex)).EndInit(); + this.pnlButton.ResumeLayout(false); + this.pnlButton.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TextBox txtButtonBind; + private System.Windows.Forms.Label lblInfo; + private System.Windows.Forms.ComboBox cmbPressState; + private System.Windows.Forms.Panel pnlGamePadIndex; + private System.Windows.Forms.NumericUpDown nudGamePadIndex; + private System.Windows.Forms.Label lblInfoIndex; + private System.Windows.Forms.Panel pnlButton; + private System.Windows.Forms.Label lblLastGamePadLabel; + } +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.cs new file mode 100644 index 00000000..32d0e098 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using System.Windows.Forms; +using CommonServiceLocator; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Mapping.Triggers.GamePad; +using Key2Joy.Mapping.Triggers.Keyboard; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(GamePadButtonTrigger), + ImageResourceName = "joystick" +)] +public partial class GamePadButtonTriggerControl : UserControl, ITriggerOptionsControl +{ + private const string TEXT_CHANGE = "(press any button to select it as the trigger)"; + private const string TEXT_CHANGE_INSTRUCTION = "(click here, then press any button to set it as the trigger)"; + private const string TEXT_LAST_GAMEPAD = "Last GamePad used was #: {0}"; + + public event EventHandler OptionsChanged; + + private readonly IXInputService xInputService; + private GamePadButton button; + + public GamePadButtonTriggerControl() + { + this.InitializeComponent(); + + this.xInputService = ServiceLocator.Current.GetInstance(); + this.xInputService.StateChanged += this.XInputService_StateChanged; + + this.cmbPressState.DataSource = PressStates.ALL; + this.cmbPressState.SelectedIndex = 0; + + this.nudGamePadIndex.Minimum = 0; + this.nudGamePadIndex.Maximum = XInputService.MaxDevices - 1; + + // Relieve input capturing by this mapping form + ControlRemoved += (s, e) => this.Dispose(); + this.Disposed += (s, e) => + { + if (this.xInputService == null) + { + return; + } + + this.xInputService.StateChanged -= this.XInputService_StateChanged; + this.xInputService.StopPolling(); + }; + } + + private void XInputService_StateChanged(object sender, DeviceStateChangedEventArgs e) + { + var buttons = e.NewState.Gamepad.GetPressedButtonsList(); + + if (buttons.Count == 0) + { + // May occur if the stick is moved without pressing any buttons. + return; + } + + this.button = buttons.First(); + + this.Invoke((MethodInvoker)(() => + { + // Commented because depending on when the device was plugged in, + // it may change with relation to the other (virtual) gamepads. + // this.nudGamePadIndex.Value = e.DeviceIndex; + this.lblLastGamePadLabel.Text = string.Format(TEXT_LAST_GAMEPAD, e.DeviceIndex); + this.UpdateKeys(); + this.StopTrapping(); + })); + } + + public void Select(AbstractTrigger trigger) + { + var thisTrigger = (GamePadButtonTrigger)trigger; + + this.button = thisTrigger.Button; + this.cmbPressState.SelectedItem = thisTrigger.PressState; + this.UpdateKeys(); + } + + public void Setup(AbstractTrigger trigger) + { + var thisTrigger = (GamePadButtonTrigger)trigger; + + thisTrigger.Button = this.button; + thisTrigger.PressState = (PressState)this.cmbPressState.SelectedItem; + } + + public bool CanMappingSave(AbstractTrigger trigger) + { + var thisTrigger = (GamePadButtonTrigger)trigger; + + if (thisTrigger.Button != 0) + { + return true; + } + + MessageBox.Show( + $"The trigger is not set to any button.", + "Cannot save!", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + + return false; + } + + private void StartTrapping() + { + this.txtButtonBind.Text = TEXT_CHANGE; + this.txtButtonBind.Focus(); + this.xInputService.RecognizePhysicalDevices(); + this.xInputService.StartPolling(); + } + + private void StopTrapping() + => this.xInputService.StopPolling(); + + private void UpdateKeys() + { + this.txtButtonBind.Text = $"{this.button} {TEXT_CHANGE_INSTRUCTION}"; + OptionsChanged?.Invoke(this, EventArgs.Empty); + } + + private void CmbPressedState_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void TxtKeyBind_MouseUp(object sender, MouseEventArgs e) + => this.StartTrapping(); + + private void NudGamePadIndex_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.resx b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadButtonTriggerControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.Designer.cs b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.Designer.cs new file mode 100644 index 00000000..1e680100 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.Designer.cs @@ -0,0 +1,231 @@ +namespace Key2Joy.Gui.Mapping +{ + partial class GamePadStickTriggerControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lblInfoSide = new System.Windows.Forms.Label(); + this.pnlStickSide = new System.Windows.Forms.Panel(); + this.cmbStickSide = new System.Windows.Forms.ComboBox(); + this.pnlGamePadIndex = new System.Windows.Forms.Panel(); + this.nudGamePadIndex = new System.Windows.Forms.NumericUpDown(); + this.lblInfoIndex = new System.Windows.Forms.Label(); + this.pnlDeadzone = new System.Windows.Forms.Panel(); + this.pnlDeadzoneConfig = new System.Windows.Forms.Panel(); + this.nudDeadzoneY = new System.Windows.Forms.NumericUpDown(); + this.lblInfoDeadzoneY = new System.Windows.Forms.Label(); + this.nudDeadzoneX = new System.Windows.Forms.NumericUpDown(); + this.lblInfoDeadzoneX = new System.Windows.Forms.Label(); + this.chkOverrideDeadzone = new System.Windows.Forms.CheckBox(); + this.pnlStickSide.SuspendLayout(); + this.pnlGamePadIndex.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudGamePadIndex)).BeginInit(); + this.pnlDeadzone.SuspendLayout(); + this.pnlDeadzoneConfig.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudDeadzoneY)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.nudDeadzoneX)).BeginInit(); + this.SuspendLayout(); + // + // lblInfoSide + // + this.lblInfoSide.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoSide.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoSide.Location = new System.Drawing.Point(0, 5); + this.lblInfoSide.Name = "lblInfoSide"; + this.lblInfoSide.Size = new System.Drawing.Size(59, 16); + this.lblInfoSide.TabIndex = 8; + this.lblInfoSide.Text = "Stick Side:"; + this.lblInfoSide.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlStickSide + // + this.pnlStickSide.Controls.Add(this.cmbStickSide); + this.pnlStickSide.Controls.Add(this.lblInfoSide); + this.pnlStickSide.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlStickSide.Location = new System.Drawing.Point(5, 25); + this.pnlStickSide.Name = "pnlStickSide"; + this.pnlStickSide.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.pnlStickSide.Size = new System.Drawing.Size(297, 26); + this.pnlStickSide.TabIndex = 9; + // + // cmbStickSide + // + this.cmbStickSide.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbStickSide.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbStickSide.FormattingEnabled = true; + this.cmbStickSide.Location = new System.Drawing.Point(59, 5); + this.cmbStickSide.Name = "cmbStickSide"; + this.cmbStickSide.Size = new System.Drawing.Size(238, 21); + this.cmbStickSide.TabIndex = 9; + this.cmbStickSide.SelectedIndexChanged += new System.EventHandler(this.CmbStickSide_SelectedIndexChanged); + // + // pnlGamePadIndex + // + this.pnlGamePadIndex.Controls.Add(this.nudGamePadIndex); + this.pnlGamePadIndex.Controls.Add(this.lblInfoIndex); + this.pnlGamePadIndex.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlGamePadIndex.Location = new System.Drawing.Point(5, 5); + this.pnlGamePadIndex.Name = "pnlGamePadIndex"; + this.pnlGamePadIndex.Size = new System.Drawing.Size(297, 20); + this.pnlGamePadIndex.TabIndex = 10; + // + // nudGamePadIndex + // + this.nudGamePadIndex.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudGamePadIndex.Location = new System.Drawing.Point(69, 0); + this.nudGamePadIndex.Name = "nudGamePadIndex"; + this.nudGamePadIndex.Size = new System.Drawing.Size(228, 20); + this.nudGamePadIndex.TabIndex = 10; + this.nudGamePadIndex.ValueChanged += new System.EventHandler(this.NudGamePadIndex_ValueChanged); + // + // lblInfoIndex + // + this.lblInfoIndex.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoIndex.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoIndex.Location = new System.Drawing.Point(0, 0); + this.lblInfoIndex.Name = "lblInfoIndex"; + this.lblInfoIndex.Size = new System.Drawing.Size(69, 20); + this.lblInfoIndex.TabIndex = 9; + this.lblInfoIndex.Text = "GamePad #:"; + this.lblInfoIndex.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlDeadzone + // + this.pnlDeadzone.Controls.Add(this.pnlDeadzoneConfig); + this.pnlDeadzone.Controls.Add(this.chkOverrideDeadzone); + this.pnlDeadzone.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDeadzone.Location = new System.Drawing.Point(5, 51); + this.pnlDeadzone.Name = "pnlDeadzone"; + this.pnlDeadzone.Size = new System.Drawing.Size(297, 40); + this.pnlDeadzone.TabIndex = 11; + // + // pnlDeadzoneConfig + // + this.pnlDeadzoneConfig.Controls.Add(this.nudDeadzoneY); + this.pnlDeadzoneConfig.Controls.Add(this.lblInfoDeadzoneY); + this.pnlDeadzoneConfig.Controls.Add(this.nudDeadzoneX); + this.pnlDeadzoneConfig.Controls.Add(this.lblInfoDeadzoneX); + this.pnlDeadzoneConfig.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDeadzoneConfig.Location = new System.Drawing.Point(0, 17); + this.pnlDeadzoneConfig.Name = "pnlDeadzoneConfig"; + this.pnlDeadzoneConfig.Size = new System.Drawing.Size(297, 20); + this.pnlDeadzoneConfig.TabIndex = 12; + // + // nudDeadzoneY + // + this.nudDeadzoneY.DecimalPlaces = 4; + this.nudDeadzoneY.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudDeadzoneY.Location = new System.Drawing.Point(177, 0); + this.nudDeadzoneY.Name = "nudDeadzoneY"; + this.nudDeadzoneY.Size = new System.Drawing.Size(120, 20); + this.nudDeadzoneY.TabIndex = 11; + this.nudDeadzoneY.ValueChanged += new System.EventHandler(this.NudDeadzoneY_ValueChanged); + // + // lblInfoDeadzoneY + // + this.lblInfoDeadzoneY.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoDeadzoneY.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoDeadzoneY.Location = new System.Drawing.Point(139, 0); + this.lblInfoDeadzoneY.Name = "lblInfoDeadzoneY"; + this.lblInfoDeadzoneY.Size = new System.Drawing.Size(38, 20); + this.lblInfoDeadzoneY.TabIndex = 10; + this.lblInfoDeadzoneY.Text = "Y:"; + this.lblInfoDeadzoneY.TextAlign = System.Drawing.ContentAlignment.MiddleRight; + // + // nudDeadzoneX + // + this.nudDeadzoneX.DecimalPlaces = 4; + this.nudDeadzoneX.Dock = System.Windows.Forms.DockStyle.Left; + this.nudDeadzoneX.Location = new System.Drawing.Point(26, 0); + this.nudDeadzoneX.Name = "nudDeadzoneX"; + this.nudDeadzoneX.Size = new System.Drawing.Size(113, 20); + this.nudDeadzoneX.TabIndex = 1; + this.nudDeadzoneX.ValueChanged += new System.EventHandler(this.NudDeadzoneX_ValueChanged); + // + // lblInfoDeadzoneX + // + this.lblInfoDeadzoneX.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoDeadzoneX.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoDeadzoneX.Location = new System.Drawing.Point(0, 0); + this.lblInfoDeadzoneX.Name = "lblInfoDeadzoneX"; + this.lblInfoDeadzoneX.Size = new System.Drawing.Size(26, 20); + this.lblInfoDeadzoneX.TabIndex = 9; + this.lblInfoDeadzoneX.Text = "X:"; + this.lblInfoDeadzoneX.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // chkOverrideDeadzone + // + this.chkOverrideDeadzone.AutoSize = true; + this.chkOverrideDeadzone.Dock = System.Windows.Forms.DockStyle.Top; + this.chkOverrideDeadzone.Location = new System.Drawing.Point(0, 0); + this.chkOverrideDeadzone.Name = "chkOverrideDeadzone"; + this.chkOverrideDeadzone.Size = new System.Drawing.Size(297, 17); + this.chkOverrideDeadzone.TabIndex = 0; + this.chkOverrideDeadzone.Text = "Override default deadzone:"; + this.chkOverrideDeadzone.UseVisualStyleBackColor = true; + this.chkOverrideDeadzone.CheckedChanged += new System.EventHandler(this.ChkOverrideDeadzone_CheckedChanged); + // + // GamePadStickTriggerControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.Control; + this.Controls.Add(this.pnlDeadzone); + this.Controls.Add(this.pnlStickSide); + this.Controls.Add(this.pnlGamePadIndex); + this.ForeColor = System.Drawing.SystemColors.ControlText; + this.Name = "GamePadStickTriggerControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(307, 93); + this.pnlStickSide.ResumeLayout(false); + this.pnlGamePadIndex.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudGamePadIndex)).EndInit(); + this.pnlDeadzone.ResumeLayout(false); + this.pnlDeadzone.PerformLayout(); + this.pnlDeadzoneConfig.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudDeadzoneY)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.nudDeadzoneX)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.Label lblInfoSide; + private System.Windows.Forms.Panel pnlStickSide; + private System.Windows.Forms.ComboBox cmbStickSide; + private System.Windows.Forms.Panel pnlGamePadIndex; + private System.Windows.Forms.Label lblInfoIndex; + private System.Windows.Forms.NumericUpDown nudGamePadIndex; + private System.Windows.Forms.Panel pnlDeadzone; + private System.Windows.Forms.NumericUpDown nudDeadzoneY; + private System.Windows.Forms.Label lblInfoDeadzoneY; + private System.Windows.Forms.NumericUpDown nudDeadzoneX; + private System.Windows.Forms.Label lblInfoDeadzoneX; + private System.Windows.Forms.CheckBox chkOverrideDeadzone; + private System.Windows.Forms.Panel pnlDeadzoneConfig; + } +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.cs new file mode 100644 index 00000000..0928bf72 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Mapping.Triggers.GamePad; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(GamePadStickTrigger), + ImageResourceName = "joystick" +)] +public partial class GamePadStickTriggerControl : UserControl, ITriggerOptionsControl +{ + public event EventHandler OptionsChanged; + + public GamePadStickTriggerControl() + { + this.InitializeComponent(); + + List sides = new(); + + foreach (GamePadSide side in Enum.GetValues(typeof(GamePadSide))) + { + sides.Add(side); + } + + this.cmbStickSide.DataSource = sides; + this.nudGamePadIndex.Minimum = 0; + this.nudGamePadIndex.Maximum = XInputService.MaxDevices - 1; + + this.nudDeadzoneX.Minimum = this.nudDeadzoneY.Minimum = -1; + this.nudDeadzoneX.Maximum = this.nudDeadzoneY.Maximum = 1; + + this.UpdateDeadzoneEnabled(); + } + + public void Select(AbstractTrigger trigger) + { + var thisTrigger = (GamePadStickTrigger)trigger; + + this.nudGamePadIndex.Value = thisTrigger.GamePadIndex; + this.cmbStickSide.SelectedItem = thisTrigger.StickSide; + this.chkOverrideDeadzone.Checked = thisTrigger.DeltaMargin != null; + this.nudDeadzoneX.Value = (decimal)(thisTrigger.DeltaMargin?.X ?? 0); + this.nudDeadzoneY.Value = (decimal)(thisTrigger.DeltaMargin?.Y ?? 0); + } + + public void Setup(AbstractTrigger trigger) + { + var thisTrigger = (GamePadStickTrigger)trigger; + + thisTrigger.GamePadIndex = (int)this.nudGamePadIndex.Value; + thisTrigger.StickSide = (GamePadSide)this.cmbStickSide.SelectedItem; + thisTrigger.DeltaMargin = this.chkOverrideDeadzone.Checked + ? new ExactAxisDirection( + (float)this.nudDeadzoneX.Value, + (float)this.nudDeadzoneY.Value + ) + : null; + } + + public bool CanMappingSave(AbstractTrigger trigger) => true; + + private void CmbMouseDirection_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void UpdateDeadzoneEnabled() + { + foreach (Control control in this.pnlDeadzoneConfig.Controls) + { + control.Enabled = this.chkOverrideDeadzone.Checked; + + if (control is NumericUpDown numericUpDown) + { + numericUpDown.ReadOnly = !this.chkOverrideDeadzone.Checked; + } + } + } + + private void ChkOverrideDeadzone_CheckedChanged(object sender, EventArgs e) + { + OptionsChanged?.Invoke(this, EventArgs.Empty); + this.UpdateDeadzoneEnabled(); + } + + private void NudDeadzoneX_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudDeadzoneY_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void CmbStickSide_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudGamePadIndex_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.resx b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadStickTriggerControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.Designer.cs b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.Designer.cs new file mode 100644 index 00000000..f0bf831e --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.Designer.cs @@ -0,0 +1,202 @@ +namespace Key2Joy.Gui.Mapping +{ + partial class GamePadTriggerTriggerControl + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.lblInfoSide = new System.Windows.Forms.Label(); + this.pnlStickSide = new System.Windows.Forms.Panel(); + this.cmbStickSide = new System.Windows.Forms.ComboBox(); + this.pnlGamePadIndex = new System.Windows.Forms.Panel(); + this.nudGamePadIndex = new System.Windows.Forms.NumericUpDown(); + this.lblInfoIndex = new System.Windows.Forms.Label(); + this.pnlDeadzone = new System.Windows.Forms.Panel(); + this.pnlDeadzoneConfig = new System.Windows.Forms.Panel(); + this.nudDeadzone = new System.Windows.Forms.NumericUpDown(); + this.lblInfoDeadzone = new System.Windows.Forms.Label(); + this.chkOverrideDeadzone = new System.Windows.Forms.CheckBox(); + this.pnlStickSide.SuspendLayout(); + this.pnlGamePadIndex.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudGamePadIndex)).BeginInit(); + this.pnlDeadzone.SuspendLayout(); + this.pnlDeadzoneConfig.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.nudDeadzone)).BeginInit(); + this.SuspendLayout(); + // + // lblInfoSide + // + this.lblInfoSide.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoSide.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoSide.Location = new System.Drawing.Point(0, 5); + this.lblInfoSide.Name = "lblInfoSide"; + this.lblInfoSide.Size = new System.Drawing.Size(59, 18); + this.lblInfoSide.TabIndex = 8; + this.lblInfoSide.Text = "Stick Side:"; + this.lblInfoSide.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlStickSide + // + this.pnlStickSide.Controls.Add(this.cmbStickSide); + this.pnlStickSide.Controls.Add(this.lblInfoSide); + this.pnlStickSide.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlStickSide.Location = new System.Drawing.Point(5, 25); + this.pnlStickSide.Name = "pnlStickSide"; + this.pnlStickSide.Padding = new System.Windows.Forms.Padding(0, 5, 0, 5); + this.pnlStickSide.Size = new System.Drawing.Size(297, 28); + this.pnlStickSide.TabIndex = 9; + // + // cmbStickSide + // + this.cmbStickSide.Dock = System.Windows.Forms.DockStyle.Fill; + this.cmbStickSide.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cmbStickSide.FormattingEnabled = true; + this.cmbStickSide.Location = new System.Drawing.Point(59, 5); + this.cmbStickSide.Name = "cmbStickSide"; + this.cmbStickSide.Size = new System.Drawing.Size(238, 21); + this.cmbStickSide.TabIndex = 9; + this.cmbStickSide.SelectedIndexChanged += new System.EventHandler(this.CmbStickSide_SelectedIndexChanged); + // + // pnlGamePadIndex + // + this.pnlGamePadIndex.Controls.Add(this.nudGamePadIndex); + this.pnlGamePadIndex.Controls.Add(this.lblInfoIndex); + this.pnlGamePadIndex.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlGamePadIndex.Location = new System.Drawing.Point(5, 5); + this.pnlGamePadIndex.Name = "pnlGamePadIndex"; + this.pnlGamePadIndex.Size = new System.Drawing.Size(297, 20); + this.pnlGamePadIndex.TabIndex = 10; + // + // nudGamePadIndex + // + this.nudGamePadIndex.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudGamePadIndex.Location = new System.Drawing.Point(69, 0); + this.nudGamePadIndex.Name = "nudGamePadIndex"; + this.nudGamePadIndex.Size = new System.Drawing.Size(228, 20); + this.nudGamePadIndex.TabIndex = 10; + this.nudGamePadIndex.ValueChanged += new System.EventHandler(this.NudGamePadIndex_ValueChanged); + // + // lblInfoIndex + // + this.lblInfoIndex.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoIndex.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoIndex.Location = new System.Drawing.Point(0, 0); + this.lblInfoIndex.Name = "lblInfoIndex"; + this.lblInfoIndex.Size = new System.Drawing.Size(69, 20); + this.lblInfoIndex.TabIndex = 9; + this.lblInfoIndex.Text = "GamePad #:"; + this.lblInfoIndex.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // pnlDeadzone + // + this.pnlDeadzone.Controls.Add(this.pnlDeadzoneConfig); + this.pnlDeadzone.Controls.Add(this.chkOverrideDeadzone); + this.pnlDeadzone.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDeadzone.Location = new System.Drawing.Point(5, 53); + this.pnlDeadzone.Name = "pnlDeadzone"; + this.pnlDeadzone.Size = new System.Drawing.Size(297, 40); + this.pnlDeadzone.TabIndex = 11; + // + // pnlDeadzoneConfig + // + this.pnlDeadzoneConfig.Controls.Add(this.nudDeadzone); + this.pnlDeadzoneConfig.Controls.Add(this.lblInfoDeadzone); + this.pnlDeadzoneConfig.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlDeadzoneConfig.Location = new System.Drawing.Point(0, 17); + this.pnlDeadzoneConfig.Name = "pnlDeadzoneConfig"; + this.pnlDeadzoneConfig.Size = new System.Drawing.Size(297, 20); + this.pnlDeadzoneConfig.TabIndex = 12; + // + // nudDeadzone + // + this.nudDeadzone.DecimalPlaces = 4; + this.nudDeadzone.Dock = System.Windows.Forms.DockStyle.Fill; + this.nudDeadzone.Location = new System.Drawing.Point(69, 0); + this.nudDeadzone.Name = "nudDeadzone"; + this.nudDeadzone.Size = new System.Drawing.Size(228, 20); + this.nudDeadzone.TabIndex = 1; + this.nudDeadzone.ValueChanged += new System.EventHandler(this.NudDeadzoneX_ValueChanged); + // + // lblInfoDeadzone + // + this.lblInfoDeadzone.Dock = System.Windows.Forms.DockStyle.Left; + this.lblInfoDeadzone.ForeColor = System.Drawing.SystemColors.ControlText; + this.lblInfoDeadzone.Location = new System.Drawing.Point(0, 0); + this.lblInfoDeadzone.Name = "lblInfoDeadzone"; + this.lblInfoDeadzone.Size = new System.Drawing.Size(69, 20); + this.lblInfoDeadzone.TabIndex = 9; + this.lblInfoDeadzone.Text = "Sensitivity:"; + this.lblInfoDeadzone.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // chkOverrideDeadzone + // + this.chkOverrideDeadzone.AutoSize = true; + this.chkOverrideDeadzone.Dock = System.Windows.Forms.DockStyle.Top; + this.chkOverrideDeadzone.Location = new System.Drawing.Point(0, 0); + this.chkOverrideDeadzone.Name = "chkOverrideDeadzone"; + this.chkOverrideDeadzone.Size = new System.Drawing.Size(297, 17); + this.chkOverrideDeadzone.TabIndex = 0; + this.chkOverrideDeadzone.Text = "Override default deadzone:"; + this.chkOverrideDeadzone.UseVisualStyleBackColor = true; + this.chkOverrideDeadzone.CheckedChanged += new System.EventHandler(this.ChkOverrideDeadzone_CheckedChanged); + // + // GamePadTriggerTriggerControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.SystemColors.Control; + this.Controls.Add(this.pnlDeadzone); + this.Controls.Add(this.pnlStickSide); + this.Controls.Add(this.pnlGamePadIndex); + this.ForeColor = System.Drawing.SystemColors.ControlText; + this.Name = "GamePadTriggerTriggerControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(307, 95); + this.pnlStickSide.ResumeLayout(false); + this.pnlGamePadIndex.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudGamePadIndex)).EndInit(); + this.pnlDeadzone.ResumeLayout(false); + this.pnlDeadzone.PerformLayout(); + this.pnlDeadzoneConfig.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.nudDeadzone)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.Label lblInfoSide; + private System.Windows.Forms.Panel pnlStickSide; + private System.Windows.Forms.ComboBox cmbStickSide; + private System.Windows.Forms.Panel pnlGamePadIndex; + private System.Windows.Forms.Label lblInfoIndex; + private System.Windows.Forms.NumericUpDown nudGamePadIndex; + private System.Windows.Forms.Panel pnlDeadzone; + private System.Windows.Forms.NumericUpDown nudDeadzone; + private System.Windows.Forms.Label lblInfoDeadzone; + private System.Windows.Forms.CheckBox chkOverrideDeadzone; + private System.Windows.Forms.Panel pnlDeadzoneConfig; + } +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.cs new file mode 100644 index 00000000..02ec6a2f --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Mapping.Triggers.GamePad; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(GamePadTriggerTrigger), + ImageResourceName = "joystick" +)] +public partial class GamePadTriggerTriggerControl : UserControl, ITriggerOptionsControl +{ + public event EventHandler OptionsChanged; + + public GamePadTriggerTriggerControl() + { + this.InitializeComponent(); + + List sides = new(); + + foreach (GamePadSide side in Enum.GetValues(typeof(GamePadSide))) + { + sides.Add(side); + } + + this.cmbStickSide.DataSource = sides; + this.nudGamePadIndex.Minimum = 0; + this.nudGamePadIndex.Maximum = XInputService.MaxDevices - 1; + + this.nudDeadzone.Minimum = 0; + this.nudDeadzone.Maximum = 1; + + this.UpdateDeadzoneEnabled(); + } + + public void Select(AbstractTrigger trigger) + { + var thisTrigger = (GamePadTriggerTrigger)trigger; + + this.nudGamePadIndex.Value = thisTrigger.GamePadIndex; + this.cmbStickSide.SelectedItem = thisTrigger.TriggerSide; + this.chkOverrideDeadzone.Checked = thisTrigger.DeltaMargin != null; + this.nudDeadzone.Value = (decimal)(thisTrigger.DeltaMargin ?? 0); + } + + public void Setup(AbstractTrigger trigger) + { + var thisTrigger = (GamePadTriggerTrigger)trigger; + + thisTrigger.GamePadIndex = (int)this.nudGamePadIndex.Value; + thisTrigger.TriggerSide = (GamePadSide)this.cmbStickSide.SelectedItem; + thisTrigger.DeltaMargin = this.chkOverrideDeadzone.Checked + ? (float)this.nudDeadzone.Value + : null; + } + + public bool CanMappingSave(AbstractTrigger trigger) => true; + + private void CmbMouseDirection_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void UpdateDeadzoneEnabled() + { + this.pnlDeadzoneConfig.Enabled = this.chkOverrideDeadzone.Enabled; + + foreach (Control control in this.pnlDeadzoneConfig.Controls) + { + control.Enabled = this.chkOverrideDeadzone.Checked; + + if (control is NumericUpDown numericUpDown) + { + numericUpDown.ReadOnly = !this.chkOverrideDeadzone.Checked; + } + } + } + + private void ChkOverrideDeadzone_CheckedChanged(object sender, EventArgs e) + { + OptionsChanged?.Invoke(this, EventArgs.Empty); + this.UpdateDeadzoneEnabled(); + } + + private void NudDeadzoneX_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudDeadzoneY_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void CmbStickSide_SelectedIndexChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void NudGamePadIndex_ValueChanged(object sender, EventArgs e) + => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.resx b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/Mapping/Triggers/GamePad/GamePadTriggerTriggerControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Mapping/Triggers/Keyboard/KeyboardTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/Keyboard/KeyboardTriggerControl.cs index 8930ffb2..f48f6e95 100644 --- a/Key2Joy.Gui/Mapping/Triggers/Keyboard/KeyboardTriggerControl.cs +++ b/Key2Joy.Gui/Mapping/Triggers/Keyboard/KeyboardTriggerControl.cs @@ -81,6 +81,25 @@ public void Setup(AbstractTrigger trigger) thisTrigger.PressState = (PressState)this.cmbPressState.SelectedItem; } + public bool CanMappingSave(AbstractTrigger trigger) + { + var thisTrigger = (KeyboardTrigger)trigger; + + if (thisTrigger.Keys != Keys.None) + { + return true; + } + + MessageBox.Show( + $"The trigger is not set to any key.", + "Cannot save!", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + + return false; + } + private void StartTrapping() { this.txtKeyBind.Text = TEXT_CHANGE; diff --git a/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControl.cs index 88e8d3ae..3d38778a 100644 --- a/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControl.cs +++ b/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Windows.Forms; using Key2Joy.Contracts.Mapping; @@ -16,7 +16,8 @@ public partial class CombinedTriggerControl : UserControl, ITriggerOptionsContro { public event EventHandler OptionsChanged; - public CombinedTriggerControl() => this.InitializeComponent(); + public CombinedTriggerControl() + => this.InitializeComponent(); private CombinedTriggerControlItem AddTriggerControl(AbstractTrigger trigger = null) { @@ -31,17 +32,18 @@ private CombinedTriggerControlItem AddTriggerControl(AbstractTrigger trigger = n this.pnlTriggers.Controls.Remove(control); control.Dispose(); this.PerformLayout(); + this.OptionsChanged?.Invoke(this, EventArgs.Empty); }; - triggerControl.TriggerChanged += (s, _) => this.OptionsChanged?.Invoke(this, EventArgs.Empty); + triggerControl.TriggerChanged += (s, _) + => this.OptionsChanged?.Invoke(this, EventArgs.Empty); this.pnlTriggers.Controls.Add(triggerControl); this.PerformLayout(); return triggerControl; } - private void BtnAddTrigger_Click(object sender, EventArgs e) => this.AddTriggerControl().BringToFront(); - - private void NudTimeout_ValueChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); + private void BtnAddTrigger_Click(object sender, EventArgs e) + => this.AddTriggerControl().BringToFront(); public void Select(AbstractTrigger combinedTrigger) { @@ -66,5 +68,7 @@ public void Setup(AbstractTrigger trigger) { thisTrigger.Triggers.Add((triggerControl as CombinedTriggerControlItem).Trigger); } - } + } + + public bool CanMappingSave(AbstractTrigger trigger) => true; } diff --git a/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControlItem.cs b/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControlItem.cs index bc473a51..c1e6761a 100644 --- a/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControlItem.cs +++ b/Key2Joy.Gui/Mapping/Triggers/Logic/CombinedTriggerControlItem.cs @@ -1,31 +1,34 @@ -using System; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping.Triggers; - -namespace Key2Joy.Gui.Mapping; - -public partial class CombinedTriggerControlItem : UserControl -{ - public event EventHandler RequestedRemove; - public event EventHandler TriggerChanged; - public AbstractTrigger Trigger { get; private set; } - - public CombinedTriggerControlItem() => this.InitializeComponent(); - - public CombinedTriggerControlItem(AbstractTrigger trigger) - : this() - { - this.Trigger = trigger; - - this.triggerControl.SelectTrigger(trigger); - } - - private void BtnRemove_Click(object sender, EventArgs e) => RequestedRemove?.Invoke(this, EventArgs.Empty); - - private void TriggerControl_TriggerChanged(object sender, TriggerChangedEventArgs e) - { - this.Trigger = e.Trigger; - - TriggerChanged?.Invoke(this, EventArgs.Empty); - } -} +using System; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping.Triggers; + +namespace Key2Joy.Gui.Mapping; + +public partial class CombinedTriggerControlItem : UserControl +{ + public event EventHandler RequestedRemove; + + public event EventHandler TriggerChanged; + + public AbstractTrigger Trigger { get; private set; } + + public CombinedTriggerControlItem() => this.InitializeComponent(); + + public CombinedTriggerControlItem(AbstractTrigger trigger) + : this() + { + this.Trigger = trigger; + + this.triggerControl.SelectTrigger(trigger); + } + + private void BtnRemove_Click(object sender, EventArgs e) + => RequestedRemove?.Invoke(this, EventArgs.Empty); + + private void TriggerControl_TriggerChanged(object sender, TriggerChangedEventArgs e) + { + this.Trigger = e.Trigger; + + TriggerChanged?.Invoke(this, EventArgs.Empty); + } +} diff --git a/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseButtonTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseButtonTriggerControl.cs index 8831452b..e5df7089 100644 --- a/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseButtonTriggerControl.cs +++ b/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseButtonTriggerControl.cs @@ -1,100 +1,102 @@ -using System; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.LowLevelInput; -using Key2Joy.Mapping.Triggers; -using Key2Joy.Mapping.Triggers.Mouse; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForType = typeof(MouseButtonTrigger), - ImageResourceName = "mouse" -)] -public partial class MouseButtonTriggerControl : UserControl, ITriggerOptionsControl -{ - public event EventHandler OptionsChanged; - - private Mouse.Buttons mouseButtons; - private bool isShowingError; - private bool isMouseOver; - - public MouseButtonTriggerControl() - { - this.InitializeComponent(); - - // This captures global keyboard input and blocks default behaviour by setting e.Handled - GlobalInputHook globalMouseHook = new(); - globalMouseHook.MouseInputEvent += this.OnMouseInputEvent; - - // Relieve input capturing by this mapping form - Disposed += (s, e) => - { - globalMouseHook.MouseInputEvent -= this.OnMouseInputEvent; - globalMouseHook.Dispose(); - globalMouseHook = null; - }; - ControlRemoved += (s, e) => this.Dispose(); - - this.cmbPressState.DataSource = PressStates.ALL; - this.cmbPressState.SelectedIndex = 0; - } - - private void OnMouseInputEvent(object sender, GlobalMouseHookEventArgs e) - { - // Needed to make sure the cursor is immediately over the control, and not over a comboboxitem which is over the control. - if (!this.isMouseOver) - { - return; - } - - if (e.MouseState == MouseState.Move - || !this.txtKeyBind.ClientRectangle.Contains(this.txtKeyBind.PointToClient(MousePosition))) - { - return; - } - - var isDown = false; - - try - { - this.mouseButtons = Mouse.ButtonsFromEvent(e, out isDown); - } - catch (NotImplementedException ex) - { - if (!this.isShowingError) - { - this.isShowingError = true; - MessageBox.Show($"{ex.Message}. Can't map this (yet).", "Unknown mouse button!", MessageBoxButtons.OK, MessageBoxIcon.Error); - this.isShowingError = false; - } - } - - this.txtKeyBind.Text = $"{this.mouseButtons}"; - OptionsChanged?.Invoke(this, EventArgs.Empty); - } - - public void Select(AbstractTrigger trigger) - { - var thisTrigger = (MouseButtonTrigger)trigger; - - this.mouseButtons = thisTrigger.MouseButtons; - this.cmbPressState.SelectedItem = thisTrigger.PressState; - this.txtKeyBind.Text = $"{this.mouseButtons}"; - } - - public void Setup(AbstractTrigger trigger) - { - var thisTrigger = (MouseButtonTrigger)trigger; - - thisTrigger.MouseButtons = this.mouseButtons; - thisTrigger.PressState = (PressState)this.cmbPressState.SelectedItem; - } - - private void CmbPressedState_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); - - private void TxtKeyBind_MouseEnter(object sender, EventArgs e) => this.isMouseOver = true; - - private void TxtKeyBind_MouseLeave(object sender, EventArgs e) => this.isMouseOver = false; -} +using System; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.LowLevelInput; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Mapping.Triggers.Mouse; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(MouseButtonTrigger), + ImageResourceName = "mouse" +)] +public partial class MouseButtonTriggerControl : UserControl, ITriggerOptionsControl +{ + public event EventHandler OptionsChanged; + + private Mouse.Buttons mouseButtons; + private bool isShowingError; + private bool isMouseOver; + + public MouseButtonTriggerControl() + { + this.InitializeComponent(); + + // This captures global keyboard input and blocks default behaviour by setting e.Handled + GlobalInputHook globalMouseHook = new(); + globalMouseHook.MouseInputEvent += this.OnMouseInputEvent; + + // Relieve input capturing by this mapping form + Disposed += (s, e) => + { + globalMouseHook.MouseInputEvent -= this.OnMouseInputEvent; + globalMouseHook.Dispose(); + globalMouseHook = null; + }; + ControlRemoved += (s, e) => this.Dispose(); + + this.cmbPressState.DataSource = PressStates.ALL; + this.cmbPressState.SelectedIndex = 0; + } + + private void OnMouseInputEvent(object sender, GlobalMouseHookEventArgs e) + { + // Needed to make sure the cursor is immediately over the control, and not over a comboboxitem which is over the control. + if (!this.isMouseOver) + { + return; + } + + if (e.MouseState == MouseState.Move + || !this.txtKeyBind.ClientRectangle.Contains(this.txtKeyBind.PointToClient(MousePosition))) + { + return; + } + + var isDown = false; + + try + { + this.mouseButtons = Mouse.ButtonsFromEvent(e, out isDown); + } + catch (NotImplementedException ex) + { + if (!this.isShowingError) + { + this.isShowingError = true; + MessageBox.Show($"{ex.Message}. Can't map this (yet).", "Unknown mouse button!", MessageBoxButtons.OK, MessageBoxIcon.Error); + this.isShowingError = false; + } + } + + this.txtKeyBind.Text = $"{this.mouseButtons}"; + OptionsChanged?.Invoke(this, EventArgs.Empty); + } + + public void Select(AbstractTrigger trigger) + { + var thisTrigger = (MouseButtonTrigger)trigger; + + this.mouseButtons = thisTrigger.MouseButtons; + this.cmbPressState.SelectedItem = thisTrigger.PressState; + this.txtKeyBind.Text = $"{this.mouseButtons}"; + } + + public void Setup(AbstractTrigger trigger) + { + var thisTrigger = (MouseButtonTrigger)trigger; + + thisTrigger.MouseButtons = this.mouseButtons; + thisTrigger.PressState = (PressState)this.cmbPressState.SelectedItem; + } + + public bool CanMappingSave(AbstractTrigger trigger) => true; + + private void CmbPressedState_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); + + private void TxtKeyBind_MouseEnter(object sender, EventArgs e) => this.isMouseOver = true; + + private void TxtKeyBind_MouseLeave(object sender, EventArgs e) => this.isMouseOver = false; +} diff --git a/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseMoveTriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseMoveTriggerControl.cs index 391f7742..dfb41b6e 100644 --- a/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseMoveTriggerControl.cs +++ b/Key2Joy.Gui/Mapping/Triggers/Mouse/MouseMoveTriggerControl.cs @@ -1,52 +1,43 @@ -using System; -using System.Collections.Generic; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.Mapping; -using Key2Joy.Mapping.Triggers; -using Key2Joy.Mapping.Triggers.Mouse; - -namespace Key2Joy.Gui.Mapping; - -[MappingControl( - ForType = typeof(MouseMoveTrigger), - ImageResourceName = "mouse" -)] -public partial class MouseMoveTriggerControl : UserControl, ITriggerOptionsControl -{ - public event EventHandler OptionsChanged; - - public MouseMoveTriggerControl() - { - this.InitializeComponent(); - - List directions = new(); - - foreach (AxisDirection direction in Enum.GetValues(typeof(AxisDirection))) - { - if (direction != AxisDirection.None) - { - directions.Add((AxisDirection)direction); - } - } - - this.cmbMouseDirection.DataSource = directions; - } - - public void Select(AbstractTrigger trigger) - { - var thisTrigger = (MouseMoveTrigger)trigger; - - this.cmbMouseDirection.SelectedItem = thisTrigger.AxisBinding; - } - - public void Setup(AbstractTrigger trigger) - { - var thisTrigger = (MouseMoveTrigger)trigger; - - thisTrigger.AxisBinding = (AxisDirection)this.cmbMouseDirection.SelectedItem; - } - - private void CmbMouseDirection_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); -} +using System; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Mapping.Triggers.Mouse; + +namespace Key2Joy.Gui.Mapping; + +[MappingControl( + ForType = typeof(MouseMoveTrigger), + ImageResourceName = "mouse" +)] +public partial class MouseMoveTriggerControl : UserControl, ITriggerOptionsControl +{ + public event EventHandler OptionsChanged; + + public MouseMoveTriggerControl() + { + this.InitializeComponent(); + + this.cmbMouseDirection.DataSource = Enum.GetValues(typeof(AxisDirection)); + } + + public void Select(AbstractTrigger trigger) + { + var thisTrigger = (MouseMoveTrigger)trigger; + + this.cmbMouseDirection.SelectedItem = thisTrigger.AxisBinding; + } + + public void Setup(AbstractTrigger trigger) + { + var thisTrigger = (MouseMoveTrigger)trigger; + + thisTrigger.AxisBinding = (AxisDirection)this.cmbMouseDirection.SelectedItem; + } + + public bool CanMappingSave(AbstractTrigger trigger) => true; + + private void CmbMouseDirection_SelectedIndexChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Key2Joy.Gui/Mapping/Triggers/TriggerControl.cs b/Key2Joy.Gui/Mapping/Triggers/TriggerControl.cs index 95daa86e..3bb92139 100644 --- a/Key2Joy.Gui/Mapping/Triggers/TriggerControl.cs +++ b/Key2Joy.Gui/Mapping/Triggers/TriggerControl.cs @@ -1,145 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Windows.Forms; -using Key2Joy.Contracts.Mapping.Triggers; -using Key2Joy.Mapping; -using Key2Joy.Mapping.Triggers; -using Key2Joy.Plugins; - -namespace Key2Joy.Gui.Mapping; - -public partial class TriggerControl : UserControl -{ - public AbstractTrigger Trigger { get; private set; } - - public event EventHandler TriggerChanged; - - public bool IsTopLevel { get; set; } - - private bool isLoaded = false; - private ITriggerOptionsControl options; - - private AbstractTrigger selectedTrigger = null; - - public TriggerControl() => this.InitializeComponent(); - - private void BuildTrigger() - { - if (this.cmbTrigger.SelectedItem == null) - { - TriggerChanged?.Invoke(this, TriggerChangedEventArgs.Empty); - return; - } - - var selected = (ImageComboBoxItem>>)this.cmbTrigger.SelectedItem; - var selectedTypeFactory = selected.ItemValue.Value; - var attribute = selected.ItemValue.Key; - - if (this.Trigger == null || this.Trigger.GetType().FullName != selectedTypeFactory.FullTypeName) - { - this.Trigger = selectedTypeFactory.CreateInstance(new object[] - { - attribute.NameFormat, - }); - } - - this.options?.Setup(this.Trigger); - - TriggerChanged?.Invoke(this, new TriggerChangedEventArgs(this.Trigger)); - } - - public void SelectTrigger(AbstractTrigger trigger) - { - if (trigger is DisabledTrigger) - { - trigger = null; - } - - this.selectedTrigger = trigger; - - if (!this.isLoaded) - { - return; - } - - var selected = this.cmbTrigger.Items.Cast>>>(); - var triggerFullTypeName = trigger.GetType().FullName; - var selectedType = selected.FirstOrDefault(x => x.ItemValue.Value.FullTypeName == triggerFullTypeName); - this.cmbTrigger.SelectedItem = selectedType; - } - - private void LoadTriggers() - { - var triggerTypes = TriggersRepository.GetAllTriggers(this.IsTopLevel); - - foreach (var keyValuePair in triggerTypes) - { - var mappingControlFactory = MappingControlRepository.GetMappingControlFactory(keyValuePair.Value.FullTypeName); - var customImage = mappingControlFactory.ImageResourceName; - var image = Program.ResourceBitmapFromName(customImage ?? "error"); - ImageComboBoxItem>> item = new(keyValuePair, new Bitmap(image), "Key"); - - this.cmbTrigger.Items.Add(item); - } - - this.isLoaded = true; - - if (this.selectedTrigger != null) - { - this.SelectTrigger(this.selectedTrigger); - } - } - - private void CmbTrigger_SelectedIndexChanged(object sender, EventArgs e) - { - if (!this.isLoaded) - { - return; - } - - var options = MappingForm.BuildOptionsForComboBox(this.cmbTrigger, this.pnlTriggerOptions); - - if (options != null) - { - if (this.options != null) - { - this.options.OptionsChanged -= this.OnOptionsChanged; - } - - this.options = options as ITriggerOptionsControl; - - if (this.options != null) - { - if (this.selectedTrigger != null) - { - this.options.Select(this.selectedTrigger); - } - - this.options.OptionsChanged += this.OnOptionsChanged; - } - } - - this.BuildTrigger(); - - this.selectedTrigger = null; - this.PerformLayout(); - } - - private void OnOptionsChanged(object sender, EventArgs e) - { - var selected = (ImageComboBoxItem>>)this.cmbTrigger.SelectedItem; - _ = selected.ItemValue.Key; - - if (this.options == null) // TODO: what did I use this for before refactoring to seperate logic and GUI? --> || attribute.OptionsUserControl != options.GetType() - { - return; - } - - this.BuildTrigger(); - } - - private void TriggerControl_Load(object sender, EventArgs e) => this.LoadTriggers(); -} +using System; +using System.Collections.Generic; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Triggers; +using Key2Joy.Plugins; + +namespace Key2Joy.Gui.Mapping; + +public partial class TriggerControl : UserControl +{ + public AbstractTrigger Trigger { get; private set; } + + public event EventHandler TriggerChanged; + + public bool IsTopLevel { get; set; } + + private bool isLoaded = false; + private ITriggerOptionsControl options; + + private AbstractTrigger selectedTrigger = null; + + public TriggerControl() => this.InitializeComponent(); + + private void BuildTrigger() + { + if (this.cmbTrigger.SelectedItem == null) + { + TriggerChanged?.Invoke(this, TriggerChangedEventArgs.Empty); + return; + } + + var selected = (ImageComboBoxItem>>)this.cmbTrigger.SelectedItem; + var selectedTypeFactory = selected.ItemValue.Value; + var attribute = selected.ItemValue.Key; + + if (this.Trigger == null || this.Trigger.GetType().FullName != selectedTypeFactory.FullTypeName) + { + this.Trigger = selectedTypeFactory.CreateInstance(new object[] + { + attribute.NameFormat, + }); + } + + this.options?.Setup(this.Trigger); + + TriggerChanged?.Invoke(this, new TriggerChangedEventArgs(this.Trigger)); + } + + public bool CanMappingSave(AbstractMappedOption mappedOption) + { + if (this.options != null) + { + return this.options.CanMappingSave(mappedOption.Trigger); + } + + return false; + } + + public void SelectTrigger(AbstractTrigger trigger) + { + if (trigger is DisabledTrigger) + { + trigger = null; + } + + this.selectedTrigger = trigger; + + if (!this.isLoaded) + { + return; + } + + var selected = this.cmbTrigger.Items.Cast>>>(); + var triggerFullTypeName = trigger.GetType().FullName; + var selectedType = selected.FirstOrDefault(x => x.ItemValue.Value.FullTypeName == triggerFullTypeName); + this.cmbTrigger.SelectedItem = selectedType; + } + + private void LoadTriggers() + { + if (System.Diagnostics.Process.GetCurrentProcess().ProcessName == "devenv") + { + return; // The designer can't handle the code below. + } + + var triggerTypes = TriggersRepository.GetAllTriggers(this.IsTopLevel); + + foreach (var keyValuePair in triggerTypes) + { + var mappingControlFactory = MappingControlRepository.GetMappingControlFactory(keyValuePair.Value.FullTypeName) + ?? throw new NotImplementedException("mappingControlFactory is null. Please create a Mapping Control for it."); + + var customImage = mappingControlFactory.ImageResourceName; + var image = Program.ResourceBitmapFromName(customImage ?? "error"); + ImageComboBoxItem>> item = new(keyValuePair, new Bitmap(image), "Key"); + + this.cmbTrigger.Items.Add(item); + } + + this.isLoaded = true; + + if (this.selectedTrigger != null) + { + this.SelectTrigger(this.selectedTrigger); + } + } + + private void CmbTrigger_SelectedIndexChanged(object sender, EventArgs e) + { + if (!this.isLoaded) + { + return; + } + + var options = MappingForm.BuildOptionsForComboBox(this.cmbTrigger, this.pnlTriggerOptions); + + if (options != null) + { + if (this.options != null) + { + this.options.OptionsChanged -= this.OnOptionsChanged; + } + + this.options = options as ITriggerOptionsControl; + + if (this.options != null) + { + if (this.selectedTrigger != null) + { + this.options.Select(this.selectedTrigger); + } + + this.options.OptionsChanged += this.OnOptionsChanged; + } + } + + this.BuildTrigger(); + + this.selectedTrigger = null; + this.PerformLayout(); + } + + private void OnOptionsChanged(object sender, EventArgs e) + { + var selected = (ImageComboBoxItem>>)this.cmbTrigger.SelectedItem; + _ = selected.ItemValue.Key; + + if (this.options == null) // TODO: what did I use this for before refactoring to seperate logic and GUI? --> || attribute.OptionsUserControl != options.GetType() + { + return; + } + + this.BuildTrigger(); + } + + private void TriggerControl_Load(object sender, EventArgs e) => this.LoadTriggers(); +} diff --git a/Key2Joy.Gui/MappingContextMenuBuilder.cs b/Key2Joy.Gui/MappingContextMenuBuilder.cs new file mode 100644 index 00000000..464094cb --- /dev/null +++ b/Key2Joy.Gui/MappingContextMenuBuilder.cs @@ -0,0 +1,271 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using BrightIdeasSoftware; +using Key2Joy.Mapping; +using System.Windows.Forms; +using System.Collections; +using CommandLine; +using Key2Joy.Gui.Properties; +using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Contracts.Mapping; +using System.Collections.Generic; + +namespace Key2Joy.Gui; + +internal class SelectEditMappingEventArgs : EventArgs +{ + public MappedOption MappedOption { get; } + + public SelectEditMappingEventArgs(MappedOption mappedOption) + => this.MappedOption = mappedOption; +} + +internal class SelectRemoveMappingsEventArgs : EventArgs +{ + public SelectRemoveMappingsEventArgs() + { } +} + +internal class SelectMakeMappingParentlessEventArgs : EventArgs +{ + public MappedOption MappedOption { get; } + + public SelectMakeMappingParentlessEventArgs(MappedOption mappedOption) + => this.MappedOption = mappedOption; +} + +internal class SelectChooseNewParentEventArgs : EventArgs +{ + public MappedOption MappedOption { get; } + public MappedOption NewParent { get; } + + public SelectChooseNewParentEventArgs(MappedOption mappedOption, MappedOption newParent) + { + this.MappedOption = mappedOption; + this.NewParent = newParent; + } +} + +internal class SelectMultiEditMappingEventArgs : EventArgs +{ + /// + /// The property to be editted + /// + public PropertyInfo Property { get; } + + /// + /// The trigger or actions to edit + /// + public IList MappingAspects { get; } + + /// + /// The value entered by the user + /// + public object Value { get; } + + public SelectMultiEditMappingEventArgs(PropertyInfo property, IList mappingAspects, object value) + { + this.Property = property; + this.MappingAspects = mappingAspects; + this.Value = value; + } +} + +internal class MappingContextMenuBuilder +{ + public event EventHandler SelectEditMapping; + + public event EventHandler SelectRemoveMappings; + + public event EventHandler SelectMakeMappingParentless; + + public event EventHandler SelectChooseNewParent; + + public event EventHandler SelectMultiEditMapping; + + private readonly ContextMenuStrip menu; + private readonly IList selectedItems; + private static MappedOption currentChildChoosingParent = null; + + internal MappingContextMenuBuilder(IList selectedItems) + { + this.selectedItems = selectedItems; + this.menu = new(); + } + + /// + /// Sets up the multi-selection context menu for mapping options. + /// + /// The context menu to set up. + private void SetupMultiSelectionContextMenu(ContextMenuStrip menu) + { + var selectedCount = this.selectedItems.Count; + + // Create main edit item + var editItems = new ToolStripMenuItem { Text = "Edit Multiple Mappings" }; + menu.Items.Add(editItems); + + // Create sub-items for triggers and actions + var editTriggerItems = this.AddDropDownItem(editItems, "Triggers"); + var editActionsItems = this.AddDropDownItem(editItems, "Actions"); + + // Get properties of the first selected item + var firstItem = (MappedOption)this.selectedItems[0].Cast().RowObject; + this.PopulateDropDownItems(editTriggerItems, firstItem.Trigger); + this.PopulateDropDownItems(editActionsItems, firstItem.Action); + + // Add item for removing selected mappings + var removeItems = menu.Items.Add($"Remove {selectedCount} Mappings"); + removeItems.Click += (s, _) => this.SelectRemoveMappings?.Invoke(this, new()); + } + + /// + /// Adds a dropdown item to the specified menu item. + /// + /// The parent menu item. + /// The text of the dropdown item to add. + /// The created dropdown item. + private ToolStripMenuItem AddDropDownItem(ToolStripMenuItem menuItem, string text) + { + var dropDownItem = new ToolStripMenuItem(text); + menuItem.DropDownItems.Add(dropDownItem); + return dropDownItem; + } + + /// + /// Populates dropdown items based on the properties of the specified action or trigger. + /// + /// The parent dropdown item. + /// The trigger or action whose properties are used to populate the dropdown items. + private void PopulateDropDownItems( + ToolStripMenuItem dropdownItem, + TAspect mappingAspect + ) + { + var aspectType = mappingAspect.GetType(); + var properties = mappingAspect + .GetType() + .GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + if (property.GetCustomAttribute() != null) + { + continue; + } + + var item = new ToolStripMenuItem(property.Name); + var aspectsToEdit = new List(); + dropdownItem.DropDownItems.Add(item); + + foreach (var olvItem in this.selectedItems.Cast()) + { + var mappedOption = (MappedOption)olvItem.RowObject; + var otherMappingAspect = (AbstractMappingAspect) + ( + typeof(TAspect) == typeof(AbstractTrigger) + ? mappedOption.Trigger + : mappedOption.Action + ); + + // Disable if any selected item is of a different type of aspect + if (!aspectType.Equals(otherMappingAspect.GetType())) + { + item.Enabled = false; + break; + } + + aspectsToEdit.Add(otherMappingAspect); + } + + if (item.Enabled) + { + item.Click += (s, e) => + { + var dialog = new MappingPropertyEditorForm(property, aspectsToEdit); + var result = dialog.ShowDialog(); + if (result == DialogResult.OK) + { + this.SelectMultiEditMapping?.Invoke( + this, + new( + property, + aspectsToEdit, + dialog.Value + ) + ); + } + }; + } + } + + if (dropdownItem.DropDownItems.Count == 0) + { + dropdownItem.DropDownItems.Add(new ToolStripMenuItem("No common properties")); + } + } + + internal ContextMenuStrip Build() + { + var addItem = this.menu.Items.Add("Add New Mapping"); + addItem.Click += (s, _) => this.SelectEditMapping?.Invoke(this, new(null)); + + var selectedCount = this.selectedItems.Count; + + if (selectedCount == 0) + { + return this.menu; + } + + if (selectedCount > 1) + { + this.SetupMultiSelectionContextMenu(this.menu); + return this.menu; + } + + var selected = (MappedOption)this.selectedItems[0].Cast().RowObject; + + if (selected is MappedOption mappedOption) + { + var editItem = this.menu.Items.Add("Edit Mapping"); + editItem.Click += (s, _) => this.SelectEditMapping?.Invoke(this, new(mappedOption)); + + var removeItem = this.menu.Items.Add("Remove Mapping"); + removeItem.Click += (s, _) => this.SelectRemoveMappings?.Invoke(this, new()); + + this.menu.Items.Add(new ToolStripSeparator()); + + if (mappedOption.IsChild) + { + var removeParentItem = this.menu.Items.Add("Disconnect Mapping from Parent"); + removeParentItem.Click += (s, _) => this.SelectMakeMappingParentless?.Invoke(this, new(mappedOption)); + } + + if (currentChildChoosingParent == null) + { + var chooseNewParentItem = this.menu.Items.Add("Choose New Parent for this Mapping..."); + chooseNewParentItem.Click += (s, _) => currentChildChoosingParent = mappedOption; + chooseNewParentItem.Enabled = !mappedOption.Children.Any(); + } + else + { + var chooseParentItem = this.menu.Items.Add("Choose as Parent"); + chooseParentItem.Image = Resources.tick; + chooseParentItem.Click += (s, _) => + { + this.SelectChooseNewParent?.Invoke(this, new(currentChildChoosingParent, mappedOption)); + currentChildChoosingParent = null; + }; + chooseParentItem.Enabled = !mappedOption.IsChild; + + var cancelItem = this.menu.Items.Add("Cancel Choosing Parent"); + cancelItem.Image = Resources.cross; + cancelItem.Click += (s, _) => currentChildChoosingParent = null; + } + } + + return this.menu; + } +} diff --git a/Key2Joy.Gui/MappingForm.Designer.cs b/Key2Joy.Gui/MappingForm.Designer.cs index 2ce1572b..f78f3f94 100644 --- a/Key2Joy.Gui/MappingForm.Designer.cs +++ b/Key2Joy.Gui/MappingForm.Designer.cs @@ -1,171 +1,216 @@ -namespace Key2Joy.Gui -{ - partial class MappingForm - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.pnlAction = new System.Windows.Forms.Panel(); - this.grpAction = new System.Windows.Forms.GroupBox(); - this.btnSaveMapping = new System.Windows.Forms.Button(); - this.grpTrigger = new System.Windows.Forms.GroupBox(); - this.pnlTrigger = new System.Windows.Forms.Panel(); - this.actionControl = new Key2Joy.Gui.Mapping.ActionControl(); - this.triggerControl = new Key2Joy.Gui.Mapping.TriggerControl(); - this.pnlAction.SuspendLayout(); - this.grpAction.SuspendLayout(); - this.grpTrigger.SuspendLayout(); - this.pnlTrigger.SuspendLayout(); - this.SuspendLayout(); - // - // pnlAction - // - this.pnlAction.AutoSize = true; - this.pnlAction.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.pnlAction.Controls.Add(this.grpAction); - this.pnlAction.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlAction.Location = new System.Drawing.Point(5, 69); - this.pnlAction.Name = "pnlAction"; - this.pnlAction.Padding = new System.Windows.Forms.Padding(5, 5, 5, 10); - this.pnlAction.Size = new System.Drawing.Size(486, 70); - this.pnlAction.TabIndex = 90; - // - // grpAction - // - this.grpAction.AutoSize = true; - this.grpAction.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.grpAction.Controls.Add(this.actionControl); - this.grpAction.Dock = System.Windows.Forms.DockStyle.Top; - this.grpAction.Location = new System.Drawing.Point(5, 5); - this.grpAction.Name = "grpAction"; - this.grpAction.Padding = new System.Windows.Forms.Padding(5); - this.grpAction.Size = new System.Drawing.Size(476, 55); - this.grpAction.TabIndex = 88; - this.grpAction.TabStop = false; - this.grpAction.Text = "Actions that start at the trigger"; - // - // btnSaveMapping - // - this.btnSaveMapping.Dock = System.Windows.Forms.DockStyle.Top; - this.btnSaveMapping.Location = new System.Drawing.Point(5, 139); - this.btnSaveMapping.Name = "btnSaveMapping"; - this.btnSaveMapping.Size = new System.Drawing.Size(486, 44); - this.btnSaveMapping.TabIndex = 91; - this.btnSaveMapping.Text = "Save Mapping"; - this.btnSaveMapping.UseVisualStyleBackColor = true; - this.btnSaveMapping.Click += new System.EventHandler(this.BtnSaveMapping_Click); - // - // grpTrigger - // - this.grpTrigger.AutoSize = true; - this.grpTrigger.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.grpTrigger.Controls.Add(this.triggerControl); - this.grpTrigger.Dock = System.Windows.Forms.DockStyle.Top; - this.grpTrigger.Location = new System.Drawing.Point(5, 5); - this.grpTrigger.Name = "grpTrigger"; - this.grpTrigger.Padding = new System.Windows.Forms.Padding(5); - this.grpTrigger.Size = new System.Drawing.Size(476, 54); - this.grpTrigger.TabIndex = 86; - this.grpTrigger.TabStop = false; - this.grpTrigger.Text = "Trigger that starts the action(s)"; - // - // pnlTrigger - // - this.pnlTrigger.AutoSize = true; - this.pnlTrigger.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.pnlTrigger.Controls.Add(this.grpTrigger); - this.pnlTrigger.Dock = System.Windows.Forms.DockStyle.Top; - this.pnlTrigger.Location = new System.Drawing.Point(5, 5); - this.pnlTrigger.Name = "pnlTrigger"; - this.pnlTrigger.Padding = new System.Windows.Forms.Padding(5); - this.pnlTrigger.Size = new System.Drawing.Size(486, 64); - this.pnlTrigger.TabIndex = 89; - // - // actionControl - // - this.actionControl.AutoSize = true; - this.actionControl.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.actionControl.Dock = System.Windows.Forms.DockStyle.Top; - this.actionControl.IsTopLevel = false; - this.actionControl.Location = new System.Drawing.Point(5, 18); - this.actionControl.MinimumSize = new System.Drawing.Size(300, 32); - this.actionControl.Name = "actionControl"; - this.actionControl.Padding = new System.Windows.Forms.Padding(5); - this.actionControl.Size = new System.Drawing.Size(466, 32); - this.actionControl.TabIndex = 0; - // - // triggerControl - // - this.triggerControl.AutoSize = true; - this.triggerControl.BackColor = System.Drawing.SystemColors.Control; - this.triggerControl.Dock = System.Windows.Forms.DockStyle.Top; - this.triggerControl.Location = new System.Drawing.Point(5, 18); - this.triggerControl.Name = "triggerControl"; - this.triggerControl.Padding = new System.Windows.Forms.Padding(5); - this.triggerControl.Size = new System.Drawing.Size(466, 31); - this.triggerControl.TabIndex = 0; - // - // MappingForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.AutoScroll = true; - this.AutoSize = true; - this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.ClientSize = new System.Drawing.Size(496, 188); - this.Controls.Add(this.btnSaveMapping); - this.Controls.Add(this.pnlAction); - this.Controls.Add(this.pnlTrigger); - this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.MaximumSize = new System.Drawing.Size(1024, 1024); - this.MinimizeBox = false; - this.MinimumSize = new System.Drawing.Size(512, 39); - this.Name = "MappingForm"; - this.Padding = new System.Windows.Forms.Padding(5); - this.Text = "Map triggers to actions"; - this.pnlAction.ResumeLayout(false); - this.pnlAction.PerformLayout(); - this.grpAction.ResumeLayout(false); - this.grpAction.PerformLayout(); - this.grpTrigger.ResumeLayout(false); - this.grpTrigger.PerformLayout(); - this.pnlTrigger.ResumeLayout(false); - this.pnlTrigger.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); - - } - - #endregion - private System.Windows.Forms.Panel pnlAction; - private System.Windows.Forms.Button btnSaveMapping; - private System.Windows.Forms.GroupBox grpTrigger; - private System.Windows.Forms.Panel pnlTrigger; - private System.Windows.Forms.GroupBox grpAction; - private Mapping.ActionControl actionControl; - private Mapping.TriggerControl triggerControl; - } -} \ No newline at end of file +namespace Key2Joy.Gui +{ + partial class MappingForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.pnlAction = new System.Windows.Forms.Panel(); + this.grpAction = new System.Windows.Forms.GroupBox(); + this.actionControl = new Key2Joy.Gui.Mapping.ActionControl(); + this.btnSaveMapping = new System.Windows.Forms.Button(); + this.grpTrigger = new System.Windows.Forms.GroupBox(); + this.triggerControl = new Key2Joy.Gui.Mapping.TriggerControl(); + this.pnlTrigger = new System.Windows.Forms.Panel(); + this.pnlReverse = new System.Windows.Forms.Panel(); + this.chkCreateOrUpdateReverseMapping = new System.Windows.Forms.CheckBox(); + this.pnlSave = new System.Windows.Forms.Panel(); + this.pnlAction.SuspendLayout(); + this.grpAction.SuspendLayout(); + this.grpTrigger.SuspendLayout(); + this.pnlTrigger.SuspendLayout(); + this.pnlReverse.SuspendLayout(); + this.pnlSave.SuspendLayout(); + this.SuspendLayout(); + // + // pnlAction + // + this.pnlAction.AutoSize = true; + this.pnlAction.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.pnlAction.Controls.Add(this.grpAction); + this.pnlAction.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlAction.Location = new System.Drawing.Point(5, 79); + this.pnlAction.Name = "pnlAction"; + this.pnlAction.Padding = new System.Windows.Forms.Padding(5, 5, 5, 10); + this.pnlAction.Size = new System.Drawing.Size(571, 79); + this.pnlAction.TabIndex = 90; + // + // grpAction + // + this.grpAction.AutoSize = true; + this.grpAction.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.grpAction.Controls.Add(this.actionControl); + this.grpAction.Dock = System.Windows.Forms.DockStyle.Top; + this.grpAction.Location = new System.Drawing.Point(5, 5); + this.grpAction.Name = "grpAction"; + this.grpAction.Padding = new System.Windows.Forms.Padding(5); + this.grpAction.Size = new System.Drawing.Size(561, 64); + this.grpAction.TabIndex = 88; + this.grpAction.TabStop = false; + this.grpAction.Text = "Actions that start at the trigger"; + // + // actionControl + // + this.actionControl.AutoSize = true; + this.actionControl.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.actionControl.Dock = System.Windows.Forms.DockStyle.Top; + this.actionControl.IsTopLevel = false; + this.actionControl.Location = new System.Drawing.Point(5, 18); + this.actionControl.MinimumSize = new System.Drawing.Size(300, 32); + this.actionControl.Name = "actionControl"; + this.actionControl.Padding = new System.Windows.Forms.Padding(5); + this.actionControl.Size = new System.Drawing.Size(551, 41); + this.actionControl.TabIndex = 0; + // + // btnSaveMapping + // + this.btnSaveMapping.Dock = System.Windows.Forms.DockStyle.Fill; + this.btnSaveMapping.Location = new System.Drawing.Point(0, 10); + this.btnSaveMapping.Name = "btnSaveMapping"; + this.btnSaveMapping.Size = new System.Drawing.Size(571, 51); + this.btnSaveMapping.TabIndex = 91; + this.btnSaveMapping.Text = "Save Mapping"; + this.btnSaveMapping.UseVisualStyleBackColor = true; + this.btnSaveMapping.Click += new System.EventHandler(this.BtnSaveMapping_Click); + // + // grpTrigger + // + this.grpTrigger.AutoSize = true; + this.grpTrigger.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.grpTrigger.Controls.Add(this.triggerControl); + this.grpTrigger.Dock = System.Windows.Forms.DockStyle.Top; + this.grpTrigger.Location = new System.Drawing.Point(5, 5); + this.grpTrigger.Name = "grpTrigger"; + this.grpTrigger.Padding = new System.Windows.Forms.Padding(5); + this.grpTrigger.Size = new System.Drawing.Size(561, 64); + this.grpTrigger.TabIndex = 86; + this.grpTrigger.TabStop = false; + this.grpTrigger.Text = "Trigger that starts the action(s)"; + // + // triggerControl + // + this.triggerControl.AutoSize = true; + this.triggerControl.BackColor = System.Drawing.SystemColors.Control; + this.triggerControl.Dock = System.Windows.Forms.DockStyle.Top; + this.triggerControl.IsTopLevel = false; + this.triggerControl.Location = new System.Drawing.Point(5, 18); + this.triggerControl.Name = "triggerControl"; + this.triggerControl.Padding = new System.Windows.Forms.Padding(5); + this.triggerControl.Size = new System.Drawing.Size(551, 41); + this.triggerControl.TabIndex = 0; + // + // pnlTrigger + // + this.pnlTrigger.AutoSize = true; + this.pnlTrigger.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.pnlTrigger.Controls.Add(this.grpTrigger); + this.pnlTrigger.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlTrigger.Location = new System.Drawing.Point(5, 5); + this.pnlTrigger.Name = "pnlTrigger"; + this.pnlTrigger.Padding = new System.Windows.Forms.Padding(5); + this.pnlTrigger.Size = new System.Drawing.Size(571, 74); + this.pnlTrigger.TabIndex = 89; + // + // pnlReverse + // + this.pnlReverse.Controls.Add(this.chkCreateOrUpdateReverseMapping); + this.pnlReverse.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlReverse.Location = new System.Drawing.Point(5, 158); + this.pnlReverse.Name = "pnlReverse"; + this.pnlReverse.Size = new System.Drawing.Size(571, 24); + this.pnlReverse.TabIndex = 89; + // + // chkCreateOrUpdateReverseMapping + // + this.chkCreateOrUpdateReverseMapping.AutoSize = true; + this.chkCreateOrUpdateReverseMapping.Dock = System.Windows.Forms.DockStyle.Top; + this.chkCreateOrUpdateReverseMapping.Location = new System.Drawing.Point(0, 0); + this.chkCreateOrUpdateReverseMapping.Name = "chkCreateOrUpdateReverseMapping"; + this.chkCreateOrUpdateReverseMapping.Padding = new System.Windows.Forms.Padding(5); + this.chkCreateOrUpdateReverseMapping.Size = new System.Drawing.Size(571, 27); + this.chkCreateOrUpdateReverseMapping.TabIndex = 0; + this.chkCreateOrUpdateReverseMapping.Text = "Also update the child mapping that is a useful reverse of this"; + this.chkCreateOrUpdateReverseMapping.UseVisualStyleBackColor = true; + this.chkCreateOrUpdateReverseMapping.Click += new System.EventHandler(this.ChkCreateOrUpdateReverseMapping_Click); + // + // pnlSave + // + this.pnlSave.Controls.Add(this.btnSaveMapping); + this.pnlSave.Dock = System.Windows.Forms.DockStyle.Top; + this.pnlSave.Location = new System.Drawing.Point(5, 182); + this.pnlSave.Name = "pnlSave"; + this.pnlSave.Padding = new System.Windows.Forms.Padding(0, 10, 0, 0); + this.pnlSave.Size = new System.Drawing.Size(571, 61); + this.pnlSave.TabIndex = 92; + // + // MappingForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoScroll = true; + this.AutoSize = true; + this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.ClientSize = new System.Drawing.Size(581, 243); + this.Controls.Add(this.pnlSave); + this.Controls.Add(this.pnlReverse); + this.Controls.Add(this.pnlAction); + this.Controls.Add(this.pnlTrigger); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximumSize = new System.Drawing.Size(1024, 1024); + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(512, 39); + this.Name = "MappingForm"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Text = "Map triggers to actions"; + this.pnlAction.ResumeLayout(false); + this.pnlAction.PerformLayout(); + this.grpAction.ResumeLayout(false); + this.grpAction.PerformLayout(); + this.grpTrigger.ResumeLayout(false); + this.grpTrigger.PerformLayout(); + this.pnlTrigger.ResumeLayout(false); + this.pnlTrigger.PerformLayout(); + this.pnlReverse.ResumeLayout(false); + this.pnlReverse.PerformLayout(); + this.pnlSave.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + private System.Windows.Forms.Panel pnlAction; + private System.Windows.Forms.Button btnSaveMapping; + private System.Windows.Forms.GroupBox grpTrigger; + private System.Windows.Forms.Panel pnlTrigger; + private System.Windows.Forms.GroupBox grpAction; + private Mapping.ActionControl actionControl; + private Mapping.TriggerControl triggerControl; + private System.Windows.Forms.Panel pnlReverse; + private System.Windows.Forms.CheckBox chkCreateOrUpdateReverseMapping; + private System.Windows.Forms.Panel pnlSave; + } +} diff --git a/Key2Joy.Gui/MappingForm.cs b/Key2Joy.Gui/MappingForm.cs index 83b84c74..8cc5eeef 100644 --- a/Key2Joy.Gui/MappingForm.cs +++ b/Key2Joy.Gui/MappingForm.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Mapping.Triggers; using Key2Joy.Gui.Mapping; using Key2Joy.Mapping; using Key2Joy.Plugins; @@ -11,8 +13,13 @@ namespace Key2Joy.Gui; public partial class MappingForm : Form { + private const string TEXT_CREATE_REVERSE = "Also create a child mapping that is a useful reverse of this"; + public MappedOption MappedOption { get; private set; } = null; + public MappedOption MappedOptionReverse { get; private set; } = null; + private bool dominantReverseCheckedState; + public MappingForm() { this.InitializeComponent(); @@ -26,18 +33,59 @@ public MappingForm() public MappingForm(MappedOption mappedOption) : this() - { + { + // Don't get in the user's way: we only automatically check if we're initializing or making a new mapping + this.dominantReverseCheckedState = mappedOption == null || mappedOption.Children.Any(); + this.RefreshCreateReverseMappingOption(); + + this.triggerControl.TriggerChanged += this.TriggerControl_TriggerChanged; + this.actionControl.ActionChanged += this.ActionControl_ActionChanged; + if (mappedOption == null) - { + { + this.chkCreateOrUpdateReverseMapping.Text = TEXT_CREATE_REVERSE; return; } + else if (!mappedOption.Children.Any()) + { + this.chkCreateOrUpdateReverseMapping.Text = TEXT_CREATE_REVERSE; + } this.MappedOption = mappedOption; this.triggerControl.SelectTrigger(mappedOption.Trigger); this.actionControl.SelectAction(mappedOption.Action); - } - + } + + /// + /// When the user clicks we override the dominant state, so we dont get in their way. + /// + /// + /// + private void ChkCreateOrUpdateReverseMapping_Click(object sender, EventArgs e) + => this.dominantReverseCheckedState = this.chkCreateOrUpdateReverseMapping.Checked; + + private void ActionControl_ActionChanged(object sender, ActionChangedEventArgs e) + => this.RefreshCreateReverseMappingOption(); + + private void TriggerControl_TriggerChanged(object sender, TriggerChangedEventArgs e) + => this.RefreshCreateReverseMappingOption(); + + private void RefreshCreateReverseMappingOption() + { + if (this.triggerControl.Trigger is IProvideReverseAspect + && this.actionControl.Action is IProvideReverseAspect) + { + this.chkCreateOrUpdateReverseMapping.Checked = this.dominantReverseCheckedState; + this.chkCreateOrUpdateReverseMapping.Enabled = true; + } + else + { + this.chkCreateOrUpdateReverseMapping.Checked = false; + this.chkCreateOrUpdateReverseMapping.Enabled = false; + } + } + public static Control BuildOptionsForComboBox(ComboBox comboBox, Panel optionsPanel) where TAttribute : MappingAttribute where TAspect : AbstractMappingAspect @@ -104,19 +152,53 @@ private void BtnSaveMapping_Click(object sender, EventArgs e) { MessageBox.Show("You must select an action!", "No action selected!", MessageBoxButtons.OK, MessageBoxIcon.Error); return; - } - + } + this.MappedOption ??= new MappedOption(); this.MappedOption.Trigger = trigger; - this.MappedOption.Action = action; + this.MappedOption.Action = action; + + if (!this.triggerControl.CanMappingSave(this.MappedOption)) + { + return; + } if (!this.actionControl.CanMappingSave(this.MappedOption)) { return; + } + + if (this.chkCreateOrUpdateReverseMapping.Checked) + { + var reverse = MappedOption.GenerateReverseMapping(this.MappedOption, true); + + if (!this.MappedOption.Children.Any()) + { + this.MappedOptionReverse = reverse; + this.MappedOptionReverse.SetParent(this.MappedOption); + } + else + { + // Update the existing reverse mapping + var existingReverse = this.MappedOption.Children.First(); + existingReverse.Trigger = reverse.Trigger; + existingReverse.Action = reverse.Action; + + if (this.MappedOption.Children.Count > 1) + { + // TODO: What do we do if there's multiple children? + MessageBox.Show( + $"There are {this.MappedOption.Children.Count} children of this mapping. Only the first one was updated with the reverse mapping.", + "Multiple children", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + } } this.DialogResult = DialogResult.OK; this.Close(); - } -} + } +} diff --git a/Key2Joy.Gui/MappingForm.resx b/Key2Joy.Gui/MappingForm.resx index 1af7de15..29dcb1b3 100644 --- a/Key2Joy.Gui/MappingForm.resx +++ b/Key2Joy.Gui/MappingForm.resx @@ -1,120 +1,120 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + \ No newline at end of file diff --git a/Key2Joy.Gui/MappingGroupItemComparer.cs b/Key2Joy.Gui/MappingGroupItemComparer.cs index 4c7834f2..042b72ee 100644 --- a/Key2Joy.Gui/MappingGroupItemComparer.cs +++ b/Key2Joy.Gui/MappingGroupItemComparer.cs @@ -1,44 +1,81 @@ -using System.Collections.Generic; -using System.Windows.Forms; -using BrightIdeasSoftware; -using Key2Joy.Mapping; -using Key2Joy.Mapping.Actions; - -namespace Key2Joy.Gui; - -public class MappingGroupItemComparer : IComparer -{ - private readonly OLVColumn primarySort; - private readonly SortOrder primarySortOrder; - - public MappingGroupItemComparer(OLVColumn primarySort, SortOrder primarySortOrder) - { - this.primarySort = primarySort; - this.primarySortOrder = primarySortOrder; - } - - public int Compare(OLVListItem x, OLVListItem y) - { - var mappedOptionX = x.RowObject as MappedOption; - var mappedOptionY = y.RowObject as MappedOption; - - var sortDirection = this.primarySortOrder == SortOrder.Ascending ? 1 : -1; - - if (typeof(CoreAction).IsAssignableFrom(this.primarySort.DataType)) - { - return mappedOptionX.Action.CompareTo(mappedOptionY.Action) * sortDirection; - } - - if (mappedOptionX.Trigger != null) - { - if (mappedOptionY.Trigger == null) - { - return 1 * sortDirection; - } - - return mappedOptionX.Trigger.CompareTo(mappedOptionY.Trigger) * sortDirection; - } - - return mappedOptionY.Trigger == null ? 0 : -1 * sortDirection; - } -} \ No newline at end of file +using System.Collections.Generic; +using System.Windows.Forms; +using BrightIdeasSoftware; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Actions; + +namespace Key2Joy.Gui; + +public class MappingGroupItemComparer : IComparer +{ + private readonly OLVColumn primarySort; + private readonly SortOrder primarySortOrder; + + public MappingGroupItemComparer(OLVColumn primarySort, SortOrder primarySortOrder) + { + this.primarySort = primarySort; + this.primarySortOrder = primarySortOrder; + } + + public int Compare(OLVListItem x, OLVListItem y) + { + var mappedOptionX = x.RowObject as MappedOption; + var mappedOptionY = y.RowObject as MappedOption; + + var sortDirection = this.primarySortOrder == SortOrder.Ascending ? 1 : -1; + + // Check for parent-child relationships + if (mappedOptionX.IsChildOf(mappedOptionY)) + { + return 1 * sortDirection; + } + else if (mappedOptionY.IsChildOf(mappedOptionX)) + { + return -1 * sortDirection; + } + + // If both mapped options are children of different parents, sort based on their parents + if (mappedOptionX.IsChild + && mappedOptionY.IsChild + && mappedOptionX.Parent != mappedOptionY.Parent) + { + return this.DefaultSorting(mappedOptionX.Parent, mappedOptionY.Parent) * sortDirection; + } + + // If only mappedOptionX is a child, sort it relative to mappedOptionY's position + if (mappedOptionX.IsChild && !mappedOptionY.IsChild) + { + return this.DefaultSorting(mappedOptionX.Parent, mappedOptionY) * sortDirection; + } + + // If only mappedOptionY is a child, sort it relative to mappedOptionX's position + if (mappedOptionY.IsChild && !mappedOptionX.IsChild) + { + return this.DefaultSorting(mappedOptionX, mappedOptionY.Parent) * sortDirection; + } + + // If no parent-child relationship or siblings, sort based on triggers/actions + return this.DefaultSorting(mappedOptionX, mappedOptionY) * sortDirection; + } + + private int DefaultSorting(AbstractMappedOption mappedOptionX, AbstractMappedOption mappedOptionY) + { + if (typeof(CoreAction).IsAssignableFrom(this.primarySort.DataType)) + { + return mappedOptionX.Action.CompareTo(mappedOptionY.Action); + } + + if (mappedOptionX.Trigger != null) + { + if (mappedOptionY.Trigger == null) + { + return 1; + } + + return mappedOptionX.Trigger.CompareTo(mappedOptionY.Trigger); + } + + return mappedOptionY.Trigger == null ? 0 : -1; + } +} diff --git a/Key2Joy.Gui/MappingPropertyEditorForm.Designer.cs b/Key2Joy.Gui/MappingPropertyEditorForm.Designer.cs new file mode 100644 index 00000000..dbecc488 --- /dev/null +++ b/Key2Joy.Gui/MappingPropertyEditorForm.Designer.cs @@ -0,0 +1,103 @@ +namespace Key2Joy.Gui; + +partial class MappingPropertyEditorForm +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.grpValueEditor = new System.Windows.Forms.GroupBox(); + this.btnApplyChanges = new System.Windows.Forms.Button(); + this.btnCancel = new System.Windows.Forms.Button(); + this.pnlValueInputParent = new System.Windows.Forms.Panel(); + this.grpValueEditor.SuspendLayout(); + this.SuspendLayout(); + // + // grpValueEditor + // + this.grpValueEditor.Controls.Add(this.pnlValueInputParent); + this.grpValueEditor.Dock = System.Windows.Forms.DockStyle.Fill; + this.grpValueEditor.Location = new System.Drawing.Point(5, 5); + this.grpValueEditor.Name = "grpValueEditor"; + this.grpValueEditor.Size = new System.Drawing.Size(324, 51); + this.grpValueEditor.TabIndex = 0; + this.grpValueEditor.TabStop = false; + this.grpValueEditor.Text = "Value Editor"; + // + // btnApplyChanges + // + this.btnApplyChanges.Dock = System.Windows.Forms.DockStyle.Bottom; + this.btnApplyChanges.Location = new System.Drawing.Point(5, 56); + this.btnApplyChanges.Name = "btnApplyChanges"; + this.btnApplyChanges.Size = new System.Drawing.Size(324, 33); + this.btnApplyChanges.TabIndex = 0; + this.btnApplyChanges.Text = "Apply Changes"; + this.btnApplyChanges.UseVisualStyleBackColor = true; + this.btnApplyChanges.Click += new System.EventHandler(this.BtnApplyChanges_Click); + // + // btnCancel + // + this.btnCancel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.btnCancel.Location = new System.Drawing.Point(5, 89); + this.btnCancel.Name = "btnCancel"; + this.btnCancel.Size = new System.Drawing.Size(324, 48); + this.btnCancel.TabIndex = 1; + this.btnCancel.Text = "Cancel"; + this.btnCancel.UseVisualStyleBackColor = true; + this.btnCancel.Click += new System.EventHandler(this.BtnCancel_Click); + // + // pnlValueInputParent + // + this.pnlValueInputParent.Dock = System.Windows.Forms.DockStyle.Fill; + this.pnlValueInputParent.Location = new System.Drawing.Point(3, 16); + this.pnlValueInputParent.Name = "pnlValueInputParent"; + this.pnlValueInputParent.Padding = new System.Windows.Forms.Padding(5); + this.pnlValueInputParent.Size = new System.Drawing.Size(318, 32); + this.pnlValueInputParent.TabIndex = 0; + // + // MappingPropertyEditorForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(334, 142); + this.Controls.Add(this.grpValueEditor); + this.Controls.Add(this.btnApplyChanges); + this.Controls.Add(this.btnCancel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Name = "MappingPropertyEditorForm"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Text = "Editting Multiple Properties"; + this.grpValueEditor.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.GroupBox grpValueEditor; + private System.Windows.Forms.Button btnApplyChanges; + private System.Windows.Forms.Button btnCancel; + private System.Windows.Forms.Panel pnlValueInputParent; +} diff --git a/Key2Joy.Gui/MappingPropertyEditorForm.cs b/Key2Joy.Gui/MappingPropertyEditorForm.cs new file mode 100644 index 00000000..e8c07a7d --- /dev/null +++ b/Key2Joy.Gui/MappingPropertyEditorForm.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Windows.Forms; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Util; + +namespace Key2Joy.Gui; + +public partial class MappingPropertyEditorForm : Form +{ + private const string TEXT_LABEL = "New value for '{0}'"; + public object Value { get; private set; } + + private PropertyInfo property; + private IList mappingAspects; + private Control ctrlValueInput; + + private readonly Type[] decimalLikeTypes = new[] { + typeof(decimal), + typeof(decimal?), + typeof(double), + typeof(double?), + typeof(float), + typeof(float?), + }; + + private readonly Type[] numberLikeTypes = new[] { + typeof(int), + typeof(int?), + typeof(uint), + typeof(uint?), + typeof(long), + typeof(long?), + typeof(ulong), + typeof(ulong?), + typeof(short), + typeof(short?), + typeof(ushort), + typeof(ushort?), + typeof(byte), + typeof(byte?), + typeof(sbyte), + typeof(sbyte?), + }; + + public MappingPropertyEditorForm(PropertyInfo property, IList mappingAspects) + { + this.InitializeComponent(); + + this.property = property; + this.mappingAspects = mappingAspects; + + this.Text = $"Editting {property.Name} on {mappingAspects.Count} items"; + this.grpValueEditor.Text = string.Format(TEXT_LABEL, property.Name); + + this.Load += this.MappingPropertyEditorForm_Load; + } + + private void MappingPropertyEditorForm_Load(object sender, EventArgs e) + => this.CreateValueInput(); + + /// + /// Generates a value input based on the property type + /// + // We cant simplify this since the minimum and maximum need to be set first. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0017:Simplify object initialization", Justification = "")] + private void CreateValueInput() + { + var propertyType = this.property.PropertyType; + object commonValue = null; + + foreach (var mappingAspect in this.mappingAspects) + { + var value = this.property.GetValue(mappingAspect); + + if (commonValue == null) + { + commonValue = value; + } + else if (!commonValue.Equals(value)) + { + commonValue = null; + break; + } + } + + if (propertyType == typeof(string)) + { + this.ctrlValueInput = new TextBox() + { + Text = commonValue as string ?? string.Empty + }; + } + else if (this.numberLikeTypes.Contains(propertyType)) + { + var minValue = propertyType.GetNumericMinValue(); + var maxValue = propertyType.GetNumericMaxValue(); + var nud = new NumericUpDown + { + DecimalPlaces = 0, + Minimum = TypeExtensions.ToDecimalSafe(minValue), + Maximum = TypeExtensions.ToDecimalSafe(maxValue) + }; + + nud.Value = commonValue == null ? 0 : TypeExtensions.ToDecimalSafe(commonValue); + + this.ctrlValueInput = nud; + } + else if (this.decimalLikeTypes.Contains(propertyType)) + { + var minValue = propertyType.GetNumericMinValue(); + var maxValue = propertyType.GetNumericMaxValue(); + var nud = new NumericUpDown + { + DecimalPlaces = 2, // Assuming 2 decimal places + Minimum = TypeExtensions.ToDecimalSafe(minValue), + Maximum = TypeExtensions.ToDecimalSafe(maxValue) + }; + + nud.Value = commonValue == null ? 0 : TypeExtensions.ToDecimalSafe(commonValue); + + this.ctrlValueInput = nud; + } + else if (propertyType.IsEnum) + { + this.ctrlValueInput = new ComboBox + { + DropDownStyle = ComboBoxStyle.DropDownList, + DataSource = Enum.GetValues(propertyType) + }; + } + else + { + // For all other types, error and close + MessageBox.Show( + $"Cannot (yet) edit this type of property: {propertyType.Name}", + "Property type not (yet) supported", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + this.DialogResult = DialogResult.Cancel; + this.Close(); + return; + } + + this.ctrlValueInput.Dock = DockStyle.Fill; + this.pnlValueInputParent.Controls.Add(this.ctrlValueInput); + } + + private void BtnCancel_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Close(); + } + + private void BtnApplyChanges_Click(object sender, EventArgs e) + { + var propertyType = this.property.PropertyType; + propertyType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + if (this.ctrlValueInput is TextBox textBox) + { + this.Value = textBox.Text; + } + else if (this.ctrlValueInput is NumericUpDown numericUpDown) + { + this.Value = Convert.ChangeType(numericUpDown.Value, propertyType); + } + else if (this.ctrlValueInput is ComboBox comboBox) + { + this.Value = comboBox.SelectedItem; + } + else + { + // TODO: Handle other types + this.Value = this.ctrlValueInput.Text; + } + + this.DialogResult = DialogResult.OK; + this.Close(); + } +} diff --git a/Key2Joy.Gui/MappingPropertyEditorForm.resx b/Key2Joy.Gui/MappingPropertyEditorForm.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/MappingPropertyEditorForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/NotificationBannerControl.Designer.cs b/Key2Joy.Gui/NotificationBannerControl.Designer.cs new file mode 100644 index 00000000..becd60e3 --- /dev/null +++ b/Key2Joy.Gui/NotificationBannerControl.Designer.cs @@ -0,0 +1,76 @@ +namespace Key2Joy.Gui; + +partial class NotificationBannerControl +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.btnClose = new System.Windows.Forms.Button(); + this.lblMessage = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // btnClose + // + this.btnClose.Dock = System.Windows.Forms.DockStyle.Right; + this.btnClose.Location = new System.Drawing.Point(459, 5); + this.btnClose.Name = "btnClose"; + this.btnClose.Size = new System.Drawing.Size(54, 20); + this.btnClose.TabIndex = 0; + this.btnClose.Text = "✖"; + this.btnClose.UseVisualStyleBackColor = true; + this.btnClose.Click += new System.EventHandler(this.BtnClose_Click); + // + // lblMessage + // + this.lblMessage.Dock = System.Windows.Forms.DockStyle.Fill; + this.lblMessage.Location = new System.Drawing.Point(5, 5); + this.lblMessage.Margin = new System.Windows.Forms.Padding(0); + this.lblMessage.Name = "lblMessage"; + this.lblMessage.Size = new System.Drawing.Size(454, 20); + this.lblMessage.TabIndex = 1; + this.lblMessage.Text = "..."; + this.lblMessage.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // NotificationBannerControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackColor = System.Drawing.Color.RosyBrown; + this.Controls.Add(this.lblMessage); + this.Controls.Add(this.btnClose); + this.Name = "NotificationBannerControl"; + this.Padding = new System.Windows.Forms.Padding(5); + this.Size = new System.Drawing.Size(518, 30); + this.Paint += new System.Windows.Forms.PaintEventHandler(this.NotificationBannerControl_Paint); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button btnClose; + private System.Windows.Forms.Label lblMessage; +} diff --git a/Key2Joy.Gui/NotificationBannerControl.cs b/Key2Joy.Gui/NotificationBannerControl.cs new file mode 100644 index 00000000..a22dd25a --- /dev/null +++ b/Key2Joy.Gui/NotificationBannerControl.cs @@ -0,0 +1,116 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace Key2Joy.Gui; + +public enum NotificationBannerStyle +{ + Information = 0, + Warning = 1, + Error = 2, +} + +public partial class NotificationBannerControl : UserControl +{ + private const int BORDER_WIDTH = 2; + private const int BORDER_MARGIN = 2; + + private readonly string message; + private readonly NotificationBannerStyle style; + private readonly Timer timeLeftTimer; + + public NotificationBannerControl( + string message, + NotificationBannerStyle style = NotificationBannerStyle.Information, + TimeSpan? duration = null + ) + { + this.InitializeComponent(); + + this.message = message; + this.style = style; + + this.lblMessage.Text = message; + + duration ??= TimeSpan.FromSeconds(10); + + this.timeLeftTimer = new Timer + { + Interval = (int)Math.Min(1000, duration.Value.TotalMilliseconds) + }; + this.timeLeftTimer.Tick += (s, e) => + { + // Show the remaining time in the btnClose every second, close when times up + duration -= TimeSpan.FromSeconds(1); + this.btnClose.Text = $"✖ {duration.Value.TotalSeconds:0}s"; + + if (duration.Value.TotalSeconds <= 0) + { + this.Close(); + } + }; + this.timeLeftTimer.Start(); + + switch (this.style) + { + case NotificationBannerStyle.Information: + this.lblMessage.ForeColor = Color.White; + this.BackColor = Color.DarkBlue; + break; + + case NotificationBannerStyle.Warning: + this.lblMessage.ForeColor = Color.Black; + this.BackColor = Color.Gold; + break; + + case NotificationBannerStyle.Error: + this.lblMessage.ForeColor = Color.White; + this.BackColor = Color.Crimson; + break; + } + } + + private void Close() + { + this.timeLeftTimer.Stop(); + this.timeLeftTimer.Dispose(); + this.Parent?.Controls.Remove(this); + } + + private void BtnClose_Click(object sender, System.EventArgs e) + => this.Close(); + + private void NotificationBannerControl_Paint(object sender, PaintEventArgs e) + { + const ButtonBorderStyle borderStyle = ButtonBorderStyle.Dashed; + var borderColor = Color.WhiteSmoke; + + if (this.style == NotificationBannerStyle.Warning) + { + borderColor = Color.Black; + } + + ControlPaint.DrawBorder( + e.Graphics, + new Rectangle( + this.ClientRectangle.X + BORDER_MARGIN, + this.ClientRectangle.Y + BORDER_MARGIN, + this.ClientRectangle.Width - (BORDER_MARGIN * 2), + this.ClientRectangle.Height - (BORDER_MARGIN * 2) + ), + borderColor, + BORDER_WIDTH, + borderStyle, + borderColor, + BORDER_WIDTH, + borderStyle, + borderColor, + BORDER_WIDTH, + borderStyle, + borderColor, + BORDER_WIDTH, + borderStyle + ); + } +} diff --git a/Key2Joy.Gui/NotificationBannerControl.resx b/Key2Joy.Gui/NotificationBannerControl.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Key2Joy.Gui/NotificationBannerControl.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Key2Joy.Gui/Program.cs b/Key2Joy.Gui/Program.cs index 3402fb77..820e0ee1 100644 --- a/Key2Joy.Gui/Program.cs +++ b/Key2Joy.Gui/Program.cs @@ -1,76 +1,76 @@ -using System; -using System.Drawing; -using System.Windows.Forms; -using Key2Joy.Mapping.Actions.Logic; -using Key2Joy.Plugins; - -namespace Key2Joy.Gui; - -public static class Program -{ - public static Form ActiveForm { get; set; } - public static PluginSet Plugins { get; private set; } - - /// - /// The main entry point for the application. - /// - [STAThread] - private static void Main() - { - Key2JoyManager.InitSafely( - OnRunAppCommand, - (plugins) => - { - var args = Environment.GetCommandLineArgs(); - - Plugins = plugins; - - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - - var shouldStartMinimized = false; - - foreach (var arg in args) - { - if (arg == "--minimized") - { - shouldStartMinimized = true; - } - } - - ActiveForm = new InitForm(shouldStartMinimized); - - while (ActiveForm != null && !ActiveForm.IsDisposed) - { - Application.Run(ActiveForm); - } - } - ); - - Plugins.Dispose(); - } - - internal static void GoToNextForm(Form form) - { - var oldForm = ActiveForm; - ActiveForm = form; - - oldForm.Close(); - } - - internal static Bitmap ResourceBitmapFromName(string name) - { - var rm = Properties.Resources.ResourceManager; - return (Bitmap)rm.GetObject(name); - } - - private static bool OnRunAppCommand(AppCommand command) - { - if (ActiveForm is IAcceptAppCommands form) - { - return form.RunAppCommand(command); - } - - return false; - } -} +using System; +using System.Drawing; +using System.Windows.Forms; +using Key2Joy.Mapping.Actions.Logic; +using Key2Joy.Plugins; + +namespace Key2Joy.Gui; + +public static class Program +{ + public static Form ActiveForm { get; set; } + public static PluginSet Plugins { get; private set; } + + /// + /// The main entry point for the application. + /// + [STAThread] + private static void Main() + { + Key2JoyManager.InitSafely( + OnRunAppCommand, + (plugins) => + { + var args = Environment.GetCommandLineArgs(); + + Plugins = plugins; + + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + var shouldStartMinimized = false; + + foreach (var arg in args) + { + if (arg == "--minimized") + { + shouldStartMinimized = true; + } + } + + ActiveForm = new InitForm(shouldStartMinimized); + + while (ActiveForm != null && !ActiveForm.IsDisposed) + { + Application.Run(ActiveForm); + } + } + ); + + Plugins.Dispose(); + } + + internal static void GoToNextForm(Form form) + { + var oldForm = ActiveForm; + ActiveForm = form; + + oldForm.Close(); + } + + internal static Bitmap ResourceBitmapFromName(string name) + { + var rm = Properties.Resources.ResourceManager; + return (Bitmap)rm.GetObject(name); + } + + private static bool OnRunAppCommand(AppCommand command) + { + if (ActiveForm is IAcceptAppCommands form) + { + return form.RunAppCommand(command); + } + + return false; + } +} diff --git a/Key2Joy.Gui/Properties/Resources.Designer.cs b/Key2Joy.Gui/Properties/Resources.Designer.cs index 066492e5..0e534ba6 100644 --- a/Key2Joy.Gui/Properties/Resources.Designer.cs +++ b/Key2Joy.Gui/Properties/Resources.Designer.cs @@ -1,333 +1,343 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Key2Joy.Gui.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Key2Joy.Gui.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap application_osx_terminal { - get { - object obj = ResourceManager.GetObject("application_osx_terminal", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap application_xp_terminal { - get { - object obj = ResourceManager.GetObject("application_xp_terminal", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap clock { - get { - object obj = ResourceManager.GetObject("clock", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap cog { - get { - object obj = ResourceManager.GetObject("cog", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap connect { - get { - object obj = ResourceManager.GetObject("connect", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap cursor { - get { - object obj = ResourceManager.GetObject("cursor", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap disconnect { - get { - object obj = ResourceManager.GetObject("disconnect", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap disk { - get { - object obj = ResourceManager.GetObject("disk", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap disk_multiple { - get { - object obj = ResourceManager.GetObject("disk_multiple", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap door_out { - get { - object obj = ResourceManager.GetObject("door_out", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap error { - get { - object obj = ResourceManager.GetObject("error", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap film { - get { - object obj = ResourceManager.GetObject("film", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap icon256 { - get { - object obj = ResourceManager.GetObject("icon256", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap information { - get { - object obj = ResourceManager.GetObject("information", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap joystick { - get { - object obj = ResourceManager.GetObject("joystick", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap JS { - get { - object obj = ResourceManager.GetObject("JS", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap keyboard { - get { - object obj = ResourceManager.GetObject("keyboard", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap link { - get { - object obj = ResourceManager.GetObject("link", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap Lua { - get { - object obj = ResourceManager.GetObject("Lua", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap mouse { - get { - object obj = ResourceManager.GetObject("mouse", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap PermissionWarning { - get { - object obj = ResourceManager.GetObject("PermissionWarning", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap plugin { - get { - object obj = ResourceManager.GetObject("plugin", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap script_code { - get { - object obj = ResourceManager.GetObject("script_code", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap sound { - get { - object obj = ResourceManager.GetObject("sound", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap text_list_bullets { - get { - object obj = ResourceManager.GetObject("text_list_bullets", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap text_list_numbers { - get { - object obj = ResourceManager.GetObject("text_list_numbers", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Drawing.Bitmap. - /// - internal static System.Drawing.Bitmap tick { - get { - object obj = ResourceManager.GetObject("tick", resourceCulture); - return ((System.Drawing.Bitmap)(obj)); - } - } - } -} +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Key2Joy.Gui.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Key2Joy.Gui.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap application_osx_terminal { + get { + object obj = ResourceManager.GetObject("application_osx_terminal", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap application_xp_terminal { + get { + object obj = ResourceManager.GetObject("application_xp_terminal", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap clock { + get { + object obj = ResourceManager.GetObject("clock", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap cog { + get { + object obj = ResourceManager.GetObject("cog", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap connect { + get { + object obj = ResourceManager.GetObject("connect", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap cross { + get { + object obj = ResourceManager.GetObject("cross", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap cursor { + get { + object obj = ResourceManager.GetObject("cursor", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap disconnect { + get { + object obj = ResourceManager.GetObject("disconnect", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap disk { + get { + object obj = ResourceManager.GetObject("disk", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap disk_multiple { + get { + object obj = ResourceManager.GetObject("disk_multiple", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap door_out { + get { + object obj = ResourceManager.GetObject("door_out", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap error { + get { + object obj = ResourceManager.GetObject("error", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap film { + get { + object obj = ResourceManager.GetObject("film", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icon256 { + get { + object obj = ResourceManager.GetObject("icon256", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap information { + get { + object obj = ResourceManager.GetObject("information", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap joystick { + get { + object obj = ResourceManager.GetObject("joystick", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap JS { + get { + object obj = ResourceManager.GetObject("JS", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap keyboard { + get { + object obj = ResourceManager.GetObject("keyboard", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap link { + get { + object obj = ResourceManager.GetObject("link", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Lua { + get { + object obj = ResourceManager.GetObject("Lua", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap mouse { + get { + object obj = ResourceManager.GetObject("mouse", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap PermissionWarning { + get { + object obj = ResourceManager.GetObject("PermissionWarning", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap plugin { + get { + object obj = ResourceManager.GetObject("plugin", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap script_code { + get { + object obj = ResourceManager.GetObject("script_code", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap sound { + get { + object obj = ResourceManager.GetObject("sound", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap text_list_bullets { + get { + object obj = ResourceManager.GetObject("text_list_bullets", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap text_list_numbers { + get { + object obj = ResourceManager.GetObject("text_list_numbers", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap tick { + get { + object obj = ResourceManager.GetObject("tick", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Key2Joy.Gui/Properties/Resources.resx b/Key2Joy.Gui/Properties/Resources.resx index 6d02e9cd..d241bb15 100644 --- a/Key2Joy.Gui/Properties/Resources.resx +++ b/Key2Joy.Gui/Properties/Resources.resx @@ -1,202 +1,205 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - - ..\Graphics\Lua.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\icon256.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\error.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\clock.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\JS.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\disk_multiple.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\application_osx_terminal.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\disconnect.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\sound.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\cursor.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\joystick.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\film.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\keyboard.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\connect.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\script_code.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\cog.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\text_list_bullets.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\disk.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\mouse.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\link.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\PermissionWarning.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\text_list_numbers.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\application_xp_terminal.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\tick.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\door_out.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\information.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - - ..\Graphics\Icons\plugin.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Graphics\Lua.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\icon256.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\error.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\clock.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\JS.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\disk_multiple.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\application_osx_terminal.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\disconnect.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\sound.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\cursor.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\joystick.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\film.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\keyboard.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\connect.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\script_code.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\cog.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\text_list_bullets.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\disk.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\mouse.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\link.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\PermissionWarning.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\text_list_numbers.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\application_xp_terminal.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\tick.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\door_out.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\information.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\plugin.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Graphics\Icons\cross.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/Plugins/Key2Joy.Plugin.Ffmpeg/Key2Joy.Plugin.Ffmpeg.csproj b/Plugins/Key2Joy.Plugin.Ffmpeg/Key2Joy.Plugin.Ffmpeg.csproj index 01dfaa49..7cfc8f35 100644 --- a/Plugins/Key2Joy.Plugin.Ffmpeg/Key2Joy.Plugin.Ffmpeg.csproj +++ b/Plugins/Key2Joy.Plugin.Ffmpeg/Key2Joy.Plugin.Ffmpeg.csproj @@ -15,7 +15,7 @@ NET48 latest Always - x86 + AnyCPU true Library ..\..\bin\$(MSBuildProjectName) diff --git a/Plugins/Key2Joy.Plugin.HelloWorld/Key2Joy.Plugin.HelloWorld.csproj b/Plugins/Key2Joy.Plugin.HelloWorld/Key2Joy.Plugin.HelloWorld.csproj index 4a37facb..83f6d4e3 100644 --- a/Plugins/Key2Joy.Plugin.HelloWorld/Key2Joy.Plugin.HelloWorld.csproj +++ b/Plugins/Key2Joy.Plugin.HelloWorld/Key2Joy.Plugin.HelloWorld.csproj @@ -16,7 +16,7 @@ NET48 latest Always - x86 + AnyCPU true Library ..\..\bin\$(AssemblyName) diff --git a/Plugins/Key2Joy.Plugin.HelloWorld/Mapping/Actions/GetHelloWorldActionControl.xaml.cs b/Plugins/Key2Joy.Plugin.HelloWorld/Mapping/Actions/GetHelloWorldActionControl.xaml.cs index 00649518..45f3c306 100644 --- a/Plugins/Key2Joy.Plugin.HelloWorld/Mapping/Actions/GetHelloWorldActionControl.xaml.cs +++ b/Plugins/Key2Joy.Plugin.HelloWorld/Mapping/Actions/GetHelloWorldActionControl.xaml.cs @@ -1,43 +1,43 @@ -using System; -using System.Windows; -using System.Windows.Controls; -using Key2Joy.Contracts.Mapping; -using Key2Joy.Contracts.Mapping.Actions; -using Key2Joy.Contracts.Plugins; - -namespace Key2Joy.Plugin.HelloWorld.Mapping.Actions; - -[MappingControl( - ForType = typeof(GetHelloWorldAction), - ImageResourceName = "clock" -)] -public partial class GetHelloWorldActionControl : UserControl, IPluginUserControl, IActionOptionsControl -{ - public event EventHandler OptionsChanged; - - public GetHelloWorldActionControl() => this.InitializeComponent(); - - public int GetDesiredHeight() => 50; - - private void Button_Click(object sender, RoutedEventArgs e) => MessageBox.Show("Hello from GetHelloWorldActionControl!"); - - public void Select(object action) - { - var thisAction = (GetHelloWorldAction)action; - - this.txtName.Text = thisAction.Target; - } - - public void Setup(object action) - { - var thisAction = (GetHelloWorldAction)action; - - thisAction.Target = this.txtName.Text; - } - - public bool CanMappingSave(object action) => true; - - private void BtnHelloWorld_Click(object sender, EventArgs e) => MessageBox.Show($"Hello {this.txtName.Text}!"); - - private void TxtName_TextChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); -} +using System; +using System.Windows; +using System.Windows.Controls; +using Key2Joy.Contracts.Mapping; +using Key2Joy.Contracts.Mapping.Actions; +using Key2Joy.Contracts.Plugins; + +namespace Key2Joy.Plugin.HelloWorld.Mapping.Actions; + +[MappingControl( + ForType = typeof(GetHelloWorldAction), + ImageResourceName = "clock" +)] +public partial class GetHelloWorldActionControl : UserControl, IPluginUserControl, IPluginActionOptionsControl +{ + public event EventHandler OptionsChanged; + + public GetHelloWorldActionControl() => this.InitializeComponent(); + + public int GetDesiredHeight() => 50; + + private void Button_Click(object sender, RoutedEventArgs e) => MessageBox.Show("Hello from GetHelloWorldActionControl!"); + + public void Select(PluginAction action) + { + var thisAction = (GetHelloWorldAction)action; + + this.txtName.Text = thisAction.Target; + } + + public void Setup(PluginAction action) + { + var thisAction = (GetHelloWorldAction)action; + + thisAction.Target = this.txtName.Text; + } + + public bool CanMappingSave(PluginAction action) => true; + + private void BtnHelloWorld_Click(object sender, EventArgs e) => MessageBox.Show($"Hello {this.txtName.Text}!"); + + private void TxtName_TextChanged(object sender, EventArgs e) => OptionsChanged?.Invoke(this, EventArgs.Empty); +} diff --git a/Plugins/Key2Joy.Plugin.Midi/Key2Joy.Plugin.Midi.csproj b/Plugins/Key2Joy.Plugin.Midi/Key2Joy.Plugin.Midi.csproj index 3e67277a..37a42f4d 100644 --- a/Plugins/Key2Joy.Plugin.Midi/Key2Joy.Plugin.Midi.csproj +++ b/Plugins/Key2Joy.Plugin.Midi/Key2Joy.Plugin.Midi.csproj @@ -15,7 +15,7 @@ NET48 latest Always - x86 + AnyCPU true Library ..\..\bin\$(MSBuildProjectName) diff --git a/README.md b/README.md index f6001b25..1da36452 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,9 @@ In addition to simply simulating a button being pressed on the Joystick, you can ## Usage -**⚠ Use at own risk!** Incorrect driver (un)installation may cause a blue screen. +> **Warning** +> **Use at your own risk!** Incorrect driver (un)installation may cause a blue screen. +> *Read the [`Uninstalling` section](#uninstalling) below to find out how to uninstall the driver.* ### Using Key2Joy with a Graphical User Interface @@ -41,9 +43,9 @@ In addition to simply simulating a button being pressed on the Joystick, you can **An action** is what will happen when that trigger occurs. *E.g: simulating a joystick button being pressed, a keyboard button being released, or executing a Lua/Javascript script.* -4. Tick the *Enable* checkbox for Key2Joy to start listening for triggers that will execute the configured actions. +4. Tick the *Arm Mappings* checkbox for Key2Joy to start listening for triggers that will execute the configured actions. -5. When you're done using Key2Joy, uncheck the *Enable* checkbox to stop listening for triggers. +5. When you're done using Key2Joy, uncheck the *Arm Mappings* checkbox to stop listening for triggers. *In the default profile there is also a mapping that stops Key2Joy from listening using the `Escape`-key. Don't forget to include such a mapping for your custom profiles.* @@ -82,18 +84,22 @@ Full scripting reference is available in [Docs/Scripting.md](Docs/Scripting.md) --- -## Known Issues +## Known Issues and Limitations - Windows wont let you click if you release a mouse key that hasn't been pressed before. + - Keyboard triggers do not activate when the app is in the foreground. +- If you run another app as administrator, Key2Joy can only simulate input in that app if Key2Joy is also running as administrator. + --- ## Uninstalling Use `ScpDriverInstaller.exe` to uninstall the driver. You can find the latest version here: [mogzol/ScpDriverInterface releases](https://github.com/mogzol/ScpDriverInterface/releases) -**⚠ Do not uninstall the driver through Device manager or you'll end up with an incomplete and corrupt driver installation.** +> **Warning** +> **Do not uninstall the driver through Device manager** or you'll end up with a corrupt driver installation which may cause a blue screen. You may be able to recover from this by starting Windows in Safe-mode, running `ScpDriverInstaller.exe` and choosing "Uninstall". --- @@ -105,19 +111,21 @@ Please do not hesitate to [create an issue](/../../issues/new/) when you find a --- -## Credits 😍 - -This exists only because of this awesome NuGet package ([DavidRieman/SimWinInput](https://github.com/DavidRieman/SimWinInput)) which allows simulation of gamepads from .NET. - -Simulation is made possible through installation and usage of the [nefarius/ScpVBus](https://github.com/nefarius/ScpVBus) driver. - -Inspired by [JoyToKey](https://joytokey.net/en/) which does the inverse (simulate keyboard with gamepad). +## Special Thanks 😍 -Scripting in Lua and Javascript works thanks to [NLua](https://github.com/NLua/NLua) and [Jint](https://github.com/sebastienros/jint) respectively. +Originally inspired by [JoyToKey](https://joytokey.net/en/), this project has since evolved to offer so much more. Our gratitude goes out to the following resources: -The action list in the GUI uses [ObjectListView](https://objectlistview.sourceforge.net). +**NuGet Packages**: +- [📦 DavidRieman/SimWinInput](https://github.com/DavidRieman/SimWinInput) - Simulate gamepads from .NET. +- [📦 nefarius/ScpVBus](https://github.com/nefarius/ScpVBus) - The foundational driver enabling GamePad simulation. +- [📦 mfakane/rawinput-sharp](https://github.com/mfakane/rawinput-sharp) - Facilitates reading of raw input. +- [📦 NLua](https://github.com/NLua/NLua) - Enables Lua scripting for actions. +- [📦 Jint](https://github.com/sebastienros/jint) - Supports JavaScript scripting for actions. +- [📦 ObjectListView](https://objectlistview.sourceforge.net) - Used for mapping listings in the GUI. +- [📦 Mono.Cecil](https://github.com/jbevain/cecil) - Reads attributes from plugin assemblies, underpinning the plugin system. -Some [Silk Icons](https://github.com/legacy-icons/famfamfam-silk/blob/master/LICENSE.md) are used in the GUI. +**Iconography**: +- [Silk Icons pack by Mark James](https://github.com/legacy-icons/famfamfam-silk/blob/master/LICENSE.md) - Provides the GUI icons. --- diff --git a/Support/BuildMarkdownDocs/BuildMarkdownDocs.csproj b/Support/BuildMarkdownDocs/BuildMarkdownDocs.csproj index 5ca9f8f8..843a0a4b 100644 --- a/Support/BuildMarkdownDocs/BuildMarkdownDocs.csproj +++ b/Support/BuildMarkdownDocs/BuildMarkdownDocs.csproj @@ -14,7 +14,7 @@ NET48 latest - x86 + AnyCPU Exe bin\$(MSBuildProjectName) false diff --git a/Support/Key2Joy.Setup/Key2Joy.Setup.csproj b/Support/Key2Joy.Setup/Key2Joy.Setup.csproj index 65cc2ce2..ee7bd89d 100644 --- a/Support/Key2Joy.Setup/Key2Joy.Setup.csproj +++ b/Support/Key2Joy.Setup/Key2Joy.Setup.csproj @@ -15,7 +15,7 @@ NET48 latest Graphics\Icons\app-icons.ico - x86 + AnyCPU bin\$(MSBuildProjectName) false false diff --git a/Support/Key2Joy.Tests.Stubs/TestPlugin/Key2Joy.Tests.Stubs.TestPlugin.csproj b/Support/Key2Joy.Tests.Stubs/TestPlugin/Key2Joy.Tests.Stubs.TestPlugin.csproj index 51900acc..71f92187 100644 --- a/Support/Key2Joy.Tests.Stubs/TestPlugin/Key2Joy.Tests.Stubs.TestPlugin.csproj +++ b/Support/Key2Joy.Tests.Stubs/TestPlugin/Key2Joy.Tests.Stubs.TestPlugin.csproj @@ -15,7 +15,7 @@ NET48 latest Always - x86 + AnyCPU true Library bin\$(MSBuildProjectName) diff --git a/Support/Key2Joy.Tests/BuildMarkdownDocs/Util/FileServiceTests.cs b/Support/Key2Joy.Tests/BuildMarkdownDocs/Util/FileServiceTests.cs index 12c77e53..db7448de 100644 --- a/Support/Key2Joy.Tests/BuildMarkdownDocs/Util/FileServiceTests.cs +++ b/Support/Key2Joy.Tests/BuildMarkdownDocs/Util/FileServiceTests.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Linq; using BuildMarkdownDocs.Util; diff --git a/Support/Key2Joy.Tests/BuildMarkdownDocs/sample.xml b/Support/Key2Joy.Tests/BuildMarkdownDocs/sample.xml index 06e5369b..1a8f1694 100644 --- a/Support/Key2Joy.Tests/BuildMarkdownDocs/sample.xml +++ b/Support/Key2Joy.Tests/BuildMarkdownDocs/sample.xml @@ -1,1414 +1,2428 @@ - - Key2Joy.Core - - - - - Provides methods for retrieving property information and custom attributes. - Implements the interface. - - - - - Retrieves all properties from the given type. - - The type whose properties need to be retrieved. - An enumerable of representing the properties of the given type. - - - - Retrieves the custom associated with the given property. - - The property whose custom attribute needs to be retrieved. - The associated with the property, or null if no such attribute exists. - - - - Only applied to - - - - - Only applied to - - - - - Gets all configs and their property - - - - - - Manages user configurations being loaded from and saved to disk. - - - - The path to where the config file is located. - - - - Loads the configuration or creates a default one on disk. - - - - - Turns the plugin path into a relative one from the app directory - - - - - - - Sets a plugin as enabled, the permissions checksum is stored so no changes to the permissions - are accepted when loading the plugin later. - - Set permissionsChecksumOrNull to null to disable the plugin. - - - - - - - Provides an abstraction for retrieving property information and custom attributes. - - - - - Retrieves all properties from the given type. - - The type whose properties need to be retrieved. - An enumerable of representing the properties of the given type. - - - - Retrieves the custom associated with the given property. - - The property whose custom attribute needs to be retrieved. - The associated with the property, or null if no such attribute exists. - - - - Only applied to - - - - - Only applied to - - - - - Singleton client for communication with the Key2Joy service. - This is used by the Key2Joy.Cmd CLI to send commands to the service and - enable/disable mappings. - - - - - Sends a command to the main app, for example to enable/disable mappings. - - - - - - - Read the first byte and use it to get the type struct - - - - - - - Convert a byte identifier to command info by looking it up - - - - - - - Reads the full command of a certain type from the pipe - - - - - - - - Handle a command by calling the appropriate handler - - - - - - Directory where plugins are located - - - - - Trigger listeners that should explicitly loaded. This ensures that they're available for scripts - even if no mapping option is mapped to be triggered by it. - - - - - Ensures Key2Joy is running and ready to accept commands as long as the main loop does not end. - - - - Optionally a custom config manager (probably only useful for unit testing) - - - - Starts Key2Joy, pausing until it's ready - - - - - Get the raw input state from the GamePad - - - - - Resets the GamePad state to the natural at-rest stat - - - - - Update any changes made to the state to be reflected in the gamepad - - - - - Implementation based on https://github.com/DavidRieman/SimWinInput - - - - - A virtual-key code. The code must be a value in the range 1 to 254. - - - - - A hardware scan code for the key. - - - - - The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level. - - - LLKHF_EXTENDED - (KF_EXTENDED >> 8) - Test the extended-key flag. - - - LLKHF_LOWER_IL_INJECTED - 0x00000002 - Test the event-injected (from a process running at lower integrity level) flag. - - - LLKHF_INJECTED - 0x00000010 - Test the event-injected (from any process) flag. - - - LLKHF_ALTDOWN - (KF_ALTDOWN >> 8) - Test the context code. - - - LLKHF_UP - (KF_UP >> 8) - Test the transition-state flag. - - - - - The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message. - - - - - Additional information associated with the message. - - - - - The x- and y-coordinates of the cursor, in per-monitor-aware screen coordinates. - - - - - If the message is WM_MOUSEWHEEL, the high-order word of this member is the wheel delta. The low-order word is reserved. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120. - If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, and the low-order word is reserved.This value can be one or more of the following values.Otherwise, mouseData is not used. - - - - - The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level. - - - - - The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message. - - - - - Additional information associated with the message. - - - - - Represents the type of mouse movement. - - - - - Specifies relative movement to where the cursor is now. - - - - - Specifies absolute movement of the cursor on screen. - - - - - Represents the buttons on a mouse. - - - - - No button is pressed. - - - - - The left mouse button. - - - - - The right mouse button. - - - - - The middle mouse button. - - - - - The first extra mouse button. - - - - - The second extra mouse button. - - - - - The mouse wheel is moved upward. - - - - - The mouse wheel is moved downward. - - - - - Posted when the user double-clicks the first or second X button while the cursor is in - the nonclient area of a window. This message is posted to the window that contains the - cursor. If a window has captured the mouse, this message is not posted. - - - - - Represents a class for managing native hooks. - - - - - Loads a dynamic-link library (DLL) into the address space of the calling process. - - The name of the DLL to load. - A handle to the loaded DLL if successful; otherwise, IntPtr.Zero. - - - - Frees the loaded dynamic-link library (DLL). - - A handle to the loaded DLL. - true if successful, false otherwise. - - - - Installs an application-defined hook procedure into a hook chain. - - The type of hook to be installed. - A pointer to the hook procedure. - A handle to the DLL containing the hook procedure. - The identifier of the thread with which the hook procedure is to be associated. - A handle to the hook procedure if successful; otherwise, IntPtr.Zero. - - - - Removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. - - A handle to the hook to be removed. - true if successful, false otherwise. - - - - Passes the hook information to the next hook procedure in the current hook chain. - - A handle to the current hook. - The hook code passed to the current hook procedure. - The wParam value passed to the current hook procedure. - The lParam value passed to the current hook procedure. - The return value of the next hook procedure. - - - - Loads a dynamic-link library (DLL) into the address space of the calling process. - - The name of the DLL to load. - A handle to the loaded DLL if successful; otherwise, IntPtr.Zero. - - - - Frees the loaded dynamic-link library (DLL). - - A handle to the loaded DLL. - true if successful, false otherwise. - - - - Installs an application-defined hook procedure into a hook chain. - - The type of hook to be installed. - A pointer to the hook procedure. - A handle to the DLL containing the hook procedure. - The identifier of the thread with which the hook procedure is to be associated. - A handle to the hook procedure if successful; otherwise, IntPtr.Zero. - - - - Removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. - - A handle to the hook to be removed. - true if successful, false otherwise. - - - - Passes the hook information to the next hook procedure in the current hook chain. - - A handle to the current hook. - The hook code passed to the current hook procedure. - The wParam value passed to the current hook procedure. - The lParam value passed to the current hook procedure. - The return value of the next hook procedure. - - - - The x-coordinate of the point. - - - - - The y-coordinate of the point. - - - - - The key/button is pressed down - - - - - The key/button is released (after having been pressed down) - - - - - All available press states - - - - - Left mouse button - - - - - Right mouse button - - - - - Control-break processing - - - - - Middle mouse button (three-button mouse) - - - - - Windows 2000/XP: X1 mouse button - - - - - Windows 2000/XP: X2 mouse button - - - - - BACKSPACE key - - - - - TAB key - - - - - CLEAR key - - - - - ENTER key - - - - - SHIFT key - - - - - CTRL key - - - - - ALT key - - - - - PAUSE key - - - - - CAPS LOCK key - - - - - Input Method Editor (IME) Kana mode - - - - - IME Hangul mode - - - - - IME Junja mode - - - - - IME final mode - - - - - IME Hanja mode - - - - - IME Kanji mode - - - - - ESC key - - - - - IME convert - - - - - IME nonconvert - - - - - IME accept - - - - - IME mode change request - - - - - SPACEBAR - - - - - PAGE UP key - - - - - PAGE DOWN key - - - - - END key - - - - - HOME key - - - - - LEFT ARROW key - - - - - UP ARROW key - - - - - RIGHT ARROW key - - - - - DOWN ARROW key - - - - - SELECT key - - - - - PRINT key - - - - - EXECUTE key - - - - - PRINT SCREEN key - - - - - INS key - - - - - DEL key - - - - - HELP key - - - - - 0 key - - - - - 1 key - - - - - 2 key - - - - - 3 key - - - - - 4 key - - - - - 5 key - - - - - 6 key - - - - - 7 key - - - - - 8 key - - - - - 9 key - - - - - A key - - - - - B key - - - - - C key - - - - - D key - - - - - E key - - - - - F key - - - - - G key - - - - - H key - - - - - I key - - - - - J key - - - - - K key - - - - - L key - - - - - M key - - - - - N key - - - - - O key - - - - - P key - - - - - Q key - - - - - R key - - - - - S key - - - - - T key - - - - - U key - - - - - V key - - - - - W key - - - - - X key - - - - - Y key - - - - - Z key - - - - - Left Windows key (Microsoft Natural keyboard) - - - - - Right Windows key (Natural keyboard) - - - - - Applications key (Natural keyboard) - - - - - Computer Sleep key - - - - - Numeric keypad 0 key - - - - - Numeric keypad 1 key - - - - - Numeric keypad 2 key - - - - - Numeric keypad 3 key - - - - - Numeric keypad 4 key - - - - - Numeric keypad 5 key - - - - - Numeric keypad 6 key - - - - - Numeric keypad 7 key - - - - - Numeric keypad 8 key - - - - - Numeric keypad 9 key - - - - - Multiply key - - - - - Add key - - - - - Separator key - - - - - Subtract key - - - - - Decimal key - - - - - Divide key - - - - - F1 key - - - - - F2 key - - - - - F3 key - - - - - F4 key - - - - - F5 key - - - - - F6 key - - - - - F7 key - - - - - F8 key - - - - - F9 key - - - - - F10 key - - - - - F11 key - - - - - F12 key - - - - - F13 key - - - - - F14 key - - - - - F15 key - - - - - F16 key - - - - - F17 key - - - - - F18 key - - - - - F19 key - - - - - F20 key - - - - - F21 key - - - - - F22 key, (PPC only) Key used to lock device. - - - - - F23 key - - - - - F24 key - - - - - NUM LOCK key - - - - - SCROLL LOCK key - - - - - Left SHIFT key - - - - - Right SHIFT key - - - - - Left CONTROL key - - - - - Right CONTROL key - - - - - Left MENU key - - - - - Right MENU key - - - - - Windows 2000/XP: Browser Back key - - - - - Windows 2000/XP: Browser Forward key - - - - - Windows 2000/XP: Browser Refresh key - - - - - Windows 2000/XP: Browser Stop key - - - - - Windows 2000/XP: Browser Search key - - - - - Windows 2000/XP: Browser Favorites key - - - - - Windows 2000/XP: Browser Start and Home key - - - - - Windows 2000/XP: Volume Mute key - - - - - Windows 2000/XP: Volume Down key - - - - - Windows 2000/XP: Volume Up key - - - - - Windows 2000/XP: Next Track key - - - - - Windows 2000/XP: Previous Track key - - - - - Windows 2000/XP: Stop Media key - - - - - Windows 2000/XP: Play/Pause Media key - - - - - Windows 2000/XP: Start Mail key - - - - - Windows 2000/XP: Select Media key - - - - - Windows 2000/XP: Start Application 1 key - - - - - Windows 2000/XP: Start Application 2 key - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Windows 2000/XP: For any country/region, the '+' key - - - - - Windows 2000/XP: For any country/region, the ',' key - - - - - Windows 2000/XP: For any country/region, the '-' key - - - - - Windows 2000/XP: For any country/region, the '.' key - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Used for miscellaneous characters; it can vary by keyboard. - - - - - Windows 2000/XP: Either the angle bracket key or the backslash key on the RT 102-key keyboard - - - - - Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key - - - - - Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. - The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, - see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP - - - - - Attn key - - - - - CrSel key - - - - - ExSel key - - - - - Erase EOF key - - - - - Play key - - - - - Zoom key - - - - - Reserved - - - - - PA1 key - - - - - Clear key - - - - - Declaration of external SendInput method - - - - - Define HARDWAREINPUT struct - - - - - The set of valid MapTypes used in MapVirtualKey - - - Source: http://pinvoke.net/default.aspx/user32/MapVirtualKey.html?diff=y - - - - - uCode is a virtual-key code and is translated into a scan code. - If it is a virtual-key code that does not distinguish between left- and - right-hand keys, the left-hand scan code is returned. - If there is no translation, the function returns 0. - - - - - uCode is a scan code and is translated into a virtual-key code that - does not distinguish between left- and right-hand keys. If there is no - translation, the function returns 0. - - - - - uCode is a virtual-key code and is translated into an unshifted - character value in the low-order word of the return value. Dead keys (diacritics) - are indicated by setting the top bit of the return value. If there is no - translation, the function returns 0. - - - - - Windows NT/2000/XP: uCode is a scan code and is translated into a - virtual-key code that distinguishes between left- and right-hand keys. If - there is no translation, the function returns 0. - - - - - Not currently documented - - - - - Loads all actions in the assembly, optionally merging it with additional action types. - - - - - - Gets all action type factories - - - - - - Gets all action attributes - - - - - - Gets the attribute for the provided action - - - - - - - Gets a specific action factory by its type - - - - - - - Gets all action types and their attribute annotations depending on the specified visibility - - - - - - - Graphics - Api/Graphics - - - Captures the specified Screen Region in the specified format - - - Captures the entire screen to a jpeg file on your desktop - - + Key2Joy.Core + + + + + Provides methods for retrieving property information and custom attributes. + Implements the interface. + + + + + Retrieves all properties from the given type. + + The type whose properties need to be retrieved. + + An enumerable of representing the properties of the given type. + + + + + Retrieves the custom associated with the given property. + + The property whose custom attribute needs to be retrieved. + + The associated with the property, or null if no such attribute exists. + + + + + Only applied to + + + + + Only applied to + + + + + Gets all configs and their property + + + + + + Manages user configurations being loaded from and saved to disk. + + + + The path to where the config file is located. + + + + Loads the configuration or creates a default one on disk. + + + + + Turns the plugin path into a relative one from the app directory + + + + + + + Sets a plugin as enabled, the permissions checksum is stored so no changes to the permissions + are accepted when loading the plugin later. + + Set permissionsChecksumOrNull to null to disable the plugin. + + + + + + + Provides an abstraction for retrieving property information and custom attributes. + + + + + Retrieves all properties from the given type. + + The type whose properties need to be retrieved. + + An enumerable of representing the properties of the given type. + + + + + Retrieves the custom associated with the given property. + + The property whose custom attribute needs to be retrieved. + + The associated with the property, or null if no such attribute exists. + + + + + Only applied to + + + + + Only applied to + + + + + Arms the mapping options so the triggers cause the actions to be executed. + + + Occurs when an illegal configuration can't be started + + + + Singleton client for communication with the Key2Joy service. + This is used by the Key2Joy.Cmd CLI to send commands to the service and + enable/disable mappings. + + + + + Sends a command to the main app, for example to enable/disable mappings. + + + + + + + Read the first byte and use it to get the type struct + + + + + + + Convert a byte identifier to command info by looking it up + + + + + + + Reads the full command of a certain type from the pipe + + + + + + + + Handle a command by calling the appropriate handler + + + + + + Directory where plugins are located + + + + + Trigger listeners that should explicitly loaded. This ensures that they're available for scripts + even if no mapping option is mapped to be triggered by it. + + + + + Ensures Key2Joy is running and ready to accept commands as long as the main loop does not end. + + + + Optionally a custom config manager (probably only useful for unit testing) + + + + + + + Starts Key2Joy, pausing until it's ready + + + + + A virtual-key code. The code must be a value in the range 1 to 254. + + + + + A hardware scan code for the key. + + + + + The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level. + + - LLKHF_EXTENDED + (KF_EXTENDED >> 8) + Test the extended-key flag. + + - LLKHF_LOWER_IL_INJECTED + 0x00000002 + Test the event-injected (from a process running at lower integrity level) flag. + + - LLKHF_INJECTED + 0x00000010 + Test the event-injected (from any process) flag. + + - LLKHF_ALTDOWN + (KF_ALTDOWN >> 8) + Test the context code. + + - LLKHF_UP + (KF_UP >> 8) + Test the transition-state flag. + + + + + The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message. + + + + + Additional information associated with the message. + + + + + The x- and y-coordinates of the cursor, in per-monitor-aware screen coordinates. + + + + + If the message is WM_MOUSEWHEEL, the high-order word of this member is the wheel delta. The low-order word is reserved. A positive value indicates that the wheel was rotated forward, away from the user; a negative value indicates that the wheel was rotated backward, toward the user. One wheel click is defined as WHEEL_DELTA, which is 120. + If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, and the low-order word is reserved.This value can be one or more of the following values.Otherwise, mouseData is not used. + + + + + The extended-key flag, event-injected Flags, context code, and transition-state flag. This member is specified as follows. An application can use the following values to test the keystroke Flags. Testing LLKHF_INJECTED (bit 4) will tell you whether the event was injected. If it was, then testing LLKHF_LOWER_IL_INJECTED (bit 1) will tell you whether or not the event was injected from a process running at lower integrity level. + + + + + The time stamp stamp for this message, equivalent to what GetMessageTime would return for this message. + + + + + Additional information associated with the message. + + + + + Represents the type of mouse movement. + + + + + Specifies relative movement to where the cursor is now. + + + + + Specifies absolute movement of the cursor on screen. + + + + + Represents the buttons on a mouse. + + + + + No button is pressed. + + + + + The left mouse button. + + + + + The right mouse button. + + + + + The middle mouse button. + + + + + The first extra mouse button. + + + + + The second extra mouse button. + + + + + The mouse wheel is moved upward. + + + + + The mouse wheel is moved downward. + + + + + Posted when the user double-clicks the first or second X button while the cursor is in + the nonclient area of a window. This message is posted to the window that contains the + cursor. If a window has captured the mouse, this message is not posted. + + + + + Represents a class for managing native hooks. + + + + + Loads a dynamic-link library (DLL) into the address space of the calling process. + + The name of the DLL to load. + A handle to the loaded DLL if successful; otherwise, IntPtr.Zero. + + + + Frees the loaded dynamic-link library (DLL). + + A handle to the loaded DLL. + true if successful, false otherwise. + + + + Installs an application-defined hook procedure into a hook chain. + + The type of hook to be installed. + A pointer to the hook procedure. + A handle to the DLL containing the hook procedure. + The identifier of the thread with which the hook procedure is to be associated. + A handle to the hook procedure if successful; otherwise, IntPtr.Zero. + + + + Removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. + + A handle to the hook to be removed. + true if successful, false otherwise. + + + + Passes the hook information to the next hook procedure in the current hook chain. + + A handle to the current hook. + The hook code passed to the current hook procedure. + The wParam value passed to the current hook procedure. + The lParam value passed to the current hook procedure. + The return value of the next hook procedure. + + + + Loads a dynamic-link library (DLL) into the address space of the calling process. + + The name of the DLL to load. + A handle to the loaded DLL if successful; otherwise, IntPtr.Zero. + + + + Frees the loaded dynamic-link library (DLL). + + A handle to the loaded DLL. + true if successful, false otherwise. + + + + Installs an application-defined hook procedure into a hook chain. + + The type of hook to be installed. + A pointer to the hook procedure. + A handle to the DLL containing the hook procedure. + The identifier of the thread with which the hook procedure is to be associated. + A handle to the hook procedure if successful; otherwise, IntPtr.Zero. + + + + Removes a hook procedure installed in a hook chain by the SetWindowsHookEx function. + + A handle to the hook to be removed. + true if successful, false otherwise. + + + + Passes the hook information to the next hook procedure in the current hook chain. + + A handle to the current hook. + The hook code passed to the current hook procedure. + The wParam value passed to the current hook procedure. + The lParam value passed to the current hook procedure. + The return value of the next hook procedure. + + + + The x-coordinate of the point. + + + + + The y-coordinate of the point. + + + + + The key/button is pressed down + + + + + The key/button is released (after having been pressed down) + + + + + All available press states + + + + + Represents a simulated gamepad device. + + + + + Gets the index of the simulated gamepad. + + + + + Plugs in the simulated gamepad. + + + + + Checks if the simulated gamepad is currently plugged in. + + True if the gamepad is plugged in; otherwise, false. + + + + Unplugs the simulated gamepad. + + + + + Simulates pressing and holding a control on the gamepad. + + The control to simulate. + The duration (in milliseconds) to hold the control (default is 50ms). + + + + Sets a specific control on the gamepad. + + The control to set. + + + + Releases a specific control on the gamepad. + + The control to release. + + + + Get the raw input state from the GamePad + + The raw input state of the gamepad. + + + + Resets the GamePad state to the natural at-rest stat + + + + + Update any changes made to the state to be reflected in the gamepad + + + + + Represents a service for managing simulated gamepad devices. + + + + + Gets a simulated gamepad by its index. + + The index of the gamepad to retrieve. + An instance of the simulated gamepad. + + + + Retrieves an array of all available simulated gamepad devices. + + An array of simulated gamepad instances. + + + + Initializes the simulated gamepad service. + + + + + Shuts down the simulated gamepad service. + + + + + Ensures that a specific gamepad is plugged in. + + The index of the gamepad to ensure is plugged in. + + + + Ensures that a specific gamepad is unplugged. + + The index of the gamepad to ensure is unplugged. + + + + Ensures that all simulated gamepads are unplugged. + + + + + Implementation based on https://github.com/DavidRieman/SimWinInput + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Left mouse button + + + + + Right mouse button + + + + + Control-break processing + + + + + Middle mouse button (three-button mouse) + + + + + Windows 2000/XP: X1 mouse button + + + + + Windows 2000/XP: X2 mouse button + + + + + BACKSPACE key + + + + + TAB key + + + + + CLEAR key + + + + + ENTER key + + + + + SHIFT key + + + + + CTRL key + + + + + ALT key + + + + + PAUSE key + + + + + CAPS LOCK key + + + + + Input Method Editor (IME) Kana mode + + + + + IME Hangul mode + + + + + IME Junja mode + + + + + IME final mode + + + + + IME Hanja mode + + + + + IME Kanji mode + + + + + ESC key + + + + + IME convert + + + + + IME nonconvert + + + + + IME accept + + + + + IME mode change request + + + + + SPACEBAR + + + + + PAGE UP key + + + + + PAGE DOWN key + + + + + END key + + + + + HOME key + + + + + LEFT ARROW key + + + + + UP ARROW key + + + + + RIGHT ARROW key + + + + + DOWN ARROW key + + + + + SELECT key + + + + + PRINT key + + + + + EXECUTE key + + + + + PRINT SCREEN key + + + + + INS key + + + + + DEL key + + + + + HELP key + + + + + 0 key + + + + + 1 key + + + + + 2 key + + + + + 3 key + + + + + 4 key + + + + + 5 key + + + + + 6 key + + + + + 7 key + + + + + 8 key + + + + + 9 key + + + + + A key + + + + + B key + + + + + C key + + + + + D key + + + + + E key + + + + + F key + + + + + G key + + + + + H key + + + + + I key + + + + + J key + + + + + K key + + + + + L key + + + + + M key + + + + + N key + + + + + O key + + + + + P key + + + + + Q key + + + + + R key + + + + + S key + + + + + T key + + + + + U key + + + + + V key + + + + + W key + + + + + X key + + + + + Y key + + + + + Z key + + + + + Left Windows key (Microsoft Natural keyboard) + + + + + Right Windows key (Natural keyboard) + + + + + Applications key (Natural keyboard) + + + + + Computer Sleep key + + + + + Numeric keypad 0 key + + + + + Numeric keypad 1 key + + + + + Numeric keypad 2 key + + + + + Numeric keypad 3 key + + + + + Numeric keypad 4 key + + + + + Numeric keypad 5 key + + + + + Numeric keypad 6 key + + + + + Numeric keypad 7 key + + + + + Numeric keypad 8 key + + + + + Numeric keypad 9 key + + + + + Multiply key + + + + + Add key + + + + + Separator key + + + + + Subtract key + + + + + Decimal key + + + + + Divide key + + + + + F1 key + + + + + F2 key + + + + + F3 key + + + + + F4 key + + + + + F5 key + + + + + F6 key + + + + + F7 key + + + + + F8 key + + + + + F9 key + + + + + F10 key + + + + + F11 key + + + + + F12 key + + + + + F13 key + + + + + F14 key + + + + + F15 key + + + + + F16 key + + + + + F17 key + + + + + F18 key + + + + + F19 key + + + + + F20 key + + + + + F21 key + + + + + F22 key, (PPC only) Key used to lock device. + + + + + F23 key + + + + + F24 key + + + + + NUM LOCK key + + + + + SCROLL LOCK key + + + + + Left SHIFT key + + + + + Right SHIFT key + + + + + Left CONTROL key + + + + + Right CONTROL key + + + + + Left MENU key + + + + + Right MENU key + + + + + Windows 2000/XP: Browser Back key + + + + + Windows 2000/XP: Browser Forward key + + + + + Windows 2000/XP: Browser Refresh key + + + + + Windows 2000/XP: Browser Stop key + + + + + Windows 2000/XP: Browser Search key + + + + + Windows 2000/XP: Browser Favorites key + + + + + Windows 2000/XP: Browser Start and Home key + + + + + Windows 2000/XP: Volume Mute key + + + + + Windows 2000/XP: Volume Down key + + + + + Windows 2000/XP: Volume Up key + + + + + Windows 2000/XP: Next Track key + + + + + Windows 2000/XP: Previous Track key + + + + + Windows 2000/XP: Stop Media key + + + + + Windows 2000/XP: Play/Pause Media key + + + + + Windows 2000/XP: Start Mail key + + + + + Windows 2000/XP: Select Media key + + + + + Windows 2000/XP: Start Application 1 key + + + + + Windows 2000/XP: Start Application 2 key + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Windows 2000/XP: For any country/region, the '+' key + + + + + Windows 2000/XP: For any country/region, the ',' key + + + + + Windows 2000/XP: For any country/region, the '-' key + + + + + Windows 2000/XP: For any country/region, the '.' key + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Used for miscellaneous characters; it can vary by keyboard. + + + + + Windows 2000/XP: Either the angle bracket key or the backslash key on the RT 102-key keyboard + + + + + Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key + + + + + Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. + The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, + see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP + + + + + Attn key + + + + + CrSel key + + + + + ExSel key + + + + + Erase EOF key + + + + + Play key + + + + + Zoom key + + + + + Reserved + + + + + PA1 key + + + + + Clear key + + + + + Declaration of external SendInput method + + + + + Define HARDWAREINPUT struct + + + + + The set of valid MapTypes used in MapVirtualKey + + + Source: http://pinvoke.net/default.aspx/user32/MapVirtualKey.html?diff=y + + + + + uCode is a virtual-key code and is translated into a scan code. + If it is a virtual-key code that does not distinguish between left- and + right-hand keys, the left-hand scan code is returned. + If there is no translation, the function returns 0. + + + + + uCode is a scan code and is translated into a virtual-key code that + does not distinguish between left- and right-hand keys. If there is no + translation, the function returns 0. + + + + + uCode is a virtual-key code and is translated into an unshifted + character value in the low-order word of the return value. Dead keys (diacritics) + are indicated by setting the top bit of the return value. If there is no + translation, the function returns 0. + + + + + Windows NT/2000/XP: uCode is a scan code and is translated into a + virtual-key code that distinguishes between left- and right-hand keys. If + there is no translation, the function returns 0. + + + + + Not currently documented + + + + + Represents the battery device types for XInput Game Controllers. + + + + + The device type is a gamepad. + + + + + The device type is a headset. + + + + + Represents the charge state of the battery for a wireless XInput device. + + + + + The battery is empty. + + + + + The battery is low. + + + + + The battery is at a medium charge level. + + + + + The battery is full. + + + + + Represents the type of battery for an XInput device. + + + + + This device is not connected. + + + + + Wired device, no battery. + + + + + Alkaline battery source. + + + + + Nickel Metal Hydride battery source. + + + + + Cannot determine the battery type. + + + + + Flags representing various capabilities of an XInput controller. + + + + + The device has an integrated voice device. + + + + + The device supports force feedback functionality. Note that these force-feedback features beyond rumble + are not currently supported through XINPUT on Windows. + + + + + The device is wireless. + + + + + The device supports plug-in modules. Note that plug-in modules like the text input device (TID) + are not supported currently through XINPUT on Windows. + + + + + The device lacks menu navigation buttons (START, BACK, DPAD). + + + + + requires a flag to specify which device type to retrieve + capabilities for. This is the only available flag. + + + + + Limit query to devices of Xbox 360 Controller type. + + + + + Provides data for the DevicePacketReceivedEventArgs event. + + + + + Gets the index of the device that has sent a packet. + + + + + Gets the state of the device. + + + + + Initializes a new instance of the class. + + The index of the device that has sent a packet. + The state of the device. + + + + Provides data for the DeviceStateChanged event. + + + + + Gets the index of the device that has changed state. + + + + + Gets the new state of the device. + + + + + Initializes a new instance of the class. + + The index of the device that has changed state. + The new state of the device. + + + + Specifies the subtype of an XInput device. + + + + + Unknown controller subtype. + + + + + Gamepad controller subtype. + Includes Left and Right Sticks, Left and Right Triggers, Directional Pad, + and all standard buttons (A, B, X, Y, START, BACK, LB, RB, LSB, RSB). + + + + + Racing wheel controller subtype. + Left Stick X reports the wheel rotation, Right Trigger is the acceleration pedal, + and Left Trigger is the brake pedal. Includes Directional Pad and most standard buttons + (A, B, X, Y, START, BACK, LB, RB). LSB and RSB are optional. + + + + + Arcade stick controller subtype. + Includes a Digital Stick that reports as a DPAD (up, down, left, right), and most standard buttons + (A, B, X, Y, START, BACK). The Left and Right Triggers are implemented as digital buttons and + report either 0 or 0xFF. LB, LSB, RB, and RSB are optional. + + + + + Flight stick controller subtype. + Includes a pitch and roll stick that reports as the Left Stick, a POV Hat which reports as the + Right Stick, a rudder (handle twist or rocker) that reports as Left Trigger, and a throttle control + as the Right Trigger. Includes support for a primary weapon (A), secondary weapon (B), and other + standard buttons (X, Y, START, BACK). LB, LSB, RB, and RSB are optional. + + + + + Dance pad controller subtype. + Includes the Directional Pad and standard buttons (A, B, X, Y) on the pad, plus BACK and START. + + + + + Guitar controller subtype. + The strum bar maps to DPAD (up and down), and the frets are assigned to A (green), B (red), + Y (yellow), X (blue), and LB (orange). Right Stick Y is associated with a vertical orientation sensor; + Right Stick X is the whammy bar. Includes support for BACK, START, DPAD (left, right). + Left Trigger (pickup selector), Right Trigger, RB, LSB (fret modifier), RSB are optional. + + + + + Alternate guitar controller subtype. + Supports a larger range of movement for the vertical orientation sensor. + + + + + Drum controller subtype. + The drum pads are assigned to buttons: A for green (Floor Tom), B for red (Snare Drum), + X for blue (Low Tom), Y for yellow (High Tom), and LB for the pedal (Bass Drum). + Includes Directional-Pad, BACK, and START. RB, LSB, and RSB are optional. + + + + + Bass guitar controller subtype. + Identical to Guitar, with the distinct subtype to simplify setup. + + + + + Arcade pad controller subtype. + Includes Directional Pad and most standard buttons (A, B, X, Y, START, BACK, LB, RB). + The Left and Right Triggers are implemented as digital buttons and report either 0 or 0xFF. + Left Stick, Right Stick, LSB, and RSB are optional. + + + + + Device type available in XINPUT_CAPABILITIES + + + + + The device is a game controller. + + + + + Represents a set of flags that indicate the state of various buttons on a device. + + + + + The Up button on the directional pad. + + + + + The Down button on the directional pad. + + + + + The Left button on the directional pad. + + + + + The Right button on the directional pad. + + + + + The Start button. + + + + + The Back button. + + + + + The Left Thumbstick button. + + + + + The Right Thumbstick button. + + + + + The Left Shoulder button. + + + + + The Right Shoulder button. + + + + + The A button. + + + + + The B button. + + + + + The X button. + + + + + The Y button. + + + + + Provides an interface for the XInput functionality. + + + + + Retrieves the state of a controller. + + Index of the gamer associated with the device. + Receives the current state. + Returns the result code. + + + + Sets the vibration state of a controller. + + Index of the gamer associated with the device. + The vibration information to send to the controller. + Returns the result code. + + + + Retrieves the capabilities of a controller. + + + Index of the gamer associated with the device. + Input flags that identify the device type. + Receives the capabilities. + Returns the result code. + + + + Retrieves the capabilities of a controller for the default device type (gamepad). + + + Index of the gamer associated with the device. + Receives the capabilities. + Returns the result code. + + + + Retrieves battery information for a controller. + + Index of the gamer associated with the device. + Which device on this user index. + Contains the level and types of batteries. + Returns the result code. + + + + Retrieves a keystroke event from a controller. + + Index of the gamer associated with the device. + Reserved for future use. + Pointer to an XINPUT_KEYSTROKE structure that receives an input event. + Returns the result code. + + + + Represents the interface for interacting with XInput devices. + + + + + Occurs when the state of a registered device changes. + + + + + Registers a device by its index for monitoring its state. + + The index of the device to register. + + + + Starts polling all registered devices for state changes. + + + + + Stops polling all registered devices. + + + + + Retrieves the current state of the specified device. + + The index of the device. + The current state of the device. + + + + Retrieves the capabilities of the specified device. + + The index of the device. + The capabilities of the device. + + + + Retrieves battery information for the specified device. + + The index of the device. + Specifies which type of device (e.g. gamepad, headset) on this user index to retrieve information for. + Battery information of the specified device. + + + + Retrieves a keystroke event from the specified device. + + The index of the device. + Keystroke data from the device. + + + + Vibrates the given device's left and or right motor by the specified intensity. + + The index of the device + Fraction (0-1) indicating left motor intensity + Fraction (0-1) indicating right motor intensity + How long to vibrate for + + + + Stops vibration of the given device. + + The index of the device + + + + Gets the active gamepad device indexes. + + + + + + Handles XInput functionality by calling native functions. + + + + + + + + + + + + + + + + + + + + + Get capabilities for the specified controller. + + + + + + + + Represents battery information for an XInput device, including its type and charge state. + + + + + Gets or sets the type of battery for the XInput device. + + + + + Gets or sets the charge state of the battery for the XInput device. + + + + + Returns a string representation of the XInputBatteryInformation struct. + + A string containing the battery type and charge level. + + + + Describes the capabilities of a connected controller. + The function returns this. + + + + + + Device type. + + + + + Subtype of the game controller. + + + + + Features of the controller. + + + + + XINPUT_GAMEPAD structure that describes available controller features + and control resolutions. + + + + + XINPUT_VIBRATION structure that describes available vibration functionality + and resolutions. + + + + + Device type. + + + + + Device sub type. + + + + + The capability flags. + + + + + Represents the state of a controller. + + + + + Can be used as a positive and negative value to filter left thumbstick input. + + + + + Can be used as a positive and negative value to filter right thumbstick input. + + + + + May be used as the value which bLeftTrigger and bRightTrigger must be greater than to register as pressed. This is optional, but often desirable. Xbox 360 Controller buttons do not manifest crosstalk. + + + + + + The current value of the left trigger analog control. The value is between 0 and 255. + + + + + The current value of the right trigger analog control. The value is between 0 and 255. + + + + + Left thumbstick x-axis value. Negative values signify down or to the left, + positive values signify up or to the right. The value is between -32768 and 32767. + A value of 0 is centered. Negative values signify down or to the left. Positive values + signify up or to the right. + + + + + Left thumbstick y-axis value. The value is between -32768 and 32767. + + + + + Right thumbstick x-axis value. The value is between -32768 and 32767. + + + + + Right thumbstick y-axis value. The value is between -32768 and 32767. + + + + + Checks if a specific button or buttons represented by the bitmask are pressed. + + The bitmask representing the button(s). + True if the button(s) are pressed, otherwise false. + + + + Checks if a specific button or buttons represented by the bitmask are present. + + The bitmask representing the button(s). + True if the button(s) are present, otherwise false. + + + + Returns all pressed buttons in a List. + + + + + + Checks if the thumb stick on the given side has moved past a certain threshold (or else the default deadzone) + + + + + + + + Checks if the trigger on a certain side has pulled back past a certain threshold (or else the default deadzone) + + + + + + + + Returns the stick delta for a given side as an exact axis fraction. + + + + + + + Returns the trigger delta for a given side as an exact axis fraction. + + + + + + + Copies the values from the source gamepad to the current instance. + + The source gamepad from which values should be copied. + + + + + + + + + + + + + Represents a structure containing information about a keystroke input event. + + + + + Virtual-key code of the key, button, or stick movement. + + + + + This member is unused and the value is zero. + + + + + Flags that indicate the keyboard state at the time of the input event. + + + + + Index of the signed-in gamer associated with the device. + + + + + HID code corresponding to the input. If there is no corresponding HID code, this value is zero. + + + + + Represents the result codes for XInput operations. + + Note that mentions: + "If the function fails, the return value is an error code defined in WinError.h. The function does not use SetLastError to set the calling thread's last-error code." + + This may mean that we'll have to add those possible error codes here, or at least the ones we're interested in. + + + + + The operation completed successfully. + + + + + The requested resource is empty or not found. + + + + + The XInput device is not connected or available. + + + + + Helps translate result codes from Native methods + + + + + Called when the state of a device changes. + + + + + Called whenever the device sends a new packet + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Represents the state of an Xbox 360 controller. + + + The member is incremented only if the status of the controller has changed since the controller was last polled. + + + + + State packet number. Indicates whether there have been any changes in the state of the controller. + If the member is the same in sequentially returned XInputState structures, the controller state has not changed. + + + + + XINPUT_GAMEPAD structure containing the current state of an Xbox 360 Controller. + + + + + Copies the state from another XInputState object. + + The source XInputState to copy from. + + + + + + + + + + + + + Represents motor speed levels for the vibration function of a controller. + + + + + Speed of the left motor. Valid values are in the range 0 to 65,535. + Zero signifies no motor use; 65,535 signifies 100 percent motor use. + + + + + Speed of the right motor. Valid values are in the range 0 to 65,535. + Zero signifies no motor use; 65,535 signifies 100 percent motor use. + + + + + Construct a new XInputVibration struct with the specified motor speeds as fractions. + + + + + + + Construct a new XInputVibration struct with the specified motor speeds. + + + + + + + Get the true speed for a motor by specifying a fraction of the maximum speed. + + The speed for a motor as a fraction of the maximum speed. + The true speed for a motor. + + + + Loads all actions in the assembly, optionally merging it with additional action types. + + + + + + Gets all action type factories + + + + + + Gets all action attributes + + + + + + Gets the attribute for the provided action + + + + + + + Gets a specific action factory by its type + + + + + + + Gets all action types and their attribute annotations depending on the specified visibility + + + + + + + Graphics + Api/Graphics + + + Captures the specified Screen Region in the specified format + + + Captures the entire screen to a jpeg file on your desktop + + - - - - Captures a region of 500 sq pixels around the cursor as a png to the desktop. - - + + + Captures a region of 500 sq pixels around the cursor as a png to the desktop. + + - - - - Captures a sequence of images from the screen and saves them to a folder on the desktop (frames/) - - + + + Captures a sequence of images from the screen and saves them to a folder on the desktop (frames/) + + frameCount)then ClearInterval(interval) end end, 1000 / framesPerSecond) ]]> - - - File path on device where to save the screen capture. The extension you specify decides the format. Supported extensions: .jpeg/.jpg, .png, .bmp, .gif(not animated), .ico, .emf, .exif, .tiff, .wmf - X position on screen. Defaults to first monitor X start. - Y position on screen. Defaults to first monitor Y start. - Width of region to capture. Defaults to (all) screens width. - Height of region to capture. Defaults to (all) screens height. - Graphics.CaptureScreen - - - - Graphics - Api/Graphics - - - Gets the color of a pixel at the given x and y position - - - Shows the color of the pixel at the mouse position - - + + File path on device where to save the screen capture. The extension you specify decides the format. Supported extensions: .jpeg/.jpg, .png, .bmp, .gif(not animated), .ico, .emf, .exif, .tiff, .wmf + X position on screen. Defaults to first monitor X start. + Y position on screen. Defaults to first monitor Y start. + Width of region to capture. Defaults to (all) screens width. + Height of region to capture. Defaults to (all) screens height. + Graphics.CaptureScreen + + + + Graphics + Api/Graphics + + + Gets the color of a pixel at the given x and y position + + + Shows the color of the pixel at the mouse position + + - - - A color object containing Red, Green and Blue color information - X position on screen - Y position on screen - Graphics.GetPixelColor - - - - Input - Api/Input - - - Simulate pressing or releasing (or both) gamepad buttons. - - - Shows how to press "A" on the gamepad for 500ms, then release it. - - + + A color object containing Red, Green and Blue color information + X position on screen + Y position on screen + Graphics.GetPixelColor + + + + Input + Api/Input + + + Simulate pressing or releasing (or both) gamepad buttons. + + + Shows how to press "A" on the gamepad for 500ms, then release it. + + - - - Button to simulate - Action to simulate - Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 - GamePad.Simulate - - - - Input - Api/Input - - - Reset the gamepad so the stick returns to the resting position (0,0) - - - Moves the left gamepad joystick halfway down and to the right, then resets after 500ms - - + + Button to simulate + Action to simulate + Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 + GamePad.Simulate + + + + Input + Api/Input + + + Reset the gamepad so the stick returns to the resting position (0,0) + + + Moves the left gamepad joystick halfway down and to the right, then resets after 500ms + + - - - Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 - GamePad.Reset - - - - Input - Api/Input - - - Simulate moving a gamepad joystick - - - Moves the left gamepad joystick halfway down and to the right, then resets after 500ms - - + + Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 + GamePad.Reset + + + + This describes what 'a lot of input movement' is, so we can scale + the delta and make the scaling numbers feel intuitive. + + + + + Which side to simulate + + + + + How much to simulate on the X axis. If null, then the inputbag of the input + that triggered this will be used if possible + + + + + How much to simulate on the Y axis. If null, then the inputbag of the input + that triggered this will be used if possible + + + + + If DeltaX is null, this is the scale factor to apply to the inputbag + + + + + If DeltaY is null, this is the scale factor to apply to the inputbag + + + + + After how many milliseconds should the stick be reset to 0,0? + + + + + Which gamepad to simulate + + + + + + + + Input + Api/Input + + + Simulate moving a gamepad joystick + + + Moves the left gamepad joystick halfway down and to the right, then resets after 500ms + + - - - The fraction by which to move the stick forward (negative) or backward (positive) - The fraction by which to move the stick right (positive) or left (negative) - Which gamepad stick to move, either GamePadStick.Left (default) or .Right - Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 - GamePad.SimulateMove - - - - Input - Api/Input - - - Gets the current cursor position - - - The code below prints 0, 0 when the cursor is held in the top left of the first monitor. - - + + The fraction by which to move the stick forward (negative) or backward (positive) + The fraction by which to move the stick right (positive) or left (negative) + Which gamepad stick to move, either GamePadStick.Left (default) or .Right + Which of 4 possible gamepads to simulate: 0 (default), 1, 2 or 3 + GamePad.SimulateMove + + + + Scales InputBag values to a reasonable range for gamepad sticks + + + + + + + + + + + Resets the stick to the center position if there was no input for a while + + + + + + + + + + + + + + + + Input + Api/Input + + + Gets the current cursor position + + + The code below prints 0, 0 when the cursor is held in the top left of the first monitor. + + - - - + + - - - A Point object with X and Y properties that represent the cursor X and Y - Cursor.GetPosition - - - - Input - Api/Input - - - Simulate pressing or releasing (or both) keyboard keys. - - Key to simulate - Action to simulate - Keyboard.Simulate - - - - Input - Api/Input - - - Tests if the provided keyboard key is currently pressed. - - Note: This function currently has trouble distinguishing between left and right keys. This means that `Keyboard.GetKeyDown(KeyboardKey.RightControl)` will return true even if the left control is pressed. - - You can find the keycodes in the KeyboardKey Enumeration. - - - Shows how to show all keys currently pressed. - - + + A Point object with X and Y properties that represent the cursor X and Y + Cursor.GetPosition + + + + Input + Api/Input + + + Simulate pressing or releasing (or both) keyboard keys. + + Key to simulate + Action to simulate + Keyboard.Simulate + + + + Input + Api/Input + + + Tests if the provided keyboard key is currently pressed. + + Note: This function currently has trouble distinguishing between left and right keys. This means that `Keyboard.GetKeyDown(KeyboardKey.RightControl)` will return true even if the left control is pressed. + + You can find the keycodes in the KeyboardKey Enumeration. + + + Shows how to show all keys currently pressed. + + - - - - Shows how to only simulate pressing "A" when shift is also held down. This allows binding to multiple keys, where one is the trigger and the rest of the inputs are checked in the script. - - + + + Shows how to only simulate pressing "A" when shift is also held down. This allows binding to multiple keys, where one is the trigger and the rest of the inputs are checked in the script. + + - - - The key to test for - True if the key is currently pressed down, false otherwise - Keyboard.GetKeyDown - - - - Input - Api/Input - - - Simulate pressing or releasing (or both) mouse buttons. - - Button to simulate - Action to simulate - Mouse.Simulate - - - - Input - Api/Input - - - Tests if the provided mouse button is currently pressed. - - You can find the button codes in . - - - Shows how to show all mouse buttons currently pressed. - - + + The key to test for + True if the key is currently pressed down, false otherwise + Keyboard.GetKeyDown + + + + Input + Api/Input + + + Simulate pressing or releasing (or both) mouse buttons. + + Button to simulate + Action to simulate + Mouse.Simulate + + + + Input + Api/Input + + + Tests if the provided mouse button is currently pressed. + + You can find the button codes in . + + + Shows how to show all mouse buttons currently pressed. + + - - - The button to test for - True if the button is currently pressed down, false otherwise - Mouse.GetButtonDown - - - - Input - Api/Input - - - Simulate moving the mouse - - - Nudges the cursor 100 pixels to the left from where it is now. - - + + The button to test for + True if the button is currently pressed down, false otherwise + Mouse.GetButtonDown + + + + Input + Api/Input + + + Simulate moving the mouse + + + Nudges the cursor 100 pixels to the left from where it is now. + + - - - - Moves the cursor to an absolute position on the screen. - - + + + Moves the cursor to an absolute position on the screen. + + - - - X coordinate to move by/to - Y coordinate to move by/to - Whether to move relative to the current cursor position (default) or to an absolute position on screen - Mouse.SimulateMove - - - - The possible commands to run in the app. - - - - - Aborts listening for triggers - - - - - Recreate the scripting environment (loses all variables, functions and other changes scripts made) - - - - - Logic - Api/Logic - - - Execute a command in this app. - - See the AppCommand Enumeration for a list of available commands. - - Command to execute - App.Command - - - - Logic - Api/Logic - - - Cancels an interval previously established by calling SetInterval() - - - Shows how to count up to 3 every second and then stop by using ClearInterval(); - - + + X coordinate to move by/to + Y coordinate to move by/to + Whether to move relative to the current cursor position (default) or to an absolute position on screen + Mouse.SimulateMove + + + + The possible commands to run in the app. + + + + + Aborts listening for triggers + + + + + Recreate the scripting environment (loses all variables, functions and other changes scripts made) + + + + + Logic + Api/Logic + + + Execute a command in this app. + + See the AppCommand Enumeration for a list of available commands. + + Command to execute + App.Command + + + + Logic + Api/Logic + + + Cancels an interval previously established by calling SetInterval() + + + Shows how to count up to 3 every second and then stop by using ClearInterval(); + + { Print(count++); - + if(count == 3) clearInterval(intervalId); }, 1000); - + Print(intervalId); ]]> - - - ClearInterval - Id returned by SetInterval to cancel - - - - Logic - Api/Logic - - - Cancels a timeout previously established by calling SetTimeout() - - - Shows how to set and immediately cancel a timeout. - - + + ClearInterval + Id returned by SetInterval to cancel + + + + Logic + Api/Logic + + + Cancels a timeout previously established by calling SetTimeout() + + + Shows how to set and immediately cancel a timeout. + + { Print("You shouldn't see this because the timeout will have been cancelled!"); }, 1000); - + Print(timeoutID); - + clearTimeout(timeoutID); ]]> - - - ClearTimeout - Id returned by SetTimeout to cancel - - - - Logic - Api/Logic - - - Displays a MessageBox with the given text - - The text to display - MessageBox.Show - - - - Logic - Api/Logic - - - Execute functions whilst waiting the specified time between them. - - The first function is executed immediately. - - - Shows how to count down from 3 and execute a command using Lua. - - + + ClearTimeout + Id returned by SetTimeout to cancel + + + + Logic + Api/Logic + + + Displays a MessageBox with the given text + + The text to display + MessageBox.Show + + + + Logic + Api/Logic + + + Execute functions whilst waiting the specified time between them. + + The first function is executed immediately. + + + Shows how to count down from 3 and execute a command using Lua. + + - - - Time to wait (in milliseconds) between function calls - One or more functions to execute - SetDelayedFunctions - - - - Logic - Api/Logic - - - Repeatedly calls a function or executes a code snippet, with a fixed time delay between each call - - - Shows how to count up to 10 every second and then stop by using ClearInterval(); - - + + Time to wait (in milliseconds) between function calls + One or more functions to execute + SetDelayedFunctions + + + + Logic + Api/Logic + + + Repeatedly calls a function or executes a code snippet, with a fixed time delay between each call + + + Shows how to count up to 10 every second and then stop by using ClearInterval(); + + - - - Function to execute after each wait - Time to wait (in milliseconds) - Zero or more extra parameters to pass to the function - An interval id that can be removed with clearInterval - SetInterval - - - - Logic - Api/Logic - - - Timeout for the specified duration in milliseconds, then execute the callback - - - Shows how to count down from 3 and execute a command using Javascript. - - + + Function to execute after each wait + Time to wait (in milliseconds) + Zero or more extra parameters to pass to the function + An interval id that can be removed with clearInterval + SetInterval + + + + Logic + Api/Logic + + + Timeout for the specified duration in milliseconds, then execute the callback + + + Shows how to count down from 3 and execute a command using Javascript. + + - - - - Shows how to count down from 3 each second and execute a command using Lua. - - + + + Shows how to count down from 3 each second and execute a command using Lua. + + - - - Function to execute after the wait - Time to wait (in milliseconds) - Zero or more extra parameters to pass to the function - A timeout id that can be removed with clearTimeout - SetTimeout - - - - Called to register scripting methods on the environment. - - The method to be exposed to scripting - The action on which the exposed method resides - - - - Register a transformer for certain types coming from scripts. - The transformer will get the parameter value and the type of the method parameter. - The transformer must return an object that will be passed to the method. - - - - - - - Will try to transform the parameter to the type of the method parameter. - - - - - - - - MethodInfo that can be bound to scripts - - - - - - Lua iterator 'Next' implementation. Gets the collection and current index - - - + + Function to execute after the wait + Time to wait (in milliseconds) + Zero or more extra parameters to pass to the function + A timeout id that can be removed with clearTimeout + SetTimeout + + + + Called to register scripting methods on the environment. + + The method to be exposed to scripting + The action on which the exposed method resides + + + + Register a transformer for certain types coming from scripts. + The transformer will get the parameter value and the type of the method parameter. + The transformer must return an object that will be passed to the method. + + + + + + + Will try to transform the parameter to the type of the method parameter. + + + + + + + + MethodInfo that can be bound to scripts + + + + + + Lua iterator 'Next' implementation. Gets the collection and current index + + + - - Collection to be iterated (always null? :/) - Null if first call, the current index otherwise - - The key/index of the collection - - - - Returns a function that, when called, will return the next value in the collection. - - - - - - - Util - Api/Util - - - Gets the current system time UNIX in seconds - - - The code below prints 1661456521 to the logs if the system time is 19:42:01 (GMT) on the 25th of August, 2022. - - + Collection to be iterated (always null? :/) + Null if first call, the current index otherwise + + The key/index of the collection + + + + Returns a function that, when called, will return the next value in the collection. + + + + + + + Util + Api/Util + + + Gets the current system time UNIX in seconds + + + The code below prints 1661456521 to the logs if the system time is 19:42:01 (GMT) on the 25th of August, 2022. + + - - - Time since UNIX in seconds - Util.GetUnixTimeSeconds - - - - Util - Api/Util - - - Expands system environment variables (See also: https://ss64.com/nt/syntax-variables.html). - - - Demonstrates how to get the home drive - - + + Time since UNIX in seconds + Util.GetUnixTimeSeconds + + + + Util + Api/Util + + + Expands system environment variables (See also: https://ss64.com/nt/syntax-variables.html). + + + Demonstrates how to get the home drive + + - - - String containing expanded path - The path to expand - Util.PathExpand - - - - Windows - Api/Windows - - - Find a window of a piece of software currently running. - - Window class name - Optional window title - Handle for the window - Window.Find - - - - Windows - Api/Windows - - - Fetches all windows of software currently running. - - List with handles of all the windows - Window.GetAll - - - - Windows - Api/Windows - - - Get the class name for a specified Window. - - You can use Window.GetAllAction, Window.FindAction or Window.GetForegroundAction to get handles. - - The window handle to get the class for - Class name for the window - Window.GetClass - - - - Windows - Api/Windows - - - Get the handle of a software's window that is currently in the foreground. - - Handle for the window - Window.GetForeground - - - - Windows - Api/Windows - - - Get the title of a software's window. - - You can use Window.GetAll, Window.Find or Window.GetForeground to get handles. - - The window handle to get the class for - Title of the window - Window.GetTitle - - - - Check all types for the ExposedEnumeration attribute and store them for later use. Optionally merging it with additional enumerations. - - - - - - Gets all exposed enumerations - - - - - - Prevent recursion by not including this converter in child (de)serializations - - - - - - - Check all types for the MappingControl attribute and store them for later use. Optionally merging it with additional Mapping Controls. - - - - - - Gets all mapping controls and their targetted typename - - - - - - Gets a specific mapping control factory by its type name - - - - - - - Subclasses MUST call this to have their actions executed. - - Even when they know no actions are listening, they should call this. This - lets events provide other mapped options to be injected. - - - - - - - - Called to setup the options panel with a trigger - - - - - - Called when the options panel should modify a resulting trigger - - - - - - Called when the options on a trigger change - - - - - Loads all triggers in the assembly, optionally merging it with additional trigger types. - - - - - - Gets all trigger types and their attribute annotations - - - - - - - Gets all trigger types and their attribute annotations depending on the specified visibility - - - - - - - Creates instances of the Control, simply using Activator.CreateInstance - - - - - Creates instances of the Control, simply using Activator.CreateInstance - - - - - Creates the Control by commanding the PluginHostProxy to create it. - - - - - Since we can't get the Type in the other appdomain, we return the host/contract class it derives from instead. - - - - - - Creates instances of types, simply using Activator.CreateInstance - - - - - Creates instances of types, simply using Activator.CreateInstance - - - - - Creates the type by commanding the PluginHostProxy to create it. - - - - - Since we can't get the Type in the other appdomain, we return the host/contract class it derives from instead. - - - - - - Ensures the typename is valid, splitting the long variant into its short form - - - - - - - Gets the typename, even if the object is a proxy - - - - - - - - - Gets the typename, even if the object is a proxy - - - - - - - - Plugin customizations - - - - Starts the plugin host process, setting up for communication: - - A named pipe for signalling from the plugin (child) to the host (parent) - - An IPC channel for communication towards the plugin (with authority) - - - - - - - - - - - - - Gets the plugin types' enumeration names and values, and adds them to the list of exposed enumerations. - - - - - - - Asks the plugin to construct the WPF FrameworkElement. Then places it inside an ElementHost for use in WinForms. - Can return null if the plugin crashes during creation. - - - - - - - Asks the plugin to construct the given type. - - - - - - - Plugin customizations - - - - Loads plugins from the specified directory - - The absolute path to the directory containing the plugins - - - - - Disables the plugin for next load. Note that this doesnt unload resources already - started by the plugin - TODO: Fully unload plugin - - - - - - A strongly-typed resource class, for looking up localized strings, etc. - - - - - Returns the cached ResourceManager instance used by this class. - - - - - Overrides the current thread's CurrentUICulture property for all - resource lookups using this strongly typed resource class. - - - - - Looks up a localized resource of type System.Byte[]. - - - - - Tests the given pathFormat with a number to see if it exists, if it does it tries again while increasing the number until an available filename is found. - - A string containing %VERSION% that will become the filePath - The number to place in %VERSION% on first attempt. Increments if not available. - The available file path - - - - Returns the image format for the given extension. - - - - - - - - Utilities for JSON deserializing, based on: - Source: https://github.com/dotnet/runtime/issues/29538#issuecomment-1330494636 - - - - - Deserializes json into an existing object. - - - - - - - - - Source: https://stackoverflow.com/a/457708 - - - - - - - - Copies a given array to a new array of the target type, e.g: from object[] to string[]. - - - - - - + + + String containing expanded path + The path to expand + Util.PathExpand + + + + Windows + Api/Windows + + + Find a window of a piece of software currently running. + + Window class name + Optional window title + Handle for the window + Window.Find + + + + Windows + Api/Windows + + + Fetches all windows of software currently running. + + List with handles of all the windows + Window.GetAll + + + + Windows + Api/Windows + + + Get the class name for a specified Window. + + You can use Window.GetAllAction, Window.FindAction or Window.GetForegroundAction to get handles. + + The window handle to get the class for + Class name for the window + Window.GetClass + + + + Windows + Api/Windows + + + Get the handle of a software's window that is currently in the foreground. + + Handle for the window + Window.GetForeground + + + + Windows + Api/Windows + + + Get the title of a software's window. + + You can use Window.GetAll, Window.Find or Window.GetForeground to get handles. + + The window handle to get the class for + Title of the window + Window.GetTitle + + + + More detailed specification of direction than . + + + + + A fraction from -1 to 1, where -1 is left and 1 is right. + + + + + A fraction from -1 to 1, where -1 is up and 1 is down. + + + + + Creates a new instance of . + + + + + + + + + + + + + + + + + + + Check all types for the ExposedEnumeration attribute and store them for later use. Optionally merging it with additional enumerations. + + + + + + Gets all exposed enumerations + + + + + + Prevent recursion by not including this converter in child (de)serializations + + + + + + + Check all types for the MappingControl attribute and store them for later use. Optionally merging it with additional Mapping Controls. + + + + + + Gets all mapping controls and their targetted typename + + + + + + Gets a specific mapping control factory by its type name + + + + + + + + + + Buttons that were pressed since the last update. + + + + + Buttons that were released since the last update. + + + + + The raw state of the gamepad. + + + + + + + + + + + + + + This method is called when the state of a gamepad has changed. + We'll use it to find in the lookup which mapped options are triggered based on + the triggered device index and margins (using ). + + + + + + + The raw data from the gamepad for the left stick. + + + + + The raw data from the gamepad for the right stick. + + + + + Which gamepad index activates this trigger? + + + + + Which stick activates this trigger? + + + + + With what margin should the stick be moved to trigger? + If null then this trigger will be fired on any move (taking into account the default deadzone). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This method is called when the state of a gamepad has changed. + We'll use it to find in the lookup which mapped options are triggered based on + the triggered device index and margins (using ). + + + + + + + How much the left trigger was pulled back. + + + + + How much the right trigger was pulled back. + + + + + Which gamepad index activates this trigger? + + + + + Which stick activates this trigger? + + + + + With what margin should the trigger be pulled back to activate? + If null then this trigger will be fired on any move (taking into account the default deadzone). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This method is called when the state of a gamepad has changed. + We'll use it to find in the lookup which mapped options are triggered based on + the triggered device index and margins (using ). + + + + + + + Called to setup the options panel with a trigger + + + + + + Called when the options panel should modify a resulting trigger + + + + + + Called when the options on a trigger change + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The direction that the mouse must move in order to trigger this action. + + + + + + + + + + + + + + + + + + + + Base class for trigger listeners that listen for press and release events. + + + + + + Quick lookup by hash for mappings bound to pressing input down + + + + + Quick lookup by hash for mappings bound to releasing input + + + + + + + + Loads all triggers in the assembly, optionally merging it with additional trigger types. + + + + + + Gets all trigger types and their attribute annotations + + + + + + + Gets all trigger types and their attribute annotations depending on the specified visibility + + + + + + + Creates instances of the Control, simply using Activator.CreateInstance + + + + + Creates instances of the Control, simply using Activator.CreateInstance + + + + + Creates the Control by commanding the PluginHostProxy to create it. + + + + + Since we can't get the Type in the other appdomain, we return the host/contract class it derives from instead. + + + + + + Creates instances of types, simply using Activator.CreateInstance + + + + + Creates instances of types, simply using Activator.CreateInstance + + + + + Creates the type by commanding the PluginHostProxy to create it. + + + + + Since we can't get the Type in the other appdomain, we return the host/contract class it derives from instead. + + + + + + Ensures the typename is valid, splitting the long variant into its short form + + + + + + + Gets the typename, even if the object is a proxy + + + + + + + + + Gets the typename, even if the object is a proxy + + + + + + + + Plugin customizations + + + + Starts the plugin host process, setting up for communication: + - A named pipe for signalling from the plugin (child) to the host (parent) + - An IPC channel for communication towards the plugin (with authority) + + + + + + + + + + + + Gets the plugin types' enumeration names and values, and adds them to the list of exposed enumerations. + + + + + + + Asks the plugin to construct the WPF FrameworkElement. Then places it inside an ElementHost for use in WinForms. + Can return null if the plugin crashes during creation. + + + + + + + Asks the plugin to construct the given type. + + + + + + + Plugin customizations + + + + Loads plugins from the specified directory + + The absolute path to the directory containing the plugins + + + + + Disables the plugin for next load. Note that this doesnt unload resources already + started by the plugin + TODO: Fully unload plugin + + + + + + A strongly-typed resource class, for looking up localized strings, etc. + + + + + Returns the cached ResourceManager instance used by this class. + + + + + Overrides the current thread's CurrentUICulture property for all + resource lookups using this strongly typed resource class. + + + + + Looks up a localized resource of type System.Byte[]. + + + + + Tests the given pathFormat with a number to see if it exists, if it does it tries again while increasing the number until an available filename is found. + + A string containing %VERSION% that will become the filePath + The number to place in %VERSION% on first attempt. Increments if not available. + The available file path + + + + Returns the image format for the given extension. + + + + + + + + Utilities for JSON deserializing, based on: + Source: https://github.com/dotnet/runtime/issues/29538#issuecomment-1330494636 + + + + + Deserializes json into an existing object. + + + + + + + + + Source: https://stackoverflow.com/a/457708 + + + + + + + + Copies a given array to a new array of the target type, e.g: from object[] to string[]. + + + + + + diff --git a/Support/Key2Joy.Tests/Contracts/Mappings/Actions/ActionOptionsControl.cs b/Support/Key2Joy.Tests/Contracts/Mappings/Actions/ActionOptionsControl.cs new file mode 100644 index 00000000..03c30762 --- /dev/null +++ b/Support/Key2Joy.Tests/Contracts/Mappings/Actions/ActionOptionsControl.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using Key2Joy.Contracts.Mapping.Actions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Key2Joy.Tests.Contracts.Mappings.Actions; + +[TestClass] +public class ActionOptionsControl +{ + private void CheckMembers(MemberInfo[] membersFromT1, MemberInfo[] membersFromT2) + { + Assert.AreEqual(membersFromT1.Length, membersFromT2.Length, $"Different count for members between {typeof(T1).Name} and {typeof(T2).Name}."); + + for (var i = 0; i < membersFromT1.Length; i++) + { + var memberFromT1 = membersFromT1[i]; + var memberFromT2 = membersFromT2[i]; + + Assert.AreEqual(memberFromT1.Name, memberFromT2.Name, $"Different member names for members at index {i} between {typeof(T1).Name} and {typeof(T2).Name}."); + + if (memberFromT1 is MethodInfo methodFromT1 && memberFromT2 is MethodInfo methodFromT2) + { + Assert.AreEqual( + methodFromT1.GetParameters().Length, + methodFromT2.GetParameters().Length, + $"Different parameter count for methods named {methodFromT1.Name} between {typeof(T1).Name} and {typeof(T2).Name}." + ); + } + } + } + + /// + /// Test that IPluginActionOptionsControl and IActionOptionsControl have the same methods with the same + /// parameter counts only difference is that IPluginActionOptionsControl has PluginAction as the type + /// where IActionOptionsControl has AbstractAction + /// + [TestMethod] + public void IPluginActionOptionsControl_And_IActionOptionsControl_HaveSameMembers() + { + // Check Methods + this.CheckMembers( + typeof(IPluginActionOptionsControl).GetMethods(), + typeof(IActionOptionsControl).GetMethods() + ); + + // Check Properties + this.CheckMembers( + typeof(IPluginActionOptionsControl).GetProperties(), + typeof(IActionOptionsControl).GetProperties() + ); + + // Check Events + this.CheckMembers( + typeof(IPluginActionOptionsControl).GetEvents(), + typeof(IActionOptionsControl).GetEvents() + ); + } +} diff --git a/Support/Key2Joy.Tests/Core/Config/MockConfigManager.cs b/Support/Key2Joy.Tests/Core/Config/MockConfigManager.cs index 0b5600bd..49503079 100644 --- a/Support/Key2Joy.Tests/Core/Config/MockConfigManager.cs +++ b/Support/Key2Joy.Tests/Core/Config/MockConfigManager.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Text; using Key2Joy.Config; using Key2Joy.Mapping; @@ -35,6 +36,23 @@ internal static string GetMockMappingProfilePath(string profileFileName = "") protected override string GetAppDataDirectory() => GetMockAppDataDirectory(); + private static string AdjustAndWriteContents(string contents, string targetPath) + { + var assemblyPath = Path.GetDirectoryName(typeof(Gui.Program).Assembly.Location); + + static string EscapePath(string path) => path.Replace("\\", "\\\\"); + + contents = contents.Replace("%TEST_ASSEMBLY_PATH%", EscapePath(assemblyPath)); + contents = contents.Replace("%TEST_APP_DATA_PATH%", EscapePath(Path.Combine(assemblyPath, GetMockAppDataDirectory()))); + + // We must trim the newline off the end, or else the Json Serializer will Save and not match the stub. + contents = contents.TrimEnd('\r', '\n'); + + File.WriteAllText(targetPath, contents); + + return contents; + } + /// /// Copies the specified stub to the config or mapping profile path. /// @@ -57,19 +75,30 @@ public static string CopyStub(string stubPath, string targetPath) } var contents = File.ReadAllText(stubPath); - var assemblyPath = Path.GetDirectoryName(typeof(Gui.Program).Assembly.Location); - static string EscapePath(string path) => path.Replace("\\", "\\\\"); + return AdjustAndWriteContents(contents, targetPath); + } - contents = contents.Replace("%TEST_ASSEMBLY_PATH%", EscapePath(assemblyPath)); - contents = contents.Replace("%TEST_APP_DATA_PATH%", EscapePath(Path.Combine(assemblyPath, GetMockAppDataDirectory()))); + /// + /// Copies the current default profile to the target path. + /// Gets it from the assembly's resources. + /// + /// + /// + public static string CopyStubCurrentDefaultProfile(string targetPath) + { + var directory = Path.GetDirectoryName(targetPath); - // We must trim the newline off the end, or else the Json Serializer will Save and not match the stub. - contents = contents.TrimEnd('\r', '\n'); + if (!string.IsNullOrWhiteSpace(directory) + && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } - File.WriteAllText(targetPath, contents); + var fileContents = MappingProfile.GetDefaultProfileContents(); + var contents = Encoding.UTF8.GetString(fileContents); - return contents; + return AdjustAndWriteContents(contents, targetPath); } /// diff --git a/Support/Key2Joy.Tests/Core/Config/Stubs/current-config.json b/Support/Key2Joy.Tests/Core/Config/Stubs/current-config.json index 07298f19..99f55284 100644 --- a/Support/Key2Joy.Tests/Core/Config/Stubs/current-config.json +++ b/Support/Key2Joy.Tests/Core/Config/Stubs/current-config.json @@ -1,5 +1,6 @@ { "LastInstallPath": "%TEST_ASSEMBLY_PATH%\\Key2Joy.exe", + "SelectedViewMappingGroupType": 1, "ShouldCloseButtonMinimize": true, "OverrideDefaultTriggerBehaviour": true, "LastLoadedProfile": "%TEST_APP_DATA_PATH%\\Profiles\\default-profile.k2j.json", diff --git a/Support/Key2Joy.Tests/Core/Config/Stubs/current-default-profile.k2j.json b/Support/Key2Joy.Tests/Core/Config/Stubs/current-default-profile.k2j.json deleted file mode 100644 index 2c399d8e..00000000 --- a/Support/Key2Joy.Tests/Core/Config/Stubs/current-default-profile.k2j.json +++ /dev/null @@ -1,947 +0,0 @@ -{ - "mappedOptions": [ - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadUp", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Up", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadDown", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Down", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadLeft", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Left", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadRight", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Right", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Start", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F1", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Back", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F2", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickClick", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "LControlKey", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickClick", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "RControlKey", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftShoulder", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Q", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightShoulder", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "E", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "A", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "B", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Z", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "X", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "R", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Y", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Y", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftTrigger", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D1", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightTrigger", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D2", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickLeft", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "A", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickRight", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickUp", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "W", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickDown", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "S", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Logic.AppCommandAction", - "options": { - "Command": "Abort", - "Name": "Run App Command \u0027{0}\u0027" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Escape", - "PressState": "Press", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Scripting.LuaScriptAction", - "options": { - "Script": "print(\u0022Shift \u002B A was pressed\u0022)", - "IsScriptPath": false, - "Name": "Lua Script: {0}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Logic.CombinedTrigger", - "options": { - "Triggers": [ - { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "A", - "PressState": "Press", - "Name": null - } - }, - { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "LShiftKey", - "PressState": "Press", - "Name": null - } - } - ], - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Logic.SequenceAction", - "options": { - "Name": "Run Sequence: {0}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Forward", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Logic.AppCommandAction", - "options": { - "Command": "Abort", - "Name": "Run App Command \u0027{0}\u0027" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Logic.CombinedTrigger", - "options": { - "Triggers": [ - { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D6", - "PressState": "Press", - "Name": null - } - } - ], - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickUp", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "W", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickRight", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickLeft", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "A", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickDown", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "S", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickUp", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Forward", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickDown", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Backward", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickLeft", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Left", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickRight", - "PressState": "Press", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Right", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadUp", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Up", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadRight", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Right", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadDown", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Down", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadLeft", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Left", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftTrigger", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "D1", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "DPadRight", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Right", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Start", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F1", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Back", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F2", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftStickClick", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "LControlKey", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickClick", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "RControlKey", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "LeftShoulder", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Q", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightShoulder", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "E", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "A", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "F", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "B", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Z", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "X", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "R", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "Y", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Keyboard.KeyboardTrigger", - "options": { - "Keys": "Y", - "PressState": "Release", - "Name": null - } - } - }, - { - "action": { - "$type": "Key2Joy.Mapping.Actions.Input.GamePadAction", - "options": { - "Control": "RightStickDown", - "PressState": "Release", - "GamePadIndex": 0, - "Name": "{1} {0} on GamePad #{2}" - } - }, - "trigger": { - "$type": "Key2Joy.Mapping.Triggers.Mouse.MouseMoveTrigger", - "options": { - "AxisBinding": "Backward", - "Name": null - } - } - } - ], - "name": "Default Profile", - "version": 6 -} diff --git a/Support/Key2Joy.Tests/Core/Interop/Commands/CommandInfoTests.cs b/Support/Key2Joy.Tests/Core/Interop/Commands/CommandInfoTests.cs index ad97d06b..26c85502 100644 --- a/Support/Key2Joy.Tests/Core/Interop/Commands/CommandInfoTests.cs +++ b/Support/Key2Joy.Tests/Core/Interop/Commands/CommandInfoTests.cs @@ -3,8 +3,6 @@ using System; using Key2Joy.Interop.Commands; using CommonServiceLocator; -using Key2Joy.Config; -using Key2Joy.Tests.Core.Config; using Key2Joy.Util; namespace Key2Joy.Tests.Core.Interop.Commands; diff --git a/Support/Key2Joy.Tests/Core/Interop/InteropClientTests.cs b/Support/Key2Joy.Tests/Core/Interop/InteropClientTests.cs index 64027397..3c6a7f95 100644 --- a/Support/Key2Joy.Tests/Core/Interop/InteropClientTests.cs +++ b/Support/Key2Joy.Tests/Core/Interop/InteropClientTests.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Key2Joy.Tests.Testing; using Key2Joy.Interop.Commands; +using Key2Joy.Contracts.Util; namespace Key2Joy.Tests.Core.Interop; @@ -72,7 +73,7 @@ public async Task SendCommand_Successful() await TestUtilities.TestAsyncMethodWithTimeout( tcs.Task, - TimeSpan.FromMilliseconds(50) // The result should return almost instantly, but let's give laggy tests some space to breathe + TimingHelper.FromMilliseconds(50) // The result should return almost instantly, but let's give laggy tests some space to breathe ); Assert.IsNotNull(receivedCommand); diff --git a/Support/Key2Joy.Tests/Core/Interop/InteropServerTests.cs b/Support/Key2Joy.Tests/Core/Interop/InteropServerTests.cs index 1be2a58a..e09866ba 100644 --- a/Support/Key2Joy.Tests/Core/Interop/InteropServerTests.cs +++ b/Support/Key2Joy.Tests/Core/Interop/InteropServerTests.cs @@ -82,7 +82,7 @@ public void HandleEnableCommand_Enables() this.managerMock.Setup(m => m.GetIsArmed(null)).Returns(false); var mappingProfilePath = MockConfigManager.GetMockMappingProfilePath("default-profile.k2j.json"); - MockConfigManager.CopyStub("current-default-profile.k2j.json", mappingProfilePath); + MockConfigManager.CopyStubCurrentDefaultProfile(mappingProfilePath); var command = new EnableCommand() { ProfilePath = mappingProfilePath diff --git a/Support/Key2Joy.Tests/Core/Key2JoyManagerTests.cs b/Support/Key2Joy.Tests/Core/Key2JoyManagerTests.cs index 3f7a26f8..b936ef8f 100644 --- a/Support/Key2Joy.Tests/Core/Key2JoyManagerTests.cs +++ b/Support/Key2Joy.Tests/Core/Key2JoyManagerTests.cs @@ -27,8 +27,6 @@ public MockTrigger(string name) : base(name) { } public override AbstractTriggerListener GetTriggerListener() => throw new System.NotImplementedException(); - - public override string GetUniqueKey() => throw new System.NotImplementedException(); } public class MockTriggerListener : CoreTriggerListener diff --git a/Support/Key2Joy.Tests/Core/LowLevelInput/GamePad/GamePadTests.cs b/Support/Key2Joy.Tests/Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadTests.cs similarity index 76% rename from Support/Key2Joy.Tests/Core/LowLevelInput/GamePad/GamePadTests.cs rename to Support/Key2Joy.Tests/Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadTests.cs index cf2c04e9..cb52b4d8 100644 --- a/Support/Key2Joy.Tests/Core/LowLevelInput/GamePad/GamePadTests.cs +++ b/Support/Key2Joy.Tests/Core/LowLevelInput/SimulatedGamePad/SimulatedGamePadTests.cs @@ -1,26 +1,33 @@ using System; using System.Linq; -using Key2Joy.LowLevelInput.GamePad; +using CommonServiceLocator; +using Key2Joy.LowLevelInput.SimulatedGamePad; +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Util; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -namespace Key2Joy.Tests.Core.LowLevelInput.GamePad; +namespace Key2Joy.Tests.Core.LowLevelInput.SimulatedGamePad; [TestClass] -public class GamePadTests +public class SimulatedGamePadTests { private const int NUM_GAMEPADS = 4; private SimulatedGamePadService gamePadService; - private Mock[] mockedGamePads; + private Mock[] mockedGamePads; [TestInitialize] public void Initialize() { - this.mockedGamePads = new Mock[NUM_GAMEPADS]; + var serviceLocator = new DependencyServiceLocator(); + ServiceLocator.SetLocatorProvider(() => serviceLocator); + serviceLocator.Register(new XInputService()); + + this.mockedGamePads = new Mock[NUM_GAMEPADS]; for (var i = 0; i < NUM_GAMEPADS; i++) { var isPluggedIn = false; - this.mockedGamePads[i] = new Mock(); + this.mockedGamePads[i] = new Mock(); this.mockedGamePads[i].Setup(g => g.GetIsPluggedIn()).Returns(() => isPluggedIn); this.mockedGamePads[i].Setup(g => g.PlugIn()).Callback(() => isPluggedIn = true); this.mockedGamePads[i].Setup(g => g.Unplug()).Callback(() => isPluggedIn = false); @@ -41,7 +48,7 @@ public void Initialize() [TestMethod] public void SimulatedGamePadService_ConstructsAllGamePads() { - var gamePads = this.gamePadService.GetAllGamePads(); + var gamePads = this.gamePadService.GetAllGamePads(false); Assert.AreEqual(NUM_GAMEPADS, gamePads.Length); for (var i = 0; i < NUM_GAMEPADS; i++) diff --git a/Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputGamePad.cs b/Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputGamePad.cs new file mode 100644 index 00000000..179e31dd --- /dev/null +++ b/Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputGamePad.cs @@ -0,0 +1,66 @@ +using Key2Joy.LowLevelInput.XInput; +using Key2Joy.Mapping; +using Key2Joy.Mapping.Triggers.GamePad; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Key2Joy.Tests.Core.LowLevelInput.XInput; + +[TestClass] +public class GamepadTests +{ + [TestMethod] + public void IsLeftThumbMoved_WithDefaultDeadZoneAndNoMove_ReturnsFalse() + { + var gamepad = new XInputGamePad() { LeftThumbX = 0, LeftThumbY = 0 }; + Assert.IsFalse(gamepad.IsThumbstickMoved(GamePadSide.Left)); + } + + [TestMethod] + public void IsLeftThumbMoved_WithMovePastDefaultDeadZone_ReturnsTrue() + { + var gamepad = new XInputGamePad() { LeftThumbX = XInputGamePad.XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE + 1, LeftThumbY = 0 }; + Assert.IsTrue(gamepad.IsThumbstickMoved(GamePadSide.Left)); + } + + [TestMethod] + public void IsLeftThumbMoved_WithDeltaMarginAndMovePastDelta_ReturnsTrue() + { + var gamepad = new XInputGamePad() { LeftThumbX = (int)(0.5f * 32767) + 1, LeftThumbY = 0 }; + Assert.IsTrue(gamepad.IsThumbstickMoved(GamePadSide.Left, new ExactAxisDirection { X = 0.5f, Y = 0 })); + } + + [TestMethod] + public void IsLeftThumbMoved_WithDeltaMarginAndNoMovePastDelta_ReturnsFalse() + { + var gamepad = new XInputGamePad() { LeftThumbX = (int)(0.4f * 32767), LeftThumbY = 0 }; + Assert.IsFalse(gamepad.IsThumbstickMoved(GamePadSide.Left, new ExactAxisDirection { X = 0.5f, Y = 0 })); + } + + [TestMethod] + public void IsRightThumbMoved_WithDefaultDeadZoneAndNoMove_ReturnsFalse() + { + var gamepad = new XInputGamePad() { RightThumbX = 0, RightThumbY = 0 }; + Assert.IsFalse(gamepad.IsThumbstickMoved(GamePadSide.Right)); + } + + [TestMethod] + public void IsRightThumbMoved_WithMovePastDefaultDeadZone_ReturnsTrue() + { + var gamepad = new XInputGamePad() { RightThumbX = XInputGamePad.XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE + 1, RightThumbY = 0 }; + Assert.IsTrue(gamepad.IsThumbstickMoved(GamePadSide.Right)); + } + + [TestMethod] + public void IsRightThumbMoved_WithDeltaMarginAndMovePastDelta_ReturnsTrue() + { + var gamepad = new XInputGamePad() { RightThumbX = (int)(0.5f * 32767) + 1, RightThumbY = 0 }; + Assert.IsTrue(gamepad.IsThumbstickMoved(GamePadSide.Right, new ExactAxisDirection { X = 0.5f, Y = 0 })); + } + + [TestMethod] + public void IsRightThumbMoved_WithDeltaMarginAndNoMovePastDelta_ReturnsFalse() + { + var gamepad = new XInputGamePad() { RightThumbX = (int)(0.4f * 32767), RightThumbY = 0 }; + Assert.IsFalse(gamepad.IsThumbstickMoved(GamePadSide.Right, new ExactAxisDirection { X = 0.5f, Y = 0 })); + } +} diff --git a/Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputServiceTests.cs b/Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputServiceTests.cs new file mode 100644 index 00000000..7daba4f3 --- /dev/null +++ b/Support/Key2Joy.Tests/Core/LowLevelInput/XInput/XInputServiceTests.cs @@ -0,0 +1,96 @@ +using Key2Joy.Contracts.Util; +using Key2Joy.LowLevelInput.XInput; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Threading.Tasks; + +namespace Key2Joy.Tests.Core.LowLevelInput.XInput; + +[TestClass] +public class XInputServiceTests +{ + private Mock mockXInput; + private XInputService xInputService; + + [TestInitialize] + public void TestInitialize() + { + this.mockXInput = new Mock(); + this.xInputService = new XInputService(this.mockXInput.Object); + } + + [TestMethod] + public void GetState_ShouldNotCallXInputGetStateOnUnregisteredDevice() + { + var state = this.xInputService.GetState(1); + + this.mockXInput.Verify(x => x.XInputGetState(It.IsAny(), ref It.Ref.IsAny), Times.Never); + Assert.IsNull(state); + } + + [TestMethod] + public void GetCapabilities_ShouldCallXInputGetCapabilities() + { + this.xInputService.GetCapabilities(1); + + this.mockXInput.Verify(x => x.XInputGetCapabilities(It.IsAny(), ref It.Ref.IsAny), Times.Once); + } + + [TestMethod] + public void GetBatteryInformation_ShouldCallXInputGetBatteryInformation() + { + this.xInputService.GetBatteryInformation(1, BatteryDeviceType.BATTERY_DEVTYPE_GAMEPAD); + + this.mockXInput.Verify(x => x.XInputGetBatteryInformation(It.IsAny(), It.IsAny(), ref It.Ref.IsAny), Times.Once); + } + + [TestMethod] + public void GetKeystroke_ShouldCallXInputGetKeystroke() + { + this.xInputService.GetKeystroke(1); + + this.mockXInput.Verify(x => x.XInputGetKeystroke(It.IsAny(), It.IsAny(), ref It.Ref.IsAny), Times.Once); + } + + [TestMethod] + public void Vibrate_ShouldCallXInputSetState() + { + this.xInputService.Vibrate(1, 0.5, 0.5, TimeSpan.FromSeconds(5)); + + this.mockXInput.Verify(x => x.XInputSetState(It.IsAny(), ref It.Ref.IsAny), Times.Once); + } + + [TestMethod] + public void StopVibration_ShouldCallXInputSetStateWithZeroValues() + { + this.xInputService.StopVibration(1); + + this.mockXInput.Verify(x => x.XInputSetState(It.IsAny(), ref It.Ref.IsAny), Times.Once); + + var vibrationInfo = (XInputVibration)this.mockXInput.Invocations[0].Arguments[1]; + + Assert.AreEqual(0, vibrationInfo.LeftMotorSpeed); + Assert.AreEqual(0, vibrationInfo.RightMotorSpeed); + } + + [TestMethod] + public async Task Vibration_Stops_AfterGivenTime() + { + var duration = TimeSpan.FromMilliseconds(100); + + // Start vibration + this.xInputService.Vibrate(1, 0.5, 0.5, duration); + + // Wait for the duration + a little buffer to prevent false positives + await Task.Delay(duration + TimingHelper.FromMilliseconds(500)); + + // Verify that XInputSetState was called to stop vibration after the duration + this.mockXInput.Verify(x => x.XInputSetState(It.IsAny(), ref It.Ref.IsAny), Times.Exactly(2)); + + var vibrationInfo = (XInputVibration)this.mockXInput.Invocations[1].Arguments[1]; + + Assert.AreEqual(0, vibrationInfo.LeftMotorSpeed); + Assert.AreEqual(0, vibrationInfo.RightMotorSpeed); + } +} diff --git a/Support/Key2Joy.Tests/Core/Mapping/MappedOptionTests.cs b/Support/Key2Joy.Tests/Core/Mapping/MappedOptionTests.cs index d0d4dd21..0418988d 100644 --- a/Support/Key2Joy.Tests/Core/Mapping/MappedOptionTests.cs +++ b/Support/Key2Joy.Tests/Core/Mapping/MappedOptionTests.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Key2Joy.Contracts.Mapping.Actions; using Key2Joy.Contracts.Mapping.Triggers; +using Key2Joy.Contracts.Mapping; namespace Key2Joy.Tests.Core.Mapping; @@ -29,11 +30,9 @@ public MockTrigger(string name) { } public override AbstractTriggerListener GetTriggerListener() => throw new System.NotImplementedException(); - - public override string GetUniqueKey() => throw new System.NotImplementedException(); } -public class MockPressStateAction : MockAction, IPressState +public class MockPressStateAction : MockAction, IPressState, IProvideReverseAspect { public MockPressStateAction() : base("MockPressStateAction") @@ -44,9 +43,13 @@ public MockPressStateAction(string name) { } public PressState PressState { get; set; } + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); } -public class MockPressStateTrigger : MockTrigger, IPressState +public class MockPressStateTrigger : MockTrigger, IPressState, IProvideReverseAspect { public MockPressStateTrigger() : base("MockPressStateTrigger") @@ -57,6 +60,10 @@ public MockPressStateTrigger(string name) { } public PressState PressState { get; set; } + + /// + public void MakeReverse(AbstractMappingAspect aspect) + => CommonReverseAspect.MakeReversePressState(this, aspect); } [TestClass] @@ -72,7 +79,7 @@ public void GenerateOppositePressStateMappings_ActionWithPressState_ChangesPress Trigger = new MockTrigger() }; - var mappings = MappedOption.GenerateOppositePressStateMappings(new List { option }); + var mappings = MappedOption.GenerateReverseMappings(new List { option }); Assert.AreEqual(PressState.Release, ((IPressState)mappings[0].Action).PressState); } @@ -87,7 +94,7 @@ public void GenerateOppositePressStateMappings_TriggerWithPressState_ChangesPres Trigger = new MockPressStateTrigger { PressState = PressState.Press } }; - var mappings = MappedOption.GenerateOppositePressStateMappings(new List { option }); + var mappings = MappedOption.GenerateReverseMappings(new List { option }); Assert.AreEqual(PressState.Release, ((IPressState)mappings[0].Trigger).PressState); } @@ -102,7 +109,7 @@ public void GenerateOppositePressStateMappings_ReturnsNewInstance() Trigger = new MockTrigger() }; - var mappings = MappedOption.GenerateOppositePressStateMappings(new List { option }); + var mappings = MappedOption.GenerateReverseMappings(new List { option }); Assert.AreNotSame(option, mappings[0]); Assert.AreNotSame(option.Action, mappings[0].Action); @@ -113,7 +120,7 @@ public void GenerateOppositePressStateMappings_ReturnsNewInstance() [TestMethod] public void GenerateOppositePressStateMappings_WithEmptyList_ReturnsEmptyList() { - var mappings = MappedOption.GenerateOppositePressStateMappings(new List()); + var mappings = MappedOption.GenerateReverseMappings(new List()); Assert.AreEqual(0, mappings.Count); } diff --git a/Support/Key2Joy.Tests/Core/Mapping/MappingProfileLegacyTests.cs b/Support/Key2Joy.Tests/Core/Mapping/MappingProfileLegacyTests.cs index 2a37bab1..eba37243 100644 --- a/Support/Key2Joy.Tests/Core/Mapping/MappingProfileLegacyTests.cs +++ b/Support/Key2Joy.Tests/Core/Mapping/MappingProfileLegacyTests.cs @@ -33,7 +33,7 @@ public void Initialize() public void Load_WhenCurrentMappingProfile_ShouldLoadCurrentMappingProfile() { var mappingProfilePath = MockConfigManager.GetMockMappingProfilePath("default-profile.k2j.json"); - MockConfigManager.CopyStub("current-default-profile.k2j.json", mappingProfilePath); + MockConfigManager.CopyStubCurrentDefaultProfile(mappingProfilePath); this.serviceLocator.Register(MockConfigManager.LoadOrCreateMock()); var mappingProfile = MappingProfile.Load(mappingProfilePath); diff --git a/Support/Key2Joy.Tests/Core/Plugins/PluginSetTests.cs b/Support/Key2Joy.Tests/Core/Plugins/PluginSetTests.cs index a4928a38..58be42b2 100644 --- a/Support/Key2Joy.Tests/Core/Plugins/PluginSetTests.cs +++ b/Support/Key2Joy.Tests/Core/Plugins/PluginSetTests.cs @@ -5,8 +5,6 @@ using Key2Joy.Config; using Key2Joy.Tests.Core.Config; using Key2Joy.Util; -using System.Linq; -using Key2Joy.Contracts.Plugins; namespace Key2Joy.Tests.Core.Plugins; diff --git a/Support/Key2Joy.Tests/Core/Util/DependencyServiceLocatorTests.cs b/Support/Key2Joy.Tests/Core/Util/DependencyServiceLocatorTests.cs index 5ee366c0..c207d734 100644 --- a/Support/Key2Joy.Tests/Core/Util/DependencyServiceLocatorTests.cs +++ b/Support/Key2Joy.Tests/Core/Util/DependencyServiceLocatorTests.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using CommonServiceLocator; -using Key2Joy.LowLevelInput.GamePad; +using Key2Joy.LowLevelInput; +using Key2Joy.LowLevelInput.SimulatedGamePad; using Key2Joy.Util; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -12,7 +14,7 @@ public interface IAnotherService public class TestService : IAnotherService { } -public class TestAnotherGamePadService : IGamePadService +public class TestAnotherGamePadService : ISimulatedGamePadService { public void Initialize() { } @@ -20,9 +22,9 @@ public void Initialize() public void ShutDown() { } - public IGamePad GetGamePad(int gamePadIndex) => null; + public ISimulatedGamePad GetGamePad(int gamePadIndex) => null; - public IGamePad[] GetAllGamePads() => Array.Empty(); + public ISimulatedGamePad[] GetAllGamePads(bool onlyPluggedIn = true) => Array.Empty(); public void EnsureAllUnplugged() { } @@ -32,6 +34,8 @@ public void EnsurePluggedIn(int gamePadIndex) public void EnsureUnplugged(int gamePadIndex) { } + + public IList GetActiveDevicesInfo() => Array.Empty(); } [TestClass] @@ -41,9 +45,9 @@ public class DependencyServiceLocatorTests public void Register_And_Retrieve_Service() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); - var service = serviceLocator.GetInstance(); + var service = serviceLocator.GetInstance(); Assert.IsNotNull(service); Assert.IsInstanceOfType(service, typeof(SimulatedGamePadService)); } @@ -53,17 +57,17 @@ public void Register_And_Retrieve_Service() public void Retrieve_Unregistered_Service_Throws_Exception() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.GetInstance(); + serviceLocator.GetInstance(); } [TestMethod] public void Can_Register_Multiple_Services() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); serviceLocator.Register(new TestService()); - Assert.IsNotNull(serviceLocator.GetInstance()); + Assert.IsNotNull(serviceLocator.GetInstance()); Assert.IsNotNull(serviceLocator.GetInstance()); } @@ -71,10 +75,10 @@ public void Can_Register_Multiple_Services() public void Registered_Service_Is_Singleton_By_Default() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); - var service1 = serviceLocator.GetInstance(); - var service2 = serviceLocator.GetInstance(); + var service1 = serviceLocator.GetInstance(); + var service2 = serviceLocator.GetInstance(); Assert.AreSame(service1, service2); } @@ -83,10 +87,10 @@ public void Registered_Service_Is_Singleton_By_Default() public void Can_Override_Registered_Service() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.Register(new SimulatedGamePadService()); - serviceLocator.Register(new TestAnotherGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new TestAnotherGamePadService()); - var service = serviceLocator.GetInstance(); + var service = serviceLocator.GetInstance(); Assert.IsInstanceOfType(service, typeof(TestAnotherGamePadService)); } @@ -95,9 +99,9 @@ public void Can_Override_Registered_Service() public void Can_Use_Generic_GetInstance() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); - var service = serviceLocator.GetInstance(); + var service = serviceLocator.GetInstance(); Assert.IsInstanceOfType(service, typeof(SimulatedGamePadService)); } @@ -106,9 +110,9 @@ public void Can_Use_Generic_GetInstance() public void Can_Use_NonGeneric_GetInstance() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); - var service = serviceLocator.GetInstance(typeof(IGamePadService)); + var service = serviceLocator.GetInstance(typeof(ISimulatedGamePadService)); Assert.IsInstanceOfType(service, typeof(SimulatedGamePadService)); } @@ -118,7 +122,7 @@ public void Can_Use_NonGeneric_GetInstance() public void NonGeneric_GetInstance_Unregistered_Service_Throws_Exception() { var serviceLocator = new DependencyServiceLocator(); - serviceLocator.GetInstance(typeof(IGamePadService)); + serviceLocator.GetInstance(typeof(ISimulatedGamePadService)); } [TestMethod] @@ -127,9 +131,9 @@ public void ServiceLocator_SetLocatorProvider_Works() var serviceLocator = new DependencyServiceLocator(); ServiceLocator.SetLocatorProvider(() => serviceLocator); - serviceLocator.Register(new SimulatedGamePadService()); + serviceLocator.Register(new SimulatedGamePadService()); - var service = ServiceLocator.Current.GetInstance(); + var service = ServiceLocator.Current.GetInstance(); Assert.IsNotNull(service); } diff --git a/Support/Key2Joy.Tests/Key2Joy.Tests.csproj b/Support/Key2Joy.Tests/Key2Joy.Tests.csproj index 6b3a9f78..af080bbb 100644 --- a/Support/Key2Joy.Tests/Key2Joy.Tests.csproj +++ b/Support/Key2Joy.Tests/Key2Joy.Tests.csproj @@ -14,7 +14,7 @@ NET48 latest - x86 + AnyCPU bin\$(MSBuildProjectName) false false @@ -74,9 +74,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest diff --git a/Support/Key2Joy.Tests/Testing/TestUtilities.cs b/Support/Key2Joy.Tests/Testing/TestUtilities.cs index 0c2bf0c2..c5498178 100644 --- a/Support/Key2Joy.Tests/Testing/TestUtilities.cs +++ b/Support/Key2Joy.Tests/Testing/TestUtilities.cs @@ -7,6 +7,7 @@ internal class TestUtilities { /// /// Waits for a task to complete, or throws an exception if the task does not complete within the specified timeout. + /// Be sure to get the timeout using to account for longer timeouts on GitHub Actions. /// /// /// @@ -32,7 +33,8 @@ TimeSpan timeout } /// - /// for a task to complete, or throws an exception if the task does not complete within the specified timeout. + /// Waits for a task to complete, or throws an exception if the task does not complete within the specified timeout. + /// Be sure to get the timeout using to account for longer timeouts on GitHub Actions. /// /// ///