WingMan – Rev 37

Subversion Repositories:
Rev:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Gma.System.MouseKeyHook;
using Microsoft.Win32;
using MQTTnet.Extensions.ManagedClient;
using MQTTnet.Server;
using WingMan.AutoCompletion;
using WingMan.Bindings;
using WingMan.Communication;
using WingMan.Discovery;
using WingMan.Lobby;
using WingMan.Properties;
using WingMan.Utilities;

namespace WingMan
{
    public partial class WingManForm : Form
    {
        private const int TabControlDetachPixelOffset = 20;

        public WingManForm()
        {
            InitializeComponent();

            FormTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
            FormCancellationTokenSource = new CancellationTokenSource();

            // Set up discovery.
            Discovery = new Discovery.Discovery(FormCancellationTokenSource, FormTaskScheduler);
            Discovery.OnPortMapFailed += OnDiscoveryPortMapFailed;

            // Set up autocompletion.
            AutoCompletion = new AutoCompletion.AutoCompletion(FormTaskScheduler, FormCancellationTokenSource.Token);
            AutoCompletion.OnSaveFailed += AutoCompletionOnSaveFailed;
            AutoCompletion.OnLoadFailed += AutoCompletionOnLoadFailed;

            Task.Run(() => AutoCompletion.Load(Address.Name, Address.AutoCompleteCustomSource));
            Task.Run(() => AutoCompletion.Load(Port.Name, Address.AutoCompleteCustomSource));
            Task.Run(() => AutoCompletion.Load(Nick.Name, Nick.AutoCompleteCustomSource));
            Task.Run(() => AutoCompletion.Load(LocalNameTextBox.Name, LocalNameTextBox.AutoCompleteCustomSource));

            MqttCommunication = new MqttCommunication(FormTaskScheduler, FormCancellationTokenSource.Token);
            MqttCommunication.OnClientAuthenticationFailed += OnMqttClientAuthenticationFailed;
            MqttCommunication.OnClientConnectionFailed += OnMqttClientConnectionFailed;
            MqttCommunication.OnClientDisconnected += OnMqttClientDisconnected;
            MqttCommunication.OnClientConnected += OnMqttClientConnected;
            MqttCommunication.OnServerAuthenticationFailed += OnMqttServerAuthenticationFailed;
            MqttCommunication.OnServerStopped += OnMqttServerStopped;
            MqttCommunication.OnServerStarted += OnMqttServerStarted;
            MqttCommunication.OnServerClientConnected += OnMqttServerClientConnected;
            MqttCommunication.OnServerClientDisconnected += OnMqttServerClientDisconnected;

            LocalKeyBindings = new LocalKeyBindings(new List<KeyBinding>());
            RemoteKeyBindings = new RemoteKeyBindings(new ObservableCollection<RemoteKeyBinding>());

            LocalCheckedListBoxBindingSource = new BindingSource
            {
                DataSource = LocalKeyBindings.Bindings
            };
            LocalCheckedListBoxBindingSource.ListChanged += LocalCheckedListBoxBindingSourceOnListChanged;

            LocalBindingsCheckedListBox.DisplayMember = "DisplayName";
            LocalBindingsCheckedListBox.ValueMember = "Keys";
            LocalBindingsCheckedListBox.DataSource = LocalCheckedListBoxBindingSource;

            KeyBindingsExchange = new KeyBindingsExchange
            {
                ExchangeBindings = new List<KeyBindingExchange>()
            };

            RemoteBindingsComboBoxSource = new BindingSource
            {
                DataSource = KeyBindingsExchange.ExchangeBindings
            };
            RemoteBindingsComboBoxSource.ListChanged += RemoteBindingsComboBoxSourceOnListChanged;

            RemoteBindingsComboBox.DisplayMember = "Nick";
            RemoteBindingsComboBox.ValueMember = "KeyBindings";
            RemoteBindingsComboBox.DataSource = RemoteBindingsComboBoxSource;


            // Start lobby message synchronizer.
            LobbyMessageSynchronizer = new LobbyMessageSynchronizer(MqttCommunication, FormTaskScheduler,
                FormCancellationTokenSource.Token);
            LobbyMessageSynchronizer.OnLobbyMessageReceived += OnLobbyMessageReceived;

            // Start mouse key bindings synchronizer.
            KeyBindingsSynchronizer = new KeyBindingsSynchronizer(LocalKeyBindings, MqttCommunication,
                FormTaskScheduler, FormCancellationTokenSource.Token);
            KeyBindingsSynchronizer.OnMouseKeyBindingsSynchronized += OnMouseKeyBindingsSynchronized;

            // Start mouse key interceptor.
            KeyInterceptor = new KeyInterceptor(RemoteKeyBindings, MqttCommunication, FormTaskScheduler,
                FormCancellationTokenSource.Token);
            KeyInterceptor.OnMouseKeyBindingMatched += OnMouseKeyBindingMatched;

            // Start mouse key simulator.
            KeySimulator = new KeySimulator(LocalKeyBindings, MqttCommunication, FormTaskScheduler,
                FormCancellationTokenSource.Token);
            KeySimulator.OnMouseKeyBindingExecuting += OnMouseKeyBindingExecuting;
        }

        private static Discovery.Discovery Discovery { get; set; }
        private static AutoCompletion.AutoCompletion AutoCompletion { get; set; }
        private static CancellationTokenSource FormCancellationTokenSource { get; set; }

        private static TaskScheduler FormTaskScheduler { get; set; }

        private static IKeyboardMouseEvents MouseKeyApplicationHook { get; set; }

        private List<string> MouseKeyCombo { get; set; }

        public LocalKeyBindings LocalKeyBindings { get; set; }

        private RemoteKeyBindings RemoteKeyBindings { get; }

        private BindingSource LocalCheckedListBoxBindingSource { get; }

        private BindingSource RemoteBindingsComboBoxSource { get; }

        private KeyBindingsExchange KeyBindingsExchange { get; }

        public MqttCommunication MqttCommunication { get; set; }

        public LobbyMessageSynchronizer LobbyMessageSynchronizer { get; set; }

        public KeyBindingsSynchronizer KeyBindingsSynchronizer { get; set; }

        public KeyInterceptor KeyInterceptor { get; set; }

        public KeySimulator KeySimulator { get; set; }

        private static Point TabControlClickStartPosition { get; set; }
        private static TabControl DetachedTabControl { get; set; }
        private static Form DetachedForm { get; set; }

        /// <inheritdoc />
        /// <summary>
        ///     Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            FormCancellationTokenSource?.Dispose();
            FormCancellationTokenSource = null;

            if (disposing && components != null)
            {
                components.Dispose();
                components = null;
            }

            base.Dispose(disposing);
        }

        private void RemoteBindingsComboBoxSourceOnListChanged(object sender, ListChangedEventArgs e)
        {
            UpdateRemoteBindingsListBox();
        }

        public void OnDiscoveryPortMapFailed(object sender, DiscoveryFailedEventArgs args)
        {
            switch (args.Type)
            {
                case DiscoveryType.Upnp:
                    ActivityTextBox.AppendText(
                        $"{Strings.Failed_to_create_UPnP_port_mapping}{Environment.NewLine}");
                    break;
                case DiscoveryType.Pmp:
                    ActivityTextBox.AppendText(
                        $"{Strings.Failed_to_create_PMP_port_mapping}{Environment.NewLine}");
                    break;
            }
        }

        private void LocalCheckedListBoxBindingSourceOnListChanged(object sender, ListChangedEventArgs e)
        {
            // Check items
            LocalBindingsCheckedListBox.ItemCheck -= LocalBindingsCheckedListBoxItemCheck;

            for (var i = 0; i < LocalKeyBindings.Bindings.Count; ++i)
                switch (LocalKeyBindings.Bindings[i].Enabled)
                {
                    case true:
                        LocalBindingsCheckedListBox.SetItemCheckState(i, CheckState.Checked);
                        break;
                    default:
                        LocalBindingsCheckedListBox.SetItemCheckState(i, CheckState.Unchecked);
                        break;
                }

            LocalBindingsCheckedListBox.ItemCheck += LocalBindingsCheckedListBoxItemCheck;
        }

        private void AutoCompletionOnLoadFailed(object sender, AutoCompletionFailedEventArgs args)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Failed_loading_autocomplete_source} : {args.Name} : {args.Exception.Message}{Environment.NewLine}");
        }

        private void AutoCompletionOnSaveFailed(object sender, AutoCompletionFailedEventArgs args)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Failed_saving_autocomplete_source} : {args.Name} : {args.Exception.Message}{Environment.NewLine}");
        }

        private void OnMouseKeyBindingExecuting(object sender, KeyBindingExecutingEventArgs args)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Executing_binding_from_remote_client} : {args.Nick} : {args.Name}{Environment.NewLine}");
        }

        private void OnMouseKeyBindingMatched(object sender, KeyBindingMatchedEventArgs args)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Matched_remote_key_binding} : {args.Nick} : {args.Name} : {string.Join(" + ", args.KeyCombo)}{Environment.NewLine}");
        }

        private void OnMqttServerClientDisconnected(object sender, MqttClientDisconnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_disconnected}{Environment.NewLine}");
        }

        private void OnMqttServerClientConnected(object sender, MqttClientConnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_connected}{Environment.NewLine}");
        }

        private void OnMqttServerStarted(object sender, EventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Server_started}{Environment.NewLine}");
        }

        private void OnMqttServerStopped(object sender, EventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Server_stopped}{Environment.NewLine}");
        }

        private void OnMqttClientConnected(object sender, MQTTnet.Client.MqttClientConnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_connected}{Environment.NewLine}");
        }

        private void OnMqttClientDisconnected(object sender, MQTTnet.Client.MqttClientDisconnectedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_disconnected}{Environment.NewLine}");
        }

        private void OnMqttClientConnectionFailed(object sender, MqttManagedProcessFailedEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Client_connection_failed}{Environment.NewLine}");
        }

        private void OnMqttServerAuthenticationFailed(object sender, MqttAuthenticationFailureEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Failed_to_authenticate_client} : {e.Exception}{Environment.NewLine}");
        }

        private void OnMqttClientAuthenticationFailed(object sender, MqttAuthenticationFailureEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Failed_to_authenticate_with_server} : {e.Exception}{Environment.NewLine}");
        }

        private void OnMouseKeyBindingsSynchronized(object sender, KeyBindingsSynchronizerEventArgs e)
        {
            ActivityTextBox.AppendText(
                $"{Strings.Synchronized_bindings_with_client} : {e.Bindings.Nick} : {e.Bindings.KeyBindings.Count}{Environment.NewLine}");

            var exchangeBindings = KeyBindingsExchange.ExchangeBindings.FirstOrDefault(exchangeBinding =>
                string.Equals(exchangeBinding.Nick, e.Bindings.Nick, StringComparison.Ordinal));

            // If the nick does not exist then add it.
            if (exchangeBindings == null)
            {
                KeyBindingsExchange.ExchangeBindings.Add(e.Bindings);
                RemoteBindingsComboBoxSource.ResetBindings(false);
                return;
            }

            // If the bindings for the nick have not changed then do not update.
            if (e.Bindings.KeyBindings.Count == exchangeBindings.KeyBindings.Count &&
                e.Bindings.KeyBindings.All(exchangeBindings.KeyBindings.Contains))
            {
                RemoteBindingsComboBoxSource.ResetBindings(false);
                return;
            }

            // Update the bindings.
            exchangeBindings.KeyBindings.RemoveAll(binding => !e.Bindings.KeyBindings.Contains(binding));
            exchangeBindings.KeyBindings.AddRange(
                e.Bindings.KeyBindings.Where(binding => !exchangeBindings.KeyBindings.Contains(binding)));
            RemoteBindingsComboBoxSource.ResetBindings(false);
        }

        private void UpdateRemoteBindingsListBox()
        {
            var keyBindingExchange = (KeyBindingExchange) RemoteBindingsComboBox.SelectedItem;
            if (keyBindingExchange == null)
                return;

            RemoteBindingsListBox.Items.Clear();
            var bindings = KeyBindingsExchange.ExchangeBindings
                .Where(binding => binding.Nick == keyBindingExchange.Nick)
                .SelectMany(binding => binding.KeyBindings.Select(keyBinding => keyBinding))
                .Select(binding => (object) binding).ToArray();

            if (bindings.Length == 0)
                return;

            RemoteBindingsListBox.Items.AddRange(bindings);
        }

        private void OnLobbyMessageReceived(object sender, LobbyMessageReceivedEventArgs e)
        {
            WingManNotifyIcon.BalloonTipTitle = Strings.Lobby_message;
            WingManNotifyIcon.BalloonTipText = $"{e.Nick} : {e.Message}{Environment.NewLine}";
            WingManNotifyIcon.ShowBalloonTip(1000);

            LobbyTextBox.AppendText($"{e.Nick} : {e.Message}{Environment.NewLine}");
        }

        private void AddressTextBoxClick(object sender, EventArgs e)
        {
            Address.BackColor = Color.Empty;
        }

        private void PortTextBoxClick(object sender, EventArgs e)
        {
            Port.BackColor = Color.Empty;
        }

        private async void HostButtonClickAsync(object sender, EventArgs e)
        {
            // Stop the MQTT server if it is running.
            if (MqttCommunication.Running)
            {
                // Remote UPnP and Pmp mappings.
                await Task.Delay(0, FormCancellationTokenSource.Token).ContinueWith(async _ =>
                    {
                        await Discovery.DeleteMapping(DiscoveryType.Upnp, MqttCommunication.Port);
                        await Discovery.DeleteMapping(DiscoveryType.Pmp, MqttCommunication.Port);
                    },
                    FormCancellationTokenSource.Token, TaskContinuationOptions.LongRunning, FormTaskScheduler);

                await MqttCommunication.Stop();

                HostButton.BackColor = Color.Empty;

                // Enable controls.
                ConnectButton.Enabled = true;
                Address.Enabled = true;
                Port.Enabled = true;
                Nick.Enabled = true;
                Password.Enabled = true;
                return;
            }

            if (!ValidateConnectionParameters(out var ipAddress, out var port, out var nick, out var password))
                return;

            StoreConnectionAutocomplete();

            // Try to reserve port: try UPnP followed by PMP.
            await Task.Delay(0, FormCancellationTokenSource.Token).ContinueWith(async _ =>
                {
                    if (!await Discovery.CreateMapping(DiscoveryType.Upnp, port) &&
                        !await Discovery.CreateMapping(DiscoveryType.Pmp, port))
                        ActivityTextBox.AppendText(
                            $"{Strings.Failed_creating_automatic_port_mapping_please_make_sure_the_port_is_routed_properly}{Environment.NewLine}");
                },
                FormCancellationTokenSource.Token, TaskContinuationOptions.LongRunning, FormTaskScheduler);

            // Start the MQTT server.
            if (!await MqttCommunication
                .Start(MqttCommunicationType.Server, ipAddress, port, nick, password))
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_starting_server}{Environment.NewLine}");
                return;
            }

            HostButton.BackColor = Color.Aquamarine;

            // Disable controls
            ConnectButton.Enabled = false;
            Address.Enabled = false;
            Port.Enabled = false;
            Nick.Enabled = false;
            Password.Enabled = false;
        }

        private async void StoreConnectionAutocomplete()
        {
            Address.AutoCompleteCustomSource.Add(Address.Text);

            await AutoCompletion.Save(Address.Name, Address.AutoCompleteCustomSource);

            Port.AutoCompleteCustomSource.Add(Port.Text);

            await AutoCompletion.Save(Port.Name, Port.AutoCompleteCustomSource);

            Nick.AutoCompleteCustomSource.Add(Nick.Text);

            await AutoCompletion.Save(Nick.Name, Nick.AutoCompleteCustomSource);
        }

        private bool ValidateConnectionParameters(
            out IPAddress address,
            out int port,
            out string nick,
            out string password)
        {
            address = IPAddress.Any;
            port = 0;
            nick = string.Empty;
            password = string.Empty;

            if (string.IsNullOrEmpty(Address.Text) &&
                string.IsNullOrEmpty(Port.Text) &&
                string.IsNullOrEmpty(Nick.Text) &&
                string.IsNullOrEmpty(Password.Text))
            {
                Address.BackColor = Color.LightPink;
                Port.BackColor = Color.LightPink;
                Nick.BackColor = Color.LightPink;
                Password.BackColor = Color.LightPink;
                return false;
            }

            if (!IPAddress.TryParse(Address.Text, out address))
                try
                {
                    address = Dns.GetHostAddresses(Address.Text).FirstOrDefault();
                }
                catch (Exception ex)
                {
                    ActivityTextBox.AppendText(
                        $"{Strings.Could_not_resolve_hostname} : {ex.Message}{Environment.NewLine}");

                    Address.BackColor = Color.LightPink;
                    return false;
                }

            if (!uint.TryParse(Port.Text, out var uPort))
            {
                Port.BackColor = Color.LightPink;
                return false;
            }

            port = (int) uPort;

            if (string.IsNullOrEmpty(Nick.Text))
            {
                Nick.BackColor = Color.LightPink;
                return false;
            }

            nick = Nick.Text;

            if (string.IsNullOrEmpty(Password.Text))
            {
                Password.BackColor = Color.LightPink;
                return false;
            }

            password = Aes.ExpandKey(Password.Text);

            Address.BackColor = Color.Empty;
            Port.BackColor = Color.Empty;
            Nick.BackColor = Color.Empty;
            Password.BackColor = Color.Empty;

            return true;
        }

        private async void ConnectButtonClickAsync(object sender, EventArgs e)
        {
            if (MqttCommunication.Running)
            {
                await MqttCommunication.Stop();
                ConnectButton.Text = Strings.Connect;
                ConnectButton.BackColor = Color.Empty;

                Address.Enabled = true;
                Port.Enabled = true;
                Nick.Enabled = true;
                Password.Enabled = true;
                HostButton.Enabled = true;
                return;
            }

            if (!ValidateConnectionParameters(out var ipAddress, out var port, out var nick, out var password))
                return;

            StoreConnectionAutocomplete();

            if (!await MqttCommunication
                .Start(MqttCommunicationType.Client, ipAddress, port, nick, password))
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_starting_client}{Environment.NewLine}");
                return;
            }

            ConnectButton.Text = Strings.Disconnect;
            ConnectButton.BackColor = Color.Aquamarine;

            HostButton.Enabled = false;
            Address.Enabled = false;
            Port.Enabled = false;
            Nick.Enabled = false;
            Password.Enabled = false;
        }

        private async void LobbySayTextBoxKeyDown(object sender, KeyEventArgs e)
        {
            // Do not send messages if the communication is not running.
            if (!MqttCommunication.Running)
                return;

            if (e.KeyCode != Keys.Enter)
                return;

            await LobbyMessageSynchronizer.Broadcast(LobbySayTextBox.Text);

            LobbySayTextBox.Text = string.Empty;
        }

        private async void LocalAddBindingButtonClick(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty(LocalNameTextBox.Text))
            {
                LocalNameTextBox.BackColor = Color.LightPink;
                return;
            }

            // Only unique names allowed.
            if (LocalKeyBindings.Bindings.Any(binding =>
                string.Equals(binding.Name, LocalNameTextBox.Text, StringComparison.Ordinal)))
            {
                LocalNameTextBox.BackColor = Color.LightPink;
                LocalBindingsCheckedListBox.BackColor = Color.LightPink;
                return;
            }

            LocalNameTextBox.BackColor = Color.Empty;
            LocalBindingsCheckedListBox.BackColor = Color.Empty;

            LocalNameTextBox.AutoCompleteCustomSource.Add(LocalNameTextBox.Text);

            await AutoCompletion.Save(LocalNameTextBox.Name, LocalNameTextBox.AutoCompleteCustomSource);

            ShowOverlayPanel();

            MouseKeyCombo = new List<string>();

            MouseKeyApplicationHook = Hook.GlobalEvents();
            MouseKeyApplicationHook.KeyUp += LocalMouseKeyHookOnKeyUp;
            MouseKeyApplicationHook.KeyDown += LocalMouseKeyHookOnKeyDown;
        }

        private void ShowOverlayPanel()
        {
            OverlayPanel.BringToFront();
            OverlayPanel.Visible = true;
            OverlayPanel.Invalidate();
        }

        private async void LocalMouseKeyHookOnKeyUp(object sender, KeyEventArgs e)
        {
            LocalKeyBindings.Bindings.Add(new KeyBinding(LocalNameTextBox.Text, MouseKeyCombo));

            LocalCheckedListBoxBindingSource.ResetBindings(false);

            MouseKeyApplicationHook.KeyDown -= LocalMouseKeyHookOnKeyDown;
            MouseKeyApplicationHook.KeyUp -= LocalMouseKeyHookOnKeyUp;

            MouseKeyApplicationHook.Dispose();

            LocalNameTextBox.Text = string.Empty;
            HideOverlayPanel();

            await SaveLocalMouseKeyBindings();
        }

        private void HideOverlayPanel()
        {
            OverlayPanel.SendToBack();
            OverlayPanel.Visible = false;
            OverlayPanel.Invalidate();
        }

        private void LocalMouseKeyHookOnKeyDown(object sender, KeyEventArgs e)
        {
            e.SuppressKeyPress = true;

            KeyConversion.KeysToString.TryGetValue((byte) e.KeyCode, out var key);

            MouseKeyCombo.Add(key);
        }

        private void LocalNameTextBoxClick(object sender, EventArgs e)
        {
            LocalNameTextBox.BackColor = Color.Empty;
        }

        private async void LocalBindingsRemoveButtonClick(object sender, EventArgs e)
        {
            var localBinding = (KeyBinding) LocalBindingsCheckedListBox.SelectedItem;
            if (localBinding == null)
                return;

            LocalKeyBindings.Bindings.Remove(localBinding);
            LocalCheckedListBoxBindingSource.ResetBindings(false);

            await SaveLocalMouseKeyBindings();
        }

        private async void LobbySayButtonClick(object sender, EventArgs e)
        {
            // Do not send messages if the communication is not running.
            if (!MqttCommunication.Running)
                return;

            await LobbyMessageSynchronizer.Broadcast(LobbySayTextBox.Text);

            LobbySayTextBox.Text = string.Empty;
        }

        private void RemoteBindingsComboBoxSelectionChangeCompleted(object sender, EventArgs e)
        {
            UpdateRemoteBindingsListBox();
        }

        private async void WingManFormOnLoad(object sender, EventArgs e)
        {
            using (var key = Registry.CurrentUser.OpenSubKey
                ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true))
            {
                switch (key.GetValue(Name) == null)
                {
                    case true:
                        windowsStartupCheckBox.Checked = false;
                        break;
                    default:
                        windowsStartupCheckBox.Checked = true;
                        break;
                }
            }

            await LoadLocalKeyBindings();

            await LoadRemoteKeyBindings();
        }

        private void WingManFormOnClosing(object sender, FormClosingEventArgs e)
        {
        }

        private void RemoteBindingsBindButtonClicked(object sender, EventArgs e)
        {
            if (string.IsNullOrEmpty((string) RemoteBindingsListBox.SelectedItem))
            {
                RemoteBindingsListBox.BackColor = Color.LightPink;
                return;
            }

            RemoteBindingsListBox.BackColor = Color.Empty;

            ShowOverlayPanel();

            MouseKeyCombo = new List<string>();

            MouseKeyApplicationHook = Hook.GlobalEvents();
            MouseKeyApplicationHook.KeyUp += RemoteKeyHookOnKeyUp;
            MouseKeyApplicationHook.KeyDown += RemoteKeyHookOnKeyDown;
        }

        private void RemoteBindingsUnbindButtonClicked(object sender, EventArgs e)
        {
            var item = (string) RemoteBindingsListBox.SelectedItem;
            if (string.IsNullOrEmpty(item))
            {
                RemoteBindingsListBox.BackColor = Color.LightPink;
                return;
            }

            RemoteBindingsListBox.BackColor = Color.Empty;

            var remoteKeyBinding = RemoteKeyBindings.Bindings.FirstOrDefault(binding =>
                string.Equals(binding.Name, item, StringComparison.Ordinal));

            if (remoteKeyBinding == null)
                return;

            RemoteKeyBindings.Bindings.Remove(remoteKeyBinding);
            RemoteBindingsBindToBox.Text = string.Empty;
        }

        private void RemoteKeyHookOnKeyDown(object sender, KeyEventArgs e)
        {
            e.SuppressKeyPress = true;

            if (!KeyConversion.KeysToString.TryGetValue((byte) e.KeyCode, out var key))
                return;

            MouseKeyCombo.Add(key);
        }

        private async void RemoteKeyHookOnKeyUp(object sender, KeyEventArgs e)
        {
            RemoteKeyBindings.Bindings.Add(new RemoteKeyBinding(RemoteBindingsComboBox.Text,
                (string) RemoteBindingsListBox.SelectedItem, MouseKeyCombo));

            MouseKeyApplicationHook.KeyDown -= RemoteKeyHookOnKeyDown;
            MouseKeyApplicationHook.KeyUp -= RemoteKeyHookOnKeyUp;

            MouseKeyApplicationHook.Dispose();

            RemoteBindingsBindToBox.Text = string.Join(" + ", MouseKeyCombo);
            HideOverlayPanel();

            await SaveRemoteMouseKeyBindings();
        }

        private void RemoteBindingsListBoxSelectedValueChanged(object sender, EventArgs e)
        {
            RemoteBindingsBindToBox.Text = "";

            var name = (string) RemoteBindingsListBox.SelectedItem;
            if (string.IsNullOrEmpty(name))
                return;

            var nick = RemoteBindingsComboBox.Text;
            if (string.IsNullOrEmpty(nick))
                return;

            foreach (var binding in RemoteKeyBindings.Bindings)
            {
                if (!string.Equals(binding.Nick, nick) || !string.Equals(binding.Name, name))
                    continue;

                RemoteBindingsBindToBox.Text = string.Join(" + ", binding.Keys);
                break;
            }
        }

        private void WingManFormResized(object sender, EventArgs e)
        {
            if (WindowState == FormWindowState.Minimized) Hide();
        }

        private void NotifyIconDoubleClick(object sender, EventArgs e)
        {
            Show();
            WindowState = FormWindowState.Normal;
        }

        private async void LocalBindingsCheckedListBoxItemCheck(object sender, ItemCheckEventArgs e)
        {
            var helmBinding = (KeyBinding) LocalBindingsCheckedListBox.Items[e.Index];
            if (helmBinding == null)
                return;

            switch (e.NewValue)
            {
                case CheckState.Checked:
                    helmBinding.Enabled = true;
                    break;
                case CheckState.Unchecked:
                    helmBinding.Enabled = false;
                    break;
            }

            await SaveLocalMouseKeyBindings();
        }

        private void WingManTabControlMouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button != MouseButtons.Left) return;

            TabControlClickStartPosition = e.Location;
        }

        private void WingManTabControlMouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                var mouseOffsetX = TabControlClickStartPosition.X - e.X;
                var mouseOffsetY = TabControlClickStartPosition.Y - e.Y;

                if (mouseOffsetX <= TabControlDetachPixelOffset && mouseOffsetY <= TabControlDetachPixelOffset)
                    return;

                tabControl1.DoDragDrop(tabControl1.SelectedTab, DragDropEffects.Move);
                TabControlClickStartPosition = new Point();

                return;
            }

            TabControlClickStartPosition = new Point();
        }

        private void WingManTabControlGiveFeedback(object sender, GiveFeedbackEventArgs e)
        {
            e.UseDefaultCursors = false;
        }

        private void WingManTabControlQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
        {
            if (MouseButtons != MouseButtons.Left)
            {
                e.Action = DragAction.Cancel;

                DetachedForm = new Form
                {
                    Size = new Size(
                        // Width = tab control width + tab control left and right margins + x-padding
                        tabControl1.Width +
                        tabControl1.Margin.Right +
                        tabControl1.Margin.Left +
                        tabControl1.Padding.X,
                        // Tab Height = tab control height - tab control tab page height
                        // Height = tab control height + Tab Height + top and bottom margin + x-padding
                        2 * tabControl1.Height -
                        tabControl1.SelectedTab.Height +
                        tabControl1.Margin.Top +
                        tabControl1.Margin.Bottom +
                        tabControl1.Padding.Y),
                    StartPosition = FormStartPosition.Manual,
                    Location = MousePosition,
                    MaximizeBox = false,
                    SizeGripStyle = SizeGripStyle.Hide,
                    FormBorderStyle = FormBorderStyle.FixedSingle,
                    Name = Name,
                    Text = Text,
                    Icon = Icon
                };

                DetachedTabControl = new TabControl
                {
                    Dock = DockStyle.Fill,
                    SizeMode = TabSizeMode.Fixed
                };
                DetachedTabControl.TabPages.Add(tabControl1.SelectedTab);
                DetachedForm.Controls.Add(DetachedTabControl);
                DetachedForm.FormClosing += DetachedFormOnFormClosing;
                DetachedForm.Show();
                Cursor = Cursors.Default;
                return;
            }

            e.Action = DragAction.Continue;
            Cursor = Cursors.SizeAll;
        }

        private void DetachedFormOnFormClosing(object sender, FormClosingEventArgs e)
        {
            tabControl1.TabPages.Insert(DetachedTabControl.SelectedTab.TabIndex, DetachedTabControl.SelectedTab);
            DetachedForm.FormClosing -= DetachedFormOnFormClosing;
        }

        private void LocalBindingsLoadButtonClicked(object sender, EventArgs e)
        {
            loadLocalBindingsDialog.ShowDialog();
        }

        private void LocalBindingsSaveButtonClicked(object sender, EventArgs e)
        {
            saveLocalBindingsDialog.ShowDialog();
        }

        private async void SaveLocalBindingsDialogOk(object sender, CancelEventArgs e)
        {
            using (var localBindingsStream = saveLocalBindingsDialog.OpenFile())
            {
                using (var memoryStream = new MemoryStream())
                {
                    LocalKeyBindings.XmlSerializer.Serialize(memoryStream, LocalKeyBindings);

                    memoryStream.Position = 0L;

                    await memoryStream.CopyToAsync(localBindingsStream);
                }
            }
        }

        private async void LoadLocalBindingsDialogOk(object sender, CancelEventArgs e)
        {
            using (var localBindingsStream = loadLocalBindingsDialog.OpenFile())
            {
                var loadedBindings = (LocalKeyBindings) LocalKeyBindings.XmlSerializer.Deserialize(localBindingsStream);

                LocalKeyBindings.Bindings.Clear();

                foreach (var binding in loadedBindings.Bindings)
                    LocalKeyBindings.Bindings.Add(binding);

                LocalCheckedListBoxBindingSource.ResetBindings(false);
            }

            await SaveLocalMouseKeyBindings();
        }

        private void SettingsWindowsStartupCheckboxCheckedChanged(object sender, EventArgs e)
        {
            try
            {
                switch (((CheckBox) sender).Checked)
                {
                    case true:
                        using (var key = Registry.CurrentUser.OpenSubKey
                            ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true))
                        {
                            key.SetValue(Name, Assembly.GetEntryAssembly().Location);
                        }

                        break;
                    default:
                        using (var key = Registry.CurrentUser.OpenSubKey
                            ("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true))
                        {
                            key.DeleteValue(Name, false);
                        }

                        break;
                }

                ActivityTextBox.AppendText(
                    $"{Strings.Application_Windows_startup_changed}{Environment.NewLine}");
            }
            catch
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Could_not_change_Windows_startup}{Environment.NewLine}");
            }
        }

        #region Saving and loading

        private async Task SaveLocalMouseKeyBindings()
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    LocalKeyBindings.XmlSerializer.Serialize(memoryStream, LocalKeyBindings);

                    memoryStream.Position = 0L;

                    using (var fileStream = new FileStream("LocalKeyBindings.xml", FileMode.Create))
                    {
                        await memoryStream.CopyToAsync(fileStream);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_saving_local_bindings}{Environment.NewLine}");
            }
        }

        private async Task LoadLocalKeyBindings()
        {
            try
            {
                using (var fileStream = new FileStream("LocalKeyBindings.xml", FileMode.Open))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        await fileStream.CopyToAsync(memoryStream);

                        memoryStream.Position = 0L;

                        var loadedBindings =
                            (LocalKeyBindings) LocalKeyBindings.XmlSerializer.Deserialize(memoryStream);

                        foreach (var binding in loadedBindings.Bindings)
                            LocalKeyBindings.Bindings.Add(binding);

                        LocalCheckedListBoxBindingSource.ResetBindings(false);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_loading_local_bindings}{Environment.NewLine}");
            }
        }

        private async Task SaveRemoteMouseKeyBindings()
        {
            try
            {
                using (var memoryStream = new MemoryStream())
                {
                    RemoteKeyBindings.XmlSerializer.Serialize(memoryStream, RemoteKeyBindings);

                    memoryStream.Position = 0L;

                    using (var fileStream = new FileStream("RemoteKeyBindings.xml", FileMode.Create))
                    {
                        await memoryStream.CopyToAsync(fileStream);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_saving_remote_bindings}{Environment.NewLine}");
            }
        }

        private async Task LoadRemoteKeyBindings()
        {
            try
            {
                using (var fileStream = new FileStream("RemoteKeyBindings.xml", FileMode.Open))
                {
                    using (var memoryStream = new MemoryStream())
                    {
                        await fileStream.CopyToAsync(memoryStream);

                        memoryStream.Position = 0L;

                        var loadedBindings =
                            (RemoteKeyBindings) RemoteKeyBindings.XmlSerializer.Deserialize(memoryStream);

                        foreach (var binding in loadedBindings.Bindings)
                            RemoteKeyBindings.Bindings.Add(binding);
                    }
                }
            }
            catch (Exception)
            {
                ActivityTextBox.AppendText(
                    $"{Strings.Failed_loading_remote_bindings}{Environment.NewLine}");
            }
        }

        #endregion
    }
}