From b78f6541dc5d8a32c3129960b3344dcedb8e5735 Mon Sep 17 00:00:00 2001 From: maier_S Date: Wed, 23 Mar 2022 10:15:54 +0100 Subject: [PATCH] Squashed 'NHotkey/' content from commit e16cceb git-subtree-dir: NHotkey git-subtree-split: e16cceb7b3e108413d409dbf355a67835822a5e7 --- .gitignore | 400 ++++++++++++++++++ Directory.Build.props | 22 + NHotkey.WindowsForms/ExtensionAttribute.cs | 10 + NHotkey.WindowsForms/Extensions.cs | 12 + NHotkey.WindowsForms/HotkeyManager.cs | 91 ++++ .../NHotkey.WindowsForms.csproj | 12 + NHotkey.Wpf/Extensions.cs | 17 + .../HotkeyAlreadyRegisteredEventArgs.cs | 19 + NHotkey.Wpf/HotkeyManager.cs | 249 +++++++++++ NHotkey.Wpf/NHotkey.Wpf.csproj | 12 + NHotkey.Wpf/Properties/AssemblyInfo.cs | 7 + NHotkey.Wpf/WeakReferenceCollection.cs | 45 ++ NHotkey.sln | 37 ++ NHotkey.snk | Bin 0 -> 596 bytes NHotkey/GlobalSuppressions.cs | Bin 0 -> 1366 bytes NHotkey/Hotkey.cs | 71 ++++ NHotkey/HotkeyAlreadyRegisteredException.cs | 36 ++ NHotkey/HotkeyEventArgs.cs | 21 + NHotkey/HotkeyFlags.cs | 15 + NHotkey/HotkeyManagerBase.cs | 82 ++++ NHotkey/NHotkey.csproj | 10 + NHotkey/NativeMethods.cs | 14 + NHotkey/Properties/AssemblyInfo.cs | 4 + README.md | 2 + 24 files changed, 1188 insertions(+) create mode 100644 .gitignore create mode 100644 Directory.Build.props create mode 100644 NHotkey.WindowsForms/ExtensionAttribute.cs create mode 100644 NHotkey.WindowsForms/Extensions.cs create mode 100644 NHotkey.WindowsForms/HotkeyManager.cs create mode 100644 NHotkey.WindowsForms/NHotkey.WindowsForms.csproj create mode 100644 NHotkey.Wpf/Extensions.cs create mode 100644 NHotkey.Wpf/HotkeyAlreadyRegisteredEventArgs.cs create mode 100644 NHotkey.Wpf/HotkeyManager.cs create mode 100644 NHotkey.Wpf/NHotkey.Wpf.csproj create mode 100644 NHotkey.Wpf/Properties/AssemblyInfo.cs create mode 100644 NHotkey.Wpf/WeakReferenceCollection.cs create mode 100644 NHotkey.sln create mode 100644 NHotkey.snk create mode 100644 NHotkey/GlobalSuppressions.cs create mode 100644 NHotkey/Hotkey.cs create mode 100644 NHotkey/HotkeyAlreadyRegisteredException.cs create mode 100644 NHotkey/HotkeyEventArgs.cs create mode 100644 NHotkey/HotkeyFlags.cs create mode 100644 NHotkey/HotkeyManagerBase.cs create mode 100644 NHotkey/NHotkey.csproj create mode 100644 NHotkey/NativeMethods.cs create mode 100644 NHotkey/Properties/AssemblyInfo.cs create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e7a151 --- /dev/null +++ b/.gitignore @@ -0,0 +1,400 @@ +# ---> VisualStudio +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..093c6af --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,22 @@ + + + + $(MSBuildThisFileDirectory)NHotkey.snk + true + + + + Thomas Levesque + https://github.com/thomaslevesque/NHotkey + Apache-2.0 + global;hotkey;windows + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + + + + + diff --git a/NHotkey.WindowsForms/ExtensionAttribute.cs b/NHotkey.WindowsForms/ExtensionAttribute.cs new file mode 100644 index 0000000..47b2b59 --- /dev/null +++ b/NHotkey.WindowsForms/ExtensionAttribute.cs @@ -0,0 +1,10 @@ +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + // Enables extension methods in assembly that targets .NET 2.0 + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Method)] + internal sealed class ExtensionAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/NHotkey.WindowsForms/Extensions.cs b/NHotkey.WindowsForms/Extensions.cs new file mode 100644 index 0000000..933ecdc --- /dev/null +++ b/NHotkey.WindowsForms/Extensions.cs @@ -0,0 +1,12 @@ +using System.Windows.Forms; + +namespace NHotkey.WindowsForms +{ + static class Extensions + { + public static bool HasFlag(this Keys keys, Keys flag) + { + return (keys & flag) == flag; + } + } +} diff --git a/NHotkey.WindowsForms/HotkeyManager.cs b/NHotkey.WindowsForms/HotkeyManager.cs new file mode 100644 index 0000000..445771d --- /dev/null +++ b/NHotkey.WindowsForms/HotkeyManager.cs @@ -0,0 +1,91 @@ +using System; +using System.Windows.Forms; + +namespace NHotkey.WindowsForms +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Design", + "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", + Justification = "This is a singleton; disposing it would break it")] + public class HotkeyManager : HotkeyManagerBase + { + #region Singleton implementation + + public static HotkeyManager Current { get { return LazyInitializer.Instance; } } + + private static class LazyInitializer + { + static LazyInitializer() { } + public static readonly HotkeyManager Instance = new HotkeyManager(); + } + + #endregion + + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly MessageWindow _messageWindow; + + private HotkeyManager() + { + _messageWindow = new MessageWindow(this); + SetHwnd(_messageWindow.Handle); + } + + public void AddOrReplace(string name, Keys keys, bool noRepeat, EventHandler handler) + { + var flags = GetFlags(keys, noRepeat); + var vk = unchecked((uint)(keys & ~Keys.Modifiers)); + AddOrReplace(name, vk, flags, handler); + } + + public void AddOrReplace(string name, Keys keys, EventHandler handler) + { + AddOrReplace(name, keys, false, handler); + } + + private static HotkeyFlags GetFlags(Keys hotkey, bool noRepeat) + { + var noMod = hotkey & ~Keys.Modifiers; + var flags = HotkeyFlags.None; + if (hotkey.HasFlag(Keys.Alt)) + flags |= HotkeyFlags.Alt; + if (hotkey.HasFlag(Keys.Control)) + flags |= HotkeyFlags.Control; + if (hotkey.HasFlag(Keys.Shift)) + flags |= HotkeyFlags.Shift; + if (noMod == Keys.LWin || noMod == Keys.RWin) + flags |= HotkeyFlags.Windows; + if (noRepeat) + flags |= HotkeyFlags.NoRepeat; + return flags; + } + + class MessageWindow : ContainerControl + { + private readonly HotkeyManager _hotkeyManager; + + public MessageWindow(HotkeyManager hotkeyManager) + { + _hotkeyManager = hotkeyManager; + } + + protected override CreateParams CreateParams + { + get + { + var parameters = base.CreateParams; + parameters.Parent = HwndMessage; + return parameters; + } + } + + protected override void WndProc(ref Message m) + { + bool handled = false; + Hotkey hotkey; + m.Result = _hotkeyManager.HandleHotkeyMessage(Handle, m.Msg, m.WParam, m.LParam, ref handled, out hotkey); + if (!handled) + base.WndProc(ref m); + } + } + } +} diff --git a/NHotkey.WindowsForms/NHotkey.WindowsForms.csproj b/NHotkey.WindowsForms/NHotkey.WindowsForms.csproj new file mode 100644 index 0000000..80257fc --- /dev/null +++ b/NHotkey.WindowsForms/NHotkey.WindowsForms.csproj @@ -0,0 +1,12 @@ + + + net40;net45;net472;netcoreapp3.0 + true + + + A managed library to handle global hotkeys in Windows Forms applications. This package contains the concrete HotkeyManager implementation for Windows Forms. + + + + + \ No newline at end of file diff --git a/NHotkey.Wpf/Extensions.cs b/NHotkey.Wpf/Extensions.cs new file mode 100644 index 0000000..41d98da --- /dev/null +++ b/NHotkey.Wpf/Extensions.cs @@ -0,0 +1,17 @@ +using System.Windows.Input; + +namespace NHotkey.Wpf +{ + static class Extensions + { + public static bool HasFlag(this ModifierKeys modifiers, ModifierKeys flag) + { + return (modifiers & flag) == flag; + } + + public static bool HasFlag(this HotkeyFlags flags, HotkeyFlags flag) + { + return (flags & flag) == flag; + } + } +} diff --git a/NHotkey.Wpf/HotkeyAlreadyRegisteredEventArgs.cs b/NHotkey.Wpf/HotkeyAlreadyRegisteredEventArgs.cs new file mode 100644 index 0000000..20b0308 --- /dev/null +++ b/NHotkey.Wpf/HotkeyAlreadyRegisteredEventArgs.cs @@ -0,0 +1,19 @@ +using System; + +namespace NHotkey.Wpf +{ + public class HotkeyAlreadyRegisteredEventArgs : EventArgs + { + private readonly string _name; + + public HotkeyAlreadyRegisteredEventArgs(string name) + { + _name = name; + } + + public string Name + { + get { return _name; } + } + } +} diff --git a/NHotkey.Wpf/HotkeyManager.cs b/NHotkey.Wpf/HotkeyManager.cs new file mode 100644 index 0000000..90f4a2e --- /dev/null +++ b/NHotkey.Wpf/HotkeyManager.cs @@ -0,0 +1,249 @@ +using System; +using System.ComponentModel; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; + +namespace NHotkey.Wpf +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Design", + "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", + Justification = "This is a singleton; disposing it would break it")] + public class HotkeyManager : HotkeyManagerBase + { + #region Singleton implementation + + public static HotkeyManager Current { get { return LazyInitializer.Instance; } } + + private static class LazyInitializer + { + static LazyInitializer() { } + public static readonly HotkeyManager Instance = new HotkeyManager(); + } + + public void AddOrReplace(string v, object incrementKeys, object onIncrement) + { + throw new NotImplementedException(); + } + + #endregion + + #region Attached property for KeyBindings + + [AttachedPropertyBrowsableForType(typeof(KeyBinding))] + public static bool GetRegisterGlobalHotkey(KeyBinding binding) + { + return (bool)binding.GetValue(RegisterGlobalHotkeyProperty); + } + + public static void SetRegisterGlobalHotkey(KeyBinding binding, bool value) + { + binding.SetValue(RegisterGlobalHotkeyProperty, value); + } + + public static readonly DependencyProperty RegisterGlobalHotkeyProperty = + DependencyProperty.RegisterAttached( + "RegisterGlobalHotkey", + typeof(bool), + typeof(HotkeyManager), + new PropertyMetadata( + false, + RegisterGlobalHotkeyPropertyChanged)); + + private static void RegisterGlobalHotkeyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var keyBinding = d as KeyBinding; + if (keyBinding == null) + return; + + bool oldValue = (bool) e.OldValue; + bool newValue = (bool) e.NewValue; + + if (DesignerProperties.GetIsInDesignMode(d)) + return; + + if (oldValue && !newValue) + { + Current.RemoveKeyBinding(keyBinding); + } + else if (newValue && !oldValue) + { + Current.AddKeyBinding(keyBinding); + } + } + + #endregion + + #region HotkeyAlreadyRegistered event + + public static event EventHandler HotkeyAlreadyRegistered; + + private static void OnHotkeyAlreadyRegistered(string name) + { + var handler = HotkeyAlreadyRegistered; + if (handler != null) + handler(null, new HotkeyAlreadyRegisteredEventArgs(name)); + } + + #endregion + + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly HwndSource _source; + private readonly WeakReferenceCollection _keyBindings; + + private HotkeyManager() + { + _keyBindings = new WeakReferenceCollection(); + + var parameters = new HwndSourceParameters("Hotkey sink") + { + HwndSourceHook = HandleMessage, + ParentWindow = HwndMessage + }; + _source = new HwndSource(parameters); + SetHwnd(_source.Handle); + } + + public void AddOrReplace(string name, KeyGesture gesture, EventHandler handler) + { + AddOrReplace(name, gesture, false, handler); + } + + public void AddOrReplace(string name, KeyGesture gesture, bool noRepeat, EventHandler handler) + { + AddOrReplace(name, gesture.Key, gesture.Modifiers, noRepeat, handler); + } + + public void AddOrReplace(string name, Key key, ModifierKeys modifiers, EventHandler handler) + { + AddOrReplace(name, key, modifiers, false, handler); + } + + public void AddOrReplace(string name, Key key, ModifierKeys modifiers, bool noRepeat, EventHandler handler) + { + var flags = GetFlags(modifiers, noRepeat); + var vk = (uint)KeyInterop.VirtualKeyFromKey(key); + AddOrReplace(name, vk, flags, handler); + } + + private static HotkeyFlags GetFlags(ModifierKeys modifiers, bool noRepeat) + { + var flags = HotkeyFlags.None; + if (modifiers.HasFlag(ModifierKeys.Shift)) + flags |= HotkeyFlags.Shift; + if (modifiers.HasFlag(ModifierKeys.Control)) + flags |= HotkeyFlags.Control; + if (modifiers.HasFlag(ModifierKeys.Alt)) + flags |= HotkeyFlags.Alt; + if (modifiers.HasFlag(ModifierKeys.Windows)) + flags |= HotkeyFlags.Windows; + if (noRepeat) + flags |= HotkeyFlags.NoRepeat; + return flags; + } + + private static ModifierKeys GetModifiers(HotkeyFlags flags) + { + var modifiers = ModifierKeys.None; + if (flags.HasFlag(HotkeyFlags.Shift)) + modifiers |= ModifierKeys.Shift; + if (flags.HasFlag(HotkeyFlags.Control)) + modifiers |= ModifierKeys.Control; + if (flags.HasFlag(HotkeyFlags.Alt)) + modifiers |= ModifierKeys.Alt; + if (flags.HasFlag(HotkeyFlags.Windows)) + modifiers |= ModifierKeys.Windows; + return modifiers; + } + + private void AddKeyBinding(KeyBinding keyBinding) + { + var gesture = (KeyGesture)keyBinding.Gesture; + string name = GetNameForKeyBinding(gesture); + try + { + AddOrReplace(name, gesture.Key, gesture.Modifiers, null); + _keyBindings.Add(keyBinding); + } + catch (HotkeyAlreadyRegisteredException) + { + OnHotkeyAlreadyRegistered(name); + } + } + + private void RemoveKeyBinding(KeyBinding keyBinding) + { + var gesture = (KeyGesture)keyBinding.Gesture; + string name = GetNameForKeyBinding(gesture); + Remove(name); + _keyBindings.Remove(keyBinding); + } + + private readonly KeyGestureConverter _gestureConverter = new KeyGestureConverter(); + private string GetNameForKeyBinding(KeyGesture gesture) + { + string name = gesture.DisplayString; + if (string.IsNullOrEmpty(name)) + name = _gestureConverter.ConvertToString(gesture); + return name; + } + + private IntPtr HandleMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) + { + Hotkey hotkey; + var result = HandleHotkeyMessage(hwnd, msg, wparam, lparam, ref handled, out hotkey); + if (handled) + return result; + + if (hotkey != null) + handled = ExecuteBoundCommand(hotkey); + + return result; + } + + private bool ExecuteBoundCommand(Hotkey hotkey) + { + var key = KeyInterop.KeyFromVirtualKey((int)hotkey.VirtualKey); + var modifiers = GetModifiers(hotkey.Flags); + bool handled = false; + foreach (var binding in _keyBindings) + { + if (binding.Key == key && binding.Modifiers == modifiers) + { + handled |= ExecuteCommand(binding); + } + } + return handled; + } + + private static bool ExecuteCommand(InputBinding binding) + { + var command = binding.Command; + var parameter = binding.CommandParameter; + var target = binding.CommandTarget; + + if (command == null) + return false; + + var routedCommand = command as RoutedCommand; + if (routedCommand != null) + { + if (routedCommand.CanExecute(parameter, target)) + { + routedCommand.Execute(parameter, target); + return true; + } + } + else + { + if (command.CanExecute(parameter)) + { + command.Execute(parameter); + return true; + } + } + return false; + } + } +} diff --git a/NHotkey.Wpf/NHotkey.Wpf.csproj b/NHotkey.Wpf/NHotkey.Wpf.csproj new file mode 100644 index 0000000..b87e572 --- /dev/null +++ b/NHotkey.Wpf/NHotkey.Wpf.csproj @@ -0,0 +1,12 @@ + + + net40;net45;net472;netcoreapp3.0;net6.0-windows + true + + + A managed library to handle global hotkeys in WPF applications. This package contains the concrete HotkeyManager implementation for WPF. + + + + + \ No newline at end of file diff --git a/NHotkey.Wpf/Properties/AssemblyInfo.cs b/NHotkey.Wpf/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..75ceceb --- /dev/null +++ b/NHotkey.Wpf/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +using System.Windows.Markup; + +// Mapping a custom namespace to the standard WPF namespace is usually something to avoid, +// however in this case we're only importing one type (HotkeyManager), and it's unlikely to +// collide with another type in a future version of WPF. So in this case, we do it for the +// sake of simplicity, so that the user doesn't need to map the namespace manually. +[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "NHotkey.Wpf")] diff --git a/NHotkey.Wpf/WeakReferenceCollection.cs b/NHotkey.Wpf/WeakReferenceCollection.cs new file mode 100644 index 0000000..18f8b5c --- /dev/null +++ b/NHotkey.Wpf/WeakReferenceCollection.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace NHotkey.Wpf +{ + class WeakReferenceCollection : IEnumerable + where T : class + { + private readonly List _references = new List(); + + public IEnumerator GetEnumerator() + { + var references = _references.ToList(); + foreach (var reference in references) + { + var target = reference.Target; + if (target != null) + yield return (T) target; + } + Trim(); + } + + public void Add(T item) + { + _references.Add(new WeakReference(item)); + } + + public void Remove(T item) + { + _references.RemoveAll(r => (r.Target ?? item) == item); + } + + public void Trim() + { + _references.RemoveAll(r => !r.IsAlive); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/NHotkey.sln b/NHotkey.sln new file mode 100644 index 0000000..41a87f5 --- /dev/null +++ b/NHotkey.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHotkey", "NHotkey\NHotkey.csproj", "{6CAB99C4-6F21-4508-9160-CE4DBF540840}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHotkey.WindowsForms", "NHotkey.WindowsForms\NHotkey.WindowsForms.csproj", "{F0D5CDB4-B74A-419B-9AD6-2A438A268837}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHotkey.Wpf", "NHotkey.Wpf\NHotkey.Wpf.csproj", "{FC92233F-6F08-45B7-B3EE-7C557582236B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6CAB99C4-6F21-4508-9160-CE4DBF540840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CAB99C4-6F21-4508-9160-CE4DBF540840}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CAB99C4-6F21-4508-9160-CE4DBF540840}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CAB99C4-6F21-4508-9160-CE4DBF540840}.Release|Any CPU.Build.0 = Release|Any CPU + {F0D5CDB4-B74A-419B-9AD6-2A438A268837}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0D5CDB4-B74A-419B-9AD6-2A438A268837}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0D5CDB4-B74A-419B-9AD6-2A438A268837}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0D5CDB4-B74A-419B-9AD6-2A438A268837}.Release|Any CPU.Build.0 = Release|Any CPU + {FC92233F-6F08-45B7-B3EE-7C557582236B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC92233F-6F08-45B7-B3EE-7C557582236B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC92233F-6F08-45B7-B3EE-7C557582236B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC92233F-6F08-45B7-B3EE-7C557582236B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67220BE9-A1D5-41B9-9D16-177606E9D4D5} + EndGlobalSection +EndGlobal diff --git a/NHotkey.snk b/NHotkey.snk new file mode 100644 index 0000000000000000000000000000000000000000..a4fdff98871994f27c896141a4e28c645877fbe0 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096EH}Z|@4TeT7a4cW=f}#vp0sIsp5PLIZ zFu<<}DG=q4^em7t-2PWNtC{ypk+B`r-Uh$TFuarkzOi2dJM6H=xhmO7ZjMxR^Yv&> z(<}M=Y1)KYJB#VQy@pgP)KbN}ci?fmvhV^*P#(Xim!>C}{x2#{a`I68oPu}ZeX-RK zANw(A=1Zankj)|IY$`d{N5Z`2A$b3fqv`kkW5}t&%0T095Ue6i#G`1+=>Uy~5-C54 zW614N&K+hcPPOwRh!ftl{y1vv%jty(TgpLXZ9-VYaSuPmq(3eMfJ&*u(~EY;>^idt zb{0nf9VKA2V%8lHL3sf8lE^vK2&ul zN=CP&%T7=QhCe8A3bY-OwMF@`eqEMFV9Qi+fl@oa&5XU{9?Z8y;}+;8o|)gt#sSdr zmFjKsmB3Ct|v5h*Tt2DuF81Dz5zh0FmWCY1!V8_}V?gIF;zP$NY&v i$BAehs=1KE?*D^w2)Cvxz<|2pV|yV=WVEu=WWRJq$R$Gn literal 0 HcmV?d00001 diff --git a/NHotkey/GlobalSuppressions.cs b/NHotkey/GlobalSuppressions.cs new file mode 100644 index 0000000000000000000000000000000000000000..2c7734bc25cae45355b918176994010bb770f9a0 GIT binary patch literal 1366 zcmaKsQEL-X5QXPi@INf;OAEHeJ_$o!Y-_^ncZ1qiMLzUmDRk(zUQ>I1^e%|<^Q#9*vWontyN&RAOhQA zD{ReagRR@Gg-xxu2Y!$EC4SetwX&s0dTLK#As!W89k_;UJ#WGntaomaT`{yO-U?iB zi_F3W6Xn|253F;%&uz%?ab!93yZ!{XB5FF|*$|_31~qXNy~ew6Hu5R3>c+b1qu-@t zw{c8ny2Ml17R$h*Vwbpq_%*Ki{3uR^k91F9HjdRUZYqDNzb1m>RA6d*?)I%?^)Blg zCB<6%8t?g@8Uvr1-=iX*as<0^%|eTue^5I2i{h(pOVsJ?j6AB&-0nFK<${mupX;tJ z8LR`F|Mgh(P*3!ZNXq&E_vp9Gt~~0v;-KeB_sk0Ka~)We_TKp&c5hbVEm<`w<({I2 zeSu96tA#77{!mY};PyK;EB)_|JM8GJbIeyu4$YpXFS=X2zj7vhqw2D~B=^)ORI{`O zJAt+I7qbL6)hKbEH9wr{^CKESYTtau{{oFk>&5^8 literal 0 HcmV?d00001 diff --git a/NHotkey/Hotkey.cs b/NHotkey/Hotkey.cs new file mode 100644 index 0000000..6dd22fb --- /dev/null +++ b/NHotkey/Hotkey.cs @@ -0,0 +1,71 @@ +using System; +using System.Runtime.InteropServices; + +namespace NHotkey +{ + internal class Hotkey + { + private static int _nextId; + + private readonly int _id; + private readonly uint _virtualKey; + private readonly HotkeyFlags _flags; + private readonly EventHandler _handler; + + public Hotkey(uint virtualKey, HotkeyFlags flags, EventHandler handler) + { + _id = ++_nextId; + _virtualKey = virtualKey; + _flags = flags; + _handler = handler; + } + + public int Id + { + get { return _id; } + } + + public uint VirtualKey + { + get { return _virtualKey; } + } + + public HotkeyFlags Flags + { + get { return _flags; } + } + + public EventHandler Handler + { + get { return _handler; } + } + + private IntPtr _hwnd; + + public void Register(IntPtr hwnd, string name) + { + if (!NativeMethods.RegisterHotKey(hwnd, _id, _flags, _virtualKey)) + { + var hr = Marshal.GetHRForLastWin32Error(); + var ex = Marshal.GetExceptionForHR(hr); + if ((uint) hr == 0x80070581) + throw new HotkeyAlreadyRegisteredException(name, ex); + throw ex; + } + _hwnd = hwnd; + } + + public void Unregister() + { + if (_hwnd != IntPtr.Zero) + { + if (!NativeMethods.UnregisterHotKey(_hwnd, _id)) + { + var hr = Marshal.GetHRForLastWin32Error(); + throw Marshal.GetExceptionForHR(hr); + } + _hwnd = IntPtr.Zero; + } + } + } +} \ No newline at end of file diff --git a/NHotkey/HotkeyAlreadyRegisteredException.cs b/NHotkey/HotkeyAlreadyRegisteredException.cs new file mode 100644 index 0000000..4ef2903 --- /dev/null +++ b/NHotkey/HotkeyAlreadyRegisteredException.cs @@ -0,0 +1,36 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; + +namespace NHotkey +{ + [Serializable] + public class HotkeyAlreadyRegisteredException : Exception + { + private readonly string _name; + + public HotkeyAlreadyRegisteredException(string name, Exception inner) : base(inner.Message, inner) + { + _name = name; + HResult = Marshal.GetHRForException(inner); + } + + protected HotkeyAlreadyRegisteredException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + _name = (string) info.GetValue("_name", typeof (string)); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("_name", _name); + } + + public string Name + { + get { return _name; } + } + } +} diff --git a/NHotkey/HotkeyEventArgs.cs b/NHotkey/HotkeyEventArgs.cs new file mode 100644 index 0000000..58bfff2 --- /dev/null +++ b/NHotkey/HotkeyEventArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace NHotkey +{ + public class HotkeyEventArgs : EventArgs + { + private readonly string _name; + + internal HotkeyEventArgs(string name) + { + _name = name; + } + + public string Name + { + get { return _name; } + } + + public bool Handled { get; set; } + } +} \ No newline at end of file diff --git a/NHotkey/HotkeyFlags.cs b/NHotkey/HotkeyFlags.cs new file mode 100644 index 0000000..e350b1f --- /dev/null +++ b/NHotkey/HotkeyFlags.cs @@ -0,0 +1,15 @@ +using System; + +namespace NHotkey +{ + [Flags] + internal enum HotkeyFlags : uint + { + None = 0x0000, + Alt = 0x0001, + Control = 0x0002, + Shift = 0x0004, + Windows = 0x0008, + NoRepeat = 0x4000 + } +} \ No newline at end of file diff --git a/NHotkey/HotkeyManagerBase.cs b/NHotkey/HotkeyManagerBase.cs new file mode 100644 index 0000000..15a85be --- /dev/null +++ b/NHotkey/HotkeyManagerBase.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; + +namespace NHotkey +{ + public abstract class HotkeyManagerBase + { + private readonly Dictionary _hotkeyNames = new Dictionary(); + private readonly Dictionary _hotkeys = new Dictionary(); + private IntPtr _hwnd; + internal static readonly IntPtr HwndMessage = (IntPtr)(-3); + + internal HotkeyManagerBase() + { + } + + internal void AddOrReplace(string name, uint virtualKey, HotkeyFlags flags, EventHandler handler) + { + var hotkey = new Hotkey(virtualKey, flags, handler); + lock (_hotkeys) + { + Remove(name); + _hotkeys.Add(name, hotkey); + _hotkeyNames.Add(hotkey.Id, name); + if (_hwnd != IntPtr.Zero) + hotkey.Register(_hwnd, name); + } + } + + public void Remove(string name) + { + lock (_hotkeys) + { + Hotkey hotkey; + if (_hotkeys.TryGetValue(name, out hotkey)) + { + _hotkeys.Remove(name); + _hotkeyNames.Remove(hotkey.Id); + if (_hwnd != IntPtr.Zero) + hotkey.Unregister(); + } + } + } + + public bool IsEnabled { get; set; } = true; + + internal void SetHwnd(IntPtr hwnd) + { + _hwnd = hwnd; + } + + private const int WmHotkey = 0x0312; + + internal IntPtr HandleHotkeyMessage( + IntPtr hwnd, + int msg, + IntPtr wParam, + IntPtr lParam, + ref bool handled, + out Hotkey hotkey) + { + hotkey = null; + if (IsEnabled && msg == WmHotkey) + { + int id = wParam.ToInt32(); + string name; + if (_hotkeyNames.TryGetValue(id, out name)) + { + hotkey = _hotkeys[name]; + var handler = hotkey.Handler; + if (handler != null) + { + var e = new HotkeyEventArgs(name); + handler(this, e); + handled = e.Handled; + } + } + } + return IntPtr.Zero; + } + } +} diff --git a/NHotkey/NHotkey.csproj b/NHotkey/NHotkey.csproj new file mode 100644 index 0000000..79a09f4 --- /dev/null +++ b/NHotkey/NHotkey.csproj @@ -0,0 +1,10 @@ + + + net40;net45;net472;netcoreapp3.0;net6.0-windows + + + + A managed library to handle global hotkeys in Windows Forms and WPF applications. NOTE: this package doesn't contain a concrete HotkeyManager implementation; you should add either the NHotkey.Wpf or NHotkey.WindowsForms package to get one. + + + \ No newline at end of file diff --git a/NHotkey/NativeMethods.cs b/NHotkey/NativeMethods.cs new file mode 100644 index 0000000..ea173a1 --- /dev/null +++ b/NHotkey/NativeMethods.cs @@ -0,0 +1,14 @@ +using System; +using System.Runtime.InteropServices; + +namespace NHotkey +{ + static class NativeMethods + { + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool RegisterHotKey(IntPtr hWnd, int id, HotkeyFlags fsModifiers, uint vk); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool UnregisterHotKey(IntPtr hWnd, int id); + } +} \ No newline at end of file diff --git a/NHotkey/Properties/AssemblyInfo.cs b/NHotkey/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5e8ccad --- /dev/null +++ b/NHotkey/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("NHotkey.WindowsForms, PublicKey=00240000048000009400000006020000002400005253413100040000010001000d37f28dea0d86462d702c5ff882a20c5701fc1421107b336430c0af082910e58ff42c9030dcfe5739ab99f74b91b11dd4de06bfcd30bc9402beb15f023becb0c6b92ad9496e8e5474f3f5684ed32bf9fb69da84593b8be9bebd86542bd452c5bb77e071bbb2f0024a501ebfa897a62798fe2f2a4e72f250fc9c8277e17db1d5")] +[assembly: InternalsVisibleTo("NHotkey.Wpf, PublicKey=00240000048000009400000006020000002400005253413100040000010001000d37f28dea0d86462d702c5ff882a20c5701fc1421107b336430c0af082910e58ff42c9030dcfe5739ab99f74b91b11dd4de06bfcd30bc9402beb15f023becb0c6b92ad9496e8e5474f3f5684ed32bf9fb69da84593b8be9bebd86542bd452c5bb77e071bbb2f0024a501ebfa897a62798fe2f2a4e72f250fc9c8277e17db1d5")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..975fa2d --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# NHotkey.Wpf +