From 048864fe787c06ab68b224e2cda07d689bdbb7d7 Mon Sep 17 00:00:00 2001 From: Robert Beekman Date: Sat, 19 Jun 2021 09:48:16 +0200 Subject: [PATCH 1/3] Scripting - Core implementation (#629) Scripting - Added plugin classes Layers - Fix certain blend modes not working as intended UI - Add customizable header per page Profile editor - Hide the regular header Profile editor - Added a new toolbar --- .../Artemis.Core.csproj.DotSettings | 1 + src/Artemis.Core/Models/Profile/Layer.cs | 54 +++- .../Profile/LayerProperties/ILayerProperty.cs | 11 + .../Profile/LayerProperties/LayerProperty.cs | 41 ++- src/Artemis.Core/Models/Profile/Profile.cs | 43 +++- .../ScriptingProviders/ScriptConfiguration.cs | 86 +++++++ .../ScriptingProviders/ScriptingProvider.cs | 177 +++++++++++++ .../Scripts/GlobalScript.cs | 43 ++++ .../ScriptingProviders/Scripts/LayerScript.cs | 70 +++++ .../Scripts/ProfileScript.cs | 68 +++++ .../Scripts/PropertyScript.cs | 49 ++++ .../ScriptingProviders/Scripts/Script.cs | 103 ++++++++ src/Artemis.Core/Services/CoreService.cs | 14 +- src/Artemis.Core/Services/ScriptingService.cs | 219 ++++++++++++++++ .../Storage/Interfaces/IProfileService.cs | 13 +- .../Services/Storage/ProfileService.cs | 21 ++ .../General/ScriptConfigurationEntity.cs | 13 + .../Entities/Profile/LayerEntity.cs | 3 + .../Entities/Profile/ProfileEntity.cs | 5 +- .../Entities/Profile/PropertyEntity.cs | 3 + .../Artemis.UI.Shared.csproj | 2 +- .../ScriptEditorViewModel.cs | 20 ++ src/Artemis.UI.Shared/packages.lock.json | 12 +- src/Artemis.UI/Artemis.UI.csproj | 2 +- .../Ninject/Factories/IVMFactory.cs | 6 + src/Artemis.UI/Ninject/UiModule.cs | 2 +- .../Screens/Header/SimpleHeaderView.xaml | 35 +++ .../Screens/Header/SimpleHeaderViewModel.cs | 69 +++++ src/Artemis.UI/Screens/Home/HomeViewModel.cs | 7 +- src/Artemis.UI/Screens/IScreenViewModel.cs | 17 +- .../ProfileEditor/ProfileEditorView.xaml | 241 ++++++++++++++---- .../ProfileEditor/ProfileEditorViewModel.cs | 91 ++++++- .../Visualization/ProfileView.xaml | 19 +- src/Artemis.UI/Screens/RootView.xaml | 39 +-- src/Artemis.UI/Screens/RootViewModel.cs | 49 +--- .../Scripting/ScriptsDialogViewModel.cs | 24 ++ .../Screens/Settings/SettingsTabsView.xaml | 33 +++ .../Screens/Settings/SettingsTabsViewModel.cs | 27 ++ .../Screens/Settings/SettingsView.xaml | 23 +- .../Screens/Settings/SettingsViewModel.cs | 24 +- .../SidebarProfileConfigurationView.xaml | 2 +- .../Screens/Sidebar/SidebarScreenViewModel.cs | 8 +- .../Screens/Sidebar/SidebarViewModel.cs | 6 +- .../SurfaceEditor/SurfaceEditorView.xaml | 23 +- .../SurfaceEditor/SurfaceEditorViewModel.cs | 2 +- .../Screens/Workshop/WorkshopViewModel.cs | 2 +- src/Artemis.UI/packages.lock.json | 14 +- 47 files changed, 1599 insertions(+), 237 deletions(-) create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/Scripts/GlobalScript.cs create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/Scripts/LayerScript.cs create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/Scripts/ProfileScript.cs create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/Scripts/PropertyScript.cs create mode 100644 src/Artemis.Core/Plugins/ScriptingProviders/Scripts/Script.cs create mode 100644 src/Artemis.Core/Services/ScriptingService.cs create mode 100644 src/Artemis.Storage/Entities/General/ScriptConfigurationEntity.cs create mode 100644 src/Artemis.UI.Shared/Plugins/ScriptingProviders/ScriptEditorViewModel.cs create mode 100644 src/Artemis.UI/Screens/Header/SimpleHeaderView.xaml create mode 100644 src/Artemis.UI/Screens/Header/SimpleHeaderViewModel.cs create mode 100644 src/Artemis.UI/Screens/Scripting/ScriptsDialogViewModel.cs create mode 100644 src/Artemis.UI/Screens/Settings/SettingsTabsView.xaml create mode 100644 src/Artemis.UI/Screens/Settings/SettingsTabsViewModel.cs diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 261d8170b..31cbebcb3 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -61,6 +61,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 9199aa021..6eea123b0 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; +using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; using RGB.NET.Core; @@ -37,6 +38,9 @@ namespace Artemis.Core Profile = Parent.Profile; Name = name; Suspended = false; + Scripts = new List(); + ScriptConfigurations = new List(); + _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -60,6 +64,9 @@ namespace Artemis.Core Profile = profile; Parent = parent; + Scripts = new List(); + ScriptConfigurations = new List(); + _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -75,6 +82,16 @@ namespace Artemis.Core /// public ReadOnlyCollection Leds => _leds.AsReadOnly(); + /// + /// Gets a collection of all active scripts assigned to this layer + /// + public List Scripts { get; } + + /// + /// Gets a collection of all script configurations assigned to this layer + /// + public List ScriptConfigurations { get; } + /// /// Defines the shape that is rendered by the . /// @@ -142,8 +159,10 @@ namespace Artemis.Core if (LayerBrush?.BaseProperties != null) result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); foreach (BaseLayerEffect layerEffect in LayerEffects) + { if (layerEffect.BaseProperties != null) result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties()); + } return result; } @@ -171,6 +190,9 @@ namespace Artemis.Core Disposed = true; + while (Scripts.Count > 1) + Scripts[0].Dispose(); + LayerBrushStore.LayerBrushAdded -= LayerBrushStoreOnLayerBrushAdded; LayerBrushStore.LayerBrushRemoved -= LayerBrushStoreOnLayerBrushRemoved; @@ -247,6 +269,11 @@ namespace Artemis.Core ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); LoadRenderElement(); Adapter.Load(); + + foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) + scriptConfiguration.Script?.Dispose(); + ScriptConfigurations.Clear(); + ScriptConfigurations.AddRange(LayerEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))); } internal override void Save() @@ -284,6 +311,13 @@ namespace Artemis.Core // Adaption hints Adapter.Save(); + LayerEntity.ScriptConfigurations.Clear(); + foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) + { + scriptConfiguration.Save(); + LayerEntity.ScriptConfigurations.Add(scriptConfiguration.Entity); + } + SaveRenderElement(); } @@ -316,6 +350,9 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Layer"); + foreach (LayerScript layerScript in Scripts) + layerScript.OnLayerUpdating(deltaTime); + UpdateDisplayCondition(); UpdateTimeline(deltaTime); @@ -323,6 +360,9 @@ namespace Artemis.Core Enable(); else if (Timeline.IsFinished) Disable(); + + foreach (LayerScript layerScript in Scripts) + layerScript.OnLayerUpdated(deltaTime); } /// @@ -473,19 +513,27 @@ namespace Artemis.Core if (LayerBrush == null) throw new ArtemisCoreException("The layer is not yet ready for rendering"); + foreach (LayerScript layerScript in Scripts) + layerScript.OnLayerRendering(canvas, bounds, layerPaint); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) baseLayerEffect.PreProcess(canvas, bounds, layerPaint); try { canvas.SaveLayer(layerPaint); - - // If a brush is a bad boy and tries to color outside the lines, ensure that its clipped off canvas.ClipPath(renderPath); + + // Restore the blend mode before doing the actual render + layerPaint.BlendMode = SKBlendMode.SrcOver; + LayerBrush.InternalRender(canvas, bounds, layerPaint); foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) baseLayerEffect.PostProcess(canvas, bounds, layerPaint); + + foreach (LayerScript layerScript in Scripts) + layerScript.OnLayerRendered(canvas, bounds, layerPaint); } finally @@ -500,9 +548,7 @@ namespace Artemis.Core throw new ObjectDisposedException("Layer"); if (!Leds.Any()) - { Path = new SKPath(); - } else { SKPath path = new() {FillType = SKPathFillType.Winding}; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index c9bdf5872..76b787f1d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -23,6 +24,16 @@ namespace Artemis.Core /// LayerPropertyGroup LayerPropertyGroup { get; } + /// + /// Gets a collection of all active scripts assigned to this layer property + /// + List Scripts { get; } + + /// + /// Gets a collection of all script configurations assigned to this layer property + /// + public List ScriptConfigurations { get; } + /// /// Gets the unique path of the property on the layer /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index d9ad40690..3bbb7f557 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -30,6 +31,8 @@ namespace Artemis.Core Path = null!; Entity = null!; PropertyDescription = null!; + Scripts = new List(); + ScriptConfigurations = new List(); CurrentValue = default!; DefaultValue = default!; @@ -55,15 +58,15 @@ namespace Artemis.Core /// protected virtual void Dispose(bool disposing) { - if (disposing) - { - _disposed = true; + _disposed = true; - foreach (IDataBinding dataBinding in _dataBindings) - dataBinding.Dispose(); + while (Scripts.Count > 1) + Scripts[0].Dispose(); - Disposed?.Invoke(this, EventArgs.Empty); - } + foreach (IDataBinding dataBinding in _dataBindings) + dataBinding.Dispose(); + + Disposed?.Invoke(this, EventArgs.Empty); } /// @@ -150,6 +153,12 @@ namespace Artemis.Core /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } + /// + public List Scripts { get; } + + /// + public List ScriptConfigurations { get; } + /// public string Path { get; private set; } @@ -162,12 +171,18 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("LayerProperty"); + foreach (PropertyScript propertyScript in Scripts) + propertyScript.OnPropertyUpdating(timeline.Delta.TotalSeconds); + CurrentValue = BaseValue; UpdateKeyframes(timeline); UpdateDataBindings(timeline); OnUpdated(); + + foreach (PropertyScript propertyScript in Scripts) + propertyScript.OnPropertyUpdated(timeline.Delta.TotalSeconds); } /// @@ -747,6 +762,11 @@ namespace Artemis.Core if (dataBinding != null) _dataBindings.Add(dataBinding); } + + foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) + scriptConfiguration.Script?.Dispose(); + ScriptConfigurations.Clear(); + ScriptConfigurations.AddRange(Entity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))); } /// @@ -768,6 +788,13 @@ namespace Artemis.Core Entity.DataBindingEntities.Clear(); foreach (IDataBinding dataBinding in _dataBindings) dataBinding.Save(); + + Entity.ScriptConfigurations.Clear(); + foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) + { + scriptConfiguration.Save(); + Entity.ScriptConfigurations.Add(scriptConfiguration.Entity); + } } /// diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 3107d0805..75ec5d66b 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using SkiaSharp; @@ -13,13 +14,15 @@ namespace Artemis.Core { private readonly object _lock = new(); private bool _isFreshImport; - + internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { Configuration = configuration; Profile = this; ProfileEntity = profileEntity; EntityId = profileEntity.Id; + Scripts = new List(); + ScriptConfigurations = new List(); UndoStack = new Stack(); RedoStack = new Stack(); @@ -32,6 +35,17 @@ namespace Artemis.Core /// public ProfileConfiguration Configuration { get; } + /// + /// Gets a collection of all active scripts assigned to this profile + /// + public List Scripts { get; } + + /// + /// Gets a collection of all script configurations assigned to this profile + /// + public List ScriptConfigurations { get; } + + /// /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it /// since import @@ -62,8 +76,14 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Profile"); + foreach (ProfileScript profileScript in Scripts) + profileScript.OnProfileUpdating(deltaTime); + foreach (ProfileElement profileElement in Children) profileElement.Update(deltaTime); + + foreach (ProfileScript profileScript in Scripts) + profileScript.OnProfileUpdated(deltaTime); } } @@ -75,8 +95,14 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Profile"); + foreach (ProfileScript profileScript in Scripts) + profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds); + foreach (ProfileElement profileElement in Children) profileElement.Render(canvas, basePosition); + + foreach (ProfileScript profileScript in Scripts) + profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds); } } @@ -125,6 +151,9 @@ namespace Artemis.Core if (!disposing) return; + while (Scripts.Count > 1) + Scripts[0].Dispose(); + foreach (ProfileElement profileElement in Children) profileElement.Dispose(); ChildrenList.Clear(); @@ -157,6 +186,11 @@ namespace Artemis.Core AddChild(new Folder(this, this, rootFolder)); } } + + foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) + scriptConfiguration.Script?.Dispose(); + ScriptConfigurations.Clear(); + ScriptConfigurations.AddRange(ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))); } internal override void Save() @@ -176,6 +210,13 @@ namespace Artemis.Core ProfileEntity.Layers.Clear(); ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity)); + + ProfileEntity.ScriptConfigurations.Clear(); + foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) + { + scriptConfiguration.Save(); + ProfileEntity.ScriptConfigurations.Add(scriptConfiguration.Entity); + } } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs new file mode 100644 index 000000000..10095bf4f --- /dev/null +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs @@ -0,0 +1,86 @@ +using Artemis.Storage.Entities.General; + +namespace Artemis.Core.ScriptingProviders +{ + /// + /// Represents the configuration of a script + /// + public class ScriptConfiguration : CorePropertyChanged, IStorageModel + { + private string _scriptingProviderId; + private string _name; + private string? _scriptContent; + + /// + /// Creates a new instance of the class + /// + public ScriptConfiguration(ScriptingProvider provider, string name) + { + ScriptingProviderId = provider.Id; + Name = name; + Entity = new ScriptConfigurationEntity(); + } + + internal ScriptConfiguration(ScriptConfigurationEntity entity) + { + ScriptingProviderId = null!; + Name = null!; + Entity = entity; + + Load(); + } + + /// + /// Gets or sets the ID of the scripting provider + /// + public string ScriptingProviderId + { + get => _scriptingProviderId; + set => SetAndNotify(ref _scriptingProviderId, value); + } + + /// + /// Gets or sets the name of the script + /// + public string Name + { + get => _name; + set => SetAndNotify(ref _name, value); + } + + /// + /// Gets or sets the script's content + /// + public string? ScriptContent + { + get => _scriptContent; + set => SetAndNotify(ref _scriptContent, value); + } + + /// + /// If active, gets the script + /// + public Script? Script { get; internal set; } + + internal ScriptConfigurationEntity Entity { get; } + + #region Implementation of IStorageModel + + /// + public void Load() + { + ScriptingProviderId = Entity.ScriptingProviderId; + ScriptContent = Entity.ScriptContent; + Name = Entity.Name; + } + + /// + public void Save() + { + Entity.ScriptingProviderId = ScriptingProviderId; + Entity.ScriptContent = ScriptContent; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs new file mode 100644 index 000000000..26baa7362 --- /dev/null +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Artemis.Core.ScriptingProviders +{ + /// + /// Allows you to implement and register your own scripting provider. + /// + public abstract class ScriptingProvider : ScriptingProvider + where TGlobalScript : GlobalScript + where TProfileScript : ProfileScript + where TLayerScript : LayerScript + where TPropertyScript : PropertyScript + { + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public abstract IScriptEditorViewModel CreateGlobalScriptEditor(TGlobalScript script); + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public abstract IScriptEditorViewModel CreateProfileScriptEditor(TProfileScript script); + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public abstract IScriptEditorViewModel CreateLayerScriptScriptEditor(TLayerScript script); + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public abstract IScriptEditorViewModel CreatePropertyScriptEditor(TPropertyScript script); + + #region Overrides of ScriptingProvider + + /// + internal override Type GlobalScriptType => typeof(TGlobalScript); + + /// + internal override Type ProfileScriptType => typeof(TProfileScript); + + /// + internal override Type LayerScriptType => typeof(TLayerScript); + + /// + internal override Type PropertyScriptType => typeof(TPropertyScript); + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public override IScriptEditorViewModel CreateGlobalScriptEditor(GlobalScript script) + { + if (script == null) throw new ArgumentNullException(nameof(script)); + if (script.GetType() != GlobalScriptType) + throw new ArtemisCoreException($"This scripting provider only supports global scripts of type {GlobalScriptType.Name}"); + + return CreateGlobalScriptEditor((TGlobalScript) script); + } + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public override IScriptEditorViewModel CreateProfileScriptEditor(ProfileScript script) + { + if (script == null) throw new ArgumentNullException(nameof(script)); + if (script.GetType() != ProfileScriptType) + throw new ArtemisCoreException($"This scripting provider only supports profile scripts of type {ProfileScriptType.Name}"); + + return CreateProfileScriptEditor((TProfileScript) script); + } + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public override IScriptEditorViewModel CreateLayerScriptScriptEditor(LayerScript script) + { + if (script == null) throw new ArgumentNullException(nameof(script)); + if (script.GetType() != LayerScriptType) + throw new ArtemisCoreException($"This scripting provider only supports layer scripts of type {LayerScriptType.Name}"); + + return CreateLayerScriptScriptEditor((TLayerScript) script); + } + + /// + /// Called when the UI needs a script editor for a + /// + /// The script the editor must edit + public override IScriptEditorViewModel CreatePropertyScriptEditor(PropertyScript script) + { + if (script == null) throw new ArgumentNullException(nameof(script)); + if (script.GetType() != PropertyScriptType) + throw new ArtemisCoreException($"This scripting provider only supports property scripts of type {PropertyScriptType.Name}"); + + return CreatePropertyScriptEditor((TPropertyScript) script); + } + + #endregion + + #region Overrides of PluginFeature + + /// + internal override void InternalDisable() + { + base.InternalDisable(); + + while (Scripts.Count > 0) + Scripts[0].Dispose(); + } + + #endregion + } + + /// + /// Allows you to implement and register your own scripting provider. + /// + /// Note: You can't implement this, implement + /// instead. + /// + /// + public abstract class ScriptingProvider : PluginFeature + { + internal abstract Type GlobalScriptType { get; } + internal abstract Type PropertyScriptType { get; } + internal abstract Type LayerScriptType { get; } + internal abstract Type ProfileScriptType { get; } + internal List