Skip to content

Commit

Permalink
Replace global windows hook with hotkey
Browse files Browse the repository at this point in the history
But Jarvis will fall back to using the global windows hook
if registering of the hotkey fails.

Closes #69
  • Loading branch information
patriksvensson committed Nov 8, 2018
1 parent 2acc141 commit 962cfab
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 30 deletions.
11 changes: 11 additions & 0 deletions src/Jarvis.Core/Interop/Win32.Keyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions src/Jarvis/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ namespace Jarvis
public class Bootstrapper : BootstrapperBase
{
private IContainer _container;
private IDisposable _hotKey;
private JarvisTaskbarIcon _taskbarIcon;

public Bootstrapper()
Expand Down Expand Up @@ -73,22 +72,20 @@ protected override void OnStartup(object sender, StartupEventArgs e)

// Start all background services.
IoC.Get<ServiceOrchestrator>().Start();

// Register the hotkey.
var service = IoC.Get<ApplicationService>();
_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<ServiceOrchestrator>();
services.Stop();
services.Join();

// Dispose the container.
_container.Dispose();
}

protected override object GetInstance(Type service, string key)
Expand Down
1 change: 1 addition & 0 deletions src/Jarvis/Infrastructure/Bootstrapping/JarvisModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType<ApplicationService>().SingleInstance();
builder.RegisterType<WindowService>().SingleInstance();
builder.RegisterType<SettingsService>().AsSelf().AsImplementedInterfaces().SingleInstance();
builder.RegisterType<KeyboardService>().AsSelf().AsImplementedInterfaces().SingleInstance();

// Misc
builder.RegisterType<JarvisTaskbarIcon>().SingleInstance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -35,11 +56,6 @@ public KeyboardHook(Action action)
}
}

~KeyboardHook()
{
Dispose();
}

private IntPtr Callback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
Expand All @@ -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);
}
}
}
70 changes: 70 additions & 0 deletions src/Jarvis/Infrastructure/Input/Hooks/HotKeyKeyboardHook.cs
Original file line number Diff line number Diff line change
@@ -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<int, HotKeyKeyboardHook> _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<int, HotKeyKeyboardHook>();
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;
}
}
}
}
}
}
13 changes: 13 additions & 0 deletions src/Jarvis/Infrastructure/Input/IKeyboardHook.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
5 changes: 4 additions & 1 deletion src/Jarvis/Jarvis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@
<Compile Include="Infrastructure\Bootstrapping\Seeding\GeneralSettingsSeeder.cs" />
<Compile Include="Infrastructure\Bootstrapping\UpdaterModule.cs" />
<Compile Include="Constants.cs" />
<Compile Include="Infrastructure\Utilities\KeyboardHook.cs" />
<Compile Include="Infrastructure\Input\Hooks\GlobalKeyboardHook.cs" />
<Compile Include="Infrastructure\Input\Hooks\HotKeyKeyboardHook.cs" />
<Compile Include="Infrastructure\Input\IKeyboardHook.cs" />
<Compile Include="Infrastructure\Presentation\Controls\BindableRichTextBox.cs" />
<Compile Include="Infrastructure\Presentation\Controls\IconControl.xaml.cs">
<DependentUpon>IconControl.xaml</DependentUpon>
Expand Down Expand Up @@ -154,6 +156,7 @@
</Compile>
<Compile Include="Services\ApplicationService.cs" />
<Compile Include="Services\JarvisTaskbarIcon.cs" />
<Compile Include="Services\KeyboardService.cs" />
<Compile Include="Services\Updating\GitHubReleaseAsset.cs" />
<Compile Include="Services\WindowService.cs" />
<Compile Include="Services\QueryProviderService.cs" />
Expand Down
57 changes: 57 additions & 0 deletions src/Jarvis/Services/KeyboardService.cs
Original file line number Diff line number Diff line change
@@ -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}");
}
}
}
}
}
2 changes: 1 addition & 1 deletion src/Jarvis/Services/ServiceOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 962cfab

Please sign in to comment.