// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Drawing; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Framework.Configuration.Tracking; namespace osu.Framework.Configuration { public abstract class ConfigManager : ConfigManager, ITrackableConfigManager where TLookup : struct, Enum { /// /// Whether user specified configuration elements should be set even though a default was never specified. /// protected virtual bool AddMissingEntries => true; private readonly IDictionary defaultOverrides; protected readonly Dictionary ConfigStore = new Dictionary(); /// /// Initialise a new /// /// Dictionary of overrides which should take precedence over defaults specified by the implementation. protected ConfigManager(IDictionary defaultOverrides = null) { this.defaultOverrides = defaultOverrides; } /// /// Set all required default values via Set() calls. /// Note that defaults set here may be overridden by provided in the constructor. /// protected virtual void InitialiseDefaults() { } /// /// Sets a configuration's value. /// /// The lookup key. /// The value. Will also become the default value if one has not already been initialised. /// The type of value. public void SetValue(TLookup lookup, TValue value) { var bindable = GetOriginalBindable(lookup); if (bindable == null) SetDefault(lookup, value); else bindable.Value = value; } [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 public BindableDouble Set(TLookup lookup, double value, double? min = null, double? max = null, double? precision = null) => SetDefault(lookup, value, min, max, precision); [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 public BindableFloat Set(TLookup lookup, float value, float? min = null, float? max = null, float? precision = null) => SetDefault(lookup, value, min, max, precision); [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 public BindableInt Set(TLookup lookup, int value, int? min = null, int? max = null) => SetDefault(lookup, value, min, max); [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 public BindableBool Set(TLookup lookup, bool value) => SetDefault(lookup, value); [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 public BindableSize Set(TLookup lookup, Size value, Size? min = null, Size? max = null) => SetDefault(lookup, value, min, max); [Obsolete("In derived classes, use SetDefault() to set the default value. In public contexts, use SetValue() to set the value.")] // Can be removed 20210915 public Bindable Set(TLookup lookup, TValue value) => SetDefault(lookup, value); /// /// Sets a configuration's default value. /// /// The lookup key. /// The default value. /// The minimum value. /// The maximum value. /// The value precision. /// The original bindable (not a bound copy). protected BindableDouble SetDefault(TLookup lookup, double value, double? min = null, double? max = null, double? precision = null) { value = getDefault(lookup, value); if (!(GetOriginalBindable(lookup) is BindableDouble bindable)) { bindable = new BindableDouble(value); AddBindable(lookup, bindable); } else { bindable.Value = value; } bindable.Default = value; if (min.HasValue) bindable.MinValue = min.Value; if (max.HasValue) bindable.MaxValue = max.Value; if (precision.HasValue) bindable.Precision = precision.Value; return bindable; } /// /// Sets a configuration's default value. /// /// The lookup key. /// The default value. /// The minimum value. /// The maximum value. /// The value precision. /// The original bindable (not a bound copy). protected BindableFloat SetDefault(TLookup lookup, float value, float? min = null, float? max = null, float? precision = null) { value = getDefault(lookup, value); if (!(GetOriginalBindable(lookup) is BindableFloat bindable)) { bindable = new BindableFloat(value); AddBindable(lookup, bindable); } else { bindable.Value = value; } bindable.Default = value; if (min.HasValue) bindable.MinValue = min.Value; if (max.HasValue) bindable.MaxValue = max.Value; if (precision.HasValue) bindable.Precision = precision.Value; return bindable; } /// /// Sets a configuration's default value. /// /// The lookup key. /// The default value. /// The minimum value. /// The maximum value. /// The original bindable (not a bound copy). protected BindableInt SetDefault(TLookup lookup, int value, int? min = null, int? max = null) { value = getDefault(lookup, value); if (!(GetOriginalBindable(lookup) is BindableInt bindable)) { bindable = new BindableInt(value); AddBindable(lookup, bindable); } else { bindable.Value = value; } bindable.Default = value; if (min.HasValue) bindable.MinValue = min.Value; if (max.HasValue) bindable.MaxValue = max.Value; return bindable; } /// /// Sets a configuration's default value. /// /// The lookup key. /// The default value. /// The original bindable (not a bound copy). protected BindableBool SetDefault(TLookup lookup, bool value) { value = getDefault(lookup, value); if (!(GetOriginalBindable(lookup) is BindableBool bindable)) { bindable = new BindableBool(value); AddBindable(lookup, bindable); } else { bindable.Value = value; } bindable.Default = value; return bindable; } /// /// Sets a configuration's default value. /// /// The lookup key. /// The default value. /// The minimum value. /// The maximum value. /// The original bindable (not a bound copy). protected BindableSize SetDefault(TLookup lookup, Size value, Size? min = null, Size? max = null) { value = getDefault(lookup, value); if (!(GetOriginalBindable(lookup) is BindableSize bindable)) { bindable = new BindableSize(value); AddBindable(lookup, bindable); } else { bindable.Value = value; } bindable.Default = value; if (min.HasValue) bindable.MinValue = min.Value; if (max.HasValue) bindable.MaxValue = max.Value; return bindable; } /// /// Sets a configuration's default value. /// /// The lookup key. /// The default value. /// The original bindable (not a bound copy). protected Bindable SetDefault(TLookup lookup, TValue value) { value = getDefault(lookup, value); Bindable bindable = GetOriginalBindable(lookup); if (bindable == null) bindable = set(lookup, value); else bindable.Value = value; bindable.Default = value; return bindable; } protected virtual void AddBindable(TLookup lookup, Bindable bindable) { ConfigStore[lookup] = bindable; bindable.ValueChanged += _ => QueueBackgroundSave(); } private TValue getDefault(TLookup lookup, TValue fallback) { if (defaultOverrides != null && defaultOverrides.TryGetValue(lookup, out object found)) return (TValue)found; return fallback; } private Bindable set(TLookup lookup, TValue value) { Bindable bindable = new Bindable(value); AddBindable(lookup, bindable); return bindable; } public TValue Get(TLookup lookup) => GetOriginalBindable(lookup).Value; protected Bindable GetOriginalBindable(TLookup lookup) { if (ConfigStore.TryGetValue(lookup, out IBindable obj)) { if (!(obj is Bindable)) throw new InvalidCastException($"Cannot convert bindable of type {obj.GetType()} retrieved from {nameof(ConfigManager)} to {typeof(Bindable)}."); return (Bindable)obj; } return null; } /// /// Retrieve a bindable. This will be a new instance weakly bound to the configuration backing. /// If you are further binding to events of a bindable retrieved using this method, ensure to hold /// a local reference. /// /// A weakly bound copy of the specified bindable. public Bindable GetBindable(TLookup lookup) => GetOriginalBindable(lookup)?.GetBoundCopy(); /// /// Binds a local bindable with a configuration-backed bindable. /// public void BindWith(TLookup lookup, Bindable bindable) => bindable.BindTo(GetOriginalBindable(lookup)); public virtual TrackedSettings CreateTrackedSettings() => null; public void LoadInto(TrackedSettings settings) => settings.LoadFrom(this); public class TrackedSetting : Tracking.TrackedSetting { /// /// Constructs a new . /// /// The config setting to be tracked. /// A function that generates the description for the setting, invoked every time the value changes. public TrackedSetting(TLookup setting, Func generateDescription) : base(setting, generateDescription) { } } } public abstract class ConfigManager : IDisposable { private bool hasLoaded; public void Load() { PerformLoad(); hasLoaded = true; } private int lastSave; /// /// Queue a background save operation with debounce. /// protected void QueueBackgroundSave() { var current = Interlocked.Increment(ref lastSave); Task.Delay(100).ContinueWith(task => { if (current == lastSave) Save(); }); } private readonly object saveLock = new object(); public bool Save() { if (!hasLoaded) return false; lock (saveLock) { Interlocked.Increment(ref lastSave); return PerformSave(); } } protected abstract void PerformLoad(); protected abstract bool PerformSave(); #region IDisposable Support private bool isDisposed; protected virtual void Dispose(bool disposing) { if (!isDisposed) { Save(); isDisposed = true; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #endregion } }