using System; using System.Collections.Generic; using Unity.Properties; using UnityEngine; using UnityEngine.UIElements; namespace Uigenx.Binding { /// /// Exposes a plain object through Unity.Properties. Unity builds a reflected property /// bag for the target the first time a path resolves, so no attribute or code generation is /// required for public members. Changes are found by polling, because a POCO cannot announce /// them; a source that can should implement directly or derive /// from . /// public sealed class UgxReflectiveDataSource : IUgxDataSource, INotifyBindablePropertyChanged, IDataSourceViewHashProvider { readonly object _target; readonly List _watched = new List(); readonly Dictionary _last = new Dictionary(StringComparer.Ordinal); double _nextPollTime; long _version; /// Polls per second. Zero or less polls on every call. public float Hz { get; set; } = 20f; public UgxReflectiveDataSource(object target) { _target = target ?? throw new ArgumentNullException(nameof(target)); } public object Target => _target; public event EventHandler propertyChanged; public event Action PathChanged; public long GetViewHashCode() => _version; public IEnumerable Paths { get { foreach (var entry in UgxSchemaWalker.Collect(_target.GetType())) yield return entry.Path; } } public bool TryGet(string path, out object value) { if (string.IsNullOrEmpty(path)) { value = null; return false; } var container = _target; return PropertyContainer.TryGetValue(ref container, UgxPropertyPaths.Get(path), out value); } public bool TrySet(string path, object value) { if (string.IsNullOrEmpty(path)) return false; var container = _target; if (!PropertyContainer.TrySetValue(ref container, UgxPropertyPaths.Get(path), value)) return false; Raise(path, value); return true; } /// Adds a path to the polled set. Only watched paths raise change notifications. public void Watch(string path) { if (string.IsNullOrEmpty(path) || _watched.Contains(path)) return; _watched.Add(path); if (TryGet(path, out var value)) _last[path] = value; } public void Unwatch(string path) { if (!_watched.Remove(path)) return; _last.Remove(path); } public void UnwatchAll() { _watched.Clear(); _last.Clear(); } /// /// Compares every watched path against its last value and raises the changed ones. Honours /// so callers can drive it from an unconditional per frame update. /// public void Poll(double now) { if (Hz > 0f) { if (now < _nextPollTime) return; _nextPollTime = now + 1d / Hz; } for (int i = 0; i < _watched.Count; i++) { var path = _watched[i]; if (!TryGet(path, out var value)) continue; if (_last.TryGetValue(path, out var previous) && Equals(previous, value)) continue; _last[path] = value; Raise(path, value); } } public void Poll() => Poll(Time.unscaledTimeAsDouble); void Raise(string path, object value) { _last[path] = value; _version++; try { PathChanged?.Invoke(path); } catch (Exception e) { Debug.LogError("[ugx] a data source listener threw for '" + path + "': " + e); } try { propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(path)); } catch (Exception e) { Debug.LogError("[ugx] a binding listener threw for '" + path + "': " + e); } } } }