// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Elements; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Options { /// /// The input binding container. /// [Serializable] [HideInEditor] [TypeConverter(typeof(InputBindingConverter))] [CustomEditor(typeof(InputBindingEditor))] public struct InputBinding { /// /// The key to bind. /// public KeyboardKeys Key; /// /// The first modifier ( if not used). /// public KeyboardKeys Modifier1; /// /// The second modifier ( if not used). /// public KeyboardKeys Modifier2; /// /// Initializes a new instance of the struct. /// /// The key. public InputBinding(KeyboardKeys key) { Key = key; Modifier1 = KeyboardKeys.None; Modifier2 = KeyboardKeys.None; } /// /// Initializes a new instance of the struct. /// /// The key. /// The first modifier. public InputBinding(KeyboardKeys key, KeyboardKeys modifier1) { Key = key; Modifier1 = modifier1; Modifier2 = KeyboardKeys.None; } /// /// Initializes a new instance of the struct. /// /// The key. /// The first modifier. /// The second modifier. public InputBinding(KeyboardKeys key, KeyboardKeys modifier1, KeyboardKeys modifier2) { Key = key; Modifier1 = modifier1; Modifier2 = modifier2; } /// /// Parses the specified key text value. /// /// The value. /// The result (valid only if parsing succeed). /// True if parsing succeed, otherwise false. public static bool Parse(string value, out KeyboardKeys result) { if (string.Equals(value, "Ctrl", StringComparison.OrdinalIgnoreCase)) { result = KeyboardKeys.Control; return true; } return Enum.TryParse(value, true, out result); } /// /// Returns a that represents the key enum (for UI). /// /// The key. /// A that represents the key. public static string ToString(KeyboardKeys key) { switch (key) { case KeyboardKeys.Control: return "Ctrl"; default: return key.ToString(); } } /// /// Tries the parse the input text value to the . /// /// The value. /// The result value (valid only if method returns true). /// True if parsing succeed, otherwise false. public static bool TryParse(string value, out InputBinding result) { result = new InputBinding(); string[] v = value.Split('+'); switch (v.Length) { case 3: if (Parse(v[2], out result.Key) && Parse(v[1], out result.Modifier1) && Parse(v[0], out result.Modifier2)) return true; break; case 2: if (Parse(v[1], out result.Key) && Parse(v[0], out result.Modifier1)) return true; break; case 1: if (Parse(v[0], out result.Key)) return true; break; } return false; } private bool ProcessModifiers(Control control) { return ProcessModifiers(control.Root.GetKey); } private bool ProcessModifiers(Window window) { return ProcessModifiers(window.GetKey); } private bool ProcessModifiers(Func getKeyFunc) { bool ctrlPressed = getKeyFunc(KeyboardKeys.Control); bool shiftPressed = getKeyFunc(KeyboardKeys.Shift); bool altPressed = getKeyFunc(KeyboardKeys.Alt); bool mod1 = false; if (Modifier1 == KeyboardKeys.None) mod1 = true; else if (Modifier1 == KeyboardKeys.Control) { mod1 = ctrlPressed; ctrlPressed = false; } else if (Modifier1 == KeyboardKeys.Shift) { mod1 = shiftPressed; shiftPressed = false; } else if (Modifier1 == KeyboardKeys.Alt) { mod1 = altPressed; altPressed = false; } bool mod2 = false; if (Modifier2 == KeyboardKeys.None) mod2 = true; else if (Modifier2 == KeyboardKeys.Control) { mod2 = ctrlPressed; ctrlPressed = false; } else if (Modifier2 == KeyboardKeys.Shift) { mod2 = shiftPressed; shiftPressed = false; } else if (Modifier2 == KeyboardKeys.Alt) { mod2 = altPressed; altPressed = false; } // Check if any unhandled modifiers are not pressed if (mod1 && mod2) return !ctrlPressed && !shiftPressed && !altPressed; return false; } /// /// Processes this input binding to check if state matches. /// /// The input providing control. /// True if input has been processed, otherwise false. public bool Process(Control control) { var root = control.Root; return root.GetKey(Key) && ProcessModifiers(control); } /// /// Processes this input binding to check if state matches. /// /// The input providing control. /// The input key. /// True if input has been processed, otherwise false. public bool Process(Control control, KeyboardKeys key) { if (key != Key) return false; return ProcessModifiers(control); } /// /// Processes this input binding to check if state matches. /// /// The input providing window. /// True if input has been processed, otherwise false. public bool Process(Window window) { return window.GetKey(Key) && ProcessModifiers(window); } /// public override string ToString() { string result = string.Empty; if (Modifier2 != KeyboardKeys.None) { result = ToString(Modifier2); } if (Modifier1 != KeyboardKeys.None) { if (result.Length != 0) result += '+'; result += ToString(Modifier1); } if (Key != KeyboardKeys.None) { if (result.Length != 0) result += '+'; result += ToString(Key); } return result; } } class InputBindingConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) { return true; } return base.CanConvertFrom(context, sourceType); } /// public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) { return false; } return base.CanConvertTo(context, destinationType); } /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { InputBinding.TryParse(str, out var result); return result; } return base.ConvertFrom(context, culture, value); } /// public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { if (destinationType == typeof(string)) { return ((InputBinding)value).ToString(); } return base.ConvertTo(context, culture, value, destinationType); } } class InputBindingEditor : CustomEditor { private CustomElement _element; private class InputBindingBox : TextBox { private InputBinding _binding; /// protected override void OnEditBegin() { base.OnEditBegin(); // Reset _text = string.Empty; _binding = new InputBinding(); } /// public override bool OnCharInput(char c) { // Skip text return true; } /// public override bool OnKeyDown(KeyboardKeys key) { // Skip already added keys if (_binding.Key == key || _binding.Modifier1 == key || _binding.Modifier2 == key) return true; switch (key) { // Skip case KeyboardKeys.Spacebar: break; // Modifiers case KeyboardKeys.Control: case KeyboardKeys.Shift: if (_binding.Modifier1 == KeyboardKeys.None) { _binding.Modifier1 = key; _text = _binding.ToString(); } else if (_binding.Modifier2 == KeyboardKeys.None) { _binding.Modifier2 = key; _text = _binding.ToString(); } break; // Keys default: if (_binding.Key == KeyboardKeys.None) { _binding.Key = key; _text = _binding.ToString(); Defocus(); } break; } return true; } } /// public override DisplayStyle Style => DisplayStyle.Inline; /// public override void Initialize(LayoutElementsContainer layout) { var grid = layout.CustomContainer(); var gridControl = grid.CustomControl; gridControl.ClipChildren = false; gridControl.Height = TextBox.DefaultHeight; gridControl.RowFill = new[] { 1.0f, }; gridControl.ColumnFill = new[] { 0.9f, 0.1f }; _element = grid.Custom(); SetText(); _element.CustomControl.WatermarkText = "Type a binding"; _element.CustomControl.EditEnd += OnValueChanged; var button = grid.Button("X"); button.Button.TooltipText = "Remove binding"; button.Button.Clicked += OnXButtonClicked; } private void OnXButtonClicked() { SetValue(new InputBinding()); } private void OnValueChanged() { if (InputBinding.TryParse(_element.CustomControl.Text, out var value)) SetValue(value); else SetText(); } private void SetText() { _element.CustomControl.Text = ((InputBinding)Values[0]).ToString(); } /// public override void Refresh() { base.Refresh(); SetText(); } } /// /// The input actions processing helper that handles input bindings configuration layer. /// public class InputActionsContainer { /// /// The binding. /// public struct Binding { /// /// The binded options callback. /// public Func Binder; /// /// The action callback. /// public Action Callback; /// /// Initializes a new instance of the struct. /// /// The input binding options getter (can read from editor options or use constant binding). /// The callback to invoke on user input. public Binding(Func binder, Action callback) { Binder = binder; Callback = callback; } } /// /// List of all available bindings. /// public List Bindings; /// /// Initializes a new instance of the class. /// public InputActionsContainer() { Bindings = new List(); } /// /// Initializes a new instance of the class. /// /// The input bindings collection. public InputActionsContainer(params Binding[] bindings) { Bindings = new List(bindings); } /// /// Adds the specified binding. /// /// The input binding. public void Add(Binding binding) { Bindings.Add(binding); } /// /// Adds the specified binding. /// /// The input binding options getter (can read from editor options or use constant binding). /// The callback to invoke on user input. public void Add(Func binder, Action callback) { Bindings.Add(new Binding(binder, callback)); } /// /// Adds the specified bindings. /// /// The input bindings collection. public void Add(params Binding[] bindings) { Bindings.AddRange(bindings); } /// /// Processes the specified key input and tries to invoke first matching callback for the current user input state. /// /// The editor instance. /// The input providing control. /// The input key. /// True if event has been handled, otherwise false. public bool Process(Editor editor, Control control, KeyboardKeys key) { var root = control.Root; var options = editor.Options.Options.Input; for (int i = 0; i < Bindings.Count; i++) { var binding = Bindings[i].Binder(options); if (binding.Process(control, key)) { Bindings[i].Callback(); return true; } } return false; } } }