using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Playback; using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding; using Artemis.UI.Screens.ProfileEditor.Properties.Dialogs; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.ProfileEditor; using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor.Properties; public class PropertiesViewModel : ActivatableViewModelBase { private readonly Dictionary _cachedPropertyViewModels; private readonly IDataBindingVmFactory _dataBindingVmFactory; private readonly ILayerEffectService _layerEffectService; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly IProfileEditorService _profileEditorService; private readonly ISettingsService _settingsService; private readonly IWindowService _windowService; private DataBindingViewModel? _backgroundDataBindingViewModel; private DataBindingViewModel? _dataBindingViewModel; private ObservableAsPropertyHelper? _layerProperty; private ObservableAsPropertyHelper? _pixelsPerSecond; private ObservableAsPropertyHelper? _profileElement; private ObservableAsPropertyHelper? _suspendedEditing; /// public PropertiesViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService, ILayerPropertyVmFactory layerPropertyVmFactory, IDataBindingVmFactory dataBindingVmFactory, IWindowService windowService, ILayerEffectService layerEffectService, PlaybackViewModel playbackViewModel) { _profileEditorService = profileEditorService; _settingsService = settingsService; _layerPropertyVmFactory = layerPropertyVmFactory; _dataBindingVmFactory = dataBindingVmFactory; _windowService = windowService; _layerEffectService = layerEffectService; _cachedPropertyViewModels = new Dictionary(); PropertyGroupViewModels = new ObservableCollection(); PlaybackViewModel = playbackViewModel; TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels); AddEffect = ReactiveCommand.CreateFromTask(ExecuteAddEffect); // React to service profile element changes as long as the VM is active this.WhenActivated(d => { _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d); _pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d); _layerProperty = profileEditorService.LayerProperty.ToProperty(this, vm => vm.LayerProperty).DisposeWith(d); _suspendedEditing = profileEditorService.SuspendedEditing.ToProperty(this, vm => vm.SuspendedEditing).DisposeWith(d); Disposable.Create(() => { _settingsService.SaveAllSettings(); foreach ((LayerPropertyGroup _, PropertyGroupViewModel value) in _cachedPropertyViewModels) value.Dispose(); _cachedPropertyViewModels.Clear(); }).DisposeWith(d); }); // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 this.WhenAnyValue(vm => vm.ProfileElement) .Select(p => p is Layer l ? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x) : Observable.Never>()) .Switch() .Subscribe(_ => UpdatePropertyGroups()); this.WhenAnyValue(vm => vm.ProfileElement) .Select(p => p != null ? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x) : Observable.Never>()) .Switch() .Subscribe(_ => UpdatePropertyGroups()); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups()); this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel()); } public ObservableCollection PropertyGroupViewModels { get; } public PlaybackViewModel PlaybackViewModel { get; } public TimelineViewModel TimelineViewModel { get; } public ReactiveCommand AddEffect { get; } public DataBindingViewModel? DataBindingViewModel { get => _dataBindingViewModel; set => RaiseAndSetIfChanged(ref _dataBindingViewModel, value); } public RenderProfileElement? ProfileElement => _profileElement?.Value; public Layer? Layer => _profileElement?.Value as Layer; public ILayerProperty? LayerProperty => _layerProperty?.Value; public bool SuspendedEditing => _suspendedEditing?.Value ?? false; public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0; public IObservable Playing => _profileEditorService.Playing; public PluginSetting PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0); private async Task ExecuteAddEffect() { if (ProfileElement == null) return; await _windowService.CreateContentDialog() .WithTitle("Add layer effect") .WithViewModel(out AddEffectViewModel _, ("renderProfileElement", ProfileElement)) .WithCloseButtonText("Cancel") .ShowAsync(); } private void UpdatePropertyGroups() { if (ProfileElement == null) { PropertyGroupViewModels.Clear(); return; } ObservableCollection viewModels = new(); if (Layer != null) { // Add base VMs viewModels.Add(GetOrCreatePropertyViewModel(Layer.General, null, null)); viewModels.Add(GetOrCreatePropertyViewModel(Layer.Transform, null, null)); // Add brush VM if the brush has properties if (Layer.LayerBrush?.BaseProperties != null) viewModels.Add(GetOrCreatePropertyViewModel(Layer.LayerBrush.BaseProperties, Layer.LayerBrush, null)); } // Add effect VMs foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects.OrderBy(e => e.Order)) { if (layerEffect.BaseProperties != null) viewModels.Add(GetOrCreatePropertyViewModel(layerEffect.BaseProperties, null, layerEffect)); } // Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible for (int index = 0; index < viewModels.Count; index++) { PropertyGroupViewModel propertyGroupViewModel = viewModels[index]; if (index > PropertyGroupViewModels.Count - 1) PropertyGroupViewModels.Add(propertyGroupViewModel); else if (!ReferenceEquals(PropertyGroupViewModels[index], propertyGroupViewModel)) PropertyGroupViewModels[index] = propertyGroupViewModel; } while (PropertyGroupViewModels.Count > viewModels.Count) PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1); } private PropertyGroupViewModel GetOrCreatePropertyViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect) { if (_cachedPropertyViewModels.TryGetValue(layerPropertyGroup, out PropertyGroupViewModel? cachedVm)) return cachedVm; PropertyGroupViewModel createdVm; if (layerBrush != null) createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerBrush); else if (layerEffect != null) createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerEffect); else createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup); _cachedPropertyViewModels[layerPropertyGroup] = createdVm; return createdVm; } private void UpdateTimelineViewModel() { if (LayerProperty == null) { DataBindingViewModel = null; } else { _backgroundDataBindingViewModel ??= _dataBindingVmFactory.DataBindingViewModel(); DataBindingViewModel = _backgroundDataBindingViewModel; } } }