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); }
}