mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Profile editor - Ported most VMs of the properties tree and timeline
This commit is contained in:
parent
bf1aad1549
commit
c04bff1f48
@ -249,6 +249,7 @@ namespace Artemis.Core
|
||||
if (_keyframesEnabled == value) return;
|
||||
_keyframesEnabled = value;
|
||||
OnKeyframesToggled();
|
||||
OnPropertyChanged(nameof(KeyframesEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -115,8 +115,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
|
||||
public interface ITimelinePropertyViewModel : IScreen
|
||||
{
|
||||
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
|
||||
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
|
||||
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
|
||||
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
|
||||
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
|
||||
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ namespace Artemis.UI.Shared.Services.ProfileEditor
|
||||
IObservable<RenderProfileElement?> ProfileElement { get; }
|
||||
IObservable<ProfileEditorHistory?> History { get; }
|
||||
IObservable<TimeSpan> Time { get; }
|
||||
IObservable<double> PixelsPerSecond { get; }
|
||||
|
||||
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
|
||||
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement);
|
||||
|
||||
@ -15,6 +15,7 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
|
||||
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
|
||||
private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero);
|
||||
private readonly BehaviorSubject<double> _pixelsPerSecondSubject = new(300);
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IWindowService _windowService;
|
||||
|
||||
@ -22,9 +23,12 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
{
|
||||
_profileService = profileService;
|
||||
_windowService = windowService;
|
||||
|
||||
ProfileConfiguration = _profileConfigurationSubject.AsObservable();
|
||||
ProfileElement = _profileElementSubject.AsObservable();
|
||||
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
|
||||
Time = _timeSubject.AsObservable();
|
||||
PixelsPerSecond = _pixelsPerSecondSubject.AsObservable();
|
||||
}
|
||||
|
||||
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
|
||||
@ -43,6 +47,7 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
public IObservable<RenderProfileElement?> ProfileElement { get; }
|
||||
public IObservable<ProfileEditorHistory?> History { get; }
|
||||
public IObservable<TimeSpan> Time { get; }
|
||||
public IObservable<double> PixelsPerSecond { get; }
|
||||
|
||||
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||
{
|
||||
@ -59,6 +64,11 @@ internal class ProfileEditorService : IProfileEditorService
|
||||
_timeSubject.OnNext(time);
|
||||
}
|
||||
|
||||
public void ChangePixelsPerSecond(double pixelsPerSecond)
|
||||
{
|
||||
_pixelsPerSecondSubject.OnNext(pixelsPerSecond);
|
||||
}
|
||||
|
||||
public void ExecuteCommand(IProfileEditorCommand command)
|
||||
{
|
||||
try
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
public interface IPropertyInputService : IArtemisSharedUIService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a read-only collection of all registered property editors
|
||||
/// </summary>
|
||||
ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new property input view model used in the profile editor for the generic type defined in
|
||||
/// <see cref="PropertyInputViewModel{T}" />
|
||||
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new property input view model used in the profile editor for the generic type defined in
|
||||
/// <see cref="PropertyInputViewModel{T}" />
|
||||
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
|
||||
/// </summary>
|
||||
/// <param name="viewModelType"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the property input view model
|
||||
/// </summary>
|
||||
/// <param name="registration"></param>
|
||||
void RemovePropertyInput(PropertyInputRegistration registration);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there is a matching registration for the provided layer property
|
||||
/// </summary>
|
||||
/// <param name="layerProperty">The layer property to try to find a view model for</param>
|
||||
bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty);
|
||||
|
||||
/// <summary>
|
||||
/// If a matching registration is found, creates a new <see cref="PropertyInputViewModel{T}" /> supporting
|
||||
/// <typeparamref name="T" />
|
||||
/// </summary>
|
||||
PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty);
|
||||
}
|
||||
@ -2,99 +2,118 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Ninject;
|
||||
using Ninject.Parameters;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.PropertyInput
|
||||
namespace Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
internal class PropertyInputService : IPropertyInputService
|
||||
{
|
||||
internal class PropertyInputService : IPropertyInputService
|
||||
private readonly IKernel _kernel;
|
||||
private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
|
||||
|
||||
public PropertyInputService(IKernel kernel)
|
||||
{
|
||||
private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
|
||||
_kernel = kernel;
|
||||
_registeredPropertyEditors = new List<PropertyInputRegistration>();
|
||||
RegisteredPropertyEditors = new ReadOnlyCollection<PropertyInputRegistration>(_registeredPropertyEditors);
|
||||
}
|
||||
|
||||
public PropertyInputService()
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
|
||||
|
||||
public PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel
|
||||
{
|
||||
return RegisterPropertyInput(typeof(T), plugin);
|
||||
}
|
||||
|
||||
public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin)
|
||||
{
|
||||
if (!typeof(PropertyInputViewModel).IsAssignableFrom(viewModelType))
|
||||
throw new ArtemisSharedUIException($"Property input VM type must implement {nameof(PropertyInputViewModel)}");
|
||||
|
||||
lock (_registeredPropertyEditors)
|
||||
{
|
||||
_registeredPropertyEditors = new List<PropertyInputRegistration>();
|
||||
RegisteredPropertyEditors = new ReadOnlyCollection<PropertyInputRegistration>(_registeredPropertyEditors);
|
||||
}
|
||||
// Indirectly checked if there's a BaseType above
|
||||
Type supportedType = viewModelType.BaseType!.GetGenericArguments()[0];
|
||||
// If the supported type is a generic, assume there is a base type
|
||||
if (supportedType.IsGenericParameter)
|
||||
{
|
||||
if (supportedType.BaseType == null)
|
||||
throw new ArtemisSharedUIException("Generic property input VM type must have a type constraint");
|
||||
supportedType = supportedType.BaseType;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
|
||||
PropertyInputRegistration? existing = _registeredPropertyEditors.FirstOrDefault(r => r.SupportedType == supportedType);
|
||||
if (existing != null)
|
||||
{
|
||||
if (existing.Plugin != plugin)
|
||||
throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " +
|
||||
$"registered by {existing.Plugin}");
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemovePropertyInput(PropertyInputRegistration registration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
_kernel.Bind(viewModelType).ToSelf();
|
||||
PropertyInputRegistration registration = new(this, plugin, supportedType, viewModelType);
|
||||
_registeredPropertyEditors.Add(registration);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IPropertyInputService : IArtemisSharedUIService
|
||||
public void RemovePropertyInput(PropertyInputRegistration registration)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a read-only collection of all registered property editors
|
||||
/// </summary>
|
||||
ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors { get; }
|
||||
lock (_registeredPropertyEditors)
|
||||
{
|
||||
if (_registeredPropertyEditors.Contains(registration))
|
||||
{
|
||||
registration.Unsubscribe();
|
||||
_registeredPropertyEditors.Remove(registration);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new property input view model used in the profile editor for the generic type defined in
|
||||
/// <see cref="PropertyInputViewModel{T}" />
|
||||
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
PropertyInputRegistration RegisterPropertyInput<T>(Plugin plugin) where T : PropertyInputViewModel;
|
||||
_kernel.Unbind(registration.ViewModelType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new property input view model used in the profile editor for the generic type defined in
|
||||
/// <see cref="PropertyInputViewModel{T}" />
|
||||
/// <para>Note: DataBindingProperty will remove itself on plugin disable so you don't have to</para>
|
||||
/// </summary>
|
||||
/// <param name="viewModelType"></param>
|
||||
/// <param name="plugin"></param>
|
||||
/// <returns></returns>
|
||||
PropertyInputRegistration RegisterPropertyInput(Type viewModelType, Plugin plugin);
|
||||
public bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty)
|
||||
{
|
||||
PropertyInputRegistration? registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == layerProperty.PropertyType);
|
||||
if (registration == null && layerProperty.PropertyType.IsEnum)
|
||||
registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum));
|
||||
|
||||
/// <summary>
|
||||
/// Removes the property input view model
|
||||
/// </summary>
|
||||
/// <param name="registration"></param>
|
||||
void RemovePropertyInput(PropertyInputRegistration registration);
|
||||
return registration != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there is a matching registration for the provided layer property
|
||||
/// </summary>
|
||||
/// <param name="layerProperty">The layer property to try to find a view model for</param>
|
||||
bool CanCreatePropertyInputViewModel(ILayerProperty layerProperty);
|
||||
public PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty)
|
||||
{
|
||||
Type? viewModelType = null;
|
||||
PropertyInputRegistration? registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(T));
|
||||
|
||||
/// <summary>
|
||||
/// If a matching registration is found, creates a new <see cref="PropertyInputViewModel{T}" /> supporting
|
||||
/// <typeparamref name="T" />
|
||||
/// </summary>
|
||||
PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty);
|
||||
// Check for enums if no supported type was found
|
||||
if (registration == null && typeof(T).IsEnum)
|
||||
{
|
||||
// The enum VM will likely be a generic, that requires creating a generic type matching the layer property
|
||||
registration = RegisteredPropertyEditors.FirstOrDefault(r => r.SupportedType == typeof(Enum));
|
||||
if (registration != null && registration.ViewModelType.IsGenericType)
|
||||
viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments);
|
||||
}
|
||||
else if (registration != null)
|
||||
{
|
||||
viewModelType = registration.ViewModelType;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewModelType == null)
|
||||
return null;
|
||||
|
||||
ConstructorArgument parameter = new("layerProperty", layerProperty);
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
// When you've just spent the last 2 hours trying to figure out a deadlock and reach this line, I'm so, so sorry. I thought this would be fine.
|
||||
IKernel kernel = registration?.Plugin.Kernel ?? _kernel;
|
||||
return (PropertyInputViewModel<T>) kernel.Get(viewModelType, parameter);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ using Artemis.UI.Screens.Device;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Screens.ProfileEditor;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
|
||||
using Artemis.UI.Screens.Settings;
|
||||
@ -75,4 +76,10 @@ namespace Artemis.UI.Ninject.Factories
|
||||
// TimelineViewModel TimelineViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
// TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
|
||||
}
|
||||
|
||||
public interface IPropertyVmFactory
|
||||
{
|
||||
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel);
|
||||
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Ninject.Extensions.Factory;
|
||||
|
||||
namespace Artemis.UI.Ninject.InstanceProviders
|
||||
{
|
||||
public class LayerPropertyViewModelInstanceProvider : StandardInstanceProvider
|
||||
{
|
||||
protected override Type GetType(MethodInfo methodInfo, object[] arguments)
|
||||
{
|
||||
if (methodInfo.ReturnType != typeof(ITreePropertyViewModel) && methodInfo.ReturnType != typeof(ITimelinePropertyViewModel))
|
||||
return base.GetType(methodInfo, arguments);
|
||||
|
||||
// Find LayerProperty type
|
||||
Type? layerPropertyType = arguments[0].GetType();
|
||||
while (layerPropertyType != null && (!layerPropertyType.IsGenericType || layerPropertyType.GetGenericTypeDefinition() != typeof(LayerProperty<>)))
|
||||
layerPropertyType = layerPropertyType.BaseType;
|
||||
if (layerPropertyType == null)
|
||||
return base.GetType(methodInfo, arguments);
|
||||
|
||||
if (methodInfo.ReturnType == typeof(ITreePropertyViewModel))
|
||||
return typeof(TreePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments());
|
||||
if (methodInfo.ReturnType == typeof(ITimelinePropertyViewModel))
|
||||
return typeof(TimelinePropertyViewModel<>).MakeGenericType(layerPropertyType.GetGenericArguments());
|
||||
|
||||
return base.GetType(methodInfo, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Ninject.InstanceProviders;
|
||||
using Artemis.UI.Screens;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Shared.PlatformSupport;
|
||||
using Ninject.Extensions.Conventions;
|
||||
using Ninject.Extensions.Factory;
|
||||
using Ninject.Modules;
|
||||
using Ninject.Planning.Bindings.Resolvers;
|
||||
|
||||
@ -46,6 +48,8 @@ namespace Artemis.UI.Ninject
|
||||
.BindToFactory();
|
||||
});
|
||||
|
||||
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
|
||||
|
||||
// Bind all UI services as singletons
|
||||
Kernel.Bind(x =>
|
||||
{
|
||||
|
||||
@ -1,28 +1,63 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.PropertyInput;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
|
||||
public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private readonly IPropertyInputService _propertyInputService;
|
||||
private bool _isVisible;
|
||||
private bool _isExpanded;
|
||||
private bool _hasChildren;
|
||||
|
||||
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService)
|
||||
{
|
||||
Children = new ObservableCollection<ActivatableViewModelBase>();
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
_propertyInputService = propertyInputService;
|
||||
Children = new ObservableCollection<ViewModelBase>();
|
||||
LayerPropertyGroup = layerPropertyGroup;
|
||||
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
|
||||
|
||||
IsVisible = !LayerPropertyGroup.IsHidden;
|
||||
// TODO: Update visiblity on change, can't do it atm because not sure how to unsubscribe from the event
|
||||
PopulateChildren();
|
||||
}
|
||||
|
||||
public ObservableCollection<ActivatableViewModelBase> Children { get; }
|
||||
private void PopulateChildren()
|
||||
{
|
||||
// Get all properties and property groups and create VMs for them
|
||||
// The group has methods for getting this without reflection but then we lose the order of the properties as they are defined on the group
|
||||
foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties())
|
||||
{
|
||||
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
|
||||
continue;
|
||||
|
||||
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
|
||||
{
|
||||
ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup);
|
||||
// Ensure a supported input VM was found, otherwise don't add it
|
||||
if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value))
|
||||
Children.Add(_layerPropertyVmFactory.ProfileElementPropertyViewModel(value));
|
||||
}
|
||||
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
|
||||
{
|
||||
LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup);
|
||||
if (value != null)
|
||||
Children.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(value));
|
||||
}
|
||||
}
|
||||
|
||||
HasChildren = Children.Any(i => i is ProfileElementPropertyViewModel {IsVisible: true} || i is ProfileElementPropertyGroupViewModel {IsVisible: true});
|
||||
}
|
||||
|
||||
public ObservableCollection<ViewModelBase> Children { get; }
|
||||
public LayerPropertyGroup LayerPropertyGroup { get; }
|
||||
public TreeGroupViewModel TreeGroupViewModel { get; }
|
||||
|
||||
@ -37,4 +72,10 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
|
||||
get => _isExpanded;
|
||||
set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
public bool HasChildren
|
||||
{
|
||||
get => _hasChildren;
|
||||
set => this.RaiseAndSetIfChanged(ref _hasChildren, value);
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,44 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
using Artemis.UI.Shared;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
|
||||
|
||||
public class ProfileElementPropertyViewModel
|
||||
public class ProfileElementPropertyViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private bool _isExpanded;
|
||||
private bool _isHighlighted;
|
||||
private bool _isVisible;
|
||||
|
||||
public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory)
|
||||
public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
|
||||
{
|
||||
LayerProperty = layerProperty;
|
||||
_profileEditorService = profileEditorService;
|
||||
_layerPropertyVmFactory = layerPropertyVmFactory;
|
||||
TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);
|
||||
TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this);
|
||||
}
|
||||
|
||||
public ILayerProperty LayerProperty { get; }
|
||||
public ITreePropertyViewModel TreePropertyViewModel { get; }
|
||||
public ITimelinePropertyViewModel TimelinePropertyViewModel { get; }
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
|
||||
}
|
||||
|
||||
public bool IsHighlighted
|
||||
{
|
||||
get => _isHighlighted;
|
||||
set => this.RaiseAndSetIfChanged(ref _isHighlighted, value);
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
|
||||
public interface ITimelineKeyframeViewModel
|
||||
{
|
||||
bool IsSelected { get; set; }
|
||||
TimeSpan Position { get; }
|
||||
ILayerPropertyKeyframe Keyframe { get; }
|
||||
|
||||
#region Movement
|
||||
|
||||
void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source);
|
||||
void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source);
|
||||
void UpdatePosition(TimeSpan position);
|
||||
void ReleaseMovement();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
void PopulateEasingViewModels();
|
||||
void ClearEasingViewModels();
|
||||
void Delete(bool save = true);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
|
||||
public interface ITimelinePropertyViewModel : IReactiveObject
|
||||
{
|
||||
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
|
||||
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
|
||||
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Avalonia;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
|
||||
|
||||
public class TimelineEasingViewModel : ViewModelBase
|
||||
{
|
||||
private bool _isEasingModeSelected;
|
||||
|
||||
public TimelineEasingViewModel(Easings.Functions easingFunction, bool isSelected)
|
||||
{
|
||||
_isEasingModeSelected = isSelected;
|
||||
|
||||
EasingFunction = easingFunction;
|
||||
Description = easingFunction.Humanize();
|
||||
|
||||
EasingPoints = new List<Point>();
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
int x = i;
|
||||
double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
|
||||
EasingPoints.Add(new Point(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
public Easings.Functions EasingFunction { get; }
|
||||
public List<Point> EasingPoints { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public bool IsEasingModeSelected
|
||||
{
|
||||
get => _isEasingModeSelected;
|
||||
set
|
||||
{
|
||||
_isEasingModeSelected = value;
|
||||
if (value) OnEasingModeSelected();
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler EasingModeSelected;
|
||||
|
||||
protected virtual void OnEasingModeSelected()
|
||||
{
|
||||
EasingModeSelected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
using Disposable = System.Reactive.Disposables.Disposable;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
|
||||
{
|
||||
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
|
||||
{
|
||||
|
||||
private bool _isSelected;
|
||||
private string _timestamp;
|
||||
private double _x;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyKeyframe = layerPropertyKeyframe;
|
||||
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_profileEditorService.PixelsPerSecond.Subscribe(p =>
|
||||
{
|
||||
_pixelsPerSecond = p;
|
||||
_profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
|
||||
Disposable.Create(() =>
|
||||
{
|
||||
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
|
||||
timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected;
|
||||
}).DisposeWith(d);
|
||||
}).DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
|
||||
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
|
||||
|
||||
public double X
|
||||
{
|
||||
get => _x;
|
||||
set => this.RaiseAndSetIfChanged(ref _x, value);
|
||||
}
|
||||
|
||||
public string Timestamp
|
||||
{
|
||||
get => _timestamp;
|
||||
set => this.RaiseAndSetIfChanged(ref _timestamp, value);
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public TimeSpan Position => LayerPropertyKeyframe.Position;
|
||||
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
|
||||
|
||||
public void Update()
|
||||
{
|
||||
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
|
||||
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
|
||||
}
|
||||
|
||||
|
||||
#region Movement
|
||||
|
||||
private TimeSpan? _offset;
|
||||
private double _pixelsPerSecond;
|
||||
|
||||
public void ReleaseMovement()
|
||||
{
|
||||
_offset = null;
|
||||
}
|
||||
|
||||
public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source)
|
||||
{
|
||||
if (source == this)
|
||||
{
|
||||
_offset = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_offset != null)
|
||||
return;
|
||||
|
||||
_offset = LayerPropertyKeyframe.Position - source.Position;
|
||||
}
|
||||
|
||||
public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source)
|
||||
{
|
||||
if (source == this || _offset == null)
|
||||
return;
|
||||
|
||||
UpdatePosition(source.Position + _offset.Value);
|
||||
}
|
||||
|
||||
public void UpdatePosition(TimeSpan position)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
// if (position < TimeSpan.Zero)
|
||||
// LayerPropertyKeyframe.Position = TimeSpan.Zero;
|
||||
// else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
|
||||
// LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
|
||||
// else
|
||||
// LayerPropertyKeyframe.Position = position;
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Easing
|
||||
|
||||
public void PopulateEasingViewModels()
|
||||
{
|
||||
if (EasingViewModels.Any())
|
||||
return;
|
||||
|
||||
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions))
|
||||
.Cast<Easings.Functions>()
|
||||
.Select(e => new TimelineEasingViewModel(e, e == LayerPropertyKeyframe.EasingFunction)));
|
||||
|
||||
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
|
||||
timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected;
|
||||
}
|
||||
|
||||
public void ClearEasingViewModels()
|
||||
{
|
||||
EasingViewModels.Clear();
|
||||
}
|
||||
|
||||
private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is TimelineEasingViewModel timelineEasingViewModel)
|
||||
SelectEasingMode(timelineEasingViewModel);
|
||||
}
|
||||
|
||||
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
|
||||
// Set every selection to false except on the VM that made the change
|
||||
foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
|
||||
propertyTrackEasingViewModel.IsEasingModeSelected = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public void Delete(bool save = true)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
|
||||
{
|
||||
public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelinePropertyViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
public LayerProperty<T> LayerProperty { get; }
|
||||
public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
|
||||
public ObservableCollection<TimelineKeyframeViewModel<T>> KeyframeViewModels { get; }
|
||||
|
||||
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerProperty = layerProperty;
|
||||
ProfileElementPropertyViewModel = profileElementPropertyViewModel;
|
||||
KeyframeViewModels = new ObservableCollection<TimelineKeyframeViewModel<T>>();
|
||||
}
|
||||
|
||||
#region Implementation of ITimelinePropertyViewModel
|
||||
|
||||
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels()
|
||||
{
|
||||
return KeyframeViewModels.Cast<ITimelineKeyframeViewModel>().ToList();
|
||||
}
|
||||
|
||||
public void WipeKeyframes(TimeSpan? start, TimeSpan? end)
|
||||
{
|
||||
start ??= TimeSpan.Zero;
|
||||
end ??= TimeSpan.MaxValue;
|
||||
|
||||
|
||||
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList();
|
||||
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
|
||||
LayerProperty.RemoveKeyframe(keyframe);
|
||||
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
public void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount)
|
||||
{
|
||||
start ??= TimeSpan.Zero;
|
||||
end ??= TimeSpan.MaxValue;
|
||||
|
||||
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList();
|
||||
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
|
||||
keyframe.Position += amount;
|
||||
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void UpdateKeyframes()
|
||||
{
|
||||
// Only show keyframes if they are enabled
|
||||
if (LayerProperty.KeyframesEnabled)
|
||||
{
|
||||
List<LayerPropertyKeyframe<T>> keyframes = LayerProperty.Keyframes.ToList();
|
||||
|
||||
List<TimelineKeyframeViewModel<T>> toRemove = KeyframeViewModels.Where(t => !keyframes.Contains(t.LayerPropertyKeyframe)).ToList();
|
||||
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toRemove)
|
||||
KeyframeViewModels.Remove(timelineKeyframeViewModel);
|
||||
List<TimelineKeyframeViewModel<T>> toAdd = keyframes.Where(k => KeyframeViewModels.All(t => t.LayerPropertyKeyframe != k)).Select(k => new TimelineKeyframeViewModel<T>(k, _profileEditorService)).ToList();
|
||||
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toAdd)
|
||||
KeyframeViewModels.Add(timelineKeyframeViewModel);
|
||||
}
|
||||
else
|
||||
KeyframeViewModels.Clear();
|
||||
|
||||
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in KeyframeViewModels)
|
||||
timelineKeyframeViewModel.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
|
||||
public interface ITreePropertyViewModel : IReactiveObject
|
||||
{
|
||||
bool HasDataBinding { get; }
|
||||
double GetDepth();
|
||||
}
|
||||
@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
@ -24,8 +22,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
|
||||
public class TreeGroupViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IWindowService _windowService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly IWindowService _windowService;
|
||||
private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel;
|
||||
private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel;
|
||||
|
||||
@ -41,12 +39,14 @@ public class TreeGroupViewModel : ActivatableViewModelBase
|
||||
ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
|
||||
Disposable.Create(CloseViewModels).DisposeWith(d);
|
||||
});
|
||||
|
||||
// TODO: Update ProfileElementPropertyGroupViewModel visibility on change (can remove the sub on line 41 as well then)
|
||||
}
|
||||
|
||||
|
||||
public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; }
|
||||
public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup;
|
||||
public ObservableCollection<ActivatableViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null;
|
||||
public ObservableCollection<ViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null;
|
||||
|
||||
public LayerPropertyGroupType GroupType { get; private set; }
|
||||
|
||||
@ -96,7 +96,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase
|
||||
// Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure
|
||||
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
|
||||
ConstructorArgument argument = new(effectParameter.Name!, layerEffect);
|
||||
EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel)layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
|
||||
EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
|
||||
|
||||
_effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel);
|
||||
await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel);
|
||||
@ -138,7 +138,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase
|
||||
_effectConfigurationWindowViewModel?.Close(null);
|
||||
_brushConfigurationWindowViewModel?.Close(null);
|
||||
}
|
||||
|
||||
|
||||
private void DetermineGroupType()
|
||||
{
|
||||
if (LayerPropertyGroup is LayerGeneralProperties)
|
||||
|
||||
@ -4,16 +4,32 @@ using Artemis.UI.Shared.Services.PropertyInput;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
|
||||
|
||||
internal class TreePropertyViewModel<T> : ActivatableViewModelBase
|
||||
internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropertyViewModel
|
||||
{
|
||||
public TreePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel layerPropertyViewModel, IPropertyInputService propertyInputService)
|
||||
public TreePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IPropertyInputService propertyInputService)
|
||||
{
|
||||
LayerProperty = layerProperty;
|
||||
LayerPropertyViewModel = layerPropertyViewModel;
|
||||
ProfileElementPropertyViewModel = profileElementPropertyViewModel;
|
||||
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
|
||||
|
||||
// TODO: Update ProfileElementPropertyViewModel visibility on change
|
||||
}
|
||||
|
||||
public LayerProperty<T> LayerProperty { get; }
|
||||
public ProfileElementPropertyViewModel LayerPropertyViewModel { get; }
|
||||
public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
|
||||
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
|
||||
public bool HasDataBinding => LayerProperty.HasDataBinding;
|
||||
|
||||
public double GetDepth()
|
||||
{
|
||||
int depth = 0;
|
||||
LayerPropertyGroup? current = LayerProperty.LayerPropertyGroup;
|
||||
while (current != null)
|
||||
{
|
||||
depth++;
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user