using System; using System.Diagnostics.CodeAnalysis; using System.Reactive.Linq; using Artemis.Core; using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Avalonia.Controls.Mixins; using Avalonia.Threading; using ReactiveUI; using ReactiveUI.Validation.Helpers; namespace Artemis.UI.Shared.Services.PropertyInput; /// /// Represents the base class for a property input view model that is used to edit layer properties /// /// The type of property this input view model supports public abstract class PropertyInputViewModel : PropertyInputViewModel { [AllowNull] private T _inputValue; private LayerPropertyPreview? _preview; private TimeSpan _time; private bool _updating; /// /// Creates a new instance of the class /// protected PropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) { LayerProperty = layerProperty; ProfileEditorService = profileEditorService; PropertyInputService = propertyInputService; _inputValue = default!; this.WhenActivated(d => { ProfileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d); UpdateInputValue(); Observable.FromEventPattern(x => LayerProperty.Updated += x, x => LayerProperty.Updated -= x) .Subscribe(_ => UpdateInputValue()) .DisposeWith(d); Observable.FromEventPattern(x => LayerProperty.CurrentValueSet += x, x => LayerProperty.CurrentValueSet -= x) .Subscribe(_ => UpdateInputValue()) .DisposeWith(d); Observable.FromEventPattern(x => LayerProperty.DataBinding.DataBindingEnabled += x, x => LayerProperty.DataBinding.DataBindingEnabled -= x) .Subscribe(_ => UpdateDataBinding()) .DisposeWith(d); Observable.FromEventPattern(x => LayerProperty.DataBinding.DataBindingDisabled += x, x => LayerProperty.DataBinding.DataBindingDisabled -= x) .Subscribe(_ => UpdateDataBinding()) .DisposeWith(d); }); } /// /// Gets the layer property this view model is editing /// public LayerProperty LayerProperty { get; } /// /// Gets a boolean indicating whether the layer property should be enabled /// public bool IsEnabled => !LayerProperty.HasDataBinding; /// /// Gets the profile editor service /// public IProfileEditorService ProfileEditorService { get; } /// /// Gets the property input service /// public IPropertyInputService PropertyInputService { get; } /// /// Gets or boolean indicating whether the current input is being previewed, the value won't be applied until /// /// Only applicable when using something like a , see /// and /// /// public bool IsPreviewing => _preview != null; /// /// Gets or sets the input value /// [MaybeNull] public T InputValue { get => _inputValue; set { this.RaiseAndSetIfChanged(ref _inputValue, value); ApplyInputValue(); } } /// /// Gets the prefix to show before input elements /// public string? Prefix => LayerProperty.PropertyDescription.InputPrefix; /// /// Gets the affix to show after input elements /// public string? Affix => LayerProperty.PropertyDescription.InputAffix; internal override object InternalGuard { get; } = new(); /// /// Starts the preview of the current property, allowing updates without causing real changes to the property. /// public void StartPreview() { _preview?.DiscardPreview(); _preview = new LayerPropertyPreview(LayerProperty, _time); } /// /// Applies the current preview to the property. /// public void ApplyPreview() { if (_preview == null) return; if (_preview.DiscardPreview() && _preview.PreviewValue != null) ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _preview.PreviewValue, _time)); _preview = null; } /// /// Discard the preview of the property. /// public void DiscardPreview() { if (_preview == null) return; _preview.DiscardPreview(); _preview = null; } /// /// Called when the input value has changed /// protected virtual void OnInputValueChanged() { } /// /// Called when data bindings have been enabled or disabled on the layer property /// protected virtual void OnDataBindingsChanged() { } /// /// Applies the input value to the layer property or the currently active preview. /// protected virtual void ApplyInputValue() { // Avoid reapplying the latest value by checking if we're currently updating if (_updating || !ValidationContext.IsValid) return; if (_preview != null) _preview.Preview(_inputValue); else ProfileEditorService.ExecuteCommand(new UpdateLayerProperty(LayerProperty, _inputValue, _time)); } private void UpdateInputValue() { // Always run this on the UI thread to avoid race conditions with ApplyInputValue Dispatcher.UIThread.Post(() => { try { _updating = true; // Avoid unnecessary UI updates and validator cycles if (Equals(_inputValue, LayerProperty.CurrentValue)) return; // Override the input value _inputValue = LayerProperty.CurrentValue; // Notify a change in the input value OnInputValueChanged(); this.RaisePropertyChanged(nameof(InputValue)); } finally { _updating = false; } }); } private void UpdateDataBinding() { this.RaisePropertyChanged(nameof(IsEnabled)); OnDataBindingsChanged(); } } /// /// For internal use only, implement instead. /// public abstract class PropertyInputViewModel : ReactiveValidationObject, IActivatableViewModel, IDisposable { /// /// Prevents this type being implemented directly, implement /// instead. /// // ReSharper disable once UnusedMember.Global internal abstract object InternalGuard { get; } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// /// to release both managed and unmanaged resources; /// to release only unmanaged resources. /// protected virtual void Dispose(bool disposing) { } #region Implementation of IActivatableViewModel /// public ViewModelActivator Activator { get; } = new(); #endregion /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }