using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core; /// /// Represents a profile containing folders and layers /// public sealed class Profile : ProfileElement { private readonly object _lock = new(); private readonly ObservableCollection _scriptConfigurations; private readonly ObservableCollection _scripts; private bool _isFreshImport; private ProfileElement? _lastSelectedProfileElement; internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { _scripts = new ObservableCollection(); _scriptConfigurations = new ObservableCollection(); Opacity = 0d; ShouldDisplay = true; Configuration = configuration; Profile = this; ProfileEntity = profileEntity; EntityId = profileEntity.Id; Exceptions = new List(); Scripts = new ReadOnlyObservableCollection(_scripts); ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations); Load(); } /// /// Gets the profile configuration of this profile /// public ProfileConfiguration Configuration { get; } /// /// Gets a collection of all active scripts assigned to this profile /// public ReadOnlyObservableCollection Scripts { get; } /// /// Gets a collection of all script configurations assigned to this profile /// public ReadOnlyObservableCollection 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 /// /// Note: As long as this is , profile adaption will be performed on load and any surface /// changes /// /// public bool IsFreshImport { get => _isFreshImport; set => SetAndNotify(ref _isFreshImport, value); } /// /// Gets or sets the last selected profile element of this profile /// public ProfileElement? LastSelectedProfileElement { get => _lastSelectedProfileElement; set => SetAndNotify(ref _lastSelectedProfileElement, value); } /// /// Gets the profile entity this profile uses for persistent storage /// public ProfileEntity ProfileEntity { get; internal set; } internal List Exceptions { get; } internal bool ShouldDisplay { get; set; } internal double Opacity { get; private set; } /// public override void Update(double deltaTime) { lock (_lock) { 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); const double OPACITY_PER_SECOND = 1; if (ShouldDisplay && Opacity < 1) Opacity = Math.Clamp(Opacity + OPACITY_PER_SECOND * deltaTime, 0d, 1d); if (!ShouldDisplay && Opacity > 0) Opacity = Math.Clamp(Opacity - OPACITY_PER_SECOND * deltaTime, 0d, 1d); } } /// public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus) { lock (_lock) { if (Disposed) throw new ObjectDisposedException("Profile"); foreach (ProfileScript profileScript in Scripts) profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds); SKPaint? opacityPaint = null; bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1; if (applyOpacityLayer) { opacityPaint = new SKPaint(); opacityPaint.Color = new SKColor(0, 0, 0, (byte)(255d * Easings.CubicEaseInOut(Opacity))); canvas.SaveLayer(opacityPaint); } foreach (ProfileElement profileElement in Children) profileElement.Render(canvas, basePosition, editorFocus); if (applyOpacityLayer) { canvas.Restore(); opacityPaint?.Dispose(); } 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); } } /// public override void Reset() { foreach (ProfileElement child in Children) child.Reset(); } /// /// Retrieves the root folder of this profile /// /// The root folder of the profile /// public Folder GetRootFolder() { if (Disposed) throw new ObjectDisposedException("Profile"); return (Folder) Children.Single(); } /// public override string ToString() { return $"[Profile] {nameof(Name)}: {Name}"; } /// /// Populates all the LEDs on the elements in this profile /// /// The devices to use while populating LEDs public void PopulateLeds(IEnumerable devices) { if (Disposed) throw new ObjectDisposedException("Profile"); foreach (Layer layer in GetAllLayers()) layer.PopulateLeds(devices); } #region Overrides of BreakableModel /// public override IEnumerable GetBrokenHierarchy() { return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy()); } #endregion /// protected override void Dispose(bool disposing) { if (!disposing) return; while (Scripts.Count > 0) RemoveScript(Scripts[0]); foreach (ProfileElement profileElement in Children) profileElement.Dispose(); ChildrenList.Clear(); Disposed = true; } internal override void Load() { if (Disposed) throw new ObjectDisposedException("Profile"); Name = Configuration.Name; IsFreshImport = ProfileEntity.IsFreshImport; lock (ChildrenList) { // Remove the old profile tree foreach (ProfileElement profileElement in Children) profileElement.Dispose(); ChildrenList.Clear(); // Populate the profile starting at the root, the rest is populated recursively FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId); if (rootFolder == null) AddChild(new Folder(this, "Root folder")); else AddChild(new Folder(this, this, rootFolder)); } List renderElements = GetAllRenderElements(); if (ProfileEntity.LastSelectedProfileElement != Guid.Empty) LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); else LastSelectedProfileElement = null; while (_scriptConfigurations.Any()) RemoveScriptConfiguration(_scriptConfigurations[0]); foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))) AddScriptConfiguration(scriptConfiguration); // Load node scripts last since they may rely on the profile structure being in place foreach (RenderProfileElement renderProfileElement in renderElements) renderProfileElement.LoadNodeScript(); } /// /// Removes a script configuration from the profile, if the configuration has an active script it is also removed. /// internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration) { if (!_scriptConfigurations.Contains(scriptConfiguration)) return; Script? script = scriptConfiguration.Script; if (script != null) RemoveScript((ProfileScript) script); _scriptConfigurations.Remove(scriptConfiguration); } /// /// Adds a script configuration to the profile but does not instantiate it's script. /// internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration) { if (!_scriptConfigurations.Contains(scriptConfiguration)) _scriptConfigurations.Add(scriptConfiguration); } /// /// Adds a script that has a script configuration belonging to this profile. /// internal void AddScript(ProfileScript script) { if (!_scriptConfigurations.Contains(script.ScriptConfiguration)) throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile."); if (!_scripts.Contains(script)) _scripts.Add(script); } /// /// Removes a script from the profile and disposes it. /// internal void RemoveScript(ProfileScript script) { _scripts.Remove(script); script.Dispose(); } internal override void Save() { if (Disposed) throw new ObjectDisposedException("Profile"); ProfileEntity.Id = EntityId; ProfileEntity.Name = Configuration.Name; ProfileEntity.IsFreshImport = IsFreshImport; ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty; foreach (ProfileElement profileElement in Children) profileElement.Save(); ProfileEntity.Folders.Clear(); ProfileEntity.Folders.AddRange(GetAllFolders().Select(f => f.FolderEntity)); 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); } } }