• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

luttje / Key2Joy / 6557152774

18 Oct 2023 06:36AM UTC coverage: 52.519% (+39.8%) from 12.718%
6557152774

push

github

web-flow
Adding more tests, fixing bugs in the process (#48)

* Add config manager tests

* legacy config and mapping profile tests (should fix #42)

* remove comment, problem was caused by transfer across appdomain (or to/fro scripting environment)

* Test core functionality #48 + includes minor refactoring to be able to test + added docs

* Add interop tests + implement and test async test utility (refactors away from singletons)

* fix not all tests running in workflow

* config and interop tests

* Refactor and allow mocking global input hook class

* add capture action test

* Make Execute override optional for script only methods

* add dependency injection + refactor and try test gamepad service

* Refactor config singleton to using dependency injection

* add tests for scripting

* add tests for plugin set + fix plugin showing as loaded even if checksum match failed

* fix tests failing because it relied on config exist (I guess the test order was accidentally correct earlier, this means we should really fix cleanup so we catch this sooner)

* refactor docs code + fix wrong enum summary

* refactor docs builder and start testing it a bit

* fix cmd project structure

* ignore designer files in tests

* cleanup and refactor UI code + show latest version in help

* truncate listview action column

* allow user config to minimize app when pressing X (defaults to shut down app) resolves #45

696 of 1757 branches covered (0.0%)

Branch coverage included in aggregate %.

4597 of 4597 new or added lines in 138 files covered. (100.0%)

3619 of 6459 relevant lines covered (56.03%)

17089.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.46
/Core/Key2Joy.Core/Key2JoyManager.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Diagnostics;
4
using System.IO;
5
using System.Linq;
6
using System.Reflection;
7
using System.Windows.Forms;
8
using CommonServiceLocator;
9
using Key2Joy.Config;
10
using Key2Joy.Contracts.Mapping;
11
using Key2Joy.Contracts.Mapping.Actions;
12
using Key2Joy.Contracts.Mapping.Triggers;
13
using Key2Joy.Interop;
14
using Key2Joy.Interop.Commands;
15
using Key2Joy.LowLevelInput.GamePad;
16
using Key2Joy.Mapping;
17
using Key2Joy.Mapping.Actions.Logic;
18
using Key2Joy.Mapping.Triggers.Keyboard;
19
using Key2Joy.Mapping.Triggers.Mouse;
20
using Key2Joy.Plugins;
21
using Key2Joy.Util;
22

23
namespace Key2Joy;
24

25
public delegate bool AppCommandRunner(AppCommand command);
26

27
public class Key2JoyManager : IKey2JoyManager, IMessageFilter
28
{
29
    /// <summary>
30
    /// Directory where plugins are located
31
    /// </summary>
32
    public const string PluginsDirectory = "Plugins";
33

34
    public event EventHandler<StatusChangedEventArgs> StatusChanged;
35

36
    public static Key2JoyManager instance;
37

38
    public static Key2JoyManager Instance
39
    {
40
        get
41
        {
42
            if (instance == null)
6!
43
            {
44
                throw new Exception("Key2JoyManager not initialized using InitSafely yet!");
×
45
            }
46

47
            return instance;
6✔
48
        }
49
    }
50

51
    /// <summary>
52
    /// Trigger listeners that should explicitly loaded. This ensures that they're available for scripts
53
    /// even if no mapping option is mapped to be triggered by it.
54
    /// </summary>
55
    public IList<AbstractTriggerListener> ExplicitTriggerListeners { get; set; }
8✔
56

57
    private const string READY_MESSAGE = "Key2Joy is ready";
58
    private static AppCommandRunner commandRunner;
59
    private MappingProfile armedProfile;
60
    private IHaveHandleAndInvoke handleAndInvoker;
61
    private readonly List<IWndProcHandler> wndProcListeners = new();
2✔
62

63
    private Key2JoyManager()
2✔
64
    { }
2✔
65

66
    /// <summary>
67
    /// Ensures Key2Joy is running and ready to accept commands as long as the main loop does not end.
68
    /// </summary>
69
    /// <param name="commandRunner"></param>
70
    /// <param name="mainLoop"></param>
71
    /// <param name="configManager">Optionally a custom config manager (probably only useful for unit testing)</param>
72
    public static void InitSafely(AppCommandRunner commandRunner, Action<PluginSet> mainLoop, IConfigManager configManager = null)
73
    {
74
        // Setup dependency injection and services
75
        var serviceLocator = new DependencyServiceLocator();
2✔
76
        ServiceLocator.SetLocatorProvider(() => serviceLocator);
7✔
77

78
        instance = new Key2JoyManager
2✔
79
        {
2✔
80
            ExplicitTriggerListeners = new List<AbstractTriggerListener>()
2✔
81
            {
2✔
82
                // Always add these listeners so scripts can ask them if stuff has happened.
2✔
83
                KeyboardTriggerListener.Instance,
2✔
84
                MouseButtonTriggerListener.Instance,
2✔
85
                MouseMoveTriggerListener.Instance
2✔
86
            }
2✔
87
        };
2✔
88
        serviceLocator.Register<IKey2JoyManager>(instance);
2✔
89

90
#pragma warning disable IDE0001 // Simplify Names
91
        serviceLocator.Register<IConfigManager>(configManager ??= new ConfigManager());
2!
92
#pragma warning restore IDE0001 // Simplify Names
93

94
        var gamePadService = new SimulatedGamePadService();
2✔
95
        serviceLocator.Register<IGamePadService>(gamePadService);
2✔
96

97
        var commandRepository = new CommandRepository();
2✔
98
        serviceLocator.Register<ICommandRepository>(commandRepository);
2✔
99

100
        // Load plugins
101
        var pluginDirectoriesPaths = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
2✔
102
        pluginDirectoriesPaths = Path.Combine(pluginDirectoriesPaths, PluginsDirectory);
2✔
103

104
        PluginSet plugins = new(pluginDirectoriesPaths);
2✔
105
        plugins.LoadAll();
2✔
106
        plugins.RefreshPluginTypes();
2✔
107

108
        foreach (var loadState in plugins.AllPluginLoadStates.Values)
8✔
109
        {
110
            if (loadState.LoadState == PluginLoadStates.FailedToLoad)
2!
111
            {
112
                System.Windows.MessageBox.Show(
×
113
                    $"One of your plugins located at {loadState.AssemblyPath} failed to load. This was the error: " +
×
114
                    loadState.LoadErrorMessage,
×
115
                    "Failed to load plugin!",
×
116
                    System.Windows.MessageBoxButton.OK,
×
117
                    System.Windows.MessageBoxImage.Warning
×
118
                );
×
119
            }
120
        }
121

122
        Key2JoyManager.commandRunner = commandRunner;
2✔
123

124
        var interopServer = new InteropServer(instance, commandRepository);
2✔
125

126
        try
127
        {
128
            interopServer.RestartListening();
2✔
129
            mainLoop(plugins);
2✔
130
        }
2✔
131
        finally
132
        {
133
            interopServer.StopListening();
2✔
134
            gamePadService.ShutDown();
2✔
135
        }
2✔
136
    }
2✔
137

138
    // Run the event on the same thread as the main control/form
139
    public void CallOnUiThread(Action action) => this.handleAndInvoker.Invoke(action);
×
140

141
    internal static bool RunAppCommand(AppCommand command) => commandRunner != null && commandRunner(command);
×
142

143
    public bool PreFilterMessage(ref System.Windows.Forms.Message m)
144
    {
145
        for (var i = 0; i < this.wndProcListeners.Count; i++)
×
146
        {
147
            // Check if the proc listeners haven't changed (this can happen when a plugin opens a MessageBox, the user aborts, and we then close the messagebox)
148
            if (i >= this.wndProcListeners.Count)
×
149
            {
150
                Debug.WriteLine("Key2JoyManager.PreFilterMessage: wndProcListeners changed while processing message!");
151
                break;
152
            }
153

154
            var wndProcListener = this.wndProcListeners[i];
×
155

156
            wndProcListener.WndProc(new Contracts.Mapping.Message(m.HWnd, m.Msg, m.WParam, m.LParam));
×
157
        }
158

159
        return false;
×
160
    }
161

162
    public void SetHandlerWithInvoke(IHaveHandleAndInvoke handleAndInvoker)
163
    {
164
        this.handleAndInvoker = handleAndInvoker;
×
165
        Application.AddMessageFilter(this);
×
166

167
        Console.WriteLine(READY_MESSAGE);
×
168
    }
×
169

170
    public bool GetIsArmed(MappingProfile profile = null)
171
    {
172
        if (profile == null)
×
173
        {
174
            return this.armedProfile != null;
×
175
        }
176

177
        return this.armedProfile == profile;
×
178
    }
179

180
    public void ArmMappings(MappingProfile profile)
181
    {
182
        this.armedProfile = profile;
2✔
183

184
        var allListeners = new List<AbstractTriggerListener>();
2✔
185
        allListeners.AddRange(this.ExplicitTriggerListeners);
2✔
186

187
        var allActions = (IList<AbstractAction>)profile.MappedOptions.Select(m => m.Action).ToList();
4✔
188

189
        foreach (var mappedOption in profile.MappedOptions)
8✔
190
        {
191
            if (mappedOption.Trigger == null)
2✔
192
            {
193
                continue;
194
            }
195

196
            var listener = mappedOption.Trigger.GetTriggerListener();
2✔
197

198
            if (!allListeners.Contains(listener))
2✔
199
            {
200
                allListeners.Add(listener);
2✔
201
            }
202

203
            if (listener is IWndProcHandler listenerWndProcHAndler)
2!
204
            {
205
                this.wndProcListeners.Add(listenerWndProcHAndler);
×
206
            }
207

208
            mappedOption.Action.OnStartListening(listener, ref allActions);
2✔
209
            listener.AddMappedOption(mappedOption);
2✔
210
        }
211

212
        var allListenersForSharing = (IList<AbstractTriggerListener>)allListeners;
2✔
213

214
        foreach (var listener in allListeners)
8✔
215
        {
216
            if (listener is IWndProcHandler listenerWndProcHAndler)
2!
217
            {
218
                listenerWndProcHAndler.Handle = this.handleAndInvoker.Handle;
×
219
            }
220

221
            listener.StartListening(ref allListenersForSharing);
2✔
222
        }
223

224
        StatusChanged?.Invoke(this, new StatusChangedEventArgs
2!
225
        {
2✔
226
            IsEnabled = true,
2✔
227
            Profile = this.armedProfile
2✔
228
        });
2✔
229
    }
×
230

231
    public void DisarmMappings()
232
    {
233
        var listeners = this.ExplicitTriggerListeners;
2✔
234
        this.wndProcListeners.Clear();
2✔
235

236
        // Clear all intervals
237
        IdPool.CancelAll();
2✔
238

239
        foreach (var mappedOption in this.armedProfile.MappedOptions)
8✔
240
        {
241
            if (mappedOption.Trigger == null)
2✔
242
            {
243
                continue;
244
            }
245

246
            var listener = mappedOption.Trigger.GetTriggerListener();
2✔
247
            mappedOption.Action.OnStopListening(listener);
2✔
248

249
            if (!listeners.Contains(listener))
2✔
250
            {
251
                listeners.Add(listener);
2✔
252
            }
253
        }
254

255
        foreach (var listener in listeners)
8✔
256
        {
257
            listener.StopListening();
2✔
258
        }
259

260
        var gamePadService = ServiceLocator.Current.GetInstance<IGamePadService>();
2✔
261
        gamePadService.EnsureAllUnplugged();
2✔
262

263
        this.armedProfile = null;
2✔
264

265
        StatusChanged?.Invoke(this, new StatusChangedEventArgs
2!
266
        {
2✔
267
            IsEnabled = false,
2✔
268
        });
2✔
269
    }
×
270

271
    /// <summary>
272
    /// Starts Key2Joy, pausing until it's ready
273
    /// </summary>
274
    public static void StartKey2Joy(bool startMinimized = true, bool pauseUntilReady = true)
275
    {
276
        var configManager = ServiceLocator.Current.GetInstance<IConfigManager>();
×
277
        var executablePath = configManager.GetConfigState().LastInstallPath;
×
278

279
        if (executablePath == null)
×
280
        {
281
            Console.WriteLine("Error! Key2Joy executable path is not known, please start Key2Joy at least once!");
×
282
            return;
×
283
        }
284

285
        if (!File.Exists(executablePath))
×
286
        {
287
            Console.WriteLine("Error! Key2Joy executable path is invalid, please start Key2Joy at least once (and don't move the executable)!");
×
288
            return;
×
289
        }
290

291
        Process process = new()
×
292
        {
×
293
            StartInfo = new ProcessStartInfo
×
294
            {
×
295
                FileName = executablePath,
×
296
                Arguments = startMinimized ? "--minimized" : "",
×
297
                UseShellExecute = false,
×
298
                RedirectStandardOutput = true
×
299
            }
×
300
        };
×
301

302
        process.Start();
×
303

304
        if (!pauseUntilReady)
×
305
        {
306
            return;
×
307
        }
308

309
        while (!process.StandardOutput.EndOfStream)
×
310
        {
311
            if (process.StandardOutput.ReadLine() == READY_MESSAGE)
×
312
            {
313
                break;
314
            }
315
        }
316
    }
×
317
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc