using System.Collections.Concurrent; namespace Terminal.Gui.Input; /// /// Abstract class for and . /// This class is thread-safe for all public operations. /// /// The type of the event (e.g. or ). /// The binding type (e.g. ). public abstract class InputBindings where TBinding : IInputBinding, new() where TEvent : notnull { /// /// Initializes a new instance. /// /// /// protected InputBindings (Func constructBinding, IEqualityComparer equalityComparer) { _constructBinding = constructBinding; _bindings = new (equalityComparer); } /// /// The bindings. /// private readonly ConcurrentDictionary _bindings; private readonly Func _constructBinding; /// Adds a bound to to the collection. /// /// public void Add (TEvent eventArgs, TBinding binding) { if (!IsValid (eventArgs)) { throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); } // IMPORTANT: Add a COPY of the eventArgs. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus // IMPORTANT: Apply will update the Dictionary with the new eventArgs, but the old eventArgs will still be in the dictionary. // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); } } /// /// Adds a new that will trigger the commands in . /// /// If the is already bound to a different set of s it will be /// rebound /// . /// /// /// The event to check. /// /// The command to invoked on the when is received. When /// multiple commands are provided,they will be applied in sequence. The bound event /// will be /// consumed if any took effect. /// public void Add (TEvent eventArgs, params Command [] commands) { if (commands.Length == 0) { throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } if (!IsValid (eventArgs)) { throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); } TBinding binding = _constructBinding (commands, eventArgs); if (!_bindings.TryAdd (eventArgs, binding)) { throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); } } /// Removes all objects from the collection. public void Clear () { _bindings.Clear (); } /// /// Removes all bindings that trigger the given command set. Views can have multiple different /// /// bound to /// the same command sets and this method will clear all of them. /// /// public void Clear (params Command [] command) { // ToArray() creates a snapshot to avoid modification during enumeration KeyValuePair [] kvps = _bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); foreach (KeyValuePair kvp in kvps) { Remove (kvp.Key); } } /// Gets the for the specified . /// /// public TBinding? Get (TEvent eventArgs) { if (TryGet (eventArgs, out TBinding? binding)) { return binding; } throw new InvalidOperationException ($"{eventArgs} is not bound."); } /// Gets all bound to the set of commands specified by . /// The set of commands to search. /// /// The s bound to the set of commands specified by . An empty /// list if /// the /// set of commands was not found. /// public IEnumerable GetAllFromCommands (params Command [] commands) { // ToList() creates a snapshot to ensure thread-safe enumeration return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key).ToList (); } /// /// Gets the bindings. /// /// public IEnumerable> GetBindings () { // ConcurrentDictionary provides a snapshot enumeration that is safe for concurrent access return _bindings; } /// Gets the array of s bound to if it exists. /// The to check. /// /// The array of s if is bound. An empty array /// if not. /// public Command [] GetCommands (TEvent eventArgs) { if (TryGet (eventArgs, out TBinding? bindings)) { return bindings!.Commands; } return []; } /// /// Gets the first matching bound to the set of commands specified by /// . /// /// The set of commands to search. /// /// The first matching bound to the set of commands specified by /// . if the set of commands was not found. /// public TEvent? GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } /// /// Tests whether is valid or not. /// /// /// public abstract bool IsValid (TEvent eventArgs); /// Removes a from the collection. /// public void Remove (TEvent eventArgs) { _bindings.TryRemove (eventArgs, out _); } /// Replaces a combination already bound to a set of s. /// /// The to be replaced. /// /// The new to be used. /// public void Replace (TEvent oldEventArgs, TEvent newEventArgs) { if (!IsValid (newEventArgs)) { throw new ArgumentException (@"Invalid newEventArgs", nameof (newEventArgs)); } // Thread-safe: Handle the case where oldEventArgs == newEventArgs if (EqualityComparer.Default.Equals (oldEventArgs, newEventArgs)) { // Same key - nothing to do, binding stays as-is return; } // Thread-safe: Get the binding from oldEventArgs, or create default if it doesn't exist // This is atomic - either gets existing or adds new TBinding binding = _bindings.GetOrAdd (oldEventArgs, _ => new TBinding ()); // Thread-safe: Atomically add/update newEventArgs with the binding from oldEventArgs // The updateValueFactory is only called if the key already exists, ensuring we don't // accidentally overwrite a binding that was added by another thread _bindings.AddOrUpdate ( newEventArgs, binding, // Add this binding if newEventArgs doesn't exist (_, _) => binding); // Thread-safe: Remove oldEventArgs only after newEventArgs has been set // This ensures we don't lose the binding if another thread is reading it _bindings.TryRemove (oldEventArgs, out _); } /// Replaces the commands already bound to a combination of . /// /// /// If the of is not already bound, it will be added. /// /// /// The combination of bound to the command to be replaced. /// The set of commands to replace the old ones with. public void ReplaceCommands (TEvent eventArgs, params Command [] newCommands) { TBinding newBinding = _constructBinding (newCommands, eventArgs); // Thread-safe: Add or update atomically _bindings.AddOrUpdate (eventArgs, newBinding, (_, _) => newBinding); } /// Gets the commands bound with the specified . /// /// The to check. /// /// When this method returns, contains the commands bound with the , if the /// is /// not /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the is bound; otherwise . public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } }