// This code is distributed under MIT license. // Copyright (c) 2010-2018 George Mamaladze // See license.txt or https://mit-license.org/ using System; using System.Collections.Generic; using System.Linq; using Gma.System.MouseKeyHook.Implementation; namespace Gma.System.MouseKeyHook { /// /// Extension methods to detect key combinations /// public static class KeyCombinationExtensions { /// /// Detects a key or key combination and triggers the corresponding action. /// /// /// An instance of Global or Application hook. Use or to /// create it. /// /// /// This map contains the list of key combinations mapped to corresponding actions. You can use a dictionary initilizer /// to easily create it. /// Whenever a listed combination will be detected a corresponding action will be triggered. /// /// /// This optional action will be executed when some key was pressed but it was not part of any wanted combinations. /// public static void OnCombination(this IKeyboardEvents source, IEnumerable> map, Action reset = null) { var watchlists = map.GroupBy(k => k.Key.TriggerKey) .ToDictionary(g => g.Key, g => g.ToArray()); source.KeyDown += (sender, e) => { KeyValuePair[] element; var found = watchlists.TryGetValue(e.KeyCode, out element); if (!found) { reset?.Invoke(); return; } var state = KeyboardState.GetCurrent(); var action = reset; var maxLength = 0; foreach (var current in element) { var matches = current.Key.Chord.All(state.IsDown); if (!matches) continue; if (maxLength > current.Key.ChordLength) continue; maxLength = current.Key.ChordLength; action = current.Value; } action?.Invoke(); }; } /// /// Detects a key or key combination sequence and triggers the corresponding action. /// /// /// An instance of Global or Application hook. Use or /// to create it. /// /// /// This map contains the list of sequences mapped to corresponding actions. You can use a dictionary initilizer to /// easily create it. /// Whenever a listed sequnce will be detected a corresponding action will be triggered. If two or more sequences match /// the longest one will be used. /// Example: sequences may A,B,C and B,C might be detected simultanously if user pressed first A then B then C. In this /// case only action corresponding /// to 'A,B,C' will be triggered. /// public static void OnSequence(this IKeyboardEvents source, IEnumerable> map) { var actBySeq = map.ToArray(); var endsWith = new Func, Sequence, bool>((chords, sequence) => { var skipCount = chords.Count - sequence.Length; return skipCount >= 0 && chords.Skip(skipCount).SequenceEqual(sequence); }); var max = actBySeq.Select(p => p.Key).Max(c => c.Length); var min = actBySeq.Select(p => p.Key).Min(c => c.Length); var buffer = new Queue(max); var wrapMap = actBySeq.SelectMany(p => p.Key).Select(c => new KeyValuePair(c, () => { buffer.Enqueue(c); if (buffer.Count > max) buffer.Dequeue(); if (buffer.Count < min) return; //Invoke action corresponding to the longest matching sequence actBySeq .Where(pair => endsWith(buffer, pair.Key)) .OrderBy(pair => pair.Key.Length) .Select(pair => pair.Value) .LastOrDefault() ?.Invoke(); })); OnCombination(source, wrapMap, buffer.Clear); } } }