Still hooked on Windows, I refactored the hook into a number of interfaces, and I wanted to support 2-step hotkeys, so I started with defining an IHook
that can be attached, detached, exposes a property that tells whether a hook is attached or not, and raises an event when the hook receives a message:
public interface IHook
{
event EventHandler<HookEventArgs> MessageReceived;
void OnMessageReceived();
bool IsAttached { get; }
void Attach();
void Detach();
}
The HookEventArgs
simply carry a Keys
enum value:
public class HookEventArgs : EventArgs
{
private readonly Keys _key;
public HookEventArgs(Keys key)
{
_key = key;
}
public Keys Key { get { return _key; } }
public new static HookEventArgs Empty {get { return new HookEventArgs(Keys.None); }}
}
Then I extended IHook
into a IHotKeyHook
, which exposes a HookInfo
value and a flag that indicates whether that hotkey is a 2-step hotkey, like Visual Studio's Ctrl+R,M for refactor/extract method:
public interface IHotKeyHook : IHook
{
HookInfo HookInfo { get; }
bool IsTwoStepHotKey { get; }
}
I also extracted a TimerHook
, and gave it its own interface:
public interface ITimerHook
{
event EventHandler Tick;
void Attach();
void Detach();
}
Lastly, I added a level of abstraction for an object that would be responsible for managing all these hooks and passing the events to the Rubberduck App
class:
public interface IRubberduckHooks : IHook, IDisposable
{
IEnumerable<IHook> Hooks { get; }
void AddHook<THook>(THook hook) where THook : IHook;
}
App
Purely for context, here's how the App
class consumes IRubberduckHooks
- I have yet to actually handle the hotkeys and map them to actual commands, but the backbone is here:
private async void hooks_MessageReceived(object sender, HookEventArgs e)
{
if (sender is LowLevelKeyboardHook)
{
if (_skipKeyUp)
{
_skipKeyUp = false;
return;
}
if (_isAwaitingTwoStepKey)
{
// todo: use _firstStepHotKey and e.Key to run 2-step hotkey action
AwaitNextKey(false);
return;
}
var component = _vbe.ActiveCodePane.CodeModule.Parent;
await ParseComponentAsync(component);
AwaitNextKey(false);
return;
}
var hotKey = sender as IHotKeyHook;
if (hotKey == null)
{
AwaitNextKey(false);
return;
}
if (hotKey.IsTwoStepHotKey)
{
_firstStepHotKey = hotKey.HookInfo.Key;
AwaitNextKey(true, _firstStepHotKey);
}
else
{
// todo: use e.Key to run 1-step hotkey action
_firstStepHotKey = Keys.None;
AwaitNextKey(false);
}
}
private void AwaitNextKey(bool eatNextKey = true, Keys key = default(Keys))
{
_isAwaitingTwoStepKey = eatNextKey;
foreach (var hook in _hooks.Hooks.OfType<ILowLevelKeyboardHook>())
{
hook.EatNextKey = eatNextKey;
}
// todo: include modifier keys
_stateBar.SetStatusText(eatNextKey ? "(hotkey " + key + ") was pressed. Waiting for second key..." : "Ready.");
_skipKeyUp = eatNextKey;
}
RubberduckHooks
This class is responsible for "coordinating" the hooks, and implements the main Windows message hook that picks up, notably HOTKEY messages. It's this class that the App
class calls into, to register hotkeys; it's also this class' events that the App
class is listening for.
I'm having a problem with the parameterless public void OnMessageReceived()
method that the IRubberduckHooks
interface inherits from IHook
, since the App
class isn't supposed to be able to call that method - I "solved" it by throwing a NotImplementedException
, but IMO there's a design smell here.. have a look:
public class RubberduckHooks : IRubberduckHooks
{
private readonly IntPtr _mainWindowHandle;
private readonly IntPtr _oldWndPointer;
private readonly User32.WndProc _oldWndProc;
private User32.WndProc _newWndProc;
private readonly ITimerHook _timerHook;
private const int WA_INACTIVE = 0;
private const int WA_ACTIVE = 1;
public RubberduckHooks(IntPtr mainWindowHandle, ITimerHook timerHook)
{
_mainWindowHandle = mainWindowHandle;
_oldWndProc = WindowProc;
_newWndProc = WindowProc;
_oldWndPointer = User32.SetWindowLong(_mainWindowHandle, (int)WindowLongFlags.GWL_WNDPROC, _newWndProc);
_oldWndProc = (User32.WndProc)Marshal.GetDelegateForFunctionPointer(_oldWndPointer, typeof(User32.WndProc));
_timerHook = timerHook;
_timerHook.Tick += timerHook_Tick;
}
private readonly IList<IHook> _hooks = new List<IHook>();
public IEnumerable<IHook> Hooks { get { return _hooks; } }
public void AddHook<THook>(THook hook) where THook : IHook
{
_hooks.Add(hook);
}
public event EventHandler<HookEventArgs> MessageReceived;
public void OnMessageReceived()
{
throw new NotImplementedException();
}
private void OnMessageReceived(object sender, HookEventArgs args)
{
var handler = MessageReceived;
if (handler != null)
{
handler.Invoke(sender, args);
}
}
public bool IsAttached { get; private set; }
public void Attach()
{
if (IsAttached)
{
return;
}
foreach (var hook in Hooks)
{
hook.MessageReceived += hook_MessageReceived;
hook.Attach();
}
IsAttached = true;
}
public void Detach()
{
if (!IsAttached)
{
return;
}
foreach (var hook in Hooks)
{
hook.MessageReceived -= hook_MessageReceived;
hook.Detach();
}
IsAttached = false;
}
private void hook_MessageReceived(object sender, HookEventArgs e)
{
OnMessageReceived(sender, e);
}
private void timerHook_Tick(object sender, EventArgs e)
{
if (!IsAttached && User32.GetForegroundWindow() == _mainWindowHandle)
{
Attach();
}
else
{
Detach();
}
}
public void Dispose()
{
_timerHook.Tick -= timerHook_Tick;
_timerHook.Detach();
Detach();
}
private IntPtr WindowProc(IntPtr hWnd, int uMsg, int wParam, int lParam)
{
try
{
var processed = false;
if (hWnd == _mainWindowHandle)
{
switch ((WM)uMsg)
{
case WM.HOTKEY:
if (GetWindowThread(User32.GetForegroundWindow()) == GetWindowThread(_mainWindowHandle))
{
var hook = Hooks.OfType<IHotKeyHook>().SingleOrDefault(k => k.HookInfo.HookId == (IntPtr)wParam);
if (hook != null)
{
hook.OnMessageReceived();
processed = true;
}
}
break;
case WM.ACTIVATEAPP:
switch (LoWord(wParam))
{
case WA_ACTIVE:
Attach();
break;
case WA_INACTIVE:
Detach();
break;
}
break;
}
}
if (!processed)
{
return User32.CallWindowProc(_oldWndProc, hWnd, uMsg, wParam, lParam);
}
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
return User32.CallWindowProc(_oldWndProc, hWnd, uMsg, wParam, lParam);
}
/// <summary>
/// Gets the integer portion of a word
/// </summary>
private static int LoWord(int dw)
{
return (dw & 0x8000) != 0
? 0x8000 | (dw & 0x7FFF)
: dw & 0xFFFF;
}
private IntPtr GetWindowThread(IntPtr hWnd)
{
uint hThread;
User32.GetWindowThreadProcessId(hWnd, out hThread);
return (IntPtr)hThread;
}
}
HotKeyHook
The HotKeyHook
class is responsible for attaching and detaching a single hotkey, so that the RubberduckHooks
instance can pick it up in the main Windows message hook as a WM_HOTKEY
message.
I don't like the back-and-forth going on here:
- User presses a hotkey in the VBE.
WM_HOTKEY
message gets picked up inRubberduckHooks
instance; theOnMessageReceived
method is called on the appropriateHotKeyHook
instance.RubberduckHooks
handles theHotKeyHook
instance'sMessageReceived
event by relaying it to its ownOnMessageReceived
overload.App
class receives aMessageReceived
event that conveys aHookEventArgs
that tells Rubberduck which hotkey was pressed, and that's where the hotkey gets handled.
I'm sure this can be simplified quite a bit.
public class HotKeyHook : IHotKeyHook
{
private readonly Action _action;
private readonly IntPtr _hWndVbe;
public HookInfo HookInfo { get; private set; }
public HotKeyHook(IntPtr hWndVbe, string key, bool isTwoStepHotKey)
{
_hWndVbe = hWndVbe;
_action = OnMessageReceived;
IsTwoStepHotKey = isTwoStepHotKey;
Key = key;
}
public bool IsTwoStepHotKey { get; private set; }
public bool IsAttached { get; private set; }
public string Key { get; private set; }
public event EventHandler<HookEventArgs> MessageReceived;
public void OnMessageReceived()
{
var handler = MessageReceived;
if (handler != null)
{
var hotKey = Key;
var shift = GetModifierValue(ref hotKey);
var args = new HookEventArgs(GetKey(hotKey));
handler.Invoke(this, args);
}
}
public void Attach()
{
var hotKey = Key;
var lShift = GetModifierValue(ref hotKey);
var lKey = GetKey(hotKey);
if (lKey == Keys.None)
{
throw new InvalidOperationException("Invalid key.");
}
HookKey(lKey, lShift, _action);
}
public void Detach()
{
if (!IsAttached)
{
throw new InvalidOperationException("Hook is already detached.");
}
User32.UnregisterHotKey(_hWndVbe, HookInfo.HookId);
Kernel32.GlobalDeleteAtom(HookInfo.HookId);
IsAttached = false;
}
private void HookKey(Keys key, uint shift, Action action)
{
if (IsAttached)
{
throw new InvalidOperationException("Hook is already attached.");
}
var hookId = (IntPtr)Kernel32.GlobalAddAtom(Guid.NewGuid().ToString());
var success = User32.RegisterHotKey(_hWndVbe, hookId, shift, (uint)key);
if (!success)
{
throw new Win32Exception("HotKey was not registered.");
}
HookInfo = new HookInfo(hookId, key, shift, action);
IsAttached = true;
}
private static readonly IDictionary<char,uint> Modifiers = new Dictionary<char, uint>
{
{ '+', (uint)KeyModifier.SHIFT },
{ '%', (uint)KeyModifier.ALT },
{ '^', (uint)KeyModifier.CONTROL },
};
/// <summary>
/// Gets the <see cref="KeyModifier"/> values out of a key combination.
/// </summary>
/// <param name="key">The hotkey string, returned without the modifiers.</param>
private static uint GetModifierValue(ref string key)
{
uint result = 0;
for (var i = 0; i < 3; i++)
{
var firstChar = key[0];
if (Modifiers.ContainsKey(firstChar))
{
result |= Modifiers[firstChar];
}
else
{
// first character isn't a modifier symbol:
break;
}
// truncate first character for next iteration:
key = key.Substring(1);
}
return result;
}
private Keys GetKey(string keyCode)
{
var result = Keys.None;
switch (keyCode.Substring(0, 1))
{
case "{":
_keys.TryGetValue(keyCode, out result);
break;
case "~":
result = Keys.Return;
break;
default:
if (!String.IsNullOrEmpty(keyCode))
{
result = (Keys)Enum.Parse(typeof(Keys), keyCode);
}
break;
}
return result;
}
private static readonly IDictionary<string, Keys> _keys = new Dictionary<string, Keys>
{
{ "{BACKSPACE}", Keys.Back },
{ "{BS}", Keys.Back },
{ "{BKSP}", Keys.Back },
{ "{CAPSLOCK}", Keys.CapsLock },
{ "{DELETE}", Keys.Delete },
{ "{DEL}", Keys.Delete },
{ "{DOWN}", Keys.Down },
{ "{END}", Keys.End },
{ "{ENTER}", Keys.Enter },
{ "{RETURN}", Keys.Enter },
{ "{ESC}", Keys.Escape },
{ "{HELP}", Keys.Help },
{ "{HOME}", Keys.Home },
{ "{INSERT}", Keys.Insert },
{ "{INS}", Keys.Insert },
{ "{LEFT}", Keys.Left },
{ "{NUMLOCK}", Keys.NumLock },
{ "{PGDN}", Keys.PageDown },
{ "{PGUP}", Keys.PageUp },
{ "{PRTSC}", Keys.PrintScreen },
{ "{RIGHT}", Keys.Right },
{ "{TAB}", Keys.Tab },
{ "{UP}", Keys.Up },
{ "{F1}", Keys.F1 },
{ "{F2}", Keys.F2 },
{ "{F3}", Keys.F3 },
{ "{F4}", Keys.F4 },
{ "{F5}", Keys.F5 },
{ "{F6}", Keys.F6 },
{ "{F7}", Keys.F7 },
{ "{F8}", Keys.F8 },
{ "{F9}", Keys.F9 },
{ "{F10}", Keys.F10 },
{ "{F11}", Keys.F11 },
{ "{F12}", Keys.F12 },
{ "{F13}", Keys.F13 },
{ "{F14}", Keys.F14 },
{ "{F15}", Keys.F15 },
{ "{F16}", Keys.F16 },
};
}
By cheating a bit and hard-coding the "Ctrl+" part in the 2-step hotkey handler (I'll get the hotkey modifiers in there, that's a given), I got this screenshot:
..which shows that the mechanism works as intended:
- When a single-step hotkey is pressed, it gets handled immediately.
- When a two-step hotkey is pressed, the
RubberduckHooks
enters a "gimmeh teh key" mode that "eats" the next key (it won't show up in the code pane, and the low-level keyboard hook will ignore it), and it's up to theApp
class to determine which action to execute based on what the pressed key was. - Typing code in a code pane in the VBE still triggers a parse task that processes the modified code module.
My next implementation step will be to tweak my CommandMenuItem
objects to include either some hotkey string (e.g. "Ctrl+R,R" or "Ctrl+T"), or two Keys
properties (e.g. HotKeyStep1
and HotKeyStep2
, where step 1 would include the modifier key(s), and step 2 would be Keys.None
for a single-step hotkey), so that the App
class can cycle through its menus and determine which command needs to be executed, if any.
But before I do that, is there anything I need to know about this current implementation?
ILowLevelKeyboardHook : IHook
that's inheriting it. \$\endgroup\$