using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Robust.Shared.Input { /// /// Contains a set of created s. /// [NotContentImplementable] public interface IInputContextContainer { /// /// The current "active" context that should be used for filtering key binds. /// IInputCmdContext ActiveContext { get; } /// /// Puts context switches 'on hold'. When set to false, the last deferred context switch is executed, if any. /// bool DeferringEnabled { get; set; } /// /// This event is raised when ever the Active Context is changed. /// event EventHandler ContextChanged; /// /// Adds a new unique context to the set. /// /// Unique name of the new context. /// Unique name of the parent context. Tee parent context must already exist in the set. /// Instance of the newly created context. IInputCmdContext New(string uniqueName, string parentName); /// /// Adds a new unique context to the set. /// /// Unique name of the new context. /// Context to set, as the parent context, of the newly created context. /// Instance of the newly created context. IInputCmdContext New(string uniqueName, IInputCmdContext parent); /// /// Checks if a context with a unique name exists in the set. /// /// Unique Name to search for. /// If a context exists with the given unique name in the set. bool Exists(string uniqueName); /// /// Returns the context with the given unique name from the set. /// /// Unique name of the context to search for. /// Context with the given unique name in the set. IInputCmdContext GetContext(string uniqueName); /// /// Tries to find a context with a given unique name. /// /// Unique name of the context to search for. /// The context with the given unique name (if any). /// If a context with a given unique name exists in the set. bool TryGetContext(string uniqueName, [NotNullWhen(true)] out IInputCmdContext? context); /// /// Removes the context with the given unique name. /// /// Unique name of context to remove. void Remove(string uniqueName); /// /// Sets the context with the given unique name as the Active context. /// This *may* be deferred if during input handling, and if multiple context switches are deferred, only the last 'counts'. /// /// Unique name of the context to set as active. void SetActiveContext(string uniqueName); } /// internal sealed class InputContextContainer : IInputContextContainer { /// /// Default 'root' context unique name that always exists in the set. /// public const string DefaultContextName = "common"; /// public event EventHandler? ContextChanged; private readonly Dictionary _contexts = new(); // Random scribble here: // InputManager r/n feeds the first active context using DefaultContextName // This is why SetActiveContext needs to be careful to *not* defer the switch if there's no context, // or else the client instantly crashes because there's no context on startup private InputCmdContext _activeContext = default!; /// /// This is used to hold a pending context switch, because someone decided that: /// + Reentrant input events were illegal /// and /// + Context switches ought to generate input events /// This prevents the inevitable errors this caused from input events that switch contexts, /// by deferring the context switches until we're definitely out of input event code. /// private InputCmdContext? _deferredContextSwitch = null; /// public IInputCmdContext ActiveContext => _activeContext; private bool _deferringEnabled = false; /// public bool DeferringEnabled { get => _deferringEnabled; set { // Must be first because _setActiveContextImmediately triggers input events. _deferringEnabled = value; if (!value) { if (_deferredContextSwitch != null) { var icc = _deferredContextSwitch; _deferredContextSwitch = null; _setActiveContextImmediately( icc); } } } } /// /// Creates a new instance of . /// public InputContextContainer() { _contexts.Add(DefaultContextName, new InputCmdContext(DefaultContextName)); SetActiveContext(DefaultContextName); } /// public IInputCmdContext New(string uniqueName, string parentName) { if (string.IsNullOrWhiteSpace(uniqueName)) throw new ArgumentException("String is null or whitespace.", nameof(uniqueName)); if (string.IsNullOrWhiteSpace(parentName)) throw new ArgumentException("String is null or whitespace.", nameof(parentName)); if (!_contexts.TryGetValue(parentName, out var parentContext)) throw new ArgumentException("Parent does not exist.", nameof(parentName)); if (_contexts.ContainsKey(uniqueName)) throw new ArgumentException($"Context with name {uniqueName} already exists.", nameof(uniqueName)); var newContext = new InputCmdContext(parentContext, uniqueName); _contexts.Add(uniqueName, newContext); return newContext; } /// public IInputCmdContext New(string uniqueName, IInputCmdContext parent) { if (string.IsNullOrWhiteSpace(uniqueName)) throw new ArgumentException("String is null or whitespace.", nameof(uniqueName)); if (_contexts.ContainsKey(uniqueName)) throw new ArgumentException($"Context with name {uniqueName} already exists.", nameof(uniqueName)); if (parent == null) throw new ArgumentNullException(nameof(parent)); var newContext = new InputCmdContext(parent, uniqueName); _contexts.Add(uniqueName, newContext); return newContext; } /// public bool Exists(string uniqueName) { return _contexts.ContainsKey(uniqueName); } /// public IInputCmdContext GetContext(string uniqueName) { return _contexts[uniqueName]; } /// public bool TryGetContext(string uniqueName, [NotNullWhen(true)] out IInputCmdContext? context) { if (_contexts.TryGetValue(uniqueName, out var ctext)) { context = ctext; return true; } context = default; return false; } /// public void Remove(string uniqueName) { if (uniqueName == DefaultContextName) throw new ArgumentException("The default context cannot be removed.", nameof(uniqueName)); _contexts.Remove(uniqueName); } /// public void SetActiveContext(string uniqueName) { if (!DeferringEnabled) { _setActiveContextImmediately(_contexts[uniqueName]); } else { _deferredContextSwitch = _contexts[uniqueName]; } } private void _setActiveContextImmediately(InputCmdContext icc) { var args = new ContextChangedEventArgs(_activeContext, icc); _activeContext = icc; ContextChanged?.Invoke(this, args); } } /// /// Event arguments for an input context change. /// public sealed class ContextChangedEventArgs : EventArgs { /// /// The new context that became active. /// public IInputCmdContext? NewContext { get; } /// /// The old context that used to be active. /// public IInputCmdContext? OldContext { get; } /// /// Constructs a new instance of / /// /// The old context that used to be active. /// The new context that became active. public ContextChangedEventArgs(IInputCmdContext? oldContext, IInputCmdContext? newContext) { OldContext = oldContext; NewContext = newContext; } } }