mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Profile editor - Integrated visual scripting
This commit is contained in:
1 parent
372991a69b
commit
66ea718316
96 files changed
+2413
-238
No files matched your search
+19
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition
|
||||
{
|
||||
public class ConditionTypeViewModel : ViewModelBase
|
||||
{
|
||||
public ConditionTypeViewModel(string name, string description, Type? conditionType)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
ConditionType = conditionType;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Description { get; }
|
||||
public Type? ConditionType { get; }
|
||||
}
|
||||
}
|
||||
+97
-21
@@ -4,28 +4,104 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:displayCondition="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="650"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView"
|
||||
x:DataType="displayCondition:DisplayConditionScriptViewModel">
|
||||
<Grid>
|
||||
<Grid IsVisible="{CompiledBinding NodeScriptViewModel, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<ContentControl Content="{CompiledBinding NodeScriptViewModel}" />
|
||||
<Button Classes="icon-button"
|
||||
ToolTip.Tip="Open editor"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="4"
|
||||
Command="{Binding OpenEditor}">
|
||||
<avalonia:MaterialIcon Kind="OpenInNew"></avalonia:MaterialIcon>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="ComboBox.condition-type /template/ ContentControl#ContentPresenter">
|
||||
<Setter Property="ContentTemplate">
|
||||
<Setter.Value>
|
||||
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
|
||||
<TextBlock Text="{CompiledBinding Name}" VerticalAlignment="Center" />
|
||||
</DataTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<ScrollViewer>
|
||||
<DockPanel LastChildFill="False">
|
||||
<DockPanel.Styles>
|
||||
<Style Selector="DockPanel > TextBlock">
|
||||
<Setter Property="Margin" Value="0 5"></Setter>
|
||||
</Style>
|
||||
|
||||
</DockPanel.Styles>
|
||||
<TextBlock DockPanel.Dock="Top">Condition type</TextBlock>
|
||||
<ComboBox Name="ConditionType"
|
||||
DockPanel.Dock="Top"
|
||||
Classes="condition-type"
|
||||
PlaceholderText="Select a condition type"
|
||||
Items="{CompiledBinding ConditionTypeViewModels}"
|
||||
SelectedItem="{CompiledBinding SelectedConditionTypeViewModel}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type displayCondition:ConditionTypeViewModel}">
|
||||
<StackPanel Spacing="5">
|
||||
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
<TextBlock Text="{CompiledBinding Description}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" MaxWidth="350" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<!-- Event options -->
|
||||
<DockPanel IsVisible="{CompiledBinding ShowEventOptions}" DockPanel.Dock="Top" HorizontalAlignment="Stretch">
|
||||
<TextBlock DockPanel.Dock="Top">Triggered by event</TextBlock>
|
||||
<dataModelPicker:DataModelPickerButton Placeholder="Select an event"
|
||||
DockPanel.Dock="Top"
|
||||
HorizontalAlignment="Stretch" />
|
||||
|
||||
<TextBlock DockPanel.Dock="Top">When the event fires..</TextBlock>
|
||||
<ComboBox PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" DockPanel.Dock="Top">
|
||||
<ComboBoxItem>
|
||||
Play the timeline once
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
Toggle the element on or off
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock DockPanel.Dock="Top">And if already playing..</TextBlock>
|
||||
<ComboBox PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" DockPanel.Dock="Top">
|
||||
<ComboBoxItem>
|
||||
Restart the timeline
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
Play a second copy
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem>
|
||||
Do nothing
|
||||
</ComboBoxItem>
|
||||
</ComboBox>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Static options -->
|
||||
<DockPanel IsVisible="{CompiledBinding ShowStaticOptions}" HorizontalAlignment="Stretch" DockPanel.Dock="Top">
|
||||
<TextBlock DockPanel.Dock="Top">While condition is met..</TextBlock>
|
||||
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a play mode" HorizontalAlignment="Stretch">
|
||||
<ComboBoxItem>Play the main segment once</ComboBoxItem>
|
||||
<ComboBoxItem>Repeat the main segment</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
||||
<TextBlock DockPanel.Dock="Top">And when no longer met..</TextBlock>
|
||||
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a stop mode" HorizontalAlignment="Stretch">
|
||||
<ComboBoxItem>Finish the main segment</ComboBoxItem>
|
||||
<ComboBoxItem>Skip forward to the end segment</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0 5">
|
||||
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0"></avalonia:MaterialIcon>
|
||||
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap">
|
||||
The start- and end-segments are always played once.
|
||||
</TextBlock>
|
||||
</DockPanel>
|
||||
</DockPanel>
|
||||
|
||||
<Button DockPanel.Dock="Bottom" ToolTip.Tip="Open editor" Margin="0 15 0 5" Command="{Binding OpenEditor}" HorizontalAlignment="Stretch">
|
||||
Edit condition script
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Button VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Command="{Binding EnableConditions}"
|
||||
IsVisible="{CompiledBinding NodeScriptViewModel, Converter={x:Static ObjectConverters.IsNull}}">
|
||||
Enable display conditions
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
</DockPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
+49
-10
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.VisualScripting;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
@@ -16,40 +18,77 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
|
||||
public class DisplayConditionScriptViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly ObservableAsPropertyHelper<bool> _showEventOptions;
|
||||
private readonly ObservableAsPropertyHelper<bool> _showStaticOptions;
|
||||
private readonly IWindowService _windowService;
|
||||
private ObservableAsPropertyHelper<NodeScriptViewModel?>? _nodeScriptViewModel;
|
||||
private RenderProfileElement? _profileElement;
|
||||
private ObservableAsPropertyHelper<ConditionTypeViewModel?>? _selectedConditionTypeViewModel;
|
||||
|
||||
public DisplayConditionScriptViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_windowService = windowService;
|
||||
|
||||
ConditionTypeViewModels = new ObservableCollection<ConditionTypeViewModel>
|
||||
{
|
||||
new("None", "The element is always active.", null),
|
||||
new("Regular", "The element is activated when the provided visual script ends in true.", typeof(StaticCondition)),
|
||||
new("Event", "The element is activated when the selected event fires.\r\n" +
|
||||
"Events that contain data can conditionally trigger the layer using a visual script.", typeof(EventCondition))
|
||||
};
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
|
||||
|
||||
_nodeScriptViewModel = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.DisplayCondition) ?? Observable.Never<ICondition?>())
|
||||
.Switch()
|
||||
.Select(c => c is StaticCondition staticCondition ? nodeVmFactory.NodeScriptViewModel(staticCondition.Script, true) : null)
|
||||
.Select(c => c is INodeScriptCondition {NodeScript: NodeScript nodeScript} ? nodeVmFactory.NodeScriptViewModel(nodeScript, true) : null)
|
||||
.ToProperty(this, vm => vm.NodeScriptViewModel)
|
||||
.DisposeWith(d);
|
||||
_selectedConditionTypeViewModel = profileEditorService.ProfileElement
|
||||
.Select(p => p?.WhenAnyValue(element => element.DisplayCondition) ?? Observable.Never<ICondition?>())
|
||||
.Switch()
|
||||
.Select(c => c != null ? ConditionTypeViewModels.FirstOrDefault(vm => vm.ConditionType == c.GetType()) : null)
|
||||
.ToProperty(this, vm => vm.SelectedConditionTypeViewModel)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
_showStaticOptions = this.WhenAnyValue(vm => vm.SelectedConditionTypeViewModel)
|
||||
.Select(c => c != null && c.ConditionType == typeof(StaticCondition))
|
||||
.ToProperty(this, vm => vm.ShowStaticOptions);
|
||||
_showEventOptions = this.WhenAnyValue(vm => vm.SelectedConditionTypeViewModel)
|
||||
.Select(c => c != null && c.ConditionType == typeof(EventCondition))
|
||||
.ToProperty(this, vm => vm.ShowEventOptions);
|
||||
}
|
||||
|
||||
public NodeScriptViewModel? NodeScriptViewModel => _nodeScriptViewModel?.Value;
|
||||
public ObservableCollection<ConditionTypeViewModel> ConditionTypeViewModels { get; }
|
||||
|
||||
public async Task EnableConditions()
|
||||
public ConditionTypeViewModel? SelectedConditionTypeViewModel
|
||||
{
|
||||
bool confirmed = await _windowService.ShowConfirmContentDialog(
|
||||
"Display conditions",
|
||||
"Do you want to enable display conditions for this element? \r\n" +
|
||||
"Using display conditions you can dynamically hide or show layers and folders depending on certain parameters."
|
||||
);
|
||||
get => _selectedConditionTypeViewModel?.Value;
|
||||
set
|
||||
{
|
||||
if (_profileElement == null)
|
||||
return;
|
||||
|
||||
if (confirmed && _profileElement != null)
|
||||
_profileEditorService.ExecuteCommand(new ChangeElementDisplayCondition(_profileElement, new StaticCondition(_profileElement)));
|
||||
ICondition? condition = null;
|
||||
if (value?.ConditionType == typeof(StaticCondition))
|
||||
condition = new StaticCondition(_profileElement);
|
||||
else if (value?.ConditionType == typeof(EventCondition))
|
||||
condition = new EventCondition(_profileElement);
|
||||
|
||||
_profileEditorService.ExecuteCommand(new ChangeConditionType(_profileElement, condition));
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowStaticOptions => _showStaticOptions.Value;
|
||||
public bool ShowEventOptions => _showEventOptions.Value;
|
||||
|
||||
|
||||
public async Task OpenEditor()
|
||||
{
|
||||
if (_profileElement?.DisplayCondition is StaticCondition staticCondition)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Reactive.Disposables;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||
using ReactiveUI;
|
||||
|
||||
+52
@@ -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>
|
||||
+18
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+65
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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>
|
||||
|
||||
+12
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
@@ -25,6 +25,7 @@ public class SelectionAddToolViewModel : ToolViewModel
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
_rgbService = rgbService;
|
||||
// Not disposed when deactivated but when really disposed
|
||||
_isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled);
|
||||
|
||||
this.WhenActivated(d => profileEditorService.ProfileElement.Subscribe(p => _layer = p as Layer).DisposeWith(d));
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ public class SelectionRemoveToolViewModel : ToolViewModel
|
||||
public SelectionRemoveToolViewModel(IProfileEditorService profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
// Not disposed when deactivated but when really disposed
|
||||
_isEnabled = profileEditorService.ProfileElement.Select(p => p is Layer).ToProperty(this, vm => vm.IsEnabled);
|
||||
this.WhenActivated(d => profileEditorService.ProfileElement.Subscribe(p => _layer = p as Layer).DisposeWith(d));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user