// 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.Globalization; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.IO.Serialization; using osu.Framework.Lists; namespace osu.Framework.Bindables { /// /// A generic implementation of a /// /// The type of our stored . public class Bindable : IBindable, IBindable, IParseable, ISerializableBindable { /// /// An event which is raised when has changed (or manually via ). /// public event Action> ValueChanged; /// /// An event which is raised when has changed (or manually via ). /// public event Action DisabledChanged; /// /// An event which is raised when has changed (or manually via ). /// public event Action> DefaultChanged; private T value; private T defaultValue; private bool disabled; /// /// Whether this bindable has been disabled. When disabled, attempting to change the will result in an . /// public virtual bool Disabled { get => disabled; set { // if a lease is active, disabled can *only* be changed by that leased bindable. throwIfLeased(); if (disabled == value) return; SetDisabled(value); } } internal void SetDisabled(bool value, bool bypassChecks = false, Bindable source = null) { if (!bypassChecks) throwIfLeased(); disabled = value; TriggerDisabledChange(source ?? this, true, bypassChecks); } /// /// Check whether the current is equal to . /// public virtual bool IsDefault => EqualityComparer.Default.Equals(value, Default); /// /// Revert the current to the defined . /// public void SetDefault() => Value = Default; /// /// The current value of this bindable. /// public virtual T Value { get => value; set { // intentionally don't have throwIfLeased() here. // if the leased bindable decides to disable exclusive access (by setting Disabled = false) then anything will be able to write to Value. if (Disabled) throw new InvalidOperationException($"Can not set value to \"{value.ToString()}\" as bindable is disabled."); if (EqualityComparer.Default.Equals(this.value, value)) return; SetValue(this.value, value); } } internal void SetValue(T previousValue, T value, bool bypassChecks = false, Bindable source = null) { this.value = value; TriggerValueChange(previousValue, source ?? this, true, bypassChecks); } /// /// The default value of this bindable. Used when calling or querying . /// public virtual T Default { get => defaultValue; set { // intentionally don't have throwIfLeased() here. // if the leased bindable decides to disable exclusive access (by setting Disabled = false) then anything will be able to write to Default. if (Disabled) throw new InvalidOperationException($"Can not set default value to \"{value.ToString()}\" as bindable is disabled."); if (EqualityComparer.Default.Equals(defaultValue, value)) return; SetDefaultValue(defaultValue, value); } } internal void SetDefaultValue(T previousValue, T value, bool bypassChecks = false, Bindable source = null) { defaultValue = value; TriggerDefaultChange(previousValue, source ?? this, true, bypassChecks); } private WeakReference> weakReferenceInstance; private WeakReference> weakReference => weakReferenceInstance ??= new WeakReference>(this); /// /// Creates a new bindable instance. This is used for deserialization of bindables. /// [UsedImplicitly] private Bindable() : this(default) { } /// /// Creates a new bindable instance initialised with a default value. /// /// The initial and default value for this bindable. public Bindable(T defaultValue = default) { value = Default = defaultValue; } protected LockedWeakList> Bindings { get; private set; } void IBindable.BindTo(IBindable them) { if (!(them is Bindable tThem)) throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); BindTo(tThem); } void IBindable.BindTo(IBindable them) { if (!(them is Bindable tThem)) throw new InvalidCastException($"Can't bind to a bindable of type {them.GetType()} from a bindable of type {GetType()}."); BindTo(tThem); } /// /// An alias of provided for use in object initializer scenarios. /// Passes the provided value as the foreign (more permanent) bindable. /// public IBindable BindTarget { set => ((IBindable)this).BindTo(value); } /// /// Binds this bindable to another such that bi-directional updates are propagated. /// This will adopt any values and value limitations of the bindable bound to. /// /// The foreign bindable. This should always be the most permanent end of the bind (ie. a ConfigManager). /// Thrown when attempting to bind to an already bound object. public virtual void BindTo(Bindable them) { if (Bindings?.Contains(them) == true) throw new InvalidOperationException($"This bindable is already bound to the requested bindable ({them})."); Value = them.Value; Default = them.Default; Disabled = them.Disabled; addWeakReference(them.weakReference); them.addWeakReference(weakReference); } /// /// Bind an action to with the option of running the bound action once immediately. /// /// The action to perform when changes. /// Whether the action provided in should be run once immediately. public void BindValueChanged(Action> onChange, bool runOnceImmediately = false) { ValueChanged += onChange; if (runOnceImmediately) onChange(new ValueChangedEvent(Value, Value)); } /// /// Bind an action to with the option of running the bound action once immediately. /// /// The action to perform when changes. /// Whether the action provided in should be run once immediately. public void BindDisabledChanged(Action onChange, bool runOnceImmediately = false) { DisabledChanged += onChange; if (runOnceImmediately) onChange(Disabled); } private void addWeakReference(WeakReference> weakReference) { Bindings ??= new LockedWeakList>(); Bindings.Add(weakReference); } private void removeWeakReference(WeakReference> weakReference) => Bindings?.Remove(weakReference); /// /// Parse an object into this instance. /// An object deriving T can be parsed, or a string can be parsed if T is an enum type. /// /// The input which is to be parsed. public virtual void Parse(object input) { Type underlyingType = typeof(T).GetUnderlyingNullableType() ?? typeof(T); switch (input) { case T t: Value = t; break; case IBindable _: if (!(input is IBindable bindable)) throw new ArgumentException($"Expected bindable of type {nameof(IBindable)}<{typeof(T)}>, got {input.GetType()}", nameof(input)); Value = bindable.Value; break; default: if (underlyingType.IsEnum) Value = (T)Enum.Parse(underlyingType, input.ToString()); else Value = (T)Convert.ChangeType(input, underlyingType, CultureInfo.InvariantCulture); break; } } /// /// Raise and once, without any changes actually occurring. /// This does not propagate to any outward bound bindables. /// public virtual void TriggerChange() { TriggerValueChange(value, this, false); TriggerDisabledChange(this, false); } protected void TriggerValueChange(T previousValue, Bindable source, bool propagateToBindings = true, bool bypassChecks = false) { // check a bound bindable hasn't changed the value again (it will fire its own event) T beforePropagation = value; if (propagateToBindings && Bindings != null) { foreach (var b in Bindings) { if (b == source) continue; b.SetValue(previousValue, value, bypassChecks, this); } } if (EqualityComparer.Default.Equals(beforePropagation, value)) ValueChanged?.Invoke(new ValueChangedEvent(previousValue, value)); } protected void TriggerDefaultChange(T previousValue, Bindable source, bool propagateToBindings = true, bool bypassChecks = false) { // check a bound bindable hasn't changed the value again (it will fire its own event) T beforePropagation = defaultValue; if (propagateToBindings && Bindings != null) { foreach (var b in Bindings) { if (b == source) continue; b.SetDefaultValue(previousValue, defaultValue, bypassChecks, this); } } if (EqualityComparer.Default.Equals(beforePropagation, defaultValue)) DefaultChanged?.Invoke(new ValueChangedEvent(previousValue, defaultValue)); } protected void TriggerDisabledChange(Bindable source, bool propagateToBindings = true, bool bypassChecks = false) { // check a bound bindable hasn't changed the value again (it will fire its own event) bool beforePropagation = disabled; if (propagateToBindings && Bindings != null) { foreach (var b in Bindings) { if (b == source) continue; b.SetDisabled(disabled, bypassChecks, this); } } if (beforePropagation == disabled) DisabledChanged?.Invoke(disabled); } /// /// Unbinds any actions bound to the value changed events. /// public virtual void UnbindEvents() { ValueChanged = null; DefaultChanged = null; DisabledChanged = null; } /// /// Remove all bound s via or . /// public void UnbindBindings() { if (Bindings == null) return; // ToArray required as this may be called from an async disposal thread. // This can lead to deadlocks since each child is also enumerating its Bindings. foreach (var b in Bindings.ToArray()) UnbindFrom(b); } /// /// Calls and . /// Also returns any active lease. /// public void UnbindAll() => UnbindAllInternal(); internal virtual void UnbindAllInternal() { if (isLeased) leasedBindable.Return(); UnbindEvents(); UnbindBindings(); } public virtual void UnbindFrom(IUnbindable them) { if (!(them is Bindable tThem)) throw new InvalidCastException($"Can't unbind a bindable of type {them.GetType()} from a bindable of type {GetType()}."); removeWeakReference(tThem.weakReference); tThem.removeWeakReference(weakReference); } public string Description { get; set; } public override string ToString() => value?.ToString() ?? string.Empty; /// /// Create an unbound clone of this bindable. /// public Bindable GetUnboundCopy() { var clone = GetBoundCopy(); clone.UnbindAll(); return clone; } IBindable IBindable.CreateInstance() => CreateInstance(); /// protected virtual Bindable CreateInstance() => new Bindable(); IBindable IBindable.GetBoundCopy() => GetBoundCopy(); IBindable IBindable.GetBoundCopy() => GetBoundCopy(); /// public Bindable GetBoundCopy() => IBindable.GetBoundCopyImplementation(this); void ISerializableBindable.SerializeTo(JsonWriter writer, JsonSerializer serializer) { serializer.Serialize(writer, Value); } void ISerializableBindable.DeserializeFrom(JsonReader reader, JsonSerializer serializer) { Value = serializer.Deserialize(reader); } private LeasedBindable leasedBindable; private bool isLeased => leasedBindable != null; /// /// Takes out a mutually exclusive lease on this bindable. /// During a lease, the bindable will be set to , but changes can still be applied via the returned by this call. /// You should end a lease by calling when done. /// /// Whether the when was called should be restored when the lease ends. /// A bindable with a lease. public LeasedBindable BeginLease(bool revertValueOnReturn) { if (checkForLease(this)) throw new InvalidOperationException("Attempted to lease a bindable that is already in a leased state."); return leasedBindable = new LeasedBindable(this, revertValueOnReturn); } private bool checkForLease(Bindable source) { if (isLeased) return true; if (Bindings == null) return false; bool found = false; foreach (var b in Bindings) { if (b != source) found |= b.checkForLease(this); } return found; } /// /// Called internally by a to end a lease. /// /// The that was provided as a return of a call. internal void EndLease(ILeasedBindable returnedBindable) { if (!isLeased) throw new InvalidOperationException("Attempted to end a lease without beginning one."); if (returnedBindable != leasedBindable) throw new InvalidOperationException("Attempted to end a lease but returned a different bindable to the one used to start the lease."); leasedBindable = null; } private void throwIfLeased() { if (isLeased) throw new InvalidOperationException($"Cannot perform this operation on a {nameof(Bindable)} that is currently in a leased state."); } } }