diff --git a/SuperPutty/Properties/AssemblyInfo.cs b/SuperPutty/Properties/AssemblyInfo.cs index 2434e06f..a241231e 100644 --- a/SuperPutty/Properties/AssemblyInfo.cs +++ b/SuperPutty/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -17,6 +18,7 @@ // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] +[assembly: InternalsVisibleTo("SuperPuttyUnitTests")] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("7f061c0e-1ca2-43a8-92f5-49563ff4ee81")] diff --git a/SuperPutty/SuperPutty.csproj b/SuperPutty/SuperPutty.csproj index 9d20575b..49bf90b3 100644 --- a/SuperPutty/SuperPutty.csproj +++ b/SuperPutty/SuperPutty.csproj @@ -281,6 +281,7 @@ + @@ -518,4 +519,4 @@ SET errorlevel= --> - \ No newline at end of file + diff --git a/SuperPutty/Utils/KeyboardHookState.cs b/SuperPutty/Utils/KeyboardHookState.cs new file mode 100644 index 00000000..be4255b6 --- /dev/null +++ b/SuperPutty/Utils/KeyboardHookState.cs @@ -0,0 +1,130 @@ +using System; +using System.Windows.Forms; + +namespace SuperPutty.Utils +{ + internal sealed class KeyboardHookState + { + internal static readonly UIntPtr SuperPuttyInputMarker = new UIntPtr(0x53505459u); + + private bool isLeftControlDown; + private bool isRightControlDown; + private bool isLeftShiftDown; + private bool isRightShiftDown; + private bool isLeftAltDown; + private bool isRightAltDown; + private bool isLeftWinDown; + private bool isRightWinDown; + + internal long Version { get; private set; } + + internal bool IsControlDown { get { return isLeftControlDown || isRightControlDown; } } + internal bool IsShiftDown { get { return isLeftShiftDown || isRightShiftDown; } } + internal bool IsAltDown { get { return isLeftAltDown || isRightAltDown; } } + internal bool IsWinDown { get { return isLeftWinDown || isRightWinDown; } } + + internal void Update(Keys keyCode, bool isKeyDown) + { + switch (keyCode) + { + case Keys.LControlKey: + SetState(ref isLeftControlDown, isKeyDown); + break; + case Keys.RControlKey: + SetState(ref isRightControlDown, isKeyDown); + break; + case Keys.LShiftKey: + SetState(ref isLeftShiftDown, isKeyDown); + break; + case Keys.RShiftKey: + SetState(ref isRightShiftDown, isKeyDown); + break; + case Keys.LMenu: + SetState(ref isLeftAltDown, isKeyDown); + break; + case Keys.RMenu: + SetState(ref isRightAltDown, isKeyDown); + break; + case Keys.LWin: + SetState(ref isLeftWinDown, isKeyDown); + break; + case Keys.RWin: + SetState(ref isRightWinDown, isKeyDown); + break; + } + } + + internal void Synchronize(Func isKeyDown) + { + if (isKeyDown == null) + { + throw new ArgumentNullException("isKeyDown"); + } + + bool leftControlDown = isKeyDown(Keys.LControlKey); + bool rightControlDown = isKeyDown(Keys.RControlKey); + bool leftShiftDown = isKeyDown(Keys.LShiftKey); + bool rightShiftDown = isKeyDown(Keys.RShiftKey); + bool leftAltDown = isKeyDown(Keys.LMenu); + bool rightAltDown = isKeyDown(Keys.RMenu); + bool leftWinDown = isKeyDown(Keys.LWin); + bool rightWinDown = isKeyDown(Keys.RWin); + + bool changed = + isLeftControlDown != leftControlDown || + isRightControlDown != rightControlDown || + isLeftShiftDown != leftShiftDown || + isRightShiftDown != rightShiftDown || + isLeftAltDown != leftAltDown || + isRightAltDown != rightAltDown || + isLeftWinDown != leftWinDown || + isRightWinDown != rightWinDown; + + isLeftControlDown = leftControlDown; + isRightControlDown = rightControlDown; + isLeftShiftDown = leftShiftDown; + isRightShiftDown = rightShiftDown; + isLeftAltDown = leftAltDown; + isRightAltDown = rightAltDown; + isLeftWinDown = leftWinDown; + isRightWinDown = rightWinDown; + + if (changed) + { + Version++; + } + } + + internal void Reset() + { + if (IsControlDown || IsShiftDown || IsAltDown || IsWinDown) + { + Version++; + } + + isLeftControlDown = false; + isRightControlDown = false; + isLeftShiftDown = false; + isRightShiftDown = false; + isLeftAltDown = false; + isRightAltDown = false; + isLeftWinDown = false; + isRightWinDown = false; + } + + internal static bool IsSuperPuttyInjectedInput(NativeMethods.KBDLLHOOKSTRUCT keyboardData) + { + return (keyboardData.flags & NativeMethods.LowLevelKeyboardFlags.Injected) != 0 && + keyboardData.dwExtraInfo.Equals(SuperPuttyInputMarker); + } + + private void SetState(ref bool currentState, bool newState) + { + if (currentState != newState) + { + currentState = newState; + Version++; + } + } + } +} diff --git a/SuperPutty/Utils/NativeMethods.cs b/SuperPutty/Utils/NativeMethods.cs index 937929fb..318dfd75 100644 --- a/SuperPutty/Utils/NativeMethods.cs +++ b/SuperPutty/Utils/NativeMethods.cs @@ -68,6 +68,77 @@ public const uint KEYEVENTF_EXTENDEDKEY = 0x0001, KEYEVENTF_KEYUP = 0x0002; + public const uint INPUT_KEYBOARD = 1; + + [Flags] + public enum LowLevelKeyboardFlags : uint + { + Extended = 0x01, + LowerIntegrityLevelInjected = 0x02, + Injected = 0x10, + AltDown = 0x20, + KeyUp = 0x80 + } + + [StructLayout(LayoutKind.Sequential)] + public struct KBDLLHOOKSTRUCT + { + public uint vkCode; + public uint scanCode; + public LowLevelKeyboardFlags flags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct INPUT + { + public uint type; + public InputUnion data; + } + + [StructLayout(LayoutKind.Explicit)] + public struct InputUnion + { + [FieldOffset(0)] + public MOUSEINPUT mouse; + + [FieldOffset(0)] + public KEYBDINPUT keyboard; + + [FieldOffset(0)] + public HARDWAREINPUT hardware; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MOUSEINPUT + { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public UIntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + public struct HARDWAREINPUT + { + public uint uMsg; + public ushort wParamL; + public ushort wParamH; + } + [Flags] public enum AnimateWindowFlags { @@ -1102,9 +1173,6 @@ public static class HotKeysConstants [DllImport("user32.dll")] public static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl); - [DllImport("user32.dll")] - public static extern short GetKeyState(int nVirtKey); - [DllImport("user32.dll")] public static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk); @@ -1239,6 +1307,12 @@ public override string ToString() [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] public static extern IntPtr SetWindowsHookEx(uint idHook, LowLevelKMProc lpfn, IntPtr hMod, uint dwThreadId); + [DllImport("user32.dll", SetLastError = true)] + public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); + + [DllImport("user32.dll")] + public static extern short GetAsyncKeyState(int vKey); + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetActiveWindow(IntPtr hWnd); diff --git a/SuperPutty/frmSuperPutty.cs b/SuperPutty/frmSuperPutty.cs index dbbe8152..ad3bf42c 100644 --- a/SuperPutty/frmSuperPutty.cs +++ b/SuperPutty/frmSuperPutty.cs @@ -71,6 +71,7 @@ public partial class frmSuperPutty : Form private FormWindowState lastNonMinimizedWindowState = FormWindowState.Normal; private Rectangle lastNormalDesktopBounds; private ChildWindowFocusHelper focusHelper; + private readonly KeyboardHookState keyboardHookState = new KeyboardHookState(); int commandMRUIndex = -1; private readonly TabSwitcher tabSwitcher; @@ -161,6 +162,10 @@ public frmSuperPutty() // Low-Level Mouse and Keyboard hooks llkp = KBHookCallback; kbHookID = SetKBHook(llkp); + if (kbHookID == IntPtr.Zero) + { + Log.ErrorFormat("Unable to install the keyboard hook. Win32Error={0}", Marshal.GetLastWin32Error()); + } //llmp = MHookCallback; //mHookID = SetMHook(llmp); @@ -272,7 +277,11 @@ private void frmSuperPutty_Load(object sender, EventArgs e) protected override void OnFormClosed(FormClosedEventArgs e) { // free hooks - NativeMethods.UnhookWindowsHookEx(kbHookID); + if (kbHookID != IntPtr.Zero && !NativeMethods.UnhookWindowsHookEx(kbHookID)) + { + Log.WarnFormat("Unable to remove the keyboard hook. Win32Error={0}", Marshal.GetLastWin32Error()); + } + kbHookID = IntPtr.Zero; //NativeMethods.UnhookWindowsHookEx(mHookID); // save window size and location if not maximized or minimized @@ -352,9 +361,29 @@ public void FocusActiveDocument(string caller) private void frmSuperPutty_Activated(object sender, EventArgs e) { Log.DebugFormat("[{0}] Activated", this.Handle); + keyboardHookState.Synchronize(IsAsyncKeyDown); //dockPanel1_ActiveDocumentChanged(null, null); } + protected override void OnDeactivate(EventArgs e) + { + long modifierStateVersion = keyboardHookState.Version; + base.OnDeactivate(e); + + if (IsHandleCreated && !IsDisposed && !Disposing) + { + BeginInvoke(new Action(() => + { + if (!IsDisposed && !Disposing && + modifierStateVersion == keyboardHookState.Version && + !IsForegroundWindow(true)) + { + keyboardHookState.Reset(); + } + })); + } + } + public void SetActiveDocument(ToolWindow toolWindow) { if (this.DockPanel.ActiveDocument != toolWindow) @@ -1279,11 +1308,9 @@ private static IntPtr SetKBHook(NativeMethods.LowLevelKMProc proc) } } - private IntPtr foregroundBeforeWinDown = IntPtr.Zero; - - private static bool GetKeyDown(Keys keyCode) + private static bool IsAsyncKeyDown(Keys keyCode) { - return NativeMethods.GetKeyState((int)keyCode) < 0; + return (NativeMethods.GetAsyncKeyState((int)keyCode) & 0x8000) != 0; } // Intercept keyboard messages for Ctrl-F4 and Ctrl-Tab handling @@ -1291,24 +1318,37 @@ private IntPtr KBHookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode >= 0) { - int vkCode = Marshal.ReadInt32(lParam); - Keys keys = (Keys)vkCode; + NativeMethods.KBDLLHOOKSTRUCT keyboardData = + (NativeMethods.KBDLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(NativeMethods.KBDLLHOOKSTRUCT)); + Keys keys = (Keys)keyboardData.vkCode; + + // Let input generated by SuperPuTTY pass through without treating it + // as physical state or recursively executing SuperPuTTY shortcuts. + if (KeyboardHookState.IsSuperPuttyInjectedInput(keyboardData)) + { + return NativeMethods.CallNextHookEx(kbHookID, nCode, wParam, lParam); + } - // get key state of control/alt/shift is up/down bool isKeyDown = wParam == (IntPtr)NativeMethods.WM_KEYDOWN || wParam == (IntPtr)NativeMethods.WM_SYSKEYDOWN; - bool isControlDown = GetKeyDown(Keys.LControlKey) || GetKeyDown(Keys.RControlKey); - bool isShiftDown = GetKeyDown(Keys.LShiftKey) || GetKeyDown(Keys.RShiftKey); - bool isAltDown = GetKeyDown(Keys.LMenu) || GetKeyDown(Keys.RMenu); - bool isWinDown = GetKeyDown(Keys.LWin) || GetKeyDown(Keys.RWin); + keyboardHookState.Update(keys, isKeyDown); + + bool isControlDown = keyboardHookState.IsControlDown; + bool isShiftDown = keyboardHookState.IsShiftDown; + bool isAltDown = keyboardHookState.IsAltDown; + bool isWinDown = keyboardHookState.IsWinDown; if (Log.Logger.IsEnabledFor(Level.Trace)) { Log.DebugFormat("### KBHook: nCode={0}, wParam={1}, lParam={2} ({4,-4} - {3}) [{5}{6}{7}{8}]", - nCode, wParam, vkCode, keys, isKeyDown ? "Down" : "Up", - isControlDown ? "Ctrl" : "", isAltDown ? "Alt" : "", isAltDown ? "Shift" : "", isWinDown ? "Win" : ""); + nCode, wParam, keyboardData.vkCode, keys, isKeyDown ? "Down" : "Up", + isControlDown ? "Ctrl" : "", isAltDown ? "Alt" : "", isShiftDown ? "Shift" : "", isWinDown ? "Win" : ""); } - if (IsForegroundWindow(true)) + IntPtr foregroundWindow = NativeMethods.GetForegroundWindow(); + bool embeddedWindowIsForeground = IsForegroundWindow(foregroundWindow, false); + bool superPuttyIsForeground = embeddedWindowIsForeground || foregroundWindow == Handle; + + if (superPuttyIsForeground) { // SuperPutty or Putty is the window in front... @@ -1386,43 +1426,53 @@ private IntPtr KBHookCallback(int nCode, IntPtr wParam, IntPtr lParam) } } - if (IsForegroundWindow(false)) + if (embeddedWindowIsForeground) { if (isKeyDown && isWinDown && (keys & Keys.KeyCode) == Keys.Left) { - if ((keys & Keys.Shift) == Keys.Shift) + if (isShiftDown) { ShiftWindow(-1); } else { - SnapWindow(Keys.Left); + if (!TrySnapWindow(Keys.Left)) + { + return NativeMethods.CallNextHookEx(kbHookID, nCode, wParam, lParam); + } } return (IntPtr)1; } if (isKeyDown && isWinDown && (keys & Keys.KeyCode) == Keys.Right) { - if ((keys & Keys.Shift) == Keys.Shift) + if (isShiftDown) { ShiftWindow(1); } else { - SnapWindow(Keys.Right); + if (!TrySnapWindow(Keys.Right)) + { + return NativeMethods.CallNextHookEx(kbHookID, nCode, wParam, lParam); + } } return (IntPtr)1; } if (isKeyDown && isWinDown && (keys & Keys.KeyCode) == Keys.Up) { - SnapWindow(Keys.Up); - return (IntPtr)1; + if (TrySnapWindow(Keys.Up)) + { + return (IntPtr)1; + } } if (isKeyDown && isWinDown && (keys & Keys.KeyCode) == Keys.Down) { - SnapWindow(Keys.Down); - return (IntPtr)1; + if (TrySnapWindow(Keys.Down)) + { + return (IntPtr)1; + } } - if (isKeyDown && (keys & Keys.Modifiers) == Keys.Alt && (keys & Keys.KeyCode) == Keys.F4) + if (isKeyDown && isAltDown && (keys & Keys.KeyCode) == Keys.F4) { Application.Exit(); return (IntPtr)1; @@ -1471,7 +1521,11 @@ private bool FindChildControl(Control control, IntPtr hWnd) private bool IsForegroundWindow(bool includeMainForm) { - IntPtr fgWindow = NativeMethods.GetForegroundWindow(); + return IsForegroundWindow(NativeMethods.GetForegroundWindow(), includeMainForm); + } + + private bool IsForegroundWindow(IntPtr fgWindow, bool includeMainForm) + { if (includeMainForm && this.Handle == fgWindow) return true; // main form is FG if (FindChildControl(this.DockPanel, fgWindow)) @@ -1955,26 +2009,62 @@ private void OnIconFilterClicked(object sender, ImageListPopupEventArgs e) #endregion - private void SnapWindow(Keys direction) + private bool TrySnapWindow(Keys direction) { - NativeMethods.SetForegroundWindow(this.Handle); + IntPtr previousForegroundWindow = NativeMethods.GetForegroundWindow(); + if (!NativeMethods.SetForegroundWindow(this.Handle)) + { + Log.WarnFormat("Unable to focus SuperPuTTY before snapping {0}. Win32Error={1}", + direction, Marshal.GetLastWin32Error()); + return false; + } + + // The physical Windows key is still down. Forward only the arrow to + // the main form so Windows performs the snap without changing the + // user's physical Windows-key state. + NativeMethods.INPUT[] inputs = + { + CreateKeyboardInput(direction, NativeMethods.KEYEVENTF_EXTENDEDKEY), + CreateKeyboardInput(direction, NativeMethods.KEYEVENTF_EXTENDEDKEY | NativeMethods.KEYEVENTF_KEYUP) + }; + + uint sent = NativeMethods.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(NativeMethods.INPUT))); + if (sent != (uint)inputs.Length) + { + Log.WarnFormat("Unable to send snap input for {0}. Sent {1} of {2} events. Win32Error={3}", + direction, sent, inputs.Length, Marshal.GetLastWin32Error()); - NativeMethods.keybd_event((byte)Keys.LWin, 0, 0, 0); - NativeMethods.keybd_event((byte)direction, 0, 0, 0); - NativeMethods.keybd_event((byte)direction, 0, NativeMethods.KEYEVENTF_KEYUP, 0); - NativeMethods.keybd_event((byte)Keys.LWin, 0, NativeMethods.KEYEVENTF_KEYUP, 0); + if (previousForegroundWindow != IntPtr.Zero && previousForegroundWindow != Handle && + !NativeMethods.SetForegroundWindow(previousForegroundWindow)) + { + Log.WarnFormat("Unable to restore the previous foreground window after snap failure. Win32Error={0}", + Marshal.GetLastWin32Error()); + } + return false; + } + + return true; + } + + private static NativeMethods.INPUT CreateKeyboardInput(Keys key, uint flags) + { + return new NativeMethods.INPUT + { + type = NativeMethods.INPUT_KEYBOARD, + data = new NativeMethods.InputUnion + { + keyboard = new NativeMethods.KEYBDINPUT + { + wVk = (ushort)key, + dwFlags = flags, + dwExtraInfo = KeyboardHookState.SuperPuttyInputMarker + } + } + }; } private void ShiftWindow(int offset) { -/* Keys direction = (offset < 0) ? Keys.Left : Keys.Right; - NativeMethods.keybd_event((byte)Keys.LWin, 0, 0, 0); - NativeMethods.keybd_event((byte)Keys.ShiftKey, 0, 0, 0); - NativeMethods.keybd_event((byte)direction, 0, 0, 0); - NativeMethods.keybd_event((byte)direction, 0, NativeMethods.KEYEVENTF_KEYUP, 0); - NativeMethods.keybd_event((byte)Keys.ShiftKey, 0, NativeMethods.KEYEVENTF_KEYUP, 0); - NativeMethods.keybd_event((byte)Keys.LWin, 0, NativeMethods.KEYEVENTF_KEYUP, 0); -*/ if (Screen.AllScreens.Length < 2) return; diff --git a/SuperPuttyUnitTests/KeyboardHookStateTests.cs b/SuperPuttyUnitTests/KeyboardHookStateTests.cs new file mode 100644 index 00000000..ee49ce0e --- /dev/null +++ b/SuperPuttyUnitTests/KeyboardHookStateTests.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using NUnit.Framework; +using SuperPutty.Utils; + +namespace SuperPuttyUnitTests +{ + [TestFixture] + public class KeyboardHookStateTests + { + [Test] + public void TracksLeftAndRightModifiersIndependently() + { + KeyboardHookState state = new KeyboardHookState(); + + state.Update(Keys.LControlKey, true); + state.Update(Keys.RControlKey, true); + state.Update(Keys.LControlKey, false); + + Assert.True(state.IsControlDown); + + state.Update(Keys.RControlKey, false); + + Assert.False(state.IsControlDown); + } + + [Test] + public void ResetClearsAllModifiers() + { + KeyboardHookState state = new KeyboardHookState(); + state.Update(Keys.LControlKey, true); + state.Update(Keys.RShiftKey, true); + state.Update(Keys.LMenu, true); + state.Update(Keys.RWin, true); + + state.Reset(); + + Assert.False(state.IsControlDown); + Assert.False(state.IsShiftDown); + Assert.False(state.IsAltDown); + Assert.False(state.IsWinDown); + } + + [Test] + public void SynchronizeRestoresPhysicallyHeldModifiers() + { + KeyboardHookState state = new KeyboardHookState(); + HashSet downKeys = new HashSet { Keys.LShiftKey, Keys.RWin }; + + state.Synchronize(downKeys.Contains); + + Assert.False(state.IsControlDown); + Assert.True(state.IsShiftDown); + Assert.False(state.IsAltDown); + Assert.True(state.IsWinDown); + } + + [Test] + public void IdentifiesOnlyMarkedSuperPuttyInput() + { + NativeMethods.KBDLLHOOKSTRUCT keyboardData = new NativeMethods.KBDLLHOOKSTRUCT + { + flags = NativeMethods.LowLevelKeyboardFlags.Injected, + dwExtraInfo = KeyboardHookState.SuperPuttyInputMarker + }; + + Assert.True(KeyboardHookState.IsSuperPuttyInjectedInput(keyboardData)); + + keyboardData.dwExtraInfo = UIntPtr.Zero; + Assert.False(KeyboardHookState.IsSuperPuttyInjectedInput(keyboardData)); + + keyboardData.flags = 0; + keyboardData.dwExtraInfo = KeyboardHookState.SuperPuttyInputMarker; + Assert.False(KeyboardHookState.IsSuperPuttyInjectedInput(keyboardData)); + } + } +} diff --git a/SuperPuttyUnitTests/SuperPuttyUnitTests.csproj b/SuperPuttyUnitTests/SuperPuttyUnitTests.csproj index 623d2df3..e2bc9435 100644 --- a/SuperPuttyUnitTests/SuperPuttyUnitTests.csproj +++ b/SuperPuttyUnitTests/SuperPuttyUnitTests.csproj @@ -90,6 +90,7 @@ + @@ -156,4 +157,4 @@ --> - \ No newline at end of file +