From 3b7e226885425eb05b7e5e2502da2e806a947a9b Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 9 Jul 2021 14:07:20 +0200 Subject: [PATCH 1/2] Profile editor - Fixed an exception caused by leftover debug code --- .../Predicate/DataModelConditionListPredicateViewModel.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs index 774a10a73..411e8f735 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Predicate/DataModelConditionListPredicateViewModel.cs @@ -24,8 +24,6 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions : base(dataModelConditionListPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) { _dataModelUIService = dataModelUIService; - DataModelPathSegment dataModelPathSegment = dataModelConditionListPredicate.LeftPath.Segments.ToList()[1]; - var segmentDescription = dataModelPathSegment.GetPropertyDescription(); LeftSideColor = new SolidColorBrush(Color.FromRgb(71, 108, 188)); } From 5b6fc9eec7844047c2754139990a3850f3f7eb78 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 11 Jul 2021 19:32:27 +0200 Subject: [PATCH 2/2] Profiles - Added system that displays when a layer is causing errors --- src/Artemis.Core/Models/BreakableModel.cs | 92 +++++++++++++++++++ src/Artemis.Core/Models/IBreakableModel.cs | 59 ++++++++++++ src/Artemis.Core/Models/Profile/Folder.cs | 25 ++--- src/Artemis.Core/Models/Profile/Layer.cs | 73 ++++++++++----- src/Artemis.Core/Models/Profile/Profile.cs | 22 +++++ .../Models/Profile/ProfileElement.cs | 13 ++- .../LayerBrushes/Internal/BaseLayerBrush.cs | 19 +++- .../Plugins/LayerBrushes/LayerBrush.cs | 4 +- .../Plugins/LayerBrushes/PerLedLayerBrush.cs | 40 ++++---- .../LayerEffects/Internal/BaseLayerEffect.cs | 26 +++++- .../Screens/Exceptions/ExceptionView.xaml | 2 +- .../LayerPropertiesViewModel.cs | 9 ++ .../ProfileTree/ProfileTreeViewModel.cs | 13 +++ .../ProfileTree/TreeItem/FolderView.xaml | 49 ++++++---- .../ProfileTree/TreeItem/FolderViewModel.cs | 24 ++++- .../ProfileTree/TreeItem/LayerView.xaml | 27 ++++-- .../ProfileTree/TreeItem/LayerViewModel.cs | 31 +++++-- .../ProfileTree/TreeItem/TreeItemViewModel.cs | 24 +++++ 18 files changed, 457 insertions(+), 95 deletions(-) create mode 100644 src/Artemis.Core/Models/BreakableModel.cs create mode 100644 src/Artemis.Core/Models/IBreakableModel.cs diff --git a/src/Artemis.Core/Models/BreakableModel.cs b/src/Artemis.Core/Models/BreakableModel.cs new file mode 100644 index 000000000..97bd761cb --- /dev/null +++ b/src/Artemis.Core/Models/BreakableModel.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core +{ + /// + /// Provides a default implementation for models that can have a broken state + /// + public abstract class BreakableModel : CorePropertyChanged, IBreakableModel + { + private string? _brokenState; + private Exception? _brokenStateException; + + /// + /// Invokes the event + /// + protected virtual void OnBrokenStateChanged() + { + BrokenStateChanged?.Invoke(this, EventArgs.Empty); + } + + /// + public abstract string BrokenDisplayName { get; } + + /// + /// Gets or sets the broken state of this breakable model, if this model is not broken. + /// Note: If setting this manually you are also responsible for invoking + /// + public string? BrokenState + { + get => _brokenState; + set => SetAndNotify(ref _brokenState, value); + } + + /// + /// Gets or sets the exception that caused the broken state + /// Note: If setting this manually you are also responsible for invoking + /// + public Exception? BrokenStateException + { + get => _brokenStateException; + set => SetAndNotify(ref _brokenStateException, value); + } + + /// + public bool TryOrBreak(Action action, string breakMessage) + { + try + { + action(); + ClearBrokenState(breakMessage); + return true; + } + catch (Exception e) + { + SetBrokenState(breakMessage, e); + return false; + } + } + + /// + public void SetBrokenState(string state, Exception? exception) + { + BrokenState = state ?? throw new ArgumentNullException(nameof(state)); + BrokenStateException = exception; + OnBrokenStateChanged(); + } + + /// + public void ClearBrokenState(string state) + { + if (state == null) throw new ArgumentNullException(nameof(state)); + if (BrokenState == null) + return; + + if (BrokenState != state) return; + BrokenState = null; + BrokenStateException = null; + OnBrokenStateChanged(); + } + + /// + public virtual IEnumerable GetBrokenHierarchy() + { + if (BrokenState != null) + yield return this; + } + + /// + public event EventHandler? BrokenStateChanged; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/IBreakableModel.cs b/src/Artemis.Core/Models/IBreakableModel.cs new file mode 100644 index 000000000..88a5762d6 --- /dev/null +++ b/src/Artemis.Core/Models/IBreakableModel.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core +{ + /// + /// Represents a model that can have a broken state + /// + public interface IBreakableModel + { + /// + /// Gets the display name of this breakable model + /// + string BrokenDisplayName { get; } + + /// + /// Gets or sets the broken state of this breakable model, if this model is not broken. + /// + string? BrokenState { get; set; } + + /// + /// Gets or sets the exception that caused the broken state + /// + Exception? BrokenStateException { get; set; } + + /// + /// Try to execute the provided action. If the action succeeded the broken state is cleared if it matches + /// , if the action throws an exception and + /// are set accordingly. + /// + /// The action to attempt to execute + /// The message to clear on succeed or set on failure (exception) + /// if the action succeeded; otherwise . + bool TryOrBreak(Action action, string breakMessage); + + /// + /// Sets the broken state to the provided state and optional exception. + /// + /// The state to set the broken state to + /// The exception that caused the broken state + public void SetBrokenState(string state, Exception? exception); + + /// + /// Clears the broken state and exception if equals . + /// + /// + public void ClearBrokenState(string state); + + /// + /// Returns a list containing all broken models, including self and any children + /// + IEnumerable GetBrokenHierarchy(); + + /// + /// Occurs when the broken state of this model changes + /// + event EventHandler BrokenStateChanged; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 81ec38993..c28e9ea00 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -183,18 +183,15 @@ namespace Artemis.Core lock (Timeline) { - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - { - baseLayerEffect.BaseProperties?.Update(Timeline); - baseLayerEffect.Update(Timeline.Delta.TotalSeconds); - } + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; try { SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.PreProcess(canvas, rendererBounds, layerPaint); + baseLayerEffect.InternalPreProcess(canvas, rendererBounds, layerPaint); // No point rendering if the alpha was set to zero by one of the effects if (layerPaint.Color.Alpha == 0) @@ -208,7 +205,7 @@ namespace Artemis.Core Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top)); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.PostProcess(canvas, rendererBounds, layerPaint); + baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint); } finally { @@ -228,8 +225,6 @@ namespace Artemis.Core if (Enabled) return; - Debug.WriteLine($"Enabling {this}"); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.InternalEnable(); @@ -248,8 +243,6 @@ namespace Artemis.Core if (!Enabled) return; - Debug.WriteLine($"Disabled {this}"); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.InternalDisable(); @@ -344,5 +337,15 @@ namespace Artemis.Core { RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); } + + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + return LayerEffects.Where(e => e.BrokenState != null); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index f73fb5879..8b6cde56e 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -206,6 +206,7 @@ namespace Artemis.Core GetType().GetProperty(nameof(Transform))!, typeof(PropertyGroupDescriptionAttribute) )!; + General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; General.Initialize(this, "General.", Constants.CorePluginFeature); Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute; @@ -288,7 +289,7 @@ namespace Artemis.Core // Adaption hints Adapter.Save(); - + SaveRenderElement(); } @@ -320,7 +321,7 @@ namespace Artemis.Core { if (Disposed) throw new ObjectDisposedException("Layer"); - + UpdateDisplayCondition(); UpdateTimeline(deltaTime); @@ -366,9 +367,17 @@ namespace Artemis.Core if (Enabled) return; - LayerBrush?.InternalEnable(); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.InternalEnable(); + bool tryOrBreak = TryOrBreak(() => LayerBrush?.InternalEnable(), "Failed to enable layer brush"); + if (!tryOrBreak) + return; + + tryOrBreak = TryOrBreak(() => + { + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.InternalEnable(); + }, "Failed to enable one or more effects"); + if (!tryOrBreak) + return; Enabled = true; } @@ -393,17 +402,10 @@ namespace Artemis.Core General.Update(timeline); Transform.Update(timeline); - if (LayerBrush != null) - { - LayerBrush.BaseProperties?.Update(timeline); - LayerBrush.Update(timeline.Delta.TotalSeconds); - } + LayerBrush?.InternalUpdate(timeline); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - { - baseLayerEffect.BaseProperties?.Update(timeline); - baseLayerEffect.Update(timeline.Delta.TotalSeconds); - } + baseLayerEffect.InternalUpdate(timeline); } private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPointI basePosition) @@ -477,9 +479,9 @@ namespace Artemis.Core { if (LayerBrush == null) throw new ArtemisCoreException("The layer is not yet ready for rendering"); - + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.PreProcess(canvas, bounds, layerPaint); + baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); try { @@ -492,7 +494,7 @@ namespace Artemis.Core LayerBrush.InternalRender(canvas, bounds, layerPaint); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.PostProcess(canvas, bounds, layerPaint); + baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); } finally @@ -722,16 +724,24 @@ namespace Artemis.Core internal void ActivateLayerBrush() { - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (current == null) - return; + try + { + LayerBrushReference? current = General.BrushReference.CurrentValue; + if (current == null) + return; - LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null - ? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor - : null; - descriptor?.CreateInstance(this); + LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null + ? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor + : null; + descriptor?.CreateInstance(this); - OnLayerBrushUpdated(); + OnLayerBrushUpdated(); + ClearBrokenState("Failed to initialize layer brush"); + } + catch (Exception e) + { + SetBrokenState("Failed to initialize layer brush", e); + } } internal void DeactivateLayerBrush() @@ -747,6 +757,19 @@ namespace Artemis.Core } #endregion + + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + if (LayerBrush?.BrokenState != null) + yield return LayerBrush; + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null)) + yield return baseLayerEffect; + } + + #endregion } /// diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index a018b947a..c0c3a2528 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -27,6 +27,7 @@ namespace Artemis.Core UndoStack = new MaxStack(20); RedoStack = new MaxStack(20); + Exceptions = new List(); Load(); } @@ -77,6 +78,7 @@ namespace Artemis.Core internal MaxStack UndoStack { get; set; } internal MaxStack RedoStack { get; set; } + internal List Exceptions { get; } /// public override void Update(double deltaTime) @@ -113,6 +115,13 @@ namespace Artemis.Core foreach (ProfileScript profileScript in Scripts) profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds); + + if (!Exceptions.Any()) + return; + + List exceptions = new(Exceptions); + Exceptions.Clear(); + throw new AggregateException($"One or more exceptions while rendering profile {Name}", exceptions); } } @@ -238,5 +247,18 @@ namespace Artemis.Core ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity); } } + + #region Overrides of BreakableModel + + /// + public override IEnumerable GetBrokenHierarchy() + { + foreach (IBreakableModel breakableModel in GetAllFolders().SelectMany(folders => folders.GetBrokenHierarchy())) + yield return breakableModel; + foreach (IBreakableModel breakableModel in GetAllLayers().SelectMany(layer => layer.GetBrokenHierarchy())) + yield return breakableModel; + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 58f005f94..81537b0ba 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -9,7 +9,7 @@ namespace Artemis.Core /// /// Represents an element of a /// - public abstract class ProfileElement : CorePropertyChanged, IDisposable + public abstract class ProfileElement : BreakableModel, IDisposable { private Guid _entityId; private string? _name; @@ -25,7 +25,7 @@ namespace Artemis.Core _profile = profile; ChildrenList = new List(); } - + /// /// Gets the unique ID of this profile element /// @@ -121,6 +121,13 @@ namespace Artemis.Core return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; } + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Name ?? GetType().Name; + + #endregion + #region Hierarchy /// @@ -234,7 +241,7 @@ namespace Artemis.Core internal abstract void Save(); #endregion - + #region IDisposable /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 0154529f6..40a37b09d 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -8,7 +8,7 @@ namespace Artemis.Core.LayerBrushes /// /// For internal use only, please use or or instead /// - public abstract class BaseLayerBrush : CorePropertyChanged, IDisposable + public abstract class BaseLayerBrush : BreakableModel, IDisposable { private LayerBrushType _brushType; private ILayerBrushConfigurationDialog? _configurationDialog; @@ -134,6 +134,12 @@ namespace Artemis.Core.LayerBrushes } } + internal void InternalUpdate(Timeline timeline) + { + BaseProperties?.Update(timeline); + TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update"); + } + /// /// Enables the layer brush if it isn't already enabled /// @@ -142,7 +148,9 @@ namespace Artemis.Core.LayerBrushes if (Enabled) return; - EnableLayerBrush(); + if (!TryOrBreak(EnableLayerBrush, "Failed to enable")) + return; + Enabled = true; } @@ -170,6 +178,13 @@ namespace Artemis.Core.LayerBrushes Dispose(true); GC.SuppressFinalize(this); } + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Descriptor.DisplayName; + + #endregion } /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs index 94b8623a4..9e69380d0 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs @@ -28,12 +28,12 @@ namespace Artemis.Core.LayerBrushes internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) { - Render(canvas, bounds, paint); + TryOrBreak(() => Render(canvas, bounds, paint), "Failed to render"); } internal override void Initialize() { - InitializeProperties(); + TryOrBreak(InitializeProperties, "Failed to initialize"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs index bb48793e6..1f1948808 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs @@ -31,7 +31,7 @@ namespace Artemis.Core.LayerBrushes internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint) { // We don't want rotation on this canvas because that'll displace the LEDs, translations are applied to the points of each LED instead - if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) + if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) canvas.SetMatrix(canvas.TotalMatrix.PreConcat(Layer.GetTransformMatrix(true, false, false, true).Invert())); using SKPath pointsPath = new(); @@ -46,34 +46,38 @@ namespace Artemis.Core.LayerBrushes } // Apply the translation to the points of each LED instead - if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) + if (Layer.General.TransformMode.CurrentValue == LayerTransformMode.Normal && SupportsTransformation) pointsPath.Transform(Layer.GetTransformMatrix(true, true, true, true).Invert()); SKPoint[] points = pointsPath.Points; - for (int index = 0; index < Layer.Leds.Count; index++) + + TryOrBreak(() => { - ArtemisLed artemisLed = Layer.Leds[index]; - SKPoint renderPoint = points[index * 2 + 1]; - if (!float.IsFinite(renderPoint.X) || !float.IsFinite(renderPoint.Y)) - continue; + for (int index = 0; index < Layer.Leds.Count; index++) + { + ArtemisLed artemisLed = Layer.Leds[index]; + SKPoint renderPoint = points[index * 2 + 1]; + if (!float.IsFinite(renderPoint.X) || !float.IsFinite(renderPoint.Y)) + continue; - // Let the brush determine the color - ledPaint.Color = GetColor(artemisLed, renderPoint); + // Let the brush determine the color + ledPaint.Color = GetColor(artemisLed, renderPoint); - SKRect ledRectangle = SKRect.Create( - artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, - artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top, - artemisLed.AbsoluteRectangle.Width, - artemisLed.AbsoluteRectangle.Height - ); + SKRect ledRectangle = SKRect.Create( + artemisLed.AbsoluteRectangle.Left - Layer.Bounds.Left, + artemisLed.AbsoluteRectangle.Top - Layer.Bounds.Top, + artemisLed.AbsoluteRectangle.Width, + artemisLed.AbsoluteRectangle.Height + ); - canvas.DrawRect(ledRectangle, ledPaint); - } + canvas.DrawRect(ledRectangle, ledPaint); + } + }, "Failed to render"); } internal override void Initialize() { - InitializeProperties(); + TryOrBreak(InitializeProperties, "Failed to initialize"); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index ac9f5d27f..ce32edfd0 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -1,5 +1,4 @@ using System; -using Artemis.Core.Services; using SkiaSharp; namespace Artemis.Core.LayerEffects @@ -7,7 +6,7 @@ namespace Artemis.Core.LayerEffects /// /// For internal use only, please use instead /// - public abstract class BaseLayerEffect : CorePropertyChanged, IDisposable + public abstract class BaseLayerEffect : BreakableModel, IDisposable { private ILayerEffectConfigurationDialog? _configurationDialog; private LayerEffectDescriptor _descriptor; @@ -182,6 +181,22 @@ namespace Artemis.Core.LayerEffects internal virtual string GetEffectTypeName() => GetType().Name; + internal void InternalUpdate(Timeline timeline) + { + BaseProperties?.Update(timeline); + TryOrBreak(() => Update(timeline.Delta.TotalSeconds), "Failed to update"); + } + + internal void InternalPreProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint) + { + TryOrBreak(() => PreProcess(canvas, renderBounds, paint), "Failed to pre-process"); + } + + internal void InternalPostProcess(SKCanvas canvas, SKRect renderBounds, SKPaint paint) + { + TryOrBreak(() => PostProcess(canvas, renderBounds, paint), "Failed to post-process"); + } + /// /// Enables the layer effect if it isn't already enabled /// @@ -205,5 +220,12 @@ namespace Artemis.Core.LayerEffects DisableLayerEffect(); Enabled = false; } + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Name; + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionView.xaml b/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionView.xaml index 3019c944b..e729a5959 100644 --- a/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionView.xaml +++ b/src/Artemis.UI.Shared/Screens/Exceptions/ExceptionView.xaml @@ -10,7 +10,7 @@ xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit" xmlns:s="https://github.com/canton7/Stylet" mc:Ignorable="d" - Title="Unhandled exception" + Title="Exception | Artemis" Background="{DynamicResource MaterialDesignPaper}" FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto" UseLayoutRounding="True" diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 6fc38bf39..2b748fc01 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -501,11 +501,17 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties public void GoToEnd() { + if (SelectedProfileElement == null) + return; + ProfileEditorService.CurrentTime = SelectedProfileElement.Timeline.EndSegmentEndPosition; } public void GoToPreviousFrame() { + if (SelectedProfileElement == null) + return; + double frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value; double newTime = Math.Max(0, Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime); ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime); @@ -513,6 +519,9 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties public void GoToNextFrame() { + if (SelectedProfileElement == null) + return; + double frameTime = 1000.0 / SettingsService.GetSetting("Core.TargetFrameRate", 25).Value; double newTime = Math.Round((ProfileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime; newTime = Math.Min(newTime, SelectedProfileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds); diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs index d0659951f..7a4a0d773 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using System.Timers; using System.Windows; using System.Windows.Input; using Artemis.Core; @@ -20,11 +21,13 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree private bool _draggingTreeView; private TreeItemViewModel _selectedTreeItem; private bool _updatingTree; + private readonly Timer _timer; public ProfileTreeViewModel(IProfileEditorService profileEditorService, IProfileTreeVmFactory profileTreeVmFactory) { _profileEditorService = profileEditorService; _profileTreeVmFactory = profileTreeVmFactory; + _timer = new Timer(500); } public TreeItemViewModel SelectedTreeItem @@ -54,12 +57,15 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { Subscribe(); CreateRootFolderViewModel(); + _timer.Start(); base.OnInitialActivate(); } protected override void OnClose() { Unsubscribe(); + _timer.Stop(); + _timer.Dispose(); base.OnClose(); } @@ -203,12 +209,14 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { _profileEditorService.SelectedProfileChanged += OnSelectedProfileChanged; _profileEditorService.SelectedProfileElementChanged += OnSelectedProfileElementChanged; + _timer.Elapsed += TimerOnElapsed; } private void Unsubscribe() { _profileEditorService.SelectedProfileChanged -= OnSelectedProfileChanged; _profileEditorService.SelectedProfileElementChanged -= OnSelectedProfileElementChanged; + _timer.Elapsed -= TimerOnElapsed; } private void OnSelectedProfileElementChanged(object sender, RenderProfileElementEventArgs e) @@ -242,6 +250,11 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree CreateRootFolderViewModel(); } + private void TimerOnElapsed(object sender, ElapsedEventArgs e) + { + ActiveItem?.UpdateBrokenState(); + } + #endregion } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml index b1a9f2d85..eaba09b42 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml @@ -1,17 +1,20 @@  + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:s="https://github.com/canton7/Stylet" + xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" + xmlns:treeItem1="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem" + xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem.FolderView" + mc:Ignorable="d" + d:DesignHeight="450" d:DesignWidth="800" + d:DataContext="{d:DesignInstance {x:Type treeItem1:FolderViewModel}}"> - + + @@ -58,18 +61,32 @@ + - + + + - - - + ((Folder) ProfileElement).IsExpanded = value; } + public override void UpdateBrokenState() + { + List brokenModels = ProfileElement.GetBrokenHierarchy().ToList(); + if (!brokenModels.Any()) + BrokenState = null; + else + { + BrokenState = "Folder is in a broken state, click to view exception(s).\r\n" + + $"{string.Join("\r\n", brokenModels.Select(e => $" • {e.BrokenDisplayName} - {e.BrokenState}"))}"; + } + + foreach (TreeItemViewModel treeItemViewModel in GetChildren()) + treeItemViewModel.UpdateBrokenState(); + } + private void ProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Folder.IsExpanded)) NotifyOfPropertyChange(nameof(IsExpanded)); } - + + #region Overrides of Screen /// diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml index aa89223e3..0e1c22bfb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml @@ -6,14 +6,15 @@ xmlns:s="https://github.com/canton7/Stylet" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:treeItem1="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem" - xmlns:Converters="clr-namespace:Artemis.UI.Converters" + xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem.LayerView" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance {x:Type treeItem1:LayerViewModel}}"> - + + @@ -55,13 +56,27 @@ + - - + + + + - - + ProfileElement as Layer; public bool ShowIcons => Layer?.LayerBrush != null; public override bool SupportsChildren => false; public override bool IsExpanded { get; set; } + + public void OpenAdaptionHints() + { + _windowManager.ShowDialog(_vmFactory.LayerHintsDialogViewModel(Layer)); + } + + public override void UpdateBrokenState() + { + List brokenModels = ProfileElement.GetBrokenHierarchy().ToList(); + if (!brokenModels.Any()) + BrokenState = null; + else + { + BrokenState = "Layer is in a broken state, click to view exception(s).\r\n" + + $"{string.Join("\r\n", brokenModels.Select(e => $" • {e.BrokenDisplayName} - {e.BrokenState}"))}"; + } + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index 4c1cba42b..58d6560fd 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -23,6 +23,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem private readonly IProfileTreeVmFactory _profileTreeVmFactory; private readonly IRgbService _rgbService; private ProfileElement _profileElement; + private string _brokenState; protected TreeItemViewModel(ProfileElement profileElement, IRgbService rgbService, @@ -49,6 +50,12 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem set => SetAndNotify(ref _profileElement, value); } + public string BrokenState + { + get => _brokenState; + set => SetAndNotify(ref _brokenState, value); + } + public bool CanPasteElement => _profileEditorService.GetCanPasteProfileElement(); public abstract bool SupportsChildren { get; } @@ -286,6 +293,23 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem NotifyOfPropertyChange(nameof(CanPasteElement)); } + public abstract void UpdateBrokenState(); + + public async Task ShowBrokenStateExceptions() + { + List broken = ProfileElement.GetBrokenHierarchy().Where(b => b.BrokenStateException != null).ToList(); + + foreach (IBreakableModel current in broken) + { + _dialogService.ShowExceptionDialog($"{current.BrokenDisplayName} - {current.BrokenState}", current.BrokenStateException!); + if (broken.Last() != current) + { + if (!await _dialogService.ShowConfirmDialog("Broken state", "Do you want to view the next exception?")) + return; + } + } + } + private void Subscribe() { ProfileElement.ChildAdded += ProfileElementOnChildAdded;