From 5b6fc9eec7844047c2754139990a3850f3f7eb78 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 11 Jul 2021 19:32:27 +0200 Subject: [PATCH 1/3] 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; From d10ffcf62a5e290bec4afae8587a3b0f2ef566a7 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 11 Jul 2021 19:45:34 +0200 Subject: [PATCH 2/3] UI - Fix #640 --- .../ProfileEditor/Visualization/ProfileViewModel.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index 6f4fea3d0..498eee76b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -261,8 +261,14 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization public void ResetZoomAndPan() { + if (!Devices.Any()) + { + PanZoomViewModel.Reset(); + return; + } + // Create a rect surrounding all devices - SKRect rect = new SKRect( + SKRect rect = new( Devices.Min(d => d.Rectangle.Left), Devices.Min(d => d.Rectangle.Top), Devices.Max(d => d.Rectangle.Right), From 0adcfa65c0d4c26cf9407b2e7077af8ef825d706 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Thu, 1 Jul 2021 15:50:19 +0100 Subject: [PATCH 3/3] Core - replaced hardcoded backslashes with Path.Combine --- src/Artemis.Core/Constants.cs | 2 +- .../Models/Surface/Layout/ArtemisLayout.cs | 28 +++++++++---------- .../Models/Surface/Layout/ArtemisLedLayout.cs | 3 +- src/Artemis.Core/Ninject/LoggerProvider.cs | 3 +- src/Artemis.Core/Utilities/Utilities.cs | 4 +-- src/Artemis.Storage/StorageManager.cs | 8 +++--- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index e978e8598..7921af0bc 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -27,7 +27,7 @@ namespace Artemis.Core /// /// The full path to the Artemis data folder /// - public static readonly string DataFolder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\Artemis\\"; + public static readonly string DataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Artemis"); /// /// The plugin info used by core components of Artemis diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs index 8d46099e0..245b9eeb1 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -169,29 +169,29 @@ namespace Artemis.Core internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) { - string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts\\Artemis"); + string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis"); if (device.DeviceType == RGBDeviceType.Keyboard) { // XL layout is defined by its programmable macro keys if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32)) { if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis XL keyboard-ANSI.xml", LayoutSource.Default); - return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis XL keyboard-ISO.xml", LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default); } // L layout is defined by its numpad if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete)) { if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis L keyboard-ANSI.xml", LayoutSource.Default); - return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis L keyboard-ISO.xml", LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis L keyboard-ANSI.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis L keyboard-ISO.xml"), LayoutSource.Default); } // No numpad will result in TKL if (device.PhysicalLayout == KeyboardLayoutType.ANSI) - return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis TKL keyboard-ANSI.xml", LayoutSource.Default); - return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis TKL keyboard-ISO.xml", LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard","Artemis TKL keyboard-ISO.xml"), LayoutSource.Default); } // if (device.DeviceType == RGBDeviceType.Mouse) @@ -199,21 +199,21 @@ namespace Artemis.Core // if (device.Leds.Count == 1) // { // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) - // return new ArtemisLayout(layoutFolder + "\\Mouse\\1 LED mouse logo.xml", LayoutSource.Default); - // return new ArtemisLayout(layoutFolder + "\\Mouse\\1 LED mouse.xml", LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default); // } // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) - // return new ArtemisLayout(layoutFolder + "\\Mouse\\4 LED mouse logo.xml", LayoutSource.Default); - // return new ArtemisLayout(layoutFolder + "\\Mouse\\4 LED mouse.xml", LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default); + // return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default); // } if (device.DeviceType == RGBDeviceType.Headset) { if (device.Leds.Count == 1) - return new ArtemisLayout(layoutFolder + "\\Headset\\Artemis 1 LED headset.xml", LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default); if (device.Leds.Count == 2) - return new ArtemisLayout(layoutFolder + "\\Headset\\Artemis 2 LED headset.xml", LayoutSource.Default); - return new ArtemisLayout(layoutFolder + "\\Headset\\Artemis 4 LED headset.xml", LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default); + return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default); } return null; diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs index 838c62881..01fcce12d 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLedLayout.cs @@ -61,7 +61,6 @@ namespace Artemis.Core if (LayoutCustomLedData.LogicalLayouts == null || !LayoutCustomLedData.LogicalLayouts.Any()) return; - Uri layoutDirectory = new(Path.GetDirectoryName(DeviceLayout.FilePath)! + "\\", UriKind.Absolute); // Prefer a matching layout or else a default layout (that has no name) LayoutCustomLedDataLogicalLayout logicalLayout = LayoutCustomLedData.LogicalLayouts .OrderBy(l => l.Name == artemisDevice.LogicalLayout) @@ -69,7 +68,7 @@ namespace Artemis.Core .First(); LogicalName = logicalLayout.Name; - Image = new Uri(layoutDirectory, logicalLayout.Image); + Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/LoggerProvider.cs b/src/Artemis.Core/Ninject/LoggerProvider.cs index c39f7bace..64f5efcf3 100644 --- a/src/Artemis.Core/Ninject/LoggerProvider.cs +++ b/src/Artemis.Core/Ninject/LoggerProvider.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Ninject.Activation; using Serilog; using Serilog.Core; @@ -12,7 +13,7 @@ namespace Artemis.Core.Ninject private static readonly ILogger Logger = new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.File(Constants.DataFolder + "logs/Artemis log-.log", + .WriteTo.File(Path.Combine(Constants.DataFolder, "logs", "Artemis log-.log"), rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}") .WriteTo.Console() diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index 8577185bc..c55b07c93 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -18,8 +18,8 @@ namespace Artemis.Core public static void PrepareFirstLaunch() { CreateAccessibleDirectory(Constants.DataFolder); - CreateAccessibleDirectory(Constants.DataFolder + "plugins"); - CreateAccessibleDirectory(Constants.DataFolder + "user layouts"); + CreateAccessibleDirectory(Path.Combine(Constants.DataFolder ,"plugins")); + CreateAccessibleDirectory(Path.Combine(Constants.DataFolder ,"user layouts")); } /// diff --git a/src/Artemis.Storage/StorageManager.cs b/src/Artemis.Storage/StorageManager.cs index 267f86fea..93775b16d 100644 --- a/src/Artemis.Storage/StorageManager.cs +++ b/src/Artemis.Storage/StorageManager.cs @@ -19,11 +19,11 @@ namespace Artemis.Storage if (_inUse) throw new Exception("Storage is already in use, can't backup now."); - string database = $"{dataFolder}\\database.db"; + string database = Path.Combine(dataFolder, "database.db"); if (!File.Exists(database)) return; - string backupFolder = $"{dataFolder}\\database backups"; + string backupFolder = Path.Combine(dataFolder, "database backups"); Directory.CreateDirectory(backupFolder); FileSystemInfo[] files = new DirectoryInfo(backupFolder).GetFileSystemInfos(); if (files.Length >= 5) @@ -36,7 +36,7 @@ namespace Artemis.Storage oldest.Delete(); } - File.Copy(database, $"{backupFolder}\\database-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db"); + File.Copy(database, Path.Combine(backupFolder, $"database-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db")); } /// @@ -51,7 +51,7 @@ namespace Artemis.Storage try { _inUse = true; - return new LiteRepository($"FileName={dataFolder}\\database.db"); + return new LiteRepository($"FileName={Path.Combine(dataFolder, "database.db")}"); } catch (LiteException e) {