diff --git a/src/Eto.Gtk/Forms/KeyboardHandler.cs b/src/Eto.Gtk/Forms/KeyboardHandler.cs index ea540633e..3f6b966c0 100644 --- a/src/Eto.Gtk/Forms/KeyboardHandler.cs +++ b/src/Eto.Gtk/Forms/KeyboardHandler.cs @@ -2,6 +2,33 @@ { public class KeyboardHandler : Keyboard.IHandler { + EventHandler _modifiersChanged; + + public event EventHandler ModifiersChanged + { + add + { + if (_modifiersChanged == null) + { + Gdk.Keymap.Default.StateChanged += Keymap_StateChanged; + } + _modifiersChanged += value; + } + remove + { + _modifiersChanged -= value; + if (_modifiersChanged == null) + { + Gdk.Keymap.Default.StateChanged -= Keymap_StateChanged; + } + } + } + + private void Keymap_StateChanged(object sender, EventArgs e) + { + _modifiersChanged?.Invoke(null, EventArgs.Empty); + } + public bool IsKeyLocked(Keys key) { #if GTK3 @@ -23,6 +50,7 @@ public Keys Modifiers { get { + var ev = Gtk.Application.CurrentEvent; if (ev != null) { @@ -32,7 +60,7 @@ public Keys Modifiers return state.ToEtoKey(); } } - return Keys.None; + return ((Gdk.ModifierType)Gdk.Keymap.Default.ModifierState).ToEtoKey(); } } diff --git a/src/Eto.Mac/Forms/KeyboardHandler.cs b/src/Eto.Mac/Forms/KeyboardHandler.cs index c9d56e18d..b9f908af2 100644 --- a/src/Eto.Mac/Forms/KeyboardHandler.cs +++ b/src/Eto.Mac/Forms/KeyboardHandler.cs @@ -2,16 +2,46 @@ { public class KeyboardHandler : Keyboard.IHandler { - public bool IsKeyLocked(Keys key) + EventHandler _modifiersChanged; + NSObject _monitor; + public event EventHandler ModifiersChanged + { + add + { + if (_modifiersChanged == null) + { + _monitor = NSEvent.AddLocalMonitorForEventsMatchingMask(NSEventMask.FlagsChanged, HandleFlagsChanged); + } + _modifiersChanged += value; + } + remove + { + _modifiersChanged -= value; + if (_modifiersChanged == null && _monitor != null) + { + NSEvent.RemoveMonitor(_monitor); + _monitor = null; + } + } + } + + private NSEvent HandleFlagsChanged(NSEvent theEvent) { - return NSEvent.CurrentModifierFlags == key.ModifierMask(); + _modifiersChanged?.Invoke(null, EventArgs.Empty); + return theEvent; } - public Keys Modifiers + public bool IsKeyLocked(Keys key) { - get { return NSEvent.CurrentModifierFlags.ToEto(); } + var modifier = key.ModifierMask(); + return (ModifierFlags & modifier) == modifier; } + public Keys Modifiers => ModifierFlags.ToEto(); + + NSEventModifierMask ModifierFlags => NSEvent.CurrentModifierFlags; + // NSEventModifierMask ModifierFlags => NSApplication.SharedApplication.CurrentEvent?.ModifierFlags ?? NSEvent.CurrentModifierFlags; + public IEnumerable SupportedLockKeys { get diff --git a/src/Eto.WinForms/Eto.WinForms.csproj b/src/Eto.WinForms/Eto.WinForms.csproj index a7435644e..5c977aa7f 100755 --- a/src/Eto.WinForms/Eto.WinForms.csproj +++ b/src/Eto.WinForms/Eto.WinForms.csproj @@ -113,6 +113,9 @@ You do not need to use any of the classes of this assembly (unless customizing t Forms\DataObjectHandler.cs + + Forms\KeyboardHandler.cs + diff --git a/src/Eto.WinForms/Forms/KeyboardHandler.cs b/src/Eto.WinForms/Forms/KeyboardHandler.cs deleted file mode 100644 index ca79b2250..000000000 --- a/src/Eto.WinForms/Forms/KeyboardHandler.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Eto.WinForms.Forms -{ - public class KeyboardHandler : Keyboard.IHandler - { - public bool IsKeyLocked(Keys key) - { - return swf.Control.IsKeyLocked(key.ToSWF()); - } - - public Keys Modifiers - { - get { return swf.Control.ModifierKeys.ToEto(); } - } - - public IEnumerable SupportedLockKeys - { - get - { - yield return Keys.NumberLock; - yield return Keys.CapsLock; - yield return Keys.Insert; - yield return Keys.ScrollLock; - } - } - } -} \ No newline at end of file diff --git a/src/Eto.WinForms/Win32.cs b/src/Eto.WinForms/Win32.cs index 988c97230..a9e8a7bf5 100755 --- a/src/Eto.WinForms/Win32.cs +++ b/src/Eto.WinForms/Win32.cs @@ -182,6 +182,25 @@ public enum WM PRINT = 0x0317, SHOWWINDOW = 0x00000018 } + + public enum VK : long + { + SHIFT = 0x10, + CONTROL = 0x11, + MENU = 0x12, + CAPSLOCK = 0x14, + ESCAPE = 0x1B, + NUMLOCK = 0x90, + SCROLL = 0x91, + LSHIFT = 0xA0, + RSHIFT = 0xA1, + LCONTROL = 0xA2, + RCONTROL = 0xA3, + LMENU = 0xA4, + RMENU = 0xA5, + LWIN = 0x5B, + RWIN = 0x5C + } public enum HT { @@ -392,23 +411,41 @@ public static string GetWindowText(IntPtr hwnd) } // for tray indicator + + public enum WH + { + KEYBOARD = 2, + KEYBOARD_LL = 13, + MOUSE_LL = 14 + } + + + public static IntPtr SetHook(WH hookId, HookProc proc) + { + using (Process curProcess = Process.GetCurrentProcess()) + using (ProcessModule curModule = curProcess.MainModule) + { + return SetWindowsHookEx((IntPtr)hookId, proc, GetModuleHandle(curModule.ModuleName), 0); + } + } [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern int CallNextHookEx(int hookId, int code, int param, IntPtr dataPointer); + public static extern IntPtr CallNextHookEx(IntPtr hookId, int code, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern IntPtr GetModuleHandle(string moduleName); [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern int SetWindowsHookEx(int hookId, HookProc function, IntPtr instance, int threadId); + public static extern IntPtr SetWindowsHookEx(IntPtr hookId, HookProc function, IntPtr instance, int threadId); [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] - public static extern int UnhookWindowsHookEx(int hookId); + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool UnhookWindowsHookEx(IntPtr hookId); [DllImportAttribute("user32.dll")] public static extern bool ReleaseCapture(); - public delegate int HookProc(int code, int wParam, IntPtr structPointer); + public delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential)] public struct MouseLowLevelHook diff --git a/src/Eto.Wpf/Forms/KeyboardHandler.cs b/src/Eto.Wpf/Forms/KeyboardHandler.cs old mode 100644 new mode 100755 index a5df65385..493f4565f --- a/src/Eto.Wpf/Forms/KeyboardHandler.cs +++ b/src/Eto.Wpf/Forms/KeyboardHandler.cs @@ -1,26 +1,140 @@ -namespace Eto.Wpf.Forms +#if WINFORMS + +namespace Eto.WinForms.Forms +#else + +namespace Eto.Wpf.Forms +#endif { public class KeyboardHandler : Keyboard.IHandler { - public bool IsKeyLocked(Keys key) + EventHandler _modifiersChanged; + Keys _modifiers; + List _oldLockedKeys = new List(); + + Win32.HookProc _hookProc; + IntPtr _hookId; + Keys? _downKeys; + Keys? _upKeys; + + IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { - return swi.Keyboard.IsKeyToggled(key.ToWpfKey()); + // only trigger event when the application is active + if (nCode == 0 && Application.Instance.IsActive) + { + if (wParam == (IntPtr)Win32.WM.KEYDOWN || wParam == (IntPtr)Win32.WM.KEYUP + || wParam == (IntPtr)Win32.WM.SYSKEYDOWN || wParam == (IntPtr)Win32.WM.SYSKEYUP) + { + var kb = Marshal.PtrToStructure(lParam); + // Console.WriteLine($"Callback: {wParam:x}, {lParam}, {kb.VirtualKeyCode:x}, {kb.ScanCode:x}"); + + // this event happens before the state is updated, so we check which key was pressed. + var keys = Keys.None; + switch (kb.VirtualKeyCode) + { + case (int)Win32.VK.RMENU: + case (int)Win32.VK.LMENU: + case (int)Win32.VK.MENU: + keys = Keys.Alt; + break; + case (int)Win32.VK.RCONTROL: + case (int)Win32.VK.LCONTROL: + case (int)Win32.VK.CONTROL: + keys = Keys.Control; + break; + case (int)Win32.VK.RSHIFT: + case (int)Win32.VK.LSHIFT: + case (int)Win32.VK.SHIFT: + keys = Keys.Shift; + break; + case (int)Win32.VK.RWIN: + case (int)Win32.VK.LWIN: + keys = Keys.Application; + break; + } + if (wParam == (IntPtr)Win32.WM.KEYDOWN || wParam == (IntPtr)Win32.WM.SYSKEYDOWN) + _downKeys = keys; + else if (wParam == (IntPtr)Win32.WM.KEYUP || wParam == (IntPtr)Win32.WM.SYSKEYUP) + _upKeys = keys; + TriggerChanged(); + _downKeys = null; + _upKeys = null; + } + } + return Win32.CallNextHookEx(_hookId, nCode, wParam, lParam); } - public IEnumerable SupportedLockKeys + public event EventHandler ModifiersChanged { - get + add + { + if (_modifiersChanged == null) + { + _hookProc = new Win32.HookProc(HookCallback); + _hookId = Win32.SetHook(Win32.WH.KEYBOARD_LL, _hookProc); + _modifiers = Modifiers; + _oldLockedKeys.Clear(); + _oldLockedKeys.AddRange(SupportedLockKeys.Where(IsKeyLocked)); + } + _modifiersChanged += value; + } + remove { - yield return Keys.CapsLock; - yield return Keys.NumberLock; - yield return Keys.ScrollLock; - yield return Keys.Insert; + _modifiersChanged -= value; + if (_modifiersChanged == null && _hookId != IntPtr.Zero) + { + Win32.UnhookWindowsHookEx(_hookId); + _hookProc = null; + _hookId = IntPtr.Zero; + _modifiers = Keys.None; + _oldLockedKeys.Clear(); + } } } + private void TriggerChanged() + { + var newModifiers = Modifiers; + var newLockedKeys = SupportedLockKeys.Where(IsKeyLocked).ToList(); + + if (_modifiers != newModifiers || !_oldLockedKeys.SequenceEqual(newLockedKeys)) + { + _modifiers = newModifiers; + _oldLockedKeys = newLockedKeys; + _modifiersChanged?.Invoke(null, EventArgs.Empty); + } + } + +#if WINFORMS + public bool IsKeyLocked(Keys key) => swf.Control.IsKeyLocked(key.ToSWF()); +#else + public bool IsKeyLocked(Keys key) => swi.Keyboard.IsKeyToggled(key.ToWpfKey()); +#endif + + static readonly Keys[] _supportedLockKeys = new[] { + Keys.CapsLock, + Keys.NumberLock, + Keys.ScrollLock, + Keys.Insert + }; + + public IEnumerable SupportedLockKeys => _supportedLockKeys; + public Keys Modifiers { - get { return swi.Keyboard.Modifiers.ToEto(); } + get + { +#if WINFORMS + var modifiers = swf.Control.ModifierKeys.ToEto(); +#else + var modifiers = swi.Keyboard.Modifiers.ToEto(); +#endif + if (_downKeys != null) + modifiers |= _downKeys.Value; + if (_upKeys != null) + modifiers &= ~_upKeys.Value; + return modifiers; + } } } } diff --git a/src/Eto.Wpf/Forms/TrayIndicatorHandler.cs b/src/Eto.Wpf/Forms/TrayIndicatorHandler.cs index e51bdd0a0..00f2f5c47 100644 --- a/src/Eto.Wpf/Forms/TrayIndicatorHandler.cs +++ b/src/Eto.Wpf/Forms/TrayIndicatorHandler.cs @@ -8,19 +8,12 @@ namespace Eto.Wpf.Forms public class TrayIndicatorHandler : WidgetHandler, TrayIndicator.IHandler { - private const int KeyEscape = 27; - private const int WhKeyboardLowLevel = 13; - private const int WhMouseLowLevel = 14; - private const int WmKeydown = 0x100; - private const int WmLeftButtonDown = 0x201; - private const int WmRightButtonDown = 0x204; private Image _image; - private int _keyboardHookHandle; + private IntPtr _keyboardHookHandle; private Win32.HookProc _keyboardHookProcRef; - private int _mouseHookHandle; + private IntPtr _mouseHookHandle; private Win32.HookProc _mouseHookProcRef; - public TrayIndicatorHandler() { Control = new swf.NotifyIcon(); @@ -85,12 +78,12 @@ public override void AttachEvent(string id) private void ContextMenuClosed(object sender, RoutedEventArgs e) { - if (_mouseHookHandle != 0) + if (_mouseHookHandle != IntPtr.Zero) { Win32.UnhookWindowsHookEx(_mouseHookHandle); } - if (_keyboardHookHandle != 0) + if (_keyboardHookHandle != IntPtr.Zero) { Win32.UnhookWindowsHookEx(_keyboardHookHandle); } @@ -135,7 +128,7 @@ private static Point GetHitPoint(IntPtr structPointer) private static int GetKeyCode(IntPtr structPointer) { var keyboardHook = (Win32.KeyboardLowLevelHook) Marshal.PtrToStructure(structPointer, typeof(Win32.KeyboardLowLevelHook)); - return keyboardHook.VirtualKeyCode; + return (int)keyboardHook.VirtualKeyCode; } private void InitializeNativeHooks() @@ -146,30 +139,24 @@ private void InitializeNativeHooks() private void InstallHooks() { - using (var process = Process.GetCurrentProcess()) - using (var module = process.MainModule) + _mouseHookHandle = Win32.SetHook(Win32.WH.MOUSE_LL, _mouseHookProcRef); + if (_mouseHookHandle == IntPtr.Zero) { - _mouseHookHandle = Win32.SetWindowsHookEx(WhMouseLowLevel, _mouseHookProcRef, Win32.GetModuleHandle(module.ModuleName), 0); - if (_mouseHookHandle == 0) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - - _keyboardHookHandle = - Win32.SetWindowsHookEx(WhKeyboardLowLevel, _keyboardHookProcRef, Win32.GetModuleHandle(module.ModuleName), 0); - if (_keyboardHookHandle == 0) - { - throw new Win32Exception(Marshal.GetLastWin32Error()); - } + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + _keyboardHookHandle = Win32.SetHook(Win32.WH.KEYBOARD_LL, _keyboardHookProcRef); + if (_keyboardHookHandle == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); } } - private int KeyboardEventProc(int code, int wParam, IntPtr lParam) + private IntPtr KeyboardEventProc(int code, IntPtr wParam, IntPtr lParam) { - if (code == 0 && wParam == WmKeydown) + if (code == 0 && wParam == (IntPtr)Win32.WM.KEYDOWN) { var key = GetKeyCode(lParam); - if (key == KeyEscape) + if (key == (int)Win32.VK.ESCAPE) { var menu = ContextMenuHandler.GetControl(Menu); menu.IsOpen = false; @@ -179,10 +166,10 @@ private int KeyboardEventProc(int code, int wParam, IntPtr lParam) return Win32.CallNextHookEx(_keyboardHookHandle, code, wParam, lParam); } - private int MouseEventProc(int code, int wParam, IntPtr lParam) + private IntPtr MouseEventProc(int code, IntPtr wParam, IntPtr lParam) { var menu = ContextMenuHandler.GetControl(Menu); - if (menu.IsVisible && code == 0 && (wParam == WmLeftButtonDown || wParam == WmRightButtonDown)) + if (menu.IsVisible && code == 0 && (wParam == (IntPtr)Win32.WM.LBUTTONDOWN || wParam == (IntPtr)Win32.WM.RBUTTONDOWN)) { swc.MenuItem subMenuItem = GetCurrentSubMenuItem(menu.Items); var hitPoint = GetHitPoint(lParam); diff --git a/src/Eto/Forms/Keyboard.cs b/src/Eto/Forms/Keyboard.cs index 7db20503a..c4d994251 100644 --- a/src/Eto/Forms/Keyboard.cs +++ b/src/Eto/Forms/Keyboard.cs @@ -6,13 +6,13 @@ [Handler(typeof(IHandler))] public static class Keyboard { - static IHandler Handler { get { return Platform.Instance.CreateShared(); } } + static IHandler Handler => Platform.Instance.CreateShared(); /// /// Gets an enumeration of all keys supported by the method. /// /// The supported lock keys. - public static IEnumerable SupportedLockKeys { get { return Handler.SupportedLockKeys; } } + public static IEnumerable SupportedLockKeys => Handler.SupportedLockKeys; /// /// Determines if the specified is in a locked state, such as the , @@ -20,10 +20,7 @@ public static class Keyboard /// /// true if the specified key is locked; otherwise, false. /// Key to determine the state. - public static bool IsKeyLocked(Keys key) - { - return Handler.IsKeyLocked(key); - } + public static bool IsKeyLocked(Keys key) => Handler.IsKeyLocked(key); /// /// Gets the current modifier state for keys such as , and . @@ -31,9 +28,19 @@ public static bool IsKeyLocked(Keys key) /// /// This typically will only return a value for the current event, such as during a mouse or keyboard event. /// - public static Keys Modifiers + public static Keys Modifiers => Handler.Modifiers; + + /// + /// Event to handle when the or state has changed + /// + /// + /// Note that this event is long-lived, so if you subscribe to this event to an instance method of a control or + /// short-lived object be sure to unsubscribe it otherwise that object will never be garbage collected. + /// + public static event EventHandler ModifiersChanged { - get { return Handler.Modifiers; } + add => Handler.ModifiersChanged += value; + remove => Handler.ModifiersChanged -= value; } /// @@ -62,5 +69,14 @@ public interface IHandler /// This typically will only return a value for the current event, such as during a mouse or keyboard event. /// Keys Modifiers { get; } + + /// + /// Event to handle when the or state has changed + /// + /// + /// Note that this event is long-lived, so if you subscribe to this event to an instance method of a control or + /// short-lived object be sure to unsubscribe it otherwise that object will never be garbage collected. + /// + event EventHandler ModifiersChanged; } } \ No newline at end of file diff --git a/test/Eto.Test/Sections/Behaviors/KeyboardSection.cs b/test/Eto.Test/Sections/Behaviors/KeyboardSection.cs new file mode 100644 index 000000000..bfecc88d3 --- /dev/null +++ b/test/Eto.Test/Sections/Behaviors/KeyboardSection.cs @@ -0,0 +1,49 @@ +using System; +using Eto.Forms; + +namespace Eto.Test.Sections.Behaviors +{ + [Section("Behaviors", "Keyboard")] + public class KeyboardSection : Panel + { + Label label = new Label { TextAlignment = TextAlignment.Center }; + public KeyboardSection() + { + var layout = new DynamicLayout(); + + layout.BeginCentered(yscale: true); + + layout.Add(new Label { Text = "Press a modifier key", TextAlignment = TextAlignment.Center }); + layout.Add(label); + + layout.EndCentered(); + + Content = layout; + + Keyboard_ModifiersChanged(null, EventArgs.Empty); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + Keyboard.ModifiersChanged += Keyboard_ModifiersChanged; + } + + protected override void OnUnLoad(EventArgs e) + { + base.OnUnLoad(e); + Keyboard.ModifiersChanged -= Keyboard_ModifiersChanged; + } + + private void Keyboard_ModifiersChanged(object sender, EventArgs e) + { + Log.Write(this, $"Keyboard.ModifiersChanged: {Keyboard.Modifiers}"); + label.Text = $"Modifiers: {Keyboard.Modifiers}"; + foreach (var lockKey in Keyboard.SupportedLockKeys) + { + label.Text += $"\n{lockKey}: {Keyboard.IsKeyLocked(lockKey)}"; + } + } + } +} +