diff --git a/src/Jarvis.Core/Interop/Win32.Keyboard.cs b/src/Jarvis.Core/Interop/Win32.Keyboard.cs index 0a8d1db..0df6855 100644 --- a/src/Jarvis.Core/Interop/Win32.Keyboard.cs +++ b/src/Jarvis.Core/Interop/Win32.Keyboard.cs @@ -13,6 +13,17 @@ public static partial class Win32 { public static class Keyboard { + public static class HotKey + { + [DllImport("user32.dll")] + public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vlc); + + [DllImport("user32.dll")] + public static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + public const int WmHotKey = 0x0312; + } + public delegate IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam); #pragma warning disable SA1310 // Field names must not contain underscore diff --git a/src/Jarvis/Bootstrapper.cs b/src/Jarvis/Bootstrapper.cs index 075893a..99ac3a7 100644 --- a/src/Jarvis/Bootstrapper.cs +++ b/src/Jarvis/Bootstrapper.cs @@ -22,7 +22,6 @@ namespace Jarvis public class Bootstrapper : BootstrapperBase { private IContainer _container; - private IDisposable _hotKey; private JarvisTaskbarIcon _taskbarIcon; public Bootstrapper() @@ -73,22 +72,20 @@ protected override void OnStartup(object sender, StartupEventArgs e) // Start all background services. IoC.Get().Start(); - - // Register the hotkey. - var service = IoC.Get(); - _hotKey = new KeyboardHook(() => service.Toggle()); } protected override void OnExit(object sender, EventArgs e) { // Unregister the hot key - _hotKey?.Dispose(); _taskbarIcon?.Dispose(); // Stop the service orchestrator. var services = IoC.Get(); services.Stop(); services.Join(); + + // Dispose the container. + _container.Dispose(); } protected override object GetInstance(Type service, string key) diff --git a/src/Jarvis/Infrastructure/Bootstrapping/JarvisModule.cs b/src/Jarvis/Infrastructure/Bootstrapping/JarvisModule.cs index b290553..e68757e 100644 --- a/src/Jarvis/Infrastructure/Bootstrapping/JarvisModule.cs +++ b/src/Jarvis/Infrastructure/Bootstrapping/JarvisModule.cs @@ -36,6 +36,7 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); builder.RegisterType().AsSelf().AsImplementedInterfaces().SingleInstance(); + builder.RegisterType().AsSelf().AsImplementedInterfaces().SingleInstance(); // Misc builder.RegisterType().SingleInstance(); diff --git a/src/Jarvis/Infrastructure/Utilities/KeyboardHook.cs b/src/Jarvis/Infrastructure/Input/Hooks/GlobalKeyboardHook.cs similarity index 88% rename from src/Jarvis/Infrastructure/Utilities/KeyboardHook.cs rename to src/Jarvis/Infrastructure/Input/Hooks/GlobalKeyboardHook.cs index 69de234..f572562 100644 --- a/src/Jarvis/Infrastructure/Utilities/KeyboardHook.cs +++ b/src/Jarvis/Infrastructure/Input/Hooks/GlobalKeyboardHook.cs @@ -10,21 +10,42 @@ using System.Windows.Input; using Jarvis.Core.Interop; -namespace Jarvis.Infrastructure.Utilities +namespace Jarvis.Infrastructure.Input { [SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")] - public sealed class KeyboardHook : CriticalFinalizerObject, IDisposable + public sealed class GlobalKeyboardHook : CriticalFinalizerObject, IDisposable, IKeyboardHook { private readonly Action _action; - private readonly IntPtr _hook; private readonly Win32.Keyboard.HookCallback _callback; + private IntPtr _hook; private bool _disposed; - public KeyboardHook(Action action) + public GlobalKeyboardHook(Action action) { _action = action; _callback = Callback; + } + + ~GlobalKeyboardHook() + { + Dispose(); + } + + public void Dispose() + { + if (!_disposed) + { + if (_hook != IntPtr.Zero) + { + Win32.Keyboard.UnhookWindowsHookEx(_hook); + } + _disposed = true; + } + GC.SuppressFinalize(this); + } + public void Register() + { using (var curProcess = Process.GetCurrentProcess()) using (var curModule = curProcess.MainModule) { @@ -35,11 +56,6 @@ public KeyboardHook(Action action) } } - ~KeyboardHook() - { - Dispose(); - } - private IntPtr Callback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) @@ -61,18 +77,5 @@ private IntPtr Callback(int nCode, IntPtr wParam, IntPtr lParam) } return Win32.Keyboard.CallNextHookEx(_hook, nCode, wParam, lParam); } - - public void Dispose() - { - if (!_disposed) - { - if (_hook != IntPtr.Zero) - { - Win32.Keyboard.UnhookWindowsHookEx(_hook); - } - _disposed = true; - } - GC.SuppressFinalize(this); - } } } diff --git a/src/Jarvis/Infrastructure/Input/Hooks/HotKeyKeyboardHook.cs b/src/Jarvis/Infrastructure/Input/Hooks/HotKeyKeyboardHook.cs new file mode 100644 index 0000000..c24d984 --- /dev/null +++ b/src/Jarvis/Infrastructure/Input/Hooks/HotKeyKeyboardHook.cs @@ -0,0 +1,70 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Windows.Input; +using System.Windows.Interop; +using Jarvis.Core.Interop; + +namespace Jarvis.Infrastructure.Input +{ + public sealed class HotKeyKeyboardHook : IKeyboardHook + { + private readonly Action _action; + private int _id; + private bool _disposed; + + private static Dictionary _lookup; + + public HotKeyKeyboardHook(Action action) + { + _action = action; + } + + public void Register() + { + var virtualKeyCode = KeyInterop.VirtualKeyFromKey(Key.Space); + _id = virtualKeyCode + 65536; + + var result = Win32.Keyboard.HotKey.RegisterHotKey(IntPtr.Zero, _id, 0x0001, (uint)virtualKeyCode); + if (!result) + { + throw new InvalidOperationException("Could not register hotkey."); + } + + if (_lookup == null) + { + _lookup = new Dictionary(); + ComponentDispatcher.ThreadFilterMessage += OnThreadFilterMessage; + } + + _lookup.Add(_id, this); + } + + public void Dispose() + { + if (!_disposed) + { + Win32.Keyboard.HotKey.UnregisterHotKey(IntPtr.Zero, _id); + _disposed = true; + } + } + + private static void OnThreadFilterMessage(ref MSG msg, ref bool handled) + { + if (!handled) + { + if (msg.message == Win32.Keyboard.HotKey.WmHotKey) + { + if (_lookup.TryGetValue((int)msg.wParam, out var hotKey)) + { + hotKey._action?.Invoke(); + handled = true; + } + } + } + } + } +} diff --git a/src/Jarvis/Infrastructure/Input/IKeyboardHook.cs b/src/Jarvis/Infrastructure/Input/IKeyboardHook.cs new file mode 100644 index 0000000..5a9611e --- /dev/null +++ b/src/Jarvis/Infrastructure/Input/IKeyboardHook.cs @@ -0,0 +1,13 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Jarvis.Infrastructure.Input +{ + public interface IKeyboardHook : IDisposable + { + void Register(); + } +} diff --git a/src/Jarvis/Jarvis.csproj b/src/Jarvis/Jarvis.csproj index a6088ec..49798c7 100644 --- a/src/Jarvis/Jarvis.csproj +++ b/src/Jarvis/Jarvis.csproj @@ -120,7 +120,9 @@ - + + + IconControl.xaml @@ -154,6 +156,7 @@ + diff --git a/src/Jarvis/Services/KeyboardService.cs b/src/Jarvis/Services/KeyboardService.cs new file mode 100644 index 0000000..8f94784 --- /dev/null +++ b/src/Jarvis/Services/KeyboardService.cs @@ -0,0 +1,57 @@ +// Licensed to Spectre Systems AB under one or more agreements. +// Spectre Systems AB licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Jarvis.Core; +using Jarvis.Core.Diagnostics; +using Jarvis.Infrastructure.Input; + +namespace Jarvis.Services +{ + public sealed class KeyboardService : IInitializable, IDisposable + { + private readonly ApplicationService _applicationService; + private readonly IJarvisLog _log; + private IKeyboardHook _hook; + + public KeyboardService(ApplicationService applicationService, IJarvisLog log) + { + _applicationService = applicationService; + _log = new LogDecorator(nameof(KeyboardService), log); + } + + public void Dispose() + { + _hook?.Dispose(); + } + + public void Initialize() + { + try + { + _log.Information("Registering Windows hot key..."); + _hook = new HotKeyKeyboardHook(() => _applicationService.Toggle()); + _hook.Register(); + _log.Information("Hot key was successfully registered."); + } + catch (Exception ex) + { + _log.Error($"An error occured when registering hot key: {ex.Message}"); + + try + { + _log.Information("Registering windows keyboard hook..."); + _hook = new GlobalKeyboardHook(() => _applicationService.Toggle()); + _hook.Register(); + _log.Information("Keyboard hook was successfully registered."); + } + catch (Exception ex2) + { + // TODO: Notify the user that registering wasn't possible. + _log.Error($"Unable to register windows keyboard hook: {ex2.Message}"); + } + } + } + } +} diff --git a/src/Jarvis/Services/ServiceOrchestrator.cs b/src/Jarvis/Services/ServiceOrchestrator.cs index 7b1aaf3..80b93a6 100644 --- a/src/Jarvis/Services/ServiceOrchestrator.cs +++ b/src/Jarvis/Services/ServiceOrchestrator.cs @@ -58,7 +58,7 @@ public void Start() public void Stop() { - if (!_source.IsCancellationRequested) + if (_source != null && !_source.IsCancellationRequested) { _log.Information("We were instructed to stop."); _source.Cancel();