diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 9f26d5d28..acdf15530 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -103,6 +103,7 @@ namespace Artemis.Core { _disposed = true; + Registration.DataBinding = null; DataBindingMode?.Dispose(); } diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index cb495ca70..30062e553 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -57,6 +57,20 @@ namespace Artemis.Core } internal FolderEntity FolderEntity { get; set; } + + /// + public override List GetAllLayerProperties() + { + var result = new List(); + foreach (var layerEffect in LayerEffects) + { + if (layerEffect.BaseProperties != null) + result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); + } + + return result; + } + internal override RenderElementEntity RenderElementEntity => FolderEntity; public bool IsRootFolder => Parent == Profile; @@ -88,6 +102,16 @@ namespace Artemis.Core } } + protected internal override void UpdateTimelineLength() + { + TimelineLength = !Children.Any() ? TimeSpan.Zero : Children.OfType().Max(c => c.TimelineLength); + if (StartSegmentLength + MainSegmentLength + EndSegmentLength > TimelineLength) + TimelineLength = StartSegmentLength + MainSegmentLength + EndSegmentLength; + + if (Parent is RenderProfileElement parent) + parent.UpdateTimelineLength(); + } + public override void OverrideProgress(TimeSpan timeOverride, bool stickToMainSegment) { if (_disposed) @@ -201,6 +225,7 @@ namespace Artemis.Core throw new ObjectDisposedException("Folder"); base.AddChild(child, order); + UpdateTimelineLength(); CalculateRenderProperties(); } @@ -211,6 +236,7 @@ namespace Artemis.Core throw new ObjectDisposedException("Folder"); base.RemoveChild(child); + UpdateTimelineLength(); CalculateRenderProperties(); } diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index f1fa2cc3a..914e9de02 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -69,6 +69,24 @@ namespace Artemis.Core } internal LayerEntity LayerEntity { get; set; } + + /// + public override List GetAllLayerProperties() + { + var result = new List(); + result.AddRange(General.GetAllLayerProperties()); + result.AddRange(Transform.GetAllLayerProperties()); + if (LayerBrush?.BaseProperties != null) + result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); + foreach (var layerEffect in LayerEffects) + { + if (layerEffect.BaseProperties != null) + result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); + } + + return result; + } + internal override RenderElementEntity RenderElementEntity => LayerEntity; /// @@ -269,6 +287,11 @@ namespace Artemis.Core } } + protected internal override void UpdateTimelineLength() + { + TimelineLength = StartSegmentLength + MainSegmentLength + EndSegmentLength; + } + public override void OverrideProgress(TimeSpan timeOverride, bool stickToMainSegment) { if (_disposed) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 685972d67..aaa36796b 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -18,6 +18,11 @@ namespace Artemis.Core /// public PropertyDescriptionAttribute PropertyDescription { get; } + /// + /// Gets the unique path of the property on the layer + /// + public string Path { get; } + /// /// Initializes the layer property /// @@ -25,7 +30,7 @@ namespace Artemis.Core /// /// /// - void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup @group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); /// /// Returns a list off all data binding registrations diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index f6a7c03f1..b2403dc20 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -30,6 +30,9 @@ namespace Artemis.Core /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } + /// + public string Path { get; private set; } + /// /// Updates the property, applying keyframes and data bindings to the current value /// @@ -384,6 +387,8 @@ namespace Artemis.Core if (dataBindingRegistration.LayerProperty != this) throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); + if (dataBindingRegistration.DataBinding != null) + throw new ArtemisCoreException("Provided data binding registration already has an enabled data binding"); var dataBinding = new DataBinding(dataBindingRegistration); _dataBindings.Add(dataBinding); @@ -431,7 +436,7 @@ namespace Artemis.Core internal PropertyEntity Entity { get; set; } /// - public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) + public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); @@ -440,6 +445,7 @@ namespace Artemis.Core ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); + Path = path; Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index b3cc9ac8e..4300b13a9 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -207,21 +207,21 @@ namespace Artemis.Core private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { - var path = Path + "."; + var path = $"{Path}.{propertyInfo.Name}"; if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path + propertyInfo.Name}"); + throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path}"); var instance = (ILayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); if (instance == null) - throw new ArtemisPluginException($"Failed to create instance of layer property at {path + propertyInfo.Name}"); + throw new ArtemisPluginException($"Failed to create instance of layer property at {path}"); // Ensure the description has a name, if not this is a good point to set it based on the property info if (string.IsNullOrWhiteSpace(propertyDescription.Name)) propertyDescription.Name = propertyInfo.Name.Humanize(); - var entity = GetPropertyEntity(ProfileElement, path + propertyInfo.Name, out var fromStorage); - instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription); + var entity = GetPropertyEntity(ProfileElement, path, out var fromStorage); + instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription, path); propertyInfo.SetValue(this, instance); _layerProperties.Add(instance); } diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index e225f6b57..674126fcd 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -19,6 +19,23 @@ namespace Artemis.Core LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; } + public abstract List GetAllLayerProperties(); + + #region IDisposable + + protected override void Dispose(bool disposing) + { + LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded; + LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved; + + foreach (var baseLayerEffect in LayerEffects) + baseLayerEffect.Dispose(); + + base.Dispose(disposing); + } + + #endregion + internal void ApplyRenderElementDefaults() { MainSegmentLength = TimeSpan.FromSeconds(5); @@ -135,7 +152,13 @@ namespace Artemis.Core public TimeSpan StartSegmentLength { get => _startSegmentLength; - set => SetAndNotify(ref _startSegmentLength, value); + set + { + if (!SetAndNotify(ref _startSegmentLength, value)) return; + UpdateTimelineLength(); + if (Parent is RenderProfileElement renderElement) + renderElement.UpdateTimelineLength(); + } } /// @@ -144,7 +167,13 @@ namespace Artemis.Core public TimeSpan MainSegmentLength { get => _mainSegmentLength; - set => SetAndNotify(ref _mainSegmentLength, value); + set + { + if (!SetAndNotify(ref _mainSegmentLength, value)) return; + UpdateTimelineLength(); + if (Parent is RenderProfileElement renderElement) + renderElement.UpdateTimelineLength(); + } } /// @@ -153,7 +182,13 @@ namespace Artemis.Core public TimeSpan EndSegmentLength { get => _endSegmentLength; - set => SetAndNotify(ref _endSegmentLength, value); + set + { + if (!SetAndNotify(ref _endSegmentLength, value)) return; + UpdateTimelineLength(); + if (Parent is RenderProfileElement renderElement) + renderElement.UpdateTimelineLength(); + } } /// @@ -165,11 +200,6 @@ namespace Artemis.Core protected set => SetAndNotify(ref _timelinePosition, value); } - /// - /// Gets the total combined length of all three segments - /// - public TimeSpan TimelineLength => StartSegmentLength + MainSegmentLength + EndSegmentLength; - /// /// Gets or sets whether main timeline should repeat itself as long as display conditions are met /// @@ -188,6 +218,11 @@ namespace Artemis.Core set => SetAndNotify(ref _alwaysFinishTimeline, value); } + /// + /// Gets the max length of this element and any of its children + /// + public TimeSpan TimelineLength { get; protected set; } + protected double UpdateTimeline(double deltaTime) { var oldPosition = _timelinePosition; @@ -195,7 +230,6 @@ namespace Artemis.Core var mainSegmentEnd = StartSegmentLength + MainSegmentLength; TimelinePosition += deltaTimeSpan; - // Manage segments while the condition is met if (DisplayConditionMet) { @@ -213,6 +247,8 @@ namespace Artemis.Core return (TimelinePosition - oldPosition).TotalSeconds; } + protected internal abstract void UpdateTimelineLength(); + /// /// Overrides the progress of the element /// @@ -380,21 +416,6 @@ namespace Artemis.Core #endregion - #region IDisposable - - protected override void Dispose(bool disposing) - { - LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded; - LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved; - - foreach (var baseLayerEffect in LayerEffects) - baseLayerEffect.Dispose(); - - base.Dispose(disposing); - } - - #endregion - #region Events public event EventHandler LayerEffectsUpdated; diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 4f4a56b56..5c1368815 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml index 640f202f0..20c03d4e3 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml @@ -40,10 +40,9 @@ - + @@ -101,13 +100,19 @@ - + diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs index 666091716..c2ebd7b07 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; @@ -134,10 +135,38 @@ namespace Artemis.UI.Shared PopupOpen = !PopupOpen; e.Handled = true; } - + private void ColorGradient_OnMouseUp(object sender, MouseButtonEventArgs e) { e.Handled = true; } + + private void Slider_OnMouseDown(object sender, MouseButtonEventArgs e) + { + OnDragStarted(); + } + + private void Slider_OnMouseUp(object sender, MouseButtonEventArgs e) + { + OnDragEnded(); + } + + #region Events + + public event EventHandler DragStarted; + public event EventHandler DragEnded; + + protected virtual void OnDragStarted() + { + DragStarted?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnDragEnded() + { + DragEnded?.Invoke(this, EventArgs.Empty); + } + + #endregion + } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 5eafbff57..674e98223 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -154,23 +154,7 @@ namespace Artemis.UI.Shared.Services if (!undid) return false; - if (SelectedProfileElement is Folder folder) - SelectedProfileElement = SelectedProfile.GetAllFolders().FirstOrDefault(f => f.EntityId == folder.EntityId); - else if (SelectedProfileElement is Layer layer) - SelectedProfileElement = SelectedProfile.GetAllLayers().FirstOrDefault(l => l.EntityId == layer.EntityId); - - OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile)); - OnSelectedProfileElementChanged(new RenderProfileElementEventArgs(SelectedProfileElement)); - - if (SelectedProfileElement != null) - { - var elements = SelectedProfile.GetAllLayers().Cast().ToList(); - elements.AddRange(SelectedProfile.GetAllFolders()); - var element = elements.FirstOrDefault(l => l.EntityId == SelectedProfileElement.EntityId); - ChangeSelectedProfileElement(element); - } - - UpdateProfilePreview(); + ReloadProfile(); return true; } @@ -180,17 +164,7 @@ namespace Artemis.UI.Shared.Services if (!redid) return false; - OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile)); - - if (SelectedProfileElement != null) - { - var elements = SelectedProfile.GetAllLayers().Cast().ToList(); - elements.AddRange(SelectedProfile.GetAllFolders()); - var element = elements.FirstOrDefault(l => l.EntityId == SelectedProfileElement.EntityId); - ChangeSelectedProfileElement(element); - } - - UpdateProfilePreview(); + ReloadProfile(); return true; } @@ -307,6 +281,27 @@ namespace Artemis.UI.Shared.Services return SelectedProfile?.Module; } + private void ReloadProfile() + { + // Trigger a profile change + OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile)); + // Trigger a selected element change + var previousSelectedProfileElement = SelectedProfileElement; + if (SelectedProfileElement is Folder folder) + SelectedProfileElement = SelectedProfile.GetAllFolders().FirstOrDefault(f => f.EntityId == folder.EntityId); + else if (SelectedProfileElement is Layer layer) + SelectedProfileElement = SelectedProfile.GetAllLayers().FirstOrDefault(l => l.EntityId == layer.EntityId); + OnSelectedProfileElementChanged(new RenderProfileElementEventArgs(SelectedProfileElement, previousSelectedProfileElement)); + // Trigger selected data binding change + if (SelectedDataBinding != null) + { + SelectedDataBinding = SelectedProfileElement?.GetAllLayerProperties()?.FirstOrDefault(p => p.Path == SelectedDataBinding.Path); + OnSelectedDataBindingChanged(); + } + + UpdateProfilePreview(); + } + public event EventHandler ProfileSelected; public event EventHandler SelectedProfileUpdated; public event EventHandler ProfileElementSelected; diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index b39b8eb18..e9070cacf 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -132,11 +132,11 @@ - + - + diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml index 90f94b311..46c9ade61 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/SKColorPropertyInputView.xaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:artemis="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:propertyInput="clr-namespace:Artemis.UI.PropertyInput" + xmlns:s="https://github.com/canton7/Stylet" mc:Ignorable="d" d:DesignHeight="25" d:DesignWidth="800" d:DataContext="{d:DesignInstance propertyInput:SKColorPropertyInputViewModel}"> @@ -17,7 +18,9 @@ Margin="0 -2 0 3" Padding="0 -1" Color="{Binding InputValue, Converter={StaticResource SKColorToColorConverter}}" - IsEnabled="{Binding IsEnabled}"/> + IsEnabled="{Binding IsEnabled}" + DragStarted="{s:Action InputDragStarted}" + DragEnded="{s:Action InputDragEnded}"/> \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs index beccd44bd..72fbefd50 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DataBindingViewModel.cs @@ -20,8 +20,6 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private DataBindingModeType _selectedDataBindingMode; private TimelineEasingViewModel _selectedEasingViewModel; - private TProperty _testInputValue; - private TProperty _testResultValue; private bool _updating; private bool _isDataBindingEnabled; @@ -45,8 +43,6 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings TestInputValue = _dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), true); TestResultValue = _dataModelUIService.GetDataModelDisplayViewModel(typeof(TProperty), true); - DataBinding = Registration.DataBinding; - Initialize(); } @@ -103,12 +99,6 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings } } - public DataBinding DataBinding - { - get => _dataBinding; - set => SetAndNotify(ref _dataBinding, value); - } - public void Dispose() { _profileEditorService.ProfilePreviewUpdated -= ProfileEditorServiceOnProfilePreviewUpdated; @@ -125,13 +115,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void CreateDataBindingModeModeViewModel() { - if (DataBinding?.DataBindingMode == null) + if (Registration.DataBinding?.DataBindingMode == null) { ActiveItem = null; return; } - switch (DataBinding.DataBindingMode) + switch (Registration.DataBinding.DataBindingMode) { case DirectDataBinding directDataBinding: ActiveItem = _dataBindingsVmFactory.DirectDataBindingModeViewModel(directDataBinding); @@ -147,7 +137,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (_updating) return; - if (DataBinding == null) + if (Registration.DataBinding == null) { IsEasingTimeEnabled = false; return; @@ -156,10 +146,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings _updating = true; IsDataBindingEnabled = ActiveItem != null; - EasingTime = (int) DataBinding.EasingTime.TotalMilliseconds; - SelectedEasingViewModel = EasingViewModels.First(vm => vm.EasingFunction == DataBinding.EasingFunction); + EasingTime = (int) Registration.DataBinding.EasingTime.TotalMilliseconds; + SelectedEasingViewModel = EasingViewModels.First(vm => vm.EasingFunction == Registration.DataBinding.EasingFunction); IsEasingTimeEnabled = EasingTime > 0; - switch (DataBinding.DataBindingMode) + switch (Registration.DataBinding.DataBindingMode) { case DirectDataBinding _: SelectedDataBindingMode = DataBindingModeType.Direct; @@ -182,10 +172,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (_updating) return; - if (DataBinding != null) + if (Registration.DataBinding != null) { - DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); - DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; + Registration.DataBinding.EasingTime = TimeSpan.FromMilliseconds(EasingTime); + Registration.DataBinding.EasingFunction = SelectedEasingViewModel?.EasingFunction ?? Easings.Functions.Linear; } _profileEditorService.UpdateSelectedProfileElement(); @@ -197,17 +187,17 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings if (_updating) return; - if (DataBinding != null && SelectedDataBindingMode == DataBindingModeType.None) + if (Registration.DataBinding != null && SelectedDataBindingMode == DataBindingModeType.None) { RemoveDataBinding(); CreateDataBindingModeModeViewModel(); return; } - if (DataBinding == null && SelectedDataBindingMode != DataBindingModeType.None) + if (Registration.DataBinding == null && SelectedDataBindingMode != DataBindingModeType.None) EnableDataBinding(); - DataBinding.ChangeDataBindingMode(SelectedDataBindingMode); + Registration.DataBinding.ChangeDataBindingMode(SelectedDataBindingMode); CreateDataBindingModeModeViewModel(); _profileEditorService.UpdateSelectedProfileElement(); @@ -215,7 +205,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings private void UpdateTestResult() { - if (DataBinding == null) + if (Registration.DataBinding == null) { TestInputValue.UpdateValue(default); TestResultValue.UpdateValue(default); @@ -225,26 +215,24 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings var currentValue = Registration.Converter.ConvertFromObject(ActiveItem?.GetTestValue() ?? default(TProperty)); TestInputValue.UpdateValue(currentValue); - TestResultValue.UpdateValue(DataBinding != null ? DataBinding.GetValue(currentValue) : default); + TestResultValue.UpdateValue(Registration.DataBinding != null ? Registration.DataBinding.GetValue(currentValue) : default); } private void EnableDataBinding() { - if (DataBinding != null) + if (Registration.DataBinding != null) return; - DataBinding = Registration.LayerProperty.EnableDataBinding(Registration); + Registration.LayerProperty.EnableDataBinding(Registration); _profileEditorService.UpdateSelectedProfileElement(); } private void RemoveDataBinding() { - if (DataBinding == null) + if (Registration.DataBinding == null) return; - var toDisable = DataBinding; - DataBinding = null; - Registration.LayerProperty.DisableDataBinding(toDisable); + Registration.LayerProperty.DisableDataBinding(Registration.DataBinding); Update(); _profileEditorService.UpdateSelectedProfileElement(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Dialogs/TimelineSegmentDialogView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Dialogs/TimelineSegmentDialogView.xaml new file mode 100644 index 000000000..4a4c6da38 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Dialogs/TimelineSegmentDialogView.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Dialogs/TimelineSegmentDialogViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Dialogs/TimelineSegmentDialogViewModel.cs new file mode 100644 index 000000000..91ee090ee --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Dialogs/TimelineSegmentDialogViewModel.cs @@ -0,0 +1,98 @@ +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Artemis.UI.Shared.Services; +using Castle.Core.Internal; +using FluentValidation; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs +{ + public class TimelineSegmentDialogViewModel : DialogViewModelBase + { + private string _inputValue; + + public TimelineSegmentDialogViewModel(IModelValidator validator, TimelineSegmentViewModel segment) + : base(validator) + { + Segment = segment; + InputValue = $"{Math.Floor(Segment.SegmentLength.TotalSeconds):00}.{Segment.SegmentLength.Milliseconds:000}"; + } + + public TimelineSegmentViewModel Segment { get; } + + public string InputValue + { + get => _inputValue; + set => SetAndNotify(ref _inputValue, value); + } + + public async Task Accept() + { + await ValidateAsync(); + + if (HasErrors) + return; + + Segment.UpdateLength(TimelineSegmentDialogViewModelValidator.CreateTime(InputValue)); + Session.Close(); + } + + public void Cancel() + { + Session.Close(); + } + } + + public class TimelineSegmentDialogViewModelValidator : AbstractValidator + { + private readonly Regex _inputRegex = new Regex("^[.][-|0-9]+$|^-?[0-9]*[.]{0,1}[0-9]*$"); + + public TimelineSegmentDialogViewModelValidator() + { + CascadeMode = CascadeMode.Stop; + + RuleFor(m => m.InputValue) + .NotNull() + .WithMessage("A timeline length is required"); + RuleFor(m => m.InputValue) + .Must(ValidateTime) + .WithMessage("Input cannot be converted to a time"); + RuleFor(m => m.InputValue) + .Transform(CreateTime) + .GreaterThanOrEqualTo(TimeSpan.FromMilliseconds(100)) + .WithMessage("Minimum timeline length is 100ms"); + RuleFor(m => m.InputValue) + .Transform(CreateTime) + .LessThanOrEqualTo(TimeSpan.FromHours(24)) + .WithMessage("Maximum timeline length is 24 hours"); + } + + public static TimeSpan CreateTime(string s) + { + var parts = s.Split("."); + + // Only seconds provided + if (parts.Length == 1) + return TimeSpan.FromSeconds(double.Parse(parts[0])); + // Only milliseconds provided with a leading . + if (parts[0].IsNullOrEmpty()) + { + // Add trailing zeros so 2.5 becomes 2.500, can't seem to make double.Parse do that + while (parts[0].Length < 3) parts[0] += "0"; + return TimeSpan.FromMilliseconds(double.Parse(parts[1], CultureInfo.InvariantCulture)); + } + + // Seconds and milliseconds provided + // Add trailing zeros so 2.5 becomes 2.500, can't seem to make double.Parse do that + while (parts[1].Length < 3) parts[1] += "0"; + return TimeSpan.FromSeconds(double.Parse(parts[0])).Add(TimeSpan.FromMilliseconds(double.Parse(parts[1]))); + } + + private bool ValidateTime(string arg) + { + return _inputRegex.IsMatch(arg); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentView.xaml index 3b7338326..9c5b5d42d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentView.xaml @@ -16,11 +16,21 @@ + + + + + - + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs index 06bc9b6be..78e651ac0 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineSegmentViewModel.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using Artemis.Core; +using Artemis.UI.Screens.ProfileEditor.Dialogs; +using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Stylet; @@ -13,6 +17,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { public class TimelineSegmentViewModel : Screen, IDisposable { + private readonly IDialogService _dialogService; private bool _draggingSegment; private bool _showDisableButton; private bool _showRepeatButton; @@ -25,8 +30,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline private double _segmentStartPosition; public TimelineSegmentViewModel(SegmentViewModelType segment, BindableCollection layerPropertyGroups, - IProfileEditorService profileEditorService) + IProfileEditorService profileEditorService, IDialogService dialogService) { + _dialogService = dialogService; ProfileEditorService = profileEditorService; Segment = segment; LayerPropertyGroups = layerPropertyGroups; @@ -130,6 +136,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline set => SetAndNotify(ref _showDisableButton, value); } + public async Task OpenSettingsDialog() + { + await _dialogService.ShowDialog(new Dictionary {{"segment", this}}); + } + #region Updating private void UpdateHeader() @@ -276,6 +287,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0); + UpdateLength(newTime); + } + + public void UpdateLength(TimeSpan newTime) + { var oldSegmentLength = SegmentLength; if (Segment == SegmentViewModelType.Start) {