Profile editor - Integrated visual scripting

This commit is contained in:
Robert committed 2022-04-03 22:21:56 +02:00
1 parent 372991a69b
commit 66ea718316
96 files changed
+2413 -238

No files matched your search

@@ -0,0 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:dataBinding="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.DataBinding"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.DataBinding.DataBindingView"
x:DataType="dataBinding:DataBindingViewModel">
<Grid RowDefinitions="48,*">
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*">
<TextBlock Grid.Column="0"
Classes="h5"
VerticalAlignment="Center"
Text="{CompiledBinding LayerProperty.PropertyDescription.Name}"
Margin="10 0" />
<ToggleSwitch Grid.Column="1"
OnContent="Data binding enabled"
OffContent="Data binding disabled"
VerticalAlignment="Center"
IsChecked="{CompiledBinding DataBindingEnabled}" />
<Button Grid.Column="2"
Classes="icon-button"
ToolTip.Tip="Open editor"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Margin="10"
Command="{Binding OpenEditor}">
<avalonia:MaterialIcon Kind="OpenInNew"></avalonia:MaterialIcon>
</Button>
</Grid>
<ContentControl Grid.Row="1"
IsVisible="{CompiledBinding NodeScriptViewModel, Converter={x:Static ObjectConverters.IsNotNull}}"
Content="{CompiledBinding NodeScriptViewModel}" />
<StackPanel Grid.Row="1"
VerticalAlignment="Center"
IsVisible="{CompiledBinding NodeScriptViewModel, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock TextAlignment="Center" Classes="h5">To use data bindings enable them in with the toggle above.</TextBlock>
<TextBlock TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center" Foreground="{DynamicResource TextFillColorSecondary}">
When you enable data bindings you can no longer use keyframes or normal values for this property.
</TextBlock>
<controls:HyperlinkButton HorizontalAlignment="Center"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/profiles/data-bindings"
Margin="0 10">
Learn more
</controls:HyperlinkButton>
</StackPanel>
</Grid>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding
{
public partial class DataBindingView : ReactiveUserControl<DataBindingViewModel>
{
public DataBindingView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,65 @@
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
public class DataBindingViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<bool>? _dataBindingEnabled;
private ObservableAsPropertyHelper<ILayerProperty?>? _layerProperty;
private ObservableAsPropertyHelper<NodeScriptViewModel?>? _nodeScriptViewModel;
public DataBindingViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService)
{
_profileEditorService = profileEditorService;
_windowService = windowService;
this.WhenActivated(d =>
{
_layerProperty = profileEditorService.LayerProperty.ToProperty(this, vm => vm.LayerProperty).DisposeWith(d);
_nodeScriptViewModel = profileEditorService.LayerProperty
.Select(p => p != null ? p.BaseDataBinding.GetObservable() : Observable.Never<IDataBinding>())
.Switch()
.Select(b => b.IsEnabled ? nodeVmFactory.NodeScriptViewModel((NodeScript) b.Script, false) : null)
.ToProperty(this, vm => vm.NodeScriptViewModel)
.DisposeWith(d);
_dataBindingEnabled = profileEditorService.LayerProperty
.Select(p => p != null ? p.BaseDataBinding.GetObservable() : Observable.Never<IDataBinding>())
.Switch()
.Select(b => b.IsEnabled)
.ToProperty(this, vm => vm.DataBindingEnabled)
.DisposeWith(d);
});
}
public ILayerProperty? LayerProperty => _layerProperty?.Value;
public NodeScriptViewModel? NodeScriptViewModel => _nodeScriptViewModel?.Value;
public bool DataBindingEnabled
{
get => _dataBindingEnabled?.Value ?? false;
set
{
if (LayerProperty != null)
_profileEditorService.ExecuteCommand(new ChangeDataBindingEnabled(LayerProperty, value));
}
}
public async Task OpenEditor()
{
if (LayerProperty != null && LayerProperty.BaseDataBinding.IsEnabled)
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", LayerProperty.BaseDataBinding.Script));
}
}
@@ -44,8 +44,15 @@
Background="Transparent"
Margin="0 0 -5 0" />
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<ContentControl Grid.Column="2"
IsVisible="{CompiledBinding DataBindingViewModel, Converter={x:Static ObjectConverters.IsNotNull}}"
Content="{CompiledBinding DataBindingViewModel}"/>
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Column="2"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled"
IsVisible="{CompiledBinding DataBindingViewModel, Converter={x:Static ObjectConverters.IsNull}}">
<Grid RowDefinitions="48,*">
<!-- Timeline header body -->
<controls:TimelineHeader Grid.Row="0"
@@ -136,12 +143,8 @@
<ContentControl Content="{CompiledBinding TimelineViewModel}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
</ScrollViewer>
<!-- TODO: Databindings here -->
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
@@ -11,6 +11,7 @@ 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.Timeline;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
@@ -20,67 +21,85 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertiesViewModel : ActivatableViewModelBase
{
private readonly Dictionary<LayerPropertyGroup, PropertyGroupViewModel> _cachedViewModels;
private readonly Dictionary<LayerPropertyGroup, PropertyGroupViewModel> _cachedPropertyViewModels;
private readonly IDataBindingVmFactory _dataBindingVmFactory;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private DataBindingViewModel? _backgroundDataBindingViewModel;
private DataBindingViewModel? _dataBindingViewModel;
private ObservableAsPropertyHelper<ILayerProperty?>? _layerProperty;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
/// <inheritdoc />
public PropertiesViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
public PropertiesViewModel(IProfileEditorService profileEditorService,
ISettingsService settingsService,
ILayerPropertyVmFactory layerPropertyVmFactory,
IDataBindingVmFactory dataBindingVmFactory,
PlaybackViewModel playbackViewModel)
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
_layerPropertyVmFactory = layerPropertyVmFactory;
_cachedViewModels = new Dictionary<LayerPropertyGroup, PropertyGroupViewModel>();
_dataBindingVmFactory = dataBindingVmFactory;
_cachedPropertyViewModels = new Dictionary<LayerPropertyGroup, PropertyGroupViewModel>();
PropertyGroupViewModels = new ObservableCollection<PropertyGroupViewModel>();
PlaybackViewModel = playbackViewModel;
TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels);
// 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);
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<EventPattern<object>>())
.Switch()
.Subscribe(_ => UpdateGroups());
.Subscribe(_ => UpdatePropertyGroups());
this.WhenAnyValue(vm => vm.ProfileElement)
.Select(p => p != null
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
: Observable.Never<EventPattern<object>>())
.Switch()
.Subscribe(_ => UpdateGroups());
// 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);
Disposable.Create(() =>
{
_settingsService.SaveAllSettings();
foreach ((LayerPropertyGroup _, PropertyGroupViewModel value) in _cachedViewModels)
value.Dispose();
_cachedViewModels.Clear();
}).DisposeWith(d);
});
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups());
.Subscribe(_ => UpdatePropertyGroups());
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups());
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public PlaybackViewModel PlaybackViewModel { get; }
public TimelineViewModel TimelineViewModel { 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 int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public IObservable<bool> Playing => _profileEditorService.Playing;
public PluginSetting<double> PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0);
private void UpdateGroups()
private void UpdatePropertyGroups()
{
if (ProfileElement == null)
{
@@ -92,18 +111,20 @@ public class PropertiesViewModel : ActivatableViewModelBase
if (Layer != null)
{
// Add base VMs
viewModels.Add(GetOrCreateViewModel(Layer.General, null, null));
viewModels.Add(GetOrCreateViewModel(Layer.Transform, null, null));
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(GetOrCreateViewModel(Layer.LayerBrush.BaseProperties, Layer.LayerBrush, 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(GetOrCreateViewModel(layerEffect.BaseProperties, null, layerEffect));
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++)
@@ -119,9 +140,9 @@ public class PropertiesViewModel : ActivatableViewModelBase
PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1);
}
private PropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
private PropertyGroupViewModel GetOrCreatePropertyViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
{
if (_cachedViewModels.TryGetValue(layerPropertyGroup, out PropertyGroupViewModel? cachedVm))
if (_cachedPropertyViewModels.TryGetValue(layerPropertyGroup, out PropertyGroupViewModel? cachedVm))
return cachedVm;
PropertyGroupViewModel createdVm;
@@ -132,7 +153,20 @@ public class PropertiesViewModel : ActivatableViewModelBase
else
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup);
_cachedViewModels[layerPropertyGroup] = createdVm;
_cachedPropertyViewModels[layerPropertyGroup] = createdVm;
return createdVm;
}
private void UpdateTimelineViewModel()
{
if (LayerProperty == null)
{
DataBindingViewModel = null;
}
else
{
_backgroundDataBindingViewModel ??= _dataBindingVmFactory.DataBindingViewModel();
DataBindingViewModel = _backgroundDataBindingViewModel;
}
}
}
@@ -1,4 +1,5 @@
using Artemis.Core;
using System.Reactive;
using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
@@ -6,6 +7,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public interface ITreePropertyViewModel : IReactiveObject
{
ILayerProperty BaseLayerProperty { get; }
bool HasDataBinding { get; }
bool DataBindingEnabled { get; }
double GetDepth();
void ToggleCurrentLayerProperty();
}
@@ -12,7 +12,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Windows;
using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerBrushes;
using Artemis.UI.Shared.LayerEffects;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Ninject;
using Ninject.Parameters;
@@ -5,8 +5,10 @@
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:tree="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Tree"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreePropertyView">
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreePropertyView"
x:DataType="tree:ITreePropertyViewModel">
<UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
</UserControl.Resources>
@@ -49,14 +51,17 @@
<avalonia:MaterialIcon Kind="BackupRestore" />
</Button>
<ToggleButton Grid.Column="4" Classes="icon-button"
<ToggleButton Grid.Column="4"
Name="DataBindingToggleButton"
Classes="icon-button"
Margin="2 0 0 0"
ToolTip.Tip="Change the property's data binding"
Width="24"
Height="24"
VerticalAlignment="Center"
IsEnabled="{Binding LayerProperty.DataBindingsSupported}"
IsChecked="{Binding HasDataBinding, Mode=OneWay}">
IsChecked="{CompiledBinding DataBindingEnabled, Mode=OneWay}"
Click="DataBindingToggleButton_OnClick">
<avalonia:MaterialIcon Kind="VectorLink" />
</ToggleButton>
</Grid>
@@ -3,6 +3,8 @@ using System.Reactive.Linq;
using Artemis.Core;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
@@ -13,18 +15,23 @@ public class TreePropertyView : ReactiveUserControl<ITreePropertyViewModel>
{
public TreePropertyView()
{
InitializeComponent();
this.WhenActivated(d =>
{
if (ViewModel != null)
Observable.FromEventPattern<LayerPropertyEventArgs>(e => ViewModel.BaseLayerProperty.CurrentValueSet += e, e => ViewModel.BaseLayerProperty.CurrentValueSet -= e)
.Subscribe(_ => this.BringIntoView())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(e => ViewModel!.BaseLayerProperty.CurrentValueSet += e, e => ViewModel!.BaseLayerProperty.CurrentValueSet -= e)
.Subscribe(_ => this.BringIntoView())
.DisposeWith(d);
});
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void DataBindingToggleButton_OnClick(object? sender, RoutedEventArgs e)
{
ViewModel?.ToggleCurrentLayerProperty();
this.Find<ToggleButton>("DataBindingToggleButton").IsChecked = !this.Find<ToggleButton>("DataBindingToggleButton").IsChecked;
}
}
@@ -2,6 +2,7 @@
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Extensions;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
@@ -15,6 +16,8 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
{
private readonly IProfileEditorService _profileEditorService;
private TimeSpan _time;
private ObservableAsPropertyHelper<bool>? _isCurrentlySelected;
private ObservableAsPropertyHelper<bool>? _dataBindingEnabled;
public TreePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService)
{
@@ -27,17 +30,17 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
this.WhenActivated(d =>
{
_profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
_isCurrentlySelected = _profileEditorService.LayerProperty.Select(l => l == LayerProperty).ToProperty(this, vm => vm.IsCurrentlySelected).DisposeWith(d);
_dataBindingEnabled = LayerProperty.BaseDataBinding.GetObservable().Select(b => b.IsEnabled).ToProperty(this, vm => vm.DataBindingEnabled).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d);
});
ResetToDefault = ReactiveCommand.Create(ExecuteResetToDefault, Observable.Return(LayerProperty.DefaultValue != null));
}
private void ExecuteResetToDefault()
{
_profileEditorService.ExecuteCommand(new ResetLayerProperty<T>(LayerProperty));
}
public bool IsCurrentlySelected => _isCurrentlySelected?.Value ?? false;
public bool DataBindingEnabled => _dataBindingEnabled?.Value ?? false;
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
@@ -57,8 +60,12 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
_profileEditorService.ExecuteCommand(new ToggleLayerPropertyKeyframes<T>(LayerProperty, value, _time));
}
private void ExecuteResetToDefault()
{
_profileEditorService.ExecuteCommand(new ResetLayerProperty<T>(LayerProperty));
}
public ILayerProperty BaseLayerProperty => LayerProperty;
public bool HasDataBinding => LayerProperty.HasDataBinding;
public double GetDepth()
{
@@ -72,5 +79,10 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
return depth;
}
}
/// <inheritdoc />
public void ToggleCurrentLayerProperty()
{
_profileEditorService.ChangeCurrentLayerProperty(IsCurrentlySelected ? null : LayerProperty);
}
}