/trunk/WingMan/Bindings/KeyBinding.cs |
@@ -0,0 +1,48 @@ |
using System; |
using System.Collections.Generic; |
using System.Linq; |
|
namespace WingMan.Bindings |
{ |
public class KeyBinding : IEquatable<KeyBinding> |
{ |
public KeyBinding() |
{ |
} |
|
public KeyBinding(string name, List<string> keys) : this() |
{ |
Name = name; |
Keys = keys; |
} |
|
public string DisplayName => $"{Name} ({string.Join(" + ", Keys.ToArray())})"; |
|
public string Name { get; set; } = string.Empty; |
|
public List<string> Keys { get; set; } = new List<string>(); |
|
public bool Equals(KeyBinding other) |
{ |
if (ReferenceEquals(null, other)) return false; |
if (ReferenceEquals(this, other)) return true; |
return string.Equals(Name, other.Name) && Keys.SequenceEqual(other.Keys); |
} |
|
public override bool Equals(object obj) |
{ |
if (ReferenceEquals(null, obj)) return false; |
if (ReferenceEquals(this, obj)) return true; |
if (obj.GetType() != GetType()) return false; |
return Equals((KeyBinding) obj); |
} |
|
public override int GetHashCode() |
{ |
unchecked |
{ |
return ((Name != null ? Name.GetHashCode() : 0) * 397) ^ (Keys != null ? Keys.GetHashCode() : 0); |
} |
} |
} |
} |
/trunk/WingMan/Bindings/KeyBindingsSynchronizer.cs |
@@ -0,0 +1,103 @@ |
using System; |
using System.Collections.Concurrent; |
using System.Collections.Generic; |
using System.IO; |
using System.Linq; |
using System.Threading; |
using System.Threading.Tasks; |
using MQTTnet; |
using WingMan.Communication; |
|
namespace WingMan.Bindings |
{ |
public class KeyBindingsSynchronizer : IDisposable |
{ |
public delegate void MouseKeyBindingsSynchronized(object sender, KeyBindingsSynchronizerEventArgs e); |
|
public KeyBindingsSynchronizer(LocalKeyBindings localKeyBindings, MqttCommunication mqttCommunication, |
TaskScheduler taskScheduler, CancellationToken cancellationToken) |
{ |
LocalKeyBindings = localKeyBindings; |
MqttCommunication = mqttCommunication; |
CancellationToken = cancellationToken; |
TaskScheduler = taskScheduler; |
|
SynchronizedMouseKeyBindings = new ConcurrentDictionary<string, List<KeyBinding>>(); |
|
MqttCommunication.OnMessageReceived += MqttCommunicationOnMessageReceived; |
|
Task.Run(PeriodicSynchronize, CancellationToken); |
} |
|
private LocalKeyBindings LocalKeyBindings { get; } |
|
private ConcurrentDictionary<string, List<KeyBinding>> SynchronizedMouseKeyBindings { get; } |
|
private MqttCommunication MqttCommunication { get; } |
|
private CancellationToken CancellationToken { get; } |
private TaskScheduler TaskScheduler { get; } |
|
public void Dispose() |
{ |
MqttCommunication.OnMessageReceived -= MqttCommunicationOnMessageReceived; |
} |
|
public event MouseKeyBindingsSynchronized OnMouseKeyBindingsSynchronized; |
|
private async void MqttCommunicationOnMessageReceived(object sender, |
MqttApplicationMessageReceivedEventArgs e) |
{ |
if (e.ApplicationMessage.Topic != "exchange") |
return; |
|
using (var memoryStream = new MemoryStream(e.ApplicationMessage.Payload)) |
{ |
memoryStream.Position = 0L; |
|
var mouseKeyBindingsExchange = |
(KeyBindingExchange) KeyBindingExchange.XmlSerializer.Deserialize(memoryStream); |
|
// Do not add own bindings. |
if (string.Equals(mouseKeyBindingsExchange.Nick, MqttCommunication.Nick)) |
return; |
|
if (SynchronizedMouseKeyBindings.TryGetValue(mouseKeyBindingsExchange.Nick, out var mouseKeyBinding) && |
mouseKeyBinding.SequenceEqual(mouseKeyBindingsExchange.KeyBindings)) |
return; |
|
await Task.Delay(0) |
.ContinueWith( |
_ => OnMouseKeyBindingsSynchronized?.Invoke(sender, |
new KeyBindingsSynchronizerEventArgs( |
mouseKeyBindingsExchange)), |
CancellationToken, TaskContinuationOptions.None, TaskScheduler); |
|
// Nick does not exist so the bindings will be added. |
SynchronizedMouseKeyBindings.AddOrUpdate(mouseKeyBindingsExchange.Nick, |
mouseKeyBindingsExchange.KeyBindings, (s, list) => mouseKeyBindingsExchange.KeyBindings); |
} |
} |
|
private async Task PeriodicSynchronize() |
{ |
do |
{ |
await Task.Delay(1000, CancellationToken); |
|
if (!MqttCommunication.Running) |
continue; |
|
using (var memoryStream = new MemoryStream()) |
{ |
KeyBindingExchange.XmlSerializer.Serialize(memoryStream, |
new KeyBindingExchange(MqttCommunication.Nick, LocalKeyBindings.Bindings)); |
|
memoryStream.Position = 0L; |
|
await MqttCommunication.Broadcast("exchange", memoryStream.ToArray()); |
} |
} while (!CancellationToken.IsCancellationRequested); |
} |
} |
} |
/trunk/WingMan/Bindings/KeyInterceptor.cs |
@@ -0,0 +1,155 @@ |
using System; |
using System.Collections.Specialized; |
using System.IO; |
using System.Linq; |
using System.Threading; |
using System.Threading.Tasks; |
using System.Threading.Tasks.Dataflow; |
using System.Windows.Forms; |
using Gma.System.MouseKeyHook; |
using WingMan.Communication; |
using WingMan.Utilities; |
|
namespace WingMan.Bindings |
{ |
public class KeyInterceptor : IDisposable |
{ |
public delegate void MouseKeyBindingMatched(object sender, KeyBindingMatchedEventArgs args); |
|
private volatile bool ProcessPipe; |
|
public KeyInterceptor(RemoteKeyBindings remoteKeyBindings, MqttCommunication mqttCommunication, |
TaskScheduler taskScheduler, CancellationToken cancellationToken) |
{ |
DataFlowSemaphoreSlim = new SemaphoreSlim(1, 1); |
|
RemoteKeyBindings = remoteKeyBindings; |
RemoteKeyBindings.Bindings.CollectionChanged += OnRemoteKeyBindingsChanged; |
|
MqttCommunication = mqttCommunication; |
TaskScheduler = taskScheduler; |
CancellationToken = cancellationToken; |
|
MouseKeyGloalHook = Hook.GlobalEvents(); |
MouseKeyGloalHook.KeyUp += MouseKeyGloalHookOnKeyUp; |
MouseKeyGloalHook.KeyDown += MouseKeyGloalHookOnKeyDown; |
} |
|
private BatchBlock<string> KeyComboBatchBlock { get; set; } |
|
private ActionBlock<string[]> KeyComboActionBlock { get; set; } |
|
private IDisposable KeyComboDataFlowLink { get; set; } |
|
private SemaphoreSlim DataFlowSemaphoreSlim { get; } |
|
private RemoteKeyBindings RemoteKeyBindings { get; } |
private MqttCommunication MqttCommunication { get; } |
private TaskScheduler TaskScheduler { get; } |
private CancellationToken CancellationToken { get; } |
|
private IKeyboardMouseEvents MouseKeyGloalHook { get; set; } |
|
public void Dispose() |
{ |
MouseKeyGloalHook.KeyUp -= MouseKeyGloalHookOnKeyUp; |
MouseKeyGloalHook.KeyDown -= MouseKeyGloalHookOnKeyDown; |
RemoteKeyBindings.Bindings.CollectionChanged -= OnRemoteKeyBindingsChanged; |
|
KeyComboDataFlowLink?.Dispose(); |
KeyComboDataFlowLink = null; |
|
MouseKeyGloalHook?.Dispose(); |
MouseKeyGloalHook = null; |
} |
|
private async void OnRemoteKeyBindingsChanged(object sender, NotifyCollectionChangedEventArgs e) |
{ |
await DataFlowSemaphoreSlim.WaitAsync(CancellationToken); |
|
try |
{ |
// Break the link and dispose it. |
KeyComboDataFlowLink?.Dispose(); |
KeyComboDataFlowLink = null; |
|
// Create a sliding window of size equal to the longest key combination. |
var maxKeyComboLength = RemoteKeyBindings.Bindings.Max(binding => binding.Keys.Count); |
|
KeyComboBatchBlock = |
new BatchBlock<string>(maxKeyComboLength); |
KeyComboActionBlock = new ActionBlock<string[]>(ProcessKeyCombos, |
new ExecutionDataflowBlockOptions {CancellationToken = CancellationToken}); |
KeyComboDataFlowLink = KeyComboBatchBlock.LinkTo(KeyComboActionBlock); |
} |
finally |
{ |
DataFlowSemaphoreSlim.Release(); |
} |
} |
|
private async Task ProcessKeyCombos(string[] keys) |
{ |
await DataFlowSemaphoreSlim.WaitAsync(CancellationToken); |
|
try |
{ |
if (!ProcessPipe) |
return; |
|
foreach (var binding in RemoteKeyBindings.Bindings) |
{ |
if (!keys.SubsetEquals(binding.Keys)) |
continue; |
|
// Raise the match event. |
await Task.Delay(0, CancellationToken) |
.ContinueWith( |
_ => OnMouseKeyBindingMatched?.Invoke(this, |
new KeyBindingMatchedEventArgs(binding.Nick, binding.Name, binding.Keys)), |
CancellationToken, |
TaskContinuationOptions.None, TaskScheduler); |
|
using (var memoryStream = new MemoryStream()) |
{ |
ExecuteKeyBinding.XmlSerializer.Serialize(memoryStream, |
new ExecuteKeyBinding(binding.Nick, binding.Name)); |
|
memoryStream.Position = 0L; |
|
await MqttCommunication.Broadcast("execute", memoryStream.ToArray()); |
} |
} |
} |
finally |
{ |
DataFlowSemaphoreSlim.Release(); |
} |
} |
|
public event MouseKeyBindingMatched OnMouseKeyBindingMatched; |
|
private async void MouseKeyGloalHookOnKeyDown(object sender, KeyEventArgs e) |
{ |
ProcessPipe = true; |
|
if (!KeyConversion.KeysToString.TryGetValue((byte) e.KeyCode, out var key)) |
return; |
|
await DataFlowSemaphoreSlim.WaitAsync(CancellationToken); |
try |
{ |
if (KeyComboBatchBlock != null) await KeyComboBatchBlock.SendAsync(key, CancellationToken); |
} |
finally |
{ |
DataFlowSemaphoreSlim.Release(); |
} |
} |
|
private void MouseKeyGloalHookOnKeyUp(object sender, KeyEventArgs e) |
{ |
ProcessPipe = false; |
} |
} |
} |
/trunk/WingMan/Bindings/KeySimulator.cs |
@@ -0,0 +1,92 @@ |
using System; |
using System.IO; |
using System.Threading; |
using System.Threading.Tasks; |
using MQTTnet; |
using SimWinInput; |
using WingMan.Communication; |
using WingMan.Utilities; |
|
namespace WingMan.Bindings |
{ |
public class KeySimulator : IDisposable |
{ |
public delegate void MouseKeyBindingExecuting(object sender, KeyBindingExecutingEventArgs args); |
|
public KeySimulator(LocalKeyBindings localLocalKeyBindings, MqttCommunication mqttCommunication, |
TaskScheduler formTaskScheduler, CancellationToken cancellationToken) |
{ |
LocalLocalKeyBindings = localLocalKeyBindings; |
MqttCommunication = mqttCommunication; |
TaskScheduler = formTaskScheduler; |
CancellationToken = cancellationToken; |
|
MqttCommunication.OnMessageReceived += OnMqttMessageReceived; |
} |
|
private MqttCommunication MqttCommunication { get; } |
private TaskScheduler TaskScheduler { get; } |
private CancellationToken CancellationToken { get; } |
private LocalKeyBindings LocalLocalKeyBindings { get; } |
|
public void Dispose() |
{ |
MqttCommunication.OnMessageReceived -= OnMqttMessageReceived; |
} |
|
public event MouseKeyBindingExecuting OnMouseKeyBindingExecuting; |
|
private async void OnMqttMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e) |
{ |
if (e.ApplicationMessage.Topic != "execute") |
return; |
|
using (var memoryStream = new MemoryStream(e.ApplicationMessage.Payload)) |
{ |
var executeMouseKeyBinding = |
(ExecuteKeyBinding) ExecuteKeyBinding.XmlSerializer.Deserialize(memoryStream); |
|
// Do not process own mouse key bindings. |
if (!string.Equals(executeMouseKeyBinding.Nick, MqttCommunication.Nick, StringComparison.Ordinal)) |
return; |
|
await Task.Delay(0, CancellationToken) |
.ContinueWith( |
_ => OnMouseKeyBindingExecuting?.Invoke(sender, |
new KeyBindingExecutingEventArgs(executeMouseKeyBinding.Nick, |
executeMouseKeyBinding.Name)), |
CancellationToken, |
TaskContinuationOptions.None, TaskScheduler); |
|
Simulate(executeMouseKeyBinding); |
} |
} |
|
private void Simulate(ExecuteKeyBinding executeBinding) |
{ |
foreach (var localBinding in LocalLocalKeyBindings.Bindings) |
{ |
if (!string.Equals(localBinding.Name, executeBinding.Name, StringComparison.Ordinal)) |
continue; |
|
// Press |
foreach (var key in localBinding.Keys) |
{ |
if (!KeyConversion.StringToKeys.TryGetValue(key, out var pressKey)) |
continue; |
|
SimKeyboard.KeyDown(pressKey); |
} |
|
// Depress |
foreach (var key in localBinding.Keys) |
{ |
if (!KeyConversion.StringToKeys.TryGetValue(key, out var pressKey)) |
continue; |
|
SimKeyboard.KeyUp(pressKey); |
} |
} |
} |
} |
} |
/trunk/WingMan/Utilities/Extensions.cs |
@@ -0,0 +1,44 @@ |
using System; |
using System.Collections.Generic; |
using System.Linq; |
|
namespace WingMan.Utilities |
{ |
public static class Extensions |
{ |
/// <summary> |
/// Sequentially removes all the elements from the first sequence that are in the second sequence. |
/// </summary> |
/// <typeparam name="T">the type o the collection</typeparam> |
/// <param name="o">the first sequence to remove from</param> |
/// <param name="p">the second sequence to remove</param> |
/// <returns>the first sequence excluding the second sequence</returns> |
public static IEnumerable<T> SequenceExcept<T>(this IEnumerable<T> a, IEnumerable<T> b) where T : IEquatable<T> |
{ |
using (var ea = a.GetEnumerator()) |
{ |
using (var eb = b.GetEnumerator()) |
{ |
while (ea.MoveNext()) |
{ |
if (eb.MoveNext() && ea.Current.Equals(eb.Current)) |
continue; |
yield return ea.Current; |
} |
} |
} |
} |
|
/// <summary> |
/// Determines whether a sequence is contained within another sequence. |
/// </summary> |
/// <returns>true if and only if the first set is contained in the second set</returns> |
/// <param name="a">The set to check</param> |
/// <param name="b">The set to check against</param> |
/// <typeparam name="T">the set type</typeparam> |
public static bool SubsetEquals<T>(this IEnumerable<T> a, IEnumerable<T> b) where T : IEquatable<T> |
{ |
return !a.OrderBy(s => s).SequenceExcept(b.OrderBy(s => s)).Any(); |
} |
} |
} |
/trunk/WingMan/Utilities/KeyConversion.cs |
@@ -1,26 +1,9 @@ |
using System.Collections.Generic; |
using System.Windows.Forms; |
|
namespace WingMan |
namespace WingMan.Utilities |
{ |
public static class KeyConversion |
{ |
public static readonly Dictionary<MouseButtons, string> MouseButtonsToString = |
new Dictionary<MouseButtons, string> |
{ |
{MouseButtons.Left, "Left Mouse Button"}, |
{MouseButtons.Middle, "Middle Mouse Button"}, |
{MouseButtons.Right, "Right Mouse Button"} |
}; |
|
public static readonly Dictionary<string, MouseButtons> StringToMouseButtons = |
new Dictionary<string, MouseButtons> |
{ |
{"Left Mouse Button", MouseButtons.Left}, |
{"Middle Mouse Button", MouseButtons.Middle}, |
{"Right Mouse Button", MouseButtons.Right} |
}; |
|
public static readonly Dictionary<string, byte> StringToKeys = new Dictionary<string, byte> |
{ |
{"None", 0}, |
/trunk/WingMan/WingMan.csproj |
@@ -74,23 +74,24 @@ |
<Compile Include="Communication\MqttAuthenticationFailureEventArgs.cs" /> |
<Compile Include="Lobby\LobbyMessageReceivedEventArgs.cs" /> |
<Compile Include="Lobby\LobbyMessageSynchronizer.cs" /> |
<Compile Include="MouseKey\ExecuteKeyBinding.cs" /> |
<Compile Include="MouseKey\KeyBindingExchange.cs" /> |
<Compile Include="MouseKey\KeyBindingExecutingEventArgs.cs" /> |
<Compile Include="MouseKey\KeyBindingMatchedEventArgs.cs" /> |
<Compile Include="MouseKey\KeyBindingsSynchronizerEventArgs.cs" /> |
<Compile Include="MouseKey\KeyBindingsSynchronizer.cs" /> |
<Compile Include="Bindings\ExecuteKeyBinding.cs" /> |
<Compile Include="Bindings\KeyBindingExchange.cs" /> |
<Compile Include="Bindings\KeyBindingExecutingEventArgs.cs" /> |
<Compile Include="Bindings\KeyBindingMatchedEventArgs.cs" /> |
<Compile Include="Bindings\KeyBindingsSynchronizerEventArgs.cs" /> |
<Compile Include="Bindings\KeyBindingsSynchronizer.cs" /> |
<Compile Include="Communication\MqttCommunication.cs" /> |
<Compile Include="MouseKey\KeyBinding.cs" /> |
<Compile Include="MouseKey\LocalKeyBindings.cs" /> |
<Compile Include="MouseKey\KeyBindingsExchange.cs" /> |
<Compile Include="Bindings\KeyBinding.cs" /> |
<Compile Include="Bindings\LocalKeyBindings.cs" /> |
<Compile Include="Bindings\KeyBindingsExchange.cs" /> |
<Compile Include="Lobby\LobbyMessage.cs" /> |
<Compile Include="Communication\MqttCommunicationType.cs" /> |
<Compile Include="MouseKey\KeyInterceptor.cs" /> |
<Compile Include="MouseKey\KeySimulator.cs" /> |
<Compile Include="MouseKey\RemoteKeyBinding.cs" /> |
<Compile Include="MouseKey\RemoteKeyBindings.cs" /> |
<Compile Include="Bindings\KeyInterceptor.cs" /> |
<Compile Include="Bindings\KeySimulator.cs" /> |
<Compile Include="Bindings\RemoteKeyBinding.cs" /> |
<Compile Include="Bindings\RemoteKeyBindings.cs" /> |
<Compile Include="Utilities\AES.cs" /> |
<Compile Include="Utilities\Extensions.cs" /> |
<Compile Include="Utilities\KeyConversion.cs" /> |
<Compile Include="WingManForm.cs"> |
<SubType>Form</SubType> |
/trunk/WingMan/WingManForm.cs |
@@ -1,5 +1,6 @@ |
using System; |
using System.Collections.Generic; |
using System.Collections.ObjectModel; |
using System.Drawing; |
using System.IO; |
using System.Linq; |
@@ -10,9 +11,9 @@ |
using Gma.System.MouseKeyHook; |
using MQTTnet.Extensions.ManagedClient; |
using MQTTnet.Server; |
using WingMan.Bindings; |
using WingMan.Communication; |
using WingMan.Lobby; |
using WingMan.MouseKey; |
using WingMan.Properties; |
using WingMan.Utilities; |
|
@@ -39,7 +40,7 @@ |
MqttCommunication.OnServerClientDisconnected += OnMqttServerClientDisconnected; |
|
LocalKeyBindings = new LocalKeyBindings(new List<KeyBinding>()); |
RemoteKeyBindings = new RemoteKeyBindings(new List<RemoteKeyBinding>()); |
RemoteKeyBindings = new RemoteKeyBindings(new ObservableCollection<RemoteKeyBinding>()); |
|
LocalListBoxBindingSource = new BindingSource |
{ |
@@ -232,7 +233,7 @@ |
if (exchangeBindings == null) |
return; |
|
var replaceMouseBindings = new List<RemoteKeyBinding>(); |
var replaceMouseBindings = new ObservableCollection<RemoteKeyBinding>(); |
foreach (var remoteBinding in RemoteKeyBindings.Bindings) |
{ |
if (!exchangeBindings.Any(binding => |
@@ -448,7 +449,7 @@ |
OverlayPanel.Invalidate(); |
} |
|
private void LocalMouseKeyHookOnKeyUp(object sender, KeyEventArgs e) |
private async void LocalMouseKeyHookOnKeyUp(object sender, KeyEventArgs e) |
{ |
LocalKeyBindings.Bindings.Add(new KeyBinding(LocalNameTextBox.Text, MouseKeyCombo)); |
|
@@ -462,7 +463,7 @@ |
LocalNameTextBox.Text = string.Empty; |
HideOverlayPanel(); |
|
//await SaveLocalMouseKeyBindings(); |
await SaveLocalMouseKeyBindings(); |
} |
|
private void HideOverlayPanel() |
@@ -486,7 +487,7 @@ |
LocalNameTextBox.BackColor = Color.Empty; |
} |
|
private void LocalBindingsRemoveButtonClick(object sender, EventArgs e) |
private async void LocalBindingsRemoveButtonClick(object sender, EventArgs e) |
{ |
var helmBinding = (KeyBinding) LocalBindingsListBox.SelectedItem; |
if (helmBinding == null) |
@@ -495,7 +496,7 @@ |
LocalKeyBindings.Bindings.Remove(helmBinding); |
LocalListBoxBindingSource.ResetBindings(false); |
|
// await SaveLocalMouseKeyBindings(); |
await SaveLocalMouseKeyBindings(); |
} |
|
private async void LobbySayButtonClick(object sender, EventArgs e) |
@@ -510,11 +511,11 @@ |
UpdateRemoteItems(); |
} |
|
private void WingManFormOnLoad(object sender, EventArgs e) |
private async void WingManFormOnLoad(object sender, EventArgs e) |
{ |
// await LoadLocalMouseKeyBindings(); |
await LoadLocalMouseKeyBindings(); |
|
// await LoadRemoteMouseKeyBindings(); |
await LoadRemoteMouseKeyBindings(); |
} |
|
private void RemoteBindingsBindButtonClicked(object sender, EventArgs e) |
@@ -566,7 +567,7 @@ |
MouseKeyCombo.Add(key); |
} |
|
private void RemoteMouseKeyHookOnKeyUp(object sender, KeyEventArgs e) |
private async void RemoteMouseKeyHookOnKeyUp(object sender, KeyEventArgs e) |
{ |
RemoteKeyBindings.Bindings.Add(new RemoteKeyBinding(RemoteBindingsComboBox.Text, |
(string) RemoteBindingsListBox.SelectedItem, MouseKeyCombo)); |
@@ -579,7 +580,7 @@ |
RemoteBindingsBindToBox.Text = string.Join(" + ", MouseKeyCombo); |
HideOverlayPanel(); |
|
// await SaveRemoteMouseKeyBindings(); |
await SaveRemoteMouseKeyBindings(); |
} |
|
private void RemoteBindingsListBoxSelectedValueChanged(object sender, EventArgs e) |
@@ -590,9 +591,13 @@ |
if (string.IsNullOrEmpty(name)) |
return; |
|
var nick = RemoteBindingsComboBox.Text; |
if (string.IsNullOrEmpty(nick)) |
return; |
|
foreach (var binding in RemoteKeyBindings.Bindings) |
{ |
if (!string.Equals(binding.Name, name)) |
if (!string.Equals(binding.Nick, nick) || !string.Equals(binding.Name, name)) |
continue; |
|
RemoteBindingsBindToBox.Text = string.Join(" + ", binding.Keys); |