Skip to content

Commit

Permalink
Merge pull request #88 from patriksvensson/feature/GH-69
Browse files Browse the repository at this point in the history
Replace global windows hook with hotkey
  • Loading branch information
patriksvensson authored Nov 8, 2018
2 parents 2acc141 + 962cfab commit 40b7ffd
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 40b7ffd

Please sign in to comment.