diff --git a/.gitattributes b/.gitattributes index 1ff0c4230..0753195f4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -61,3 +61,6 @@ #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain + +# Display axaml files with XML highlighting +*.axaml linguist-language=xml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 10351521f..f25a67c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -196,3 +196,5 @@ FakesAssemblies/ src/Artemis\.Storage/Storage\.db !src/Artemis.UI/screens/Settings/Debug docfx/docfx_project/_site/ + +src/.idea/ diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 4ebae1d56..6b680e70c 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -4,124 +4,118 @@ # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: -- master + - master pr: none resources: repositories: - - repository: Plugins - type: github - endpoint: github.com_SpoinkyNL - name: Artemis-RGB/Artemis.Plugins - ref: master + - repository: Plugins + type: github + endpoint: github.com_SpoinkyNL + name: Artemis-RGB/Artemis.Plugins + ref: master pool: - vmImage: 'windows-latest' + vmImage: "windows-latest" variables: - artemisSolution: '**/Artemis.sln' - pluginProjects: '**/Artemis.Plugins.*.csproj' + windowsProject: "**/Artemis.UI.Windows/Artemis.UI.Windows.csproj" + pluginProjects: "**/Artemis.Plugins.*.csproj" BuildId: $(Build.BuildId) BuildNumber: $(Build.BuildNumber) SourceBranch: $(Build.SourceBranch) SourceVersion: $(Build.SourceVersion) - -steps: -- checkout: self - path: s/Artemis -- checkout: Plugins - path: s/Artemis.Plugins -- task: DotNetCoreCLI@2 - displayName: 'Artemis - Publish' - inputs: - command: 'publish' - publishWebProjects: false - projects: '$(artemisSolution)' - arguments: '--runtime win-x64 --self-contained false --configuration Release --output $(Build.ArtifactStagingDirectory)/build /nowarn:cs1591' - zipAfterPublish: false - modifyOutputPath: false +jobs: + - job: Windows + steps: + - checkout: self + path: s/Artemis + - checkout: Plugins + path: s/Artemis.Plugins -- task: PowerShell@2 - displayName: 'Artemis - Create buildinfo.json' - inputs: - targetType: 'inline' - script: | - $OFS = "`r`n" - SET-Content -Path 'buildinfo.json' -Value ('{' + $OFS + ' "BuildId": 0,' + $OFS + ' "BuildNumber": 0.0,' + $OFS + ' "SourceBranch": "",' + $OFS + ' "SourceVersion": ""' + $OFS + '}') - workingDirectory: '$(Build.ArtifactStagingDirectory)/build' + - task: DotNetCoreCLI@2 + displayName: "Artemis - Publish" + inputs: + command: "publish" + publishWebProjects: false + projects: "$(windowsProject)" + arguments: '--configuration Release --runtime win10-x64 --output $(Build.ArtifactStagingDirectory)/windows-build /nowarn:cs1591' + zipAfterPublish: false + modifyOutputPath: false -- task: FileTransform@1 - displayName: 'Artemis - Populate buildinfo.json' - inputs: - folderPath: '$(Build.ArtifactStagingDirectory)/build' - fileType: 'json' - targetFiles: '**/buildinfo.json' + - task: PowerShell@2 + displayName: "Artemis - Create buildinfo.json" + inputs: + targetType: "inline" + script: | + $OFS = "`r`n" + SET-Content -Path 'buildinfo.json' -Value ('{' + $OFS + ' "BuildId": 0,' + $OFS + ' "BuildNumber": 0.0,' + $OFS + ' "SourceBranch": "",' + $OFS + ' "SourceVersion": ""' + $OFS + '}') + workingDirectory: "$(Build.ArtifactStagingDirectory)/windows-build" -# Copy Artemis binaries to where plugin projects expect them -- task: CopyFiles@2 - displayName: 'Plugins - Prepare Artemis binaries' - inputs: - SourceFolder: '$(Build.ArtifactStagingDirectory)/build' - Contents: '**' - TargetFolder: 'Artemis/src/Artemis.UI/bin/net5.0-windows' + - task: FileTransform@1 + displayName: "Artemis - Populate buildinfo.json" + inputs: + folderPath: "$(Build.ArtifactStagingDirectory)/windows-build" + fileType: "json" + targetFiles: "**/buildinfo.json" -- task: PowerShell@2 - displayName: 'Plugins - Insert build number into plugin.json' - inputs: - targetType: 'inline' - script: | - Get-ChildItem -Recurse -Filter plugin.json | - Foreach-Object { - $buidNumber = "1.0.1." + $Env:BUILD_BUILDID; - $a = Get-Content $_.FullName | ConvertFrom-Json - $a.Version = $buidNumber; - $a | ConvertTo-Json | Set-Content $_.FullName - } - workingDirectory: 'Artemis.Plugins' + - task: PowerShell@2 + displayName: "Plugins - Insert build number into plugin.json" + inputs: + targetType: "inline" + script: | + Get-ChildItem -Recurse -Filter plugin.json | + Foreach-Object { + $buidNumber = "1.0.1." + $Env:BUILD_BUILDID; + $a = Get-Content $_.FullName | ConvertFrom-Json + $a.Version = $buidNumber; + $a | ConvertTo-Json | Set-Content $_.FullName + } + workingDirectory: "Artemis.Plugins" -- task: DotNetCoreCLI@2 - displayName: 'Plugins - Publish' - inputs: - command: 'publish' - publishWebProjects: false - arguments: '--runtime win-x64 --configuration Release --self-contained false --output $(Build.ArtifactStagingDirectory)/build/Plugins' - projects: '$(pluginProjects)' - zipAfterPublish: true + - task: DotNetCoreCLI@2 + displayName: "Plugins - Publish" + inputs: + command: "publish" + publishWebProjects: false + arguments: "--configuration Release --runtime win10-x64 --output $(Build.ArtifactStagingDirectory)/windows-build/Plugins" + projects: "$(pluginProjects)" + zipAfterPublish: true -- task: PublishPipelineArtifact@1 - displayName: 'Upload build to Azure Pipelines' - inputs: - targetPath: '$(Build.ArtifactStagingDirectory)/build' - artifact: 'Artemis build' - publishLocation: 'pipeline' + - task: PublishPipelineArtifact@1 + displayName: "Upload build to Azure Pipelines" + inputs: + targetPath: "$(Build.ArtifactStagingDirectory)/windows-build" + artifact: "Artemis build" + publishLocation: "pipeline" -- task: ArchiveFiles@2 - displayName: 'ZIP binaries' - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/build' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/archive/artemis-build.zip' - replaceExistingArchive: true + - task: ArchiveFiles@2 + displayName: "ZIP binaries" + inputs: + rootFolderOrFile: "$(Build.ArtifactStagingDirectory)/windows-build" + includeRootFolder: false + archiveType: "zip" + archiveFile: "$(Build.ArtifactStagingDirectory)/archive/artemis-build-windows.zip" + replaceExistingArchive: true -- task: PowerShell@2 - displayName: 'Calculate ZIP hash' - inputs: - targetType: 'inline' - script: '(Get-FileHash .\artemis-build.zip).Hash | Out-File -FilePath .\hash.txt' - workingDirectory: '$(Build.ArtifactStagingDirectory)/archive' + - task: PowerShell@2 + displayName: "Calculate ZIP hash" + inputs: + targetType: "inline" + script: '(Get-FileHash .\artemis-build-windows.zip).Hash | Out-File -FilePath .\hash-windows.txt' + workingDirectory: "$(Build.ArtifactStagingDirectory)/archive" -- task: FtpUpload@2 - displayName: 'Upload binaries to FTP' - inputs: - credentialsOption: 'inputs' - serverUrl: 'ftp://artemis-rgb.com' - username: 'devops' - password: '$(ftp_password)' - rootDirectory: '$(Build.ArtifactStagingDirectory)/archive' - filePatterns: '**' - remoteDirectory: '/builds.artemis-rgb.com/binaries/$(Build.SourceBranchName)/$(Build.BuildNumber)' - clean: false - preservePaths: true - trustSSL: false \ No newline at end of file + - task: FtpUpload@2 + displayName: "Upload binaries to FTP" + inputs: + credentialsOption: "inputs" + serverUrl: "ftp://artemis-rgb.com" + username: "devops" + password: "$(ftp_password)" + rootDirectory: "$(Build.ArtifactStagingDirectory)/archive" + filePatterns: "**" + remoteDirectory: "/builds.artemis-rgb.com/binaries/$(Build.SourceBranchName)/$(Build.BuildNumber)" + clean: false + preservePaths: true + trustSSL: false diff --git a/src/Artemis.ConsoleUI/Artemis.UI.Console.csproj b/src/Artemis.ConsoleUI/Artemis.UI.Console.csproj deleted file mode 100644 index 145ff4a6a..000000000 --- a/src/Artemis.ConsoleUI/Artemis.UI.Console.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - exe - net5.0 - x64 - Artemis.UI.Console - Artemis.UI.Console - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.ConsoleUI/Program.cs b/src/Artemis.ConsoleUI/Program.cs deleted file mode 100644 index 6d694d8d6..000000000 --- a/src/Artemis.ConsoleUI/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using Artemis.Core; -using Artemis.Core.Ninject; -using Artemis.Core.Services; -using Artemis.Storage; -using Ninject; - -namespace Artemis.UI.Console -{ - /// - /// This is just a little experiment to show that Artemis can run without the UI and even on other OSes - /// Some notes - /// - Any plugin relying on WPF and/or Artemis.UI.Shared won't load - /// - There is no input provider so key-press events and brushes won't work - /// - Device providers using Windows SDKs won't work, OpenRGB will though! - /// - You may need to fiddle around to get SkiaSharp binaries going - /// - There is no UI obviously - /// - internal class Program - { - private static readonly AutoResetEvent Closing = new(false); - - protected static void OnExit(object sender, ConsoleCancelEventArgs args) - { - Closing.Set(); - } - - private static void Main(string[] args) - { - StorageManager.CreateBackup(Constants.DataFolder); - - Utilities.PrepareFirstLaunch(); - Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; - StandardKernel kernel = new() {Settings = {InjectNonPublic = true}}; - kernel.Load(); - - ICoreService core = kernel.Get(); - core.StartupArguments = args.ToList(); - core.IsElevated = false; - core.Initialize(); - - System.Console.CancelKeyPress += OnExit; - Closing.WaitOne(); - } - - private static void UtilitiesOnShutdownRequested(object sender, EventArgs e) - { - Closing.Set(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml b/src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml deleted file mode 100644 index 36fac7487..000000000 --- a/src/Artemis.ConsoleUI/Properties/PublishProfiles/Linux.pubxml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - Release - x64 - bin\publish\linux - FileSystem - net5.0 - linux-x64 - false - False - - \ No newline at end of file diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 57ca5d067..985ec0b61 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -1,79 +1,78 @@  - - net5.0 - false - false - Artemis.Core - Artemis Core - Copyright © Robert Beekman - 2020 - bin\ - x64 - - - x64 - bin\Artemis.Core.xml - - 5 - + + net6.0 + false + false + Artemis.Core + Artemis Core + Copyright © Robert Beekman - 2020 + bin\ + x64 + + + x64 + bin\Artemis.Core.xml + + 5 + - - 1.0-{chash:6} - true - true - true - v[0-9]* - true - git - true - enable - latest - + + 1.0-{chash:6} + true + true + true + v[0-9]* + true + git + true + enable + latest + - - bin\Artemis.Core.xml - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - PreserveNewest - - + + bin\Artemis.Core.xml + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + PreserveNewest + + - - - PreserveNewest - - + + + PreserveNewest + + \ No newline at end of file diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 31cbebcb3..a18836e5e 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -1,5 +1,6 @@  True + True True True True @@ -43,12 +44,15 @@ True True True + True True True True True True True + True + True True True True @@ -63,6 +67,7 @@ True True True + True True True True @@ -71,6 +76,9 @@ True True True + True + True + True True True True @@ -83,4 +91,6 @@ True True True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 7921af0bc..69208acb3 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using Artemis.Core.JsonConverters; using Artemis.Core.Services; using Artemis.Core.Services.Core; @@ -14,6 +15,11 @@ namespace Artemis.Core /// public static class Constants { + /// + /// The Artemis.Core assembly + /// + public static readonly Assembly CoreAssembly = typeof(Constants).Assembly; + /// /// The full path to the Artemis application folder /// @@ -24,10 +30,30 @@ namespace Artemis.Core /// public static readonly string ExecutablePath = Utilities.GetCurrentLocation(); + /// + /// The base path for Artemis application data folder + /// + public static readonly string BaseFolder = Environment.GetFolderPath(OperatingSystem.IsWindows() ? Environment.SpecialFolder.CommonApplicationData : Environment.SpecialFolder.LocalApplicationData); + /// /// The full path to the Artemis data folder /// - public static readonly string DataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Artemis"); + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); + + /// + /// The full path to the Artemis logs folder + /// + public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); + + /// + /// The full path to the Artemis plugins folder + /// + public static readonly string PluginsFolder = Path.Combine(DataFolder, "Plugins"); + + /// + /// The full path to the Artemis user layouts folder + /// + public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts"); /// /// The plugin info used by core components of Artemis @@ -62,13 +88,13 @@ namespace Artemis.Core internal static JsonSerializerSettings JsonConvertSettings = new() { - Converters = new List {new SKColorConverter(), new ForgivingIntConverter()} + Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} }; internal static JsonSerializerSettings JsonConvertTypedSettings = new() { TypeNameHandling = TypeNameHandling.All, - Converters = new List {new SKColorConverter(), new ForgivingIntConverter()} + Converters = new List {new SKColorConverter(), new NumericJsonConverter(), new ForgivingIntConverter()} }; /// diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumContainsConditionOperator.cs deleted file mode 100644 index dd79f14cb..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumContainsConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class EnumContainsConditionOperator : ConditionOperator - { - public override string Description => "Contains"; - public override string Icon => "Contain"; - - public override bool Evaluate(Enum a, Enum b) - { - return a != null && b != null && a.HasFlag(b); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumNotContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumNotContainsConditionOperator.cs deleted file mode 100644 index ef78b8470..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EnumNotContainsConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class EnumNotContainsConditionOperator : ConditionOperator - { - public override string Description => "Does not contain"; - public override string Icon => "FormatStrikethrough"; - - public override bool Evaluate(Enum a, Enum b) - { - return a != null && (b == null || !a.HasFlag(b)); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs deleted file mode 100644 index 789f86d63..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/EqualsConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class EqualsConditionOperator : ConditionOperator - { - public override string Description => "Equals"; - public override string Icon => "Equal"; - - public override bool Evaluate(object a, object b) - { - return Equals(a, b); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs deleted file mode 100644 index f9a898107..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class GreaterThanConditionOperator : ConditionOperator - { - public override string Description => "Is greater than"; - public override string Icon => "GreaterThan"; - - public override bool Evaluate(double a, double b) - { - return a > b; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs deleted file mode 100644 index f0a1bf2b9..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/GreaterThanOrEqualConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class GreaterThanOrEqualConditionOperator : ConditionOperator - { - public override string Description => "Is greater than or equal to"; - public override string Icon => "GreaterThanOrEqual"; - - public override bool Evaluate(double a, double b) - { - return a >= b; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs deleted file mode 100644 index 720cd7c2c..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class LessThanConditionOperator : ConditionOperator - { - public override string Description => "Is less than"; - public override string Icon => "LessThan"; - - public override bool Evaluate(double a, double b) - { - return a < b; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs deleted file mode 100644 index 925a4afaa..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/LessThanOrEqualConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class LessThanOrEqualConditionOperator : ConditionOperator - { - public override string Description => "Is less than or equal to"; - public override string Icon => "LessThanOrEqual"; - - public override bool Evaluate(double a, double b) - { - return a <= b; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs deleted file mode 100644 index 4eeb8441a..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotEqualConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class NotEqualConditionOperator : ConditionOperator - { - public override string Description => "Does not equal"; - public override string Icon => "NotEqualVariant"; - - public override bool Evaluate(object a, object b) - { - return !Equals(a, b); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs deleted file mode 100644 index 0d8599df7..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NotNullConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class NotNullConditionOperator : ConditionOperator - { - public override string Description => "Is not null"; - public override string Icon => "CheckboxMarkedCircleOutline"; - - public override bool Evaluate(object a) - { - return a != null; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs deleted file mode 100644 index 46302a5ff..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NullConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class NullConditionOperator : ConditionOperator - { - public override string Description => "Is null"; - public override string Icon => "Null"; - - public override bool Evaluate(object a) - { - return a == null; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberEqualsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberEqualsConditionOperator.cs deleted file mode 100644 index d32104262..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberEqualsConditionOperator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class NumberEqualsConditionOperator : ConditionOperator - { - public override string Description => "Equals"; - public override string Icon => "Equal"; - - public override bool Evaluate(double a, double b) - { - // Numbers can be tricky, an epsilon like this is close enough - return Math.Abs(a - b) < 0.000001; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberNotEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberNotEqualConditionOperator.cs deleted file mode 100644 index c0c29bb7a..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/NumberNotEqualConditionOperator.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class NumberNotEqualConditionOperator : ConditionOperator - { - public override string Description => "Does not equal"; - public override string Icon => "NotEqualVariant"; - - public override bool Evaluate(double a, double b) - { - // Numbers can be tricky, an epsilon like this is close enough - return Math.Abs(a - b) > 0.000001; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs deleted file mode 100644 index 7df8eb9dc..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringContainsConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class StringContainsConditionOperator : ConditionOperator - { - public override string Description => "Contains"; - public override string Icon => "Contain"; - - public override bool Evaluate(string a, string b) - { - return a != null && b != null && a.Contains(b, StringComparison.InvariantCultureIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs deleted file mode 100644 index b8f5f91d0..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEndsWithConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class StringEndsWithConditionOperator : ConditionOperator - { - public override string Description => "Ends with"; - public override string Icon => "ContainEnd"; - - public override bool Evaluate(string a, string b) - { - return a != null && b != null && a.EndsWith(b, StringComparison.InvariantCultureIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs deleted file mode 100644 index 970f96806..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringEqualsConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class StringEqualsConditionOperator : ConditionOperator - { - public override string Description => "Equals"; - public override string Icon => "Equal"; - - public override bool Evaluate(string a, string b) - { - return string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringMatchesRegexConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringMatchesRegexConditionOperator.cs deleted file mode 100644 index ada76f5f0..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringMatchesRegexConditionOperator.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Artemis.Core { - internal class StringMatchesRegexConditionOperator : ConditionOperator - { - public override string Description => "Matches Regex"; - public override string Icon => "Regex"; - - public override bool Evaluate(string text, string regex) - { - // Ensures full match - if (!regex.StartsWith("^")) - regex = "^" + regex; - if (!regex.EndsWith("$")) - regex += "$"; - - return Regex.IsMatch(text, regex); - } - } -} diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs deleted file mode 100644 index 1b683710b..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotContainsConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class StringNotContainsConditionOperator : ConditionOperator - { - public override string Description => "Does not contain"; - public override string Icon => "FormatStrikethrough"; - - public override bool Evaluate(string a, string b) - { - return a != null && (b == null || !a.Contains(b, StringComparison.InvariantCultureIgnoreCase)); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs deleted file mode 100644 index f51b63c4b..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotEqualConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class StringNotEqualConditionOperator : ConditionOperator - { - public override string Description => "Does not equal"; - public override string Icon => "NotEqualVariant"; - - public override bool Evaluate(string a, string b) - { - return !string.Equals(a, b, StringComparison.InvariantCultureIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotNullConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotNullConditionOperator.cs deleted file mode 100644 index 3b7c7ac7a..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNotNullConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class StringNotNullConditionOperator : ConditionOperator - { - public override string Description => "Is not null"; - public override string Icon => "CheckboxMarkedCircleOutline"; - - public override bool Evaluate(string a) - { - return !string.IsNullOrWhiteSpace(a); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNullConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNullConditionOperator.cs deleted file mode 100644 index 84d1f6775..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringNullConditionOperator.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class StringNullConditionOperator : ConditionOperator - { - public override string Description => "Is null"; - public override string Icon => "Null"; - - public override bool Evaluate(string a) - { - return string.IsNullOrWhiteSpace(a); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs b/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs deleted file mode 100644 index d43b6af20..000000000 --- a/src/Artemis.Core/DefaultTypes/Conditions/Operators/StringStartsWithConditionOperator.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class StringStartsWithConditionOperator : ConditionOperator - { - public override string Description => "Starts with"; - public override string Icon => "ContainStart"; - - public override bool Evaluate(string a, string b) - { - return a != null && b != null && a.StartsWith(b, StringComparison.InvariantCultureIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs deleted file mode 100644 index 75818ac95..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/FloatDataBindingConverter.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - public class FloatDataBindingConverter : FloatDataBindingConverter - { - } - - /// - /// The type of layer property this converter is applied to - public class FloatDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public FloatDataBindingConverter() - { - SupportsSum = true; - SupportsInterpolate = true; - } - - /// - public override float Sum(float a, float b) - { - return a + b; - } - - /// - public override float Interpolate(float a, float b, double progress) - { - float diff = b - a; - return (float) (a + diff * progress); - } - - /// - public override void ApplyValue(float value) - { - if (DataBinding!.LayerProperty.PropertyDescription.MaxInputValue is float max) - value = Math.Min(value, max); - if (DataBinding!.LayerProperty.PropertyDescription.MinInputValue is float min) - value = Math.Max(value, min); - - base.ApplyValue(value); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs deleted file mode 100644 index 584967a76..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/GeneralDataBindingConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a generic data binding converter that acts as the bridge between a - /// and a and does not support - /// sum or interpolation - /// - public class GeneralDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public GeneralDataBindingConverter() - { - SupportsSum = false; - SupportsInterpolate = false; - } - - /// - public override T Sum(T a, T b) - { - throw new NotSupportedException(); - } - - /// - public override T Interpolate(T a, T b, double progress) - { - throw new NotSupportedException(); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs deleted file mode 100644 index 795145630..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/IntDataBindingConverter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - public class IntDataBindingConverter : IntDataBindingConverter - { - } - - /// - public class IntDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public IntDataBindingConverter() - { - SupportsSum = true; - SupportsInterpolate = true; - } - - /// - /// Gets or sets the mode used for rounding during interpolation. Defaults to - /// - /// - public MidpointRounding InterpolationRoundingMode { get; set; } = MidpointRounding.AwayFromZero; - - /// - public override int Sum(int a, int b) - { - return a + b; - } - - /// - public override int Interpolate(int a, int b, double progress) - { - int diff = b - a; - return (int) Math.Round(a + diff * progress, InterpolationRoundingMode); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs deleted file mode 100644 index 6b708e088..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Converters/SKColorDataBindingConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - /// - public class SKColorDataBindingConverter : DataBindingConverter - { - /// - /// Creates a new instance of the class - /// - public SKColorDataBindingConverter() - { - SupportsInterpolate = true; - SupportsSum = true; - } - - /// - public override SKColor Sum(SKColor a, SKColor b) - { - return a.Sum(b); - } - - /// - public override SKColor Interpolate(SKColor a, SKColor b, double progress) - { - return a.Interpolate(b, (float) progress); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorBrightenModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorBrightenModifierType.cs deleted file mode 100644 index 32c4255ca..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorBrightenModifierType.cs +++ /dev/null @@ -1,18 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorBrightenModifierType : DataBindingModifierType - { - public override string Name => "Brighten by"; - public override string Icon => "CarLightHigh"; - public override string Description => "Brightens the color by the amount in percent"; - - public override SKColor Apply(SKColor currentValue, float parameterValue) - { - currentValue.ToHsl(out float h, out float s, out float l); - l *= (parameterValue + 100f) / 100f; - return SKColor.FromHsl(h, s, l); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDarkenModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDarkenModifierType.cs deleted file mode 100644 index d26993e58..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDarkenModifierType.cs +++ /dev/null @@ -1,18 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorDarkenModifierType : DataBindingModifierType - { - public override string Name => "Darken by"; - public override string Icon => "CarLightDimmed"; - public override string Description => "Darkens the color by the amount in percent"; - - public override SKColor Apply(SKColor currentValue, float parameterValue) - { - currentValue.ToHsl(out float h, out float s, out float l); - l *= (parameterValue * -1 + 100f) / 100f; - return SKColor.FromHsl(h, s, l); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDesaturateModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDesaturateModifierType.cs deleted file mode 100644 index e3d370ebd..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorDesaturateModifierType.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorDesaturateModifierType : DataBindingModifierType - { - public override string Name => "Desaturate"; - public override string Icon => "ImageMinus"; - public override string Description => "Desaturates the color by the amount in percent"; - - public override SKColor Apply(SKColor currentValue, float parameterValue) - { - currentValue.ToHsl(out float h, out float s, out float l); - s -= parameterValue; - s = Math.Clamp(s, 0, 100); - - return SKColor.FromHsl(h, s, l, currentValue.Alpha); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorInvertModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorInvertModifierType.cs deleted file mode 100644 index bc46a053d..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorInvertModifierType.cs +++ /dev/null @@ -1,18 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorInvertModifierType : DataBindingModifierType - { - public override string Name => "Invert color"; - public override string Icon => "InvertColors"; - public override string Description => "Inverts the color by rotating its hue by a 180°"; - - public override SKColor Apply(SKColor currentValue) - { - currentValue.ToHsl(out float h, out float s, out float l); - h += 180; - return SKColor.FromHsl(h % 360, s, l); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorRotateHueModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorRotateHueModifierType.cs deleted file mode 100644 index 45781ce48..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorRotateHueModifierType.cs +++ /dev/null @@ -1,18 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorRotateHueModifierType : DataBindingModifierType - { - public override string Name => "Rotate Hue by"; - public override string Icon => "RotateRight"; - public override string Description => "Rotates the hue of the color by the amount in degrees"; - - public override SKColor Apply(SKColor currentValue, float parameterValue) - { - currentValue.ToHsl(out float h, out float s, out float l); - h += parameterValue; - return SKColor.FromHsl(h % 360, s, l); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSaturateModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSaturateModifierType.cs deleted file mode 100644 index 751f96c24..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSaturateModifierType.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorSaturateModifierType : DataBindingModifierType - { - public override string Name => "Saturate"; - public override string Icon => "ImagePlus"; - public override string Description => "Saturates the color by the amount in percent"; - - public override SKColor Apply(SKColor currentValue, float parameterValue) - { - currentValue.ToHsv(out float h, out float s, out float v); - s += parameterValue; - s = Math.Clamp(s, 0, 100); - - return SKColor.FromHsv(h, s, v, currentValue.Alpha); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSumModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSumModifierType.cs deleted file mode 100644 index 9c45436df..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Colors/SKColorSumModifierType.cs +++ /dev/null @@ -1,16 +0,0 @@ -using SkiaSharp; - -namespace Artemis.Core -{ - internal class SKColorSumModifierType : DataBindingModifierType - { - public override string Name => "Combine with"; - public override string Icon => "FormatColorFill"; - public override string Description => "Adds the two colors together"; - - public override SKColor Apply(SKColor currentValue, SKColor parameterValue) - { - return currentValue.Sum(parameterValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/AbsoluteModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/AbsoluteModifierType.cs deleted file mode 100644 index c58bfd3b8..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/AbsoluteModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class AbsoluteModifierType : DataBindingModifierType - { - public override string Name => "Absolute"; - public override string Icon => "NumericPositive1"; - public override string Category => "Advanced"; - public override string Description => "Converts the input value to an absolute value"; - - public override double Apply(double currentValue) - { - return Math.Abs(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/DivideModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/DivideModifierType.cs deleted file mode 100644 index 215952095..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/DivideModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Artemis.Core -{ - internal class DivideModifierType : DataBindingModifierType - { - public override string Name => "Divide by"; - public override string Icon => "Divide"; - - public override double Apply(double currentValue, double parameterValue) - { - // Ye ye none of that - if (parameterValue == 0) - return 0; - - return currentValue / parameterValue; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MaxModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MaxModifierType.cs deleted file mode 100644 index f8da7936e..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MaxModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class MaxModifierType : DataBindingModifierType - { - public override string Name => "Max"; - public override string Icon => "ChevronUpBoxOutline"; - public override string Category => "Advanced"; - public override string Description => "Keeps only the largest of input value and parameter"; - - public override double Apply(double currentValue, double parameterValue) - { - return Math.Max(currentValue, parameterValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MinModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MinModifierType.cs deleted file mode 100644 index 5ab519e01..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MinModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class MinModifierType : DataBindingModifierType - { - public override string Name => "Min"; - public override string Icon => "ChevronDownBoxOutline"; - public override string Category => "Advanced"; - public override string Description => "Keeps only the smallest of input value and parameter"; - - public override double Apply(double currentValue, double parameterValue) - { - return Math.Min(currentValue, parameterValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/ModuloModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/ModuloModifierType.cs deleted file mode 100644 index 8b710d3bf..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/ModuloModifierType.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Artemis.Core -{ - internal class ModuloModifierType : DataBindingModifierType - { - public override string Name => "Modulo"; - public override string Icon => "Stairs"; - public override string Category => "Advanced"; - public override string Description => "Calculates the remained of the division between the input value and the parameter"; - - public override double Apply(double currentValue, double parameterValue) - { - return currentValue % parameterValue; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MultiplicationModifier.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MultiplicationModifier.cs deleted file mode 100644 index 3198777cc..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/MultiplicationModifier.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class MultiplicationModifierType : DataBindingModifierType - { - public override string Name => "Multiply by"; - public override string Icon => "Close"; - - public override double Apply(double currentValue, double parameterValue) - { - return currentValue * parameterValue; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/PercentageOfModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/PercentageOfModifierType.cs deleted file mode 100644 index 298d26e30..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/PercentageOfModifierType.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Artemis.Core -{ - internal class PercentageOfModifierType : DataBindingModifierType - { - public override string Name => "Percentage of"; - public override string Icon => "Percent"; - public override string Description => "Calculates how much percent the parameter value is of the current value"; - - public override double Apply(double currentValue, double parameterValue) - { - // Ye ye none of that - if (parameterValue == 0d) - return 100d; - - return 100d / parameterValue * currentValue; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/PowerModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/PowerModifierType.cs deleted file mode 100644 index 2ea1a0709..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/PowerModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class PowerModifierType : DataBindingModifierType - { - public override string Name => "Power"; - public override string Icon => "Exponent"; - public override string Category => "Advanced"; - public override string Description => "Raises the input value to the power of the parameter value"; - - public override double Apply(double currentValue, double parameterValue) - { - return Math.Pow(currentValue, parameterValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/CeilingModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/CeilingModifierType.cs deleted file mode 100644 index c4ca8f021..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/CeilingModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class CeilingModifierType : DataBindingModifierType - { - public override string Name => "Round up"; - public override string Icon => "ArrowUp"; - public override string Category => "Rounding"; - public override string Description => "Ceils the input, rounding it up to the nearest whole number"; - - public override double Apply(double currentValue) - { - return Math.Ceiling(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/FloorModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/FloorModifierType.cs deleted file mode 100644 index e26173049..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/FloorModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class FloorModifierType : DataBindingModifierType - { - public override string Name => "Round down"; - public override string Icon => "ArrowDown"; - public override string Category => "Rounding"; - public override string Description => "Floors the input, rounding it down to the nearest whole number"; - - public override double Apply(double currentValue) - { - return Math.Floor(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/RoundModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/RoundModifierType.cs deleted file mode 100644 index 3b3283396..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Rounding/RoundModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class RoundModifierType : DataBindingModifierType - { - public override string Name => "Round"; - public override string Icon => "ArrowCollapse"; - public override string Category => "Rounding"; - public override string Description => "Rounds the input to the nearest whole number"; - - public override double Apply(double currentValue) - { - return Math.Round(currentValue, MidpointRounding.AwayFromZero); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SquareRootModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SquareRootModifierType.cs deleted file mode 100644 index 72dfa3f55..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SquareRootModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class SquareRootModifierType : DataBindingModifierType - { - public override string Name => "Square root"; - public override string Icon => "SquareRoot"; - public override string Category => "Advanced"; - public override string Description => "Calculates square root of the input value"; - - public override double Apply(double currentValue) - { - return Math.Sqrt(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SubtractModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SubtractModifierType.cs deleted file mode 100644 index 0dcda285a..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SubtractModifierType.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class SubtractModifierType : DataBindingModifierType - { - public override string Name => "Subtract"; - public override string Icon => "Minus"; - - public override double Apply(double currentValue, double parameterValue) - { - return currentValue - parameterValue; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SumModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SumModifierType.cs deleted file mode 100644 index 3c2a816a6..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/SumModifierType.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Artemis.Core -{ - internal class SumModifierType : DataBindingModifierType - { - public override string Name => "Sum"; - public override string Icon => "Plus"; - - public override double Apply(double currentValue, double parameterValue) - { - return currentValue + parameterValue; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CosecantModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CosecantModifierType.cs deleted file mode 100644 index bc79c9b79..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CosecantModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class CosecantModifierType : DataBindingModifierType - { - public override string Name => "Cosecant"; - public override string? Icon => null; - public override string Category => "Trigonometry"; - public override string Description => "Treats the input as an angle and calculates the cosecant"; - - public override double Apply(double currentValue) - { - return 1d / Math.Sin(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CosineModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CosineModifierType.cs deleted file mode 100644 index 3b4090654..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CosineModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class CosineModifierType : DataBindingModifierType - { - public override string Name => "Cosine"; - public override string Icon => "MathCos"; - public override string Category => "Trigonometry"; - public override string Description => "Treats the input as an angle and calculates the cosine"; - - public override double Apply(double currentValue) - { - return Math.Cos(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CotangentModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CotangentModifierType.cs deleted file mode 100644 index 39e514aa3..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/CotangentModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class CotangentModifierType : DataBindingModifierType - { - public override string Name => "Cotangent"; - public override string? Icon => null; - public override string Category => "Trigonometry"; - public override string Description => "Treats the input as an angle and calculates the cotangent"; - - public override double Apply(double currentValue) - { - return 1d / Math.Tan(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/SecantModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/SecantModifierType.cs deleted file mode 100644 index c4790f2dd..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/SecantModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class SecantModifierType : DataBindingModifierType - { - public override string Name => "Secant"; - public override string? Icon => null; - public override string Category => "Trigonometry"; - public override string Description => "Treats the input as an angle and calculates the secant"; - - public override double Apply(double currentValue) - { - return 1d / Math.Cos(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/SineModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/SineModifierType.cs deleted file mode 100644 index 5a315b316..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/SineModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class SineModifierType : DataBindingModifierType - { - public override string Name => "Sine"; - public override string Icon => "MathSin"; - public override string Category => "Trigonometry"; - public override string Description => "Treats the input as an angle and calculates the sine"; - - public override double Apply(double currentValue) - { - return Math.Sin(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/TangentModifierType.cs b/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/TangentModifierType.cs deleted file mode 100644 index 7a2b7c4ea..000000000 --- a/src/Artemis.Core/DefaultTypes/DataBindings/Modifiers/Numbers/Trigonometry/TangentModifierType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Artemis.Core -{ - internal class TangentModifierType : DataBindingModifierType - { - public override string Name => "Tangent"; - public override string Icon => "MathTan"; - public override string Category => "Trigonometry"; - public override string Description => "Treats the input as an angle and calculates the tangent"; - - public override double Apply(double currentValue) - { - return Math.Tan(currentValue); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs index 99a7b614a..ccaecf8b8 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/BoolLayerProperty.cs @@ -4,9 +4,14 @@ public class BoolLayerProperty : LayerProperty { internal BoolLayerProperty() + { + } + + /// + protected override void OnInitialize() { KeyframesSupported = false; - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new GeneralDataBindingConverter(), "Value"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs index e811a4e0f..c88cbd625 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs @@ -1,113 +1,92 @@ using System.Collections.Specialized; -using System.ComponentModel; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +public class ColorGradientLayerProperty : LayerProperty { - /// - public class ColorGradientLayerProperty : LayerProperty + private ColorGradient? _subscribedGradient; + + internal ColorGradientLayerProperty() { - private ColorGradient? _subscribedGradient; - - internal ColorGradientLayerProperty() - { - KeyframesSupported = false; - DataBindingsSupported = true; - DefaultValue = new ColorGradient(); - - CurrentValueSet += OnCurrentValueSet; - } - - private void CreateDataBindingRegistrations() - { - ClearDataBindingProperties(); - if (CurrentValue == null) - return; - - for (int index = 0; index < CurrentValue.Count; index++) - { - int stopIndex = index; - - void Setter(SKColor value) - { - CurrentValue[stopIndex].Color = value; - } - - RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, new ColorStopDataBindingConverter(), $"Color #{stopIndex + 1}"); - } - } - - - /// - /// Implicitly converts an to a - /// - public static implicit operator ColorGradient(ColorGradientLayerProperty p) - { - return p.CurrentValue; - } - - /// - protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) - { - throw new ArtemisCoreException("Color Gradients do not support keyframes."); - } - - private void OnCurrentValueSet(object? sender, LayerPropertyEventArgs e) - { - // Don't allow color gradients to be null - if (BaseValue == null) - BaseValue = DefaultValue ?? new ColorGradient(); - - if (_subscribedGradient != BaseValue) - { - if (_subscribedGradient != null) - _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged; - _subscribedGradient = BaseValue; - _subscribedGradient.CollectionChanged += SubscribedGradientOnPropertyChanged; - } - - CreateDataBindingRegistrations(); - } - - private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args) - { - if (CurrentValue.Count != GetAllDataBindingRegistrations().Count) - CreateDataBindingRegistrations(); - } - - #region Overrides of LayerProperty - - /// - protected override void OnInitialize() - { - // Don't allow color gradients to be null - if (BaseValue == null) - BaseValue = DefaultValue ?? new ColorGradient(); - - base.OnInitialize(); - } - - #endregion + KeyframesSupported = false; + DefaultValue = new ColorGradient(); } - internal class ColorStopDataBindingConverter : DataBindingConverter + /// + /// Implicitly converts an to a + /// + public static implicit operator ColorGradient(ColorGradientLayerProperty p) { - public ColorStopDataBindingConverter() + return p.CurrentValue; + } + + #region Overrides of LayerProperty + + /// + protected override void OnCurrentValueSet() + { + // Don't allow color gradients to be null + if (BaseValue == null!) + BaseValue = new ColorGradient(DefaultValue); + + if (!ReferenceEquals(_subscribedGradient, BaseValue)) { - SupportsInterpolate = true; - SupportsSum = true; + if (_subscribedGradient != null) + _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged; + _subscribedGradient = BaseValue; + _subscribedGradient.CollectionChanged += SubscribedGradientOnPropertyChanged; } - /// - public override SKColor Sum(SKColor a, SKColor b) - { - return a.Sum(b); - } + CreateDataBindingRegistrations(); + base.OnCurrentValueSet(); + } - /// - public override SKColor Interpolate(SKColor a, SKColor b, double progress) + #endregion + + /// + protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) + { + throw new ArtemisCoreException("Color Gradients do not support keyframes."); + } + + #region Overrides of LayerProperty + + /// + protected override void OnInitialize() + { + // Don't allow color gradients to be null + if (BaseValue == null!) + BaseValue = new ColorGradient(DefaultValue); + + base.OnInitialize(); + } + + #endregion + + private void CreateDataBindingRegistrations() + { + DataBinding.ClearDataBindingProperties(); + if (CurrentValue == null!) + return; + + for (int index = 0; index < CurrentValue.Count; index++) { - return a.Interpolate(b, (float) progress); + int stopIndex = index; + + void Setter(SKColor value) + { + CurrentValue[stopIndex].Color = value; + } + + DataBinding.RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, $"Color #{stopIndex + 1}"); } } + + private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + if (CurrentValue.Count != DataBinding.Properties.Count) + CreateDataBindingRegistrations(); + } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs index eb99a3b3e..ea7962b7a 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/EnumLayerProperty.cs @@ -8,7 +8,6 @@ namespace Artemis.Core internal EnumLayerProperty() { KeyframesSupported = false; - DataBindingsSupported = false; } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs index cb1a6eae8..2aec47126 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/FloatLayerProperty.cs @@ -5,7 +5,12 @@ { internal FloatLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new FloatDataBindingConverter(), "Value"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs index 34a3be646..56408b59a 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/FloatRangeLayerProperty.cs @@ -3,13 +3,11 @@ /// public class FloatRangeLayerProperty : LayerProperty { - internal FloatRangeLayerProperty() + /// + protected override void OnInitialize() { - RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue.Start = value, new FloatDataBindingConverter(), "Start"); - RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue.End = value, new FloatDataBindingConverter(), "End"); - - CurrentValueSet += OnCurrentValueSet; - DefaultValue = new FloatRange(); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue = new FloatRange(value, CurrentValue.End), "Start"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue = new FloatRange(CurrentValue.Start, value), "End"); } /// @@ -18,15 +16,9 @@ float startDiff = NextKeyframe!.Value.Start - CurrentKeyframe!.Value.Start; float endDiff = NextKeyframe!.Value.End - CurrentKeyframe!.Value.End; CurrentValue = new FloatRange( - (int) (CurrentKeyframe!.Value.Start + startDiff * keyframeProgressEased), - (int) (CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased) + (float) (CurrentKeyframe!.Value.Start + startDiff * keyframeProgressEased), + (float) (CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased) ); } - - private void OnCurrentValueSet(object? sender, LayerPropertyEventArgs e) - { - // Don't allow the int range to be null - BaseValue ??= DefaultValue ?? new FloatRange(); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs index f69a1bdbc..03681df72 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/IntLayerProperty.cs @@ -7,7 +7,12 @@ namespace Artemis.Core { internal IntLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new IntDataBindingConverter(), "Value"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs index 0b1942356..481c8f721 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/IntRangeLayerProperty.cs @@ -3,13 +3,11 @@ /// public class IntRangeLayerProperty : LayerProperty { - internal IntRangeLayerProperty() + /// + protected override void OnInitialize() { - RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue.Start = value, new IntDataBindingConverter(), "Start"); - RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue.End = value, new IntDataBindingConverter(), "End"); - - CurrentValueSet += OnCurrentValueSet; - DefaultValue = new IntRange(); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Start, value => CurrentValue = new IntRange(value, CurrentValue.End), "Start"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.End, value => CurrentValue = new IntRange(CurrentValue.Start, value), "End"); } /// @@ -22,11 +20,5 @@ (int) (CurrentKeyframe!.Value.End + endDiff * keyframeProgressEased) ); } - - private void OnCurrentValueSet(object? sender, LayerPropertyEventArgs e) - { - // Don't allow the int range to be null - BaseValue ??= DefaultValue ?? new IntRange(); - } } } \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs index 9bbd77eac..c3341ee32 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/LayerBrushReferenceLayerProperty.cs @@ -8,7 +8,6 @@ internal LayerBrushReferenceLayerProperty() { KeyframesSupported = false; - DataBindingsSupported = false; } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs index 46370568a..38caad133 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKColorLayerProperty.cs @@ -7,7 +7,12 @@ namespace Artemis.Core { internal SKColorLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, new SKColorDataBindingConverter(), "Value"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => CurrentValue = value, "Value"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs index 3cd654ad9..df9517eeb 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKPointLayerProperty.cs @@ -7,8 +7,13 @@ namespace Artemis.Core { internal SKPointLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue.X, value => CurrentValue = new SKPoint(value, CurrentValue.Y), new FloatDataBindingConverter(), "X"); - RegisterDataBindingProperty(() => CurrentValue.Y, value => CurrentValue = new SKPoint(CurrentValue.X, value), new FloatDataBindingConverter(), "Y"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue.X, value => CurrentValue = new SKPoint(value, CurrentValue.Y), "X"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Y, value => CurrentValue = new SKPoint(CurrentValue.X, value), "Y"); } /// diff --git a/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs index 3274c26f9..e8344e0c9 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/SKSizeLayerProperty.cs @@ -7,8 +7,13 @@ namespace Artemis.Core { internal SKSizeLayerProperty() { - RegisterDataBindingProperty(() => CurrentValue.Width, (value) => CurrentValue = new SKSize(value, CurrentValue.Height), new FloatDataBindingConverter(), "Width"); - RegisterDataBindingProperty(() => CurrentValue.Height, (value) => CurrentValue = new SKSize(CurrentValue.Width, value), new FloatDataBindingConverter(), "Height"); + } + + /// + protected override void OnInitialize() + { + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Width, (value) => CurrentValue = new SKSize(value, CurrentValue.Height), "Width"); + DataBinding.RegisterDataBindingProperty(() => CurrentValue.Height, (value) => CurrentValue = new SKSize(CurrentValue.Width, value), "Height"); } /// diff --git a/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs b/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs new file mode 100644 index 000000000..c01cfae65 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/DataBindingEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data for data binding events. + /// + public class DataBindingEventArgs : EventArgs + { + internal DataBindingEventArgs(IDataBinding dataBinding) + { + DataBinding = dataBinding; + } + + /// + /// Gets the data binding this event is related to + /// + public IDataBinding DataBinding { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/LayerAdapterHintEventArgs.cs b/src/Artemis.Core/Events/Profiles/LayerAdapterHintEventArgs.cs new file mode 100644 index 000000000..5d3600ba4 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/LayerAdapterHintEventArgs.cs @@ -0,0 +1,19 @@ +using System; + +namespace Artemis.Core; + +/// +/// Provides data for layer adapter hint events. +/// +public class LayerAdapterHintEventArgs : EventArgs +{ + internal LayerAdapterHintEventArgs(IAdaptionHint adaptionHint) + { + AdaptionHint = adaptionHint; + } + + /// + /// Gets the layer adaption hint this event is related to + /// + public IAdaptionHint AdaptionHint { get; } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/LayerPropertyKeyframeEventArgs.cs b/src/Artemis.Core/Events/Profiles/LayerPropertyKeyframeEventArgs.cs new file mode 100644 index 000000000..962240239 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/LayerPropertyKeyframeEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data for layer property events. + /// + public class LayerPropertyKeyframeEventArgs : EventArgs + { + internal LayerPropertyKeyframeEventArgs(ILayerPropertyKeyframe keyframe) + { + Keyframe = keyframe; + } + + /// + /// Gets the keyframe this event is related to + /// + public ILayerPropertyKeyframe Keyframe { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs b/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs new file mode 100644 index 000000000..4f4a740e4 --- /dev/null +++ b/src/Artemis.Core/Events/Profiles/ProfileElementEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Provides data for profile element events. + /// + public class ProfileElementEventArgs : EventArgs + { + internal ProfileElementEventArgs(ProfileElement profileElement) + { + ProfileElement = profileElement; + } + + /// + /// Gets the profile element this event is related to + /// + public ProfileElement ProfileElement { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/ConditionOperatorStoreEvent.cs b/src/Artemis.Core/Events/Stores/ConditionOperatorStoreEvent.cs deleted file mode 100644 index fb81fa23f..000000000 --- a/src/Artemis.Core/Events/Stores/ConditionOperatorStoreEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Artemis.Core -{ - internal class ConditionOperatorStoreEvent - { - public ConditionOperatorStoreEvent(ConditionOperatorRegistration registration) - { - Registration = registration; - } - - public ConditionOperatorRegistration Registration { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/DataBindingModifierTypeStoreEvent.cs b/src/Artemis.Core/Events/Stores/DataBindingModifierTypeStoreEvent.cs deleted file mode 100644 index c954a7f60..000000000 --- a/src/Artemis.Core/Events/Stores/DataBindingModifierTypeStoreEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Artemis.Core -{ - internal class DataBindingModifierTypeStoreEvent - { - public DataBindingModifierTypeStoreEvent(DataBindingModifierTypeRegistration typeRegistration) - { - TypeRegistration = typeRegistration; - } - - public DataBindingModifierTypeRegistration TypeRegistration { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs b/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs new file mode 100644 index 000000000..e91d27b6a --- /dev/null +++ b/src/Artemis.Core/Events/Stores/NodeTypeStoreEvent.cs @@ -0,0 +1,12 @@ +namespace Artemis.Core +{ + internal class NodeTypeStoreEvent + { + public NodeTypeStoreEvent(NodeTypeRegistration typeRegistration) + { + TypeRegistration = typeRegistration; + } + + public NodeTypeRegistration TypeRegistration { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/IEnumerableExtensions.cs b/src/Artemis.Core/Extensions/IEnumerableExtensions.cs index 3fe2b8caa..111851187 100644 --- a/src/Artemis.Core/Extensions/IEnumerableExtensions.cs +++ b/src/Artemis.Core/Extensions/IEnumerableExtensions.cs @@ -31,66 +31,23 @@ namespace Artemis.Core public static class IEnumerableExtensions { /// - /// Returns all distinct elements of the given source, where "distinctness" - /// is determined via a projection and the default equality comparer for the projected type. + /// Returns the index of the provided element inside the read only collection /// - /// - /// This operator uses deferred execution and streams the results, although - /// a set of already-seen keys is retained. If a key is seen multiple times, - /// only the first element with that key is returned. - /// - /// Type of the source sequence - /// Type of the projected element - /// Source sequence - /// Projection for determining "distinctness" - /// - /// A sequence consisting of distinct elements from the source sequence, - /// comparing them by the specified key projection. - /// - public static IEnumerable DistinctBy(this IEnumerable source, - Func keySelector) + /// The type of element to find + /// The collection to search in + /// The element to find + /// If found, the index of the element to find; otherwise -1 + public static int IndexOf(this IReadOnlyCollection self, T elementToFind) { - return source.DistinctBy(keySelector, null!); - } - - /// - /// Returns all distinct elements of the given source, where "distinctness" - /// is determined via a projection and the specified comparer for the projected type. - /// - /// - /// This operator uses deferred execution and streams the results, although - /// a set of already-seen keys is retained. If a key is seen multiple times, - /// only the first element with that key is returned. - /// - /// Type of the source sequence - /// Type of the projected element - /// Source sequence - /// Projection for determining "distinctness" - /// - /// The equality comparer to use to determine whether or not keys are equal. - /// If null, the default equality comparer for TSource is used. - /// - /// - /// A sequence consisting of distinct elements from the source sequence, - /// comparing them by the specified key projection. - /// - public static IEnumerable DistinctBy(this IEnumerable source, - Func keySelector, IEqualityComparer comparer) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (keySelector == null) throw new ArgumentNullException(nameof(keySelector)); - - return _(); - - IEnumerable _() + int i = 0; + foreach (T element in self) { - HashSet knownKeys = new(comparer); - foreach (TSource element in source) - { - if (knownKeys.Add(keySelector(element))) - yield return element; - } + if (Equals(element, elementToFind)) + return i; + i++; } + + return -1; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/SKColorExtensions.cs b/src/Artemis.Core/Extensions/SKColorExtensions.cs index dbc14239f..f1e9297ec 100644 --- a/src/Artemis.Core/Extensions/SKColorExtensions.cs +++ b/src/Artemis.Core/Extensions/SKColorExtensions.cs @@ -16,7 +16,7 @@ namespace Artemis.Core /// The RGB.NET color public static Color ToRgbColor(this SKColor color) { - return new(color.Alpha, color.Red, color.Green, color.Blue); + return new Color(color.Alpha, color.Red, color.Green, color.Blue); } /// @@ -49,7 +49,7 @@ namespace Artemis.Core /// The sum of the two colors public static SKColor Sum(this SKColor a, SKColor b) { - return new( + return new SKColor( ClampToByte(a.Red + b.Red), ClampToByte(a.Green + b.Green), ClampToByte(a.Blue + b.Blue), @@ -57,6 +57,19 @@ namespace Artemis.Core ); } + /// + /// Darkens the color by the specified amount + /// + /// The color to darken + /// The brightness of the new color + /// The darkened color + public static SKColor Darken(this SKColor c, float amount) + { + c.ToHsl(out float h, out float s, out float l); + l *= 1f - amount; + return SKColor.FromHsl(h, s, l); + } + private static byte ClampToByte(float value) { return (byte) Math.Clamp(value, 0, 255); diff --git a/src/Artemis.Core/Extensions/TypeExtensions.cs b/src/Artemis.Core/Extensions/TypeExtensions.cs index 44f851c16..ed5c5e36e 100644 --- a/src/Artemis.Core/Extensions/TypeExtensions.cs +++ b/src/Artemis.Core/Extensions/TypeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Humanizer; @@ -49,7 +50,7 @@ namespace Artemis.Core /// The type to check /// The generic type to match /// True if the is generic and of generic type - public static bool IsGenericType(this Type type, Type genericType) + public static bool IsGenericType(this Type? type, Type genericType) { if (type == null) return false; @@ -92,19 +93,9 @@ namespace Artemis.Core /// /// The value to check /// if the value is of a numeric type, otherwise - public static bool IsNumber(this object value) + public static bool IsNumber([NotNullWhenAttribute(true)] this object? value) { - return value is sbyte - || value is byte - || value is short - || value is ushort - || value is int - || value is uint - || value is long - || value is ulong - || value is float - || value is double - || value is decimal; + return value is sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal; } // From https://stackoverflow.com/a/2224421/5015269 but inverted and renamed to match similar framework methods @@ -130,8 +121,11 @@ namespace Artemis.Core /// at all /// /// - public static int ScoreCastability(this Type to, Type from) + public static int ScoreCastability(this Type to, Type? from) { + if (from == null) + return 0; + if (to == from) return 5; if (to.TypeIsNumber() && from.TypeIsNumber()) @@ -188,6 +182,52 @@ namespace Artemis.Core return enumerableType?.GenericTypeArguments[0]; } + /// + /// Determines if the is of a certain . + /// + /// The type to check. + /// The generic type it should be or implement + public static bool IsOfGenericType(this Type typeToCheck, Type genericType) + { + return typeToCheck.IsOfGenericType(genericType, out Type? _); + } + + private static bool IsOfGenericType(this Type? typeToCheck, Type genericType, out Type? concreteGenericType) + { + while (true) + { + concreteGenericType = null; + + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + + if (!genericType.IsGenericTypeDefinition) + throw new ArgumentException("The definition needs to be a GenericTypeDefinition", nameof(genericType)); + + if (typeToCheck == null || typeToCheck == typeof(object)) + return false; + + if (typeToCheck == genericType) + { + concreteGenericType = typeToCheck; + return true; + } + + if ((typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck) == genericType) + { + concreteGenericType = typeToCheck; + return true; + } + + if (genericType.IsInterface) + foreach (Type i in typeToCheck.GetInterfaces()) + if (i.IsOfGenericType(genericType, out concreteGenericType)) + return true; + + typeToCheck = typeToCheck.BaseType; + } + } + /// /// Determines a display name for the given type /// diff --git a/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs b/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs new file mode 100644 index 000000000..e2ffd62a0 --- /dev/null +++ b/src/Artemis.Core/JsonConverters/NumericJsonConverter.cs @@ -0,0 +1,25 @@ +using System; +using Newtonsoft.Json; + +namespace Artemis.Core.JsonConverters +{ + internal class NumericJsonConverter : JsonConverter + { + #region Overrides of JsonConverter + + /// + public override void WriteJson(JsonWriter writer, Numeric value, JsonSerializer serializer) + { + float floatValue = value; + writer.WriteValue(floatValue); + } + + /// + public override Numeric ReadJson(JsonReader reader, Type objectType, Numeric existingValue, bool hasExistingValue, JsonSerializer serializer) + { + return new Numeric(reader.Value); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/MVVM/CorePropertyChanged.cs b/src/Artemis.Core/MVVM/CorePropertyChanged.cs index 69aeef147..b9bd735e0 100644 --- a/src/Artemis.Core/MVVM/CorePropertyChanged.cs +++ b/src/Artemis.Core/MVVM/CorePropertyChanged.cs @@ -21,14 +21,14 @@ namespace Artemis.Core #region Methods /// - /// Checks if the property already matches the desirec value or needs to be updated. + /// Checks if the property already matches the desired value or needs to be updated. /// /// Type of the property. /// Reference to the backing-filed. /// Value to apply. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected virtual bool RequiresUpdate(ref T storage, T value) + protected bool RequiresUpdate(ref T storage, T value) { return !Equals(storage, value); } @@ -46,7 +46,7 @@ namespace Artemis.Core /// /// true if the value was changed, false if the existing value matched the desired value. [NotifyPropertyChangedInvocator] - protected virtual bool SetAndNotify(ref T storage, T value, [CallerMemberName] string? propertyName = null) + protected bool SetAndNotify(ref T storage, T value, [CallerMemberName] string? propertyName = null) { if (!RequiresUpdate(ref storage, value)) return false; @@ -64,7 +64,7 @@ namespace Artemis.Core /// and can be provided automatically when invoked from compilers that support /// . /// - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs index 824b27963..18e87f574 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs @@ -7,8 +7,13 @@ namespace Artemis.Core /// /// Represents a hint that adapts layers to a certain category of devices /// - public class CategoryAdaptionHint : IAdaptionHint + public class CategoryAdaptionHint : CorePropertyChanged, IAdaptionHint { + private DeviceCategory _category; + private int _skip; + private bool _limitAmount; + private int _amount; + /// /// Creates a new instance of the class /// @@ -27,22 +32,38 @@ namespace Artemis.Core /// /// Gets or sets the category of devices LEDs will be applied to /// - public DeviceCategory Category { get; set; } + public DeviceCategory Category + { + get => _category; + set => SetAndNotify(ref _category, value); + } /// /// Gets or sets the amount of devices to skip /// - public int Skip { get; set; } + public int Skip + { + get => _skip; + set => SetAndNotify(ref _skip, value); + } /// /// Gets or sets a boolean indicating whether a limited amount of devices should be used /// - public bool LimitAmount { get; set; } + public bool LimitAmount + { + get => _limitAmount; + set => SetAndNotify(ref _limitAmount, value); + } /// /// Gets or sets the amount of devices to limit to if is /// - public int Amount { get; set; } + public int Amount + { + get => _amount; + set => SetAndNotify(ref _amount, value); + } /// public override string ToString() diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs index 28058f641..62ed05b85 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs @@ -8,8 +8,13 @@ namespace Artemis.Core /// /// Represents a hint that adapts layers to a certain type of devices /// - public class DeviceAdaptionHint : IAdaptionHint + public class DeviceAdaptionHint : CorePropertyChanged, IAdaptionHint { + private RGBDeviceType _deviceType; + private int _skip; + private bool _limitAmount; + private int _amount; + /// /// Creates a new instance of the class /// @@ -28,22 +33,38 @@ namespace Artemis.Core /// /// Gets or sets the type of devices LEDs will be applied to /// - public RGBDeviceType DeviceType { get; set; } + public RGBDeviceType DeviceType + { + get => _deviceType; + set => SetAndNotify(ref _deviceType, value); + } /// /// Gets or sets the amount of devices to skip /// - public int Skip { get; set; } + public int Skip + { + get => _skip; + set => SetAndNotify(ref _skip, value); + } /// /// Gets or sets a boolean indicating whether a limited amount of devices should be used /// - public bool LimitAmount { get; set; } + public bool LimitAmount + { + get => _limitAmount; + set => SetAndNotify(ref _limitAmount, value); + } /// /// Gets or sets the amount of devices to limit to if is /// - public int Amount { get; set; } + public int Amount + { + get => _amount; + set => SetAndNotify(ref _amount, value); + } #region Implementation of IAdaptionHint diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs index 0177a8938..cf61108c4 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs @@ -9,7 +9,7 @@ namespace Artemis.Core /// /// Represents a hint that adapts layers to a certain region of keyboards /// - public class KeyboardSectionAdaptionHint : IAdaptionHint + public class KeyboardSectionAdaptionHint : CorePropertyChanged, IAdaptionHint { private static readonly Dictionary> RegionLedIds = new() { @@ -18,6 +18,8 @@ namespace Artemis.Core {KeyboardSection.Extra, Enum.GetValues().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()} }; + private KeyboardSection _section; + /// /// Creates a new instance of the class /// @@ -33,7 +35,11 @@ namespace Artemis.Core /// /// Gets or sets the section this hint will apply LEDs to /// - public KeyboardSection Section { get; set; } + public KeyboardSection Section + { + get => _section; + set => SetAndNotify(ref _section, value); + } #region Implementation of IAdaptionHint diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index bdda754a0..1e34d5348 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -6,319 +6,621 @@ using System.ComponentModel; using System.Linq; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// A gradient containing a list of s +/// +public class ColorGradient : IList, IList, INotifyCollectionChanged { - /// - /// A gradient containing a list of s - /// - public class ColorGradient : IList, INotifyCollectionChanged + private static readonly SKColor[] FastLedRainbow = { - private static readonly SKColor[] FastLedRainbow = - { - new(0xFFFF0000), // Red - new(0xFFFF9900), // Orange - new(0xFFFFFF00), // Yellow - new(0xFF00FF00), // Green - new(0xFF00FF7E), // Aqua - new(0xFF0078FF), // Blue - new(0xFF9E22FF), // Purple - new(0xFFFF34AE), // Pink - new(0xFFFF0000) // and back to Red - }; + new(0xFFFF0000), // Red + new(0xFFFF9900), // Orange + new(0xFFFFFF00), // Yellow + new(0xFF00FF00), // Green + new(0xFF00FF7E), // Aqua + new(0xFF0078FF), // Blue + new(0xFF9E22FF), // Purple + new(0xFFFF34AE), // Pink + new(0xFFFF0000) // and back to Red + }; - private readonly List _stops; + private readonly List _stops; + private bool _updating; - /// - /// Creates a new instance of the class - /// - public ColorGradient() - { - _stops = new List(); - } - - /// - /// Gets all the colors in the color gradient - /// - /// The amount of times to repeat the colors - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - /// An array containing each color in the gradient - public SKColor[] GetColorsArray(int timesToRepeat = 0, bool seamless = false) - { - List result = new(); - if (timesToRepeat == 0) - { - result = this.Select(c => c.Color).ToList(); - } - else - { - for (int i = 0; i <= timesToRepeat; i++) - result.AddRange(this.Select(c => c.Color)); - } - - if (seamless && !IsSeamless()) - result.Add(result[0]); - - return result.ToArray(); - } - - /// - /// Gets all the positions in the color gradient - /// - /// The amount of times to repeat the positions - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - /// An array containing a position for each color between 0.0 and 1.0 - public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false) - { - List result = new(); - if (timesToRepeat == 0) - { - result = this.Select(c => c.Position).ToList(); - } - else - { - // Create stops and a list of divided stops - List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); - - // For each repeat cycle, add the base stops to the end result - for (int i = 0; i <= timesToRepeat; i++) - { - float lastStop = result.LastOrDefault(); - result.AddRange(stops.Select(s => s + lastStop)); - } - } - - if (seamless && !IsSeamless()) - { - // Compress current points evenly - float compression = 1f - 1f / result.Count; - for (int index = 0; index < result.Count; index++) - result[index] = result[index] * compression; - // Add one extra point at the end - result.Add(1f); - } - - return result.ToArray(); - } - - /// - /// Gets a color at any position between 0.0 and 1.0 using interpolation - /// - /// A position between 0.0 and 1.0 - /// The amount of times to repeat the positions - /// - /// A boolean indicating whether to make the gradient seamless by adding the first color behind the - /// last color - /// - public SKColor GetColor(float position, int timesToRepeat = 0, bool seamless = false) - { - if (!this.Any()) - return new SKColor(255, 255, 255); - - SKColor[] colors = GetColorsArray(timesToRepeat, seamless); - float[] stops = GetPositionsArray(timesToRepeat, seamless); - - // If at or over the edges, return the corresponding edge - if (position <= 0) return colors[0]; - if (position >= 1) return colors[^1]; - - // Walk through the stops until we find the one at or after the requested position, that becomes the right stop - // The left stop is the previous stop before the right one was found. - float left = stops[0]; - float? right = null; - foreach (float stop in stops) - { - if (stop >= position) - { - right = stop; - break; - } - - left = stop; - } - - // Get the left stop's color - SKColor leftColor = colors[Array.IndexOf(stops, left)]; - - // If no right stop was found or the left and right stops are on the same spot, return the left stop's color - if (right == null || left == right) - return leftColor; - - // Get the right stop's color - SKColor rightColor = colors[Array.IndexOf(stops, right)]; - - // Interpolate the position between the left and right color - position = MathF.Round((position - left) / (right.Value - left), 2); - byte a = (byte) ((rightColor.Alpha - leftColor.Alpha) * position + leftColor.Alpha); - byte r = (byte) ((rightColor.Red - leftColor.Red) * position + leftColor.Red); - byte g = (byte) ((rightColor.Green - leftColor.Green) * position + leftColor.Green); - byte b = (byte) ((rightColor.Blue - leftColor.Blue) * position + leftColor.Blue); - return new SKColor(r, g, b, a); - } - - /// - /// Gets a new ColorGradient with colors looping through the HSV-spectrum - /// - /// - public static ColorGradient GetUnicornBarf() - { - ColorGradient gradient = new(); - for (int index = 0; index < FastLedRainbow.Length; index++) - { - SKColor skColor = FastLedRainbow[index]; - float position = 1f / (FastLedRainbow.Length - 1f) * index; - gradient.Add(new ColorGradientStop(skColor, position)); - } - - return gradient; - } - - /// - /// Determines whether the gradient is seamless - /// - /// if the gradient is seamless; otherwise - public bool IsSeamless() - { - return Count == 0 || this.First().Color.Equals(this.Last().Color); - } - - internal void Sort() - { - _stops.Sort((a, b) => a.Position.CompareTo(b.Position)); - } - - private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - #region Implementation of IEnumerable - - /// - public IEnumerator GetEnumerator() - { - return _stops.GetEnumerator(); - } - - /// - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - #endregion - - #region Implementation of ICollection - - /// - public void Add(ColorGradientStop item) - { - _stops.Add(item); - item.PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _stops.IndexOf(item))); - } - - - /// - public void Clear() - { - foreach (ColorGradientStop item in _stops) - item.PropertyChanged -= ItemOnPropertyChanged; - _stops.Clear(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } - - /// - public bool Contains(ColorGradientStop item) - { - return _stops.Contains(item); - } - - /// - public void CopyTo(ColorGradientStop[] array, int arrayIndex) - { - _stops.CopyTo(array, arrayIndex); - } - - /// - public bool Remove(ColorGradientStop item) - { - item.PropertyChanged -= ItemOnPropertyChanged; - int index = _stops.IndexOf(item); - bool removed = _stops.Remove(item); - if (removed) - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); - - return removed; - } - - /// - public int Count => _stops.Count; - - /// - public bool IsReadOnly => false; - - #endregion - - #region Implementation of IList - - /// - public int IndexOf(ColorGradientStop item) - { - return _stops.IndexOf(item); - } - - /// - public void Insert(int index, ColorGradientStop item) - { - _stops.Insert(index, item); - item.PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); - } - - /// - public void RemoveAt(int index) - { - _stops[index].PropertyChanged -= ItemOnPropertyChanged; - _stops.RemoveAt(index); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); - } - - /// - public ColorGradientStop this[int index] - { - get => _stops[index]; - set - { - ColorGradientStop? oldValue = _stops[index]; - oldValue.PropertyChanged -= ItemOnPropertyChanged; - _stops[index] = value; - _stops[index].PropertyChanged += ItemOnPropertyChanged; - Sort(); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); - } - } - - #endregion - - #region Implementation of INotifyCollectionChanged - - /// - public event NotifyCollectionChangedEventHandler? CollectionChanged; - - private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e); - } - - #endregion + /// + /// Creates a new instance of the class + /// + public ColorGradient() + { + _stops = new List(); } + + /// + /// Creates a new instance of the class + /// + /// The color gradient to copy + public ColorGradient(ColorGradient? colorGradient) + { + _stops = new List(); + if (colorGradient == null) + return; + + foreach (ColorGradientStop colorGradientStop in colorGradient) + { + ColorGradientStop stop = new(colorGradientStop.Color, colorGradientStop.Position); + stop.PropertyChanged += ItemOnPropertyChanged; + _stops.Add(stop); + } + } + + /// + /// Creates a new instance of the class + /// + /// The stops to copy + public ColorGradient(List stops) + { + _stops = new List(); + foreach (ColorGradientStop colorGradientStop in stops) + { + ColorGradientStop stop = new(colorGradientStop.Color, colorGradientStop.Position); + stop.PropertyChanged += ItemOnPropertyChanged; + _stops.Add(stop); + } + } + + /// + /// Gets all the colors in the color gradient + /// + /// The amount of times to repeat the colors + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + /// An array containing each color in the gradient + public SKColor[] GetColorsArray(int timesToRepeat = 0, bool seamless = false) + { + List result = new(); + if (timesToRepeat == 0) + { + result = this.Select(c => c.Color).ToList(); + } + else + { + for (int i = 0; i <= timesToRepeat; i++) + result.AddRange(this.Select(c => c.Color)); + } + + if (seamless && !IsSeamless()) + result.Add(result[0]); + + return result.ToArray(); + } + + /// + /// Gets all the positions in the color gradient + /// + /// The amount of times to repeat the positions + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + /// An array containing a position for each color between 0.0 and 1.0 + public float[] GetPositionsArray(int timesToRepeat = 0, bool seamless = false) + { + List result = new(); + if (timesToRepeat == 0) + { + result = this.Select(c => c.Position).ToList(); + } + else + { + // Create stops and a list of divided stops + List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); + + // For each repeat cycle, add the base stops to the end result + for (int i = 0; i <= timesToRepeat; i++) + { + float lastStop = result.LastOrDefault(); + result.AddRange(stops.Select(s => s + lastStop)); + } + } + + if (seamless && !IsSeamless()) + { + // Compress current points evenly + float compression = 1f - 1f / result.Count; + for (int index = 0; index < result.Count; index++) + result[index] *= compression; + // Add one extra point at the end + result.Add(1f); + } + + return result.ToArray(); + } + + /// + /// Gets a color at any position between 0.0 and 1.0 using interpolation + /// + /// A position between 0.0 and 1.0 + /// The amount of times to repeat the positions + /// + /// A boolean indicating whether to make the gradient seamless by adding the first color behind the + /// last color + /// + public SKColor GetColor(float position, int timesToRepeat = 0, bool seamless = false) + { + if (!this.Any()) + return new SKColor(255, 255, 255); + + SKColor[] colors = GetColorsArray(timesToRepeat, seamless); + float[] stops = GetPositionsArray(timesToRepeat, seamless); + + // If at or over the edges, return the corresponding edge + if (position <= 0) return colors[0]; + if (position >= 1) return colors[^1]; + + // Walk through the stops until we find the one at or after the requested position, that becomes the right stop + // The left stop is the previous stop before the right one was found. + float left = stops[0]; + float? right = null; + foreach (float stop in stops) + { + if (stop >= position) + { + right = stop; + break; + } + + left = stop; + } + + // Get the left stop's color + SKColor leftColor = colors[Array.IndexOf(stops, left)]; + + // If no right stop was found or the left and right stops are on the same spot, return the left stop's color + if (right == null || left == right) + return leftColor; + + // Get the right stop's color + SKColor rightColor = colors[Array.IndexOf(stops, right)]; + + // Interpolate the position between the left and right color + position = MathF.Round((position - left) / (right.Value - left), 2); + byte a = (byte) ((rightColor.Alpha - leftColor.Alpha) * position + leftColor.Alpha); + byte r = (byte) ((rightColor.Red - leftColor.Red) * position + leftColor.Red); + byte g = (byte) ((rightColor.Green - leftColor.Green) * position + leftColor.Green); + byte b = (byte) ((rightColor.Blue - leftColor.Blue) * position + leftColor.Blue); + return new SKColor(r, g, b, a); + } + + /// + /// Gets a new ColorGradient with colors looping through the HSV-spectrum + /// + public static ColorGradient GetUnicornBarf() + { + ColorGradient gradient = new(); + for (int index = 0; index < FastLedRainbow.Length; index++) + { + SKColor skColor = FastLedRainbow[index]; + float position = 1f / (FastLedRainbow.Length - 1f) * index; + gradient.Add(new ColorGradientStop(skColor, position)); + } + + return gradient; + } + + /// + /// Gets a new ColorGradient with random colors from the HSV-spectrum + /// + /// The amount of stops to add + public ColorGradient GetRandom(int stops) + { + ColorGradient gradient = new(); + gradient.Randomize(stops); + return gradient; + } + + /// + /// Determines whether the gradient is seamless + /// + /// if the gradient is seamless; otherwise + public bool IsSeamless() + { + return Count == 0 || this.First().Color.Equals(this.Last().Color); + } + + /// + /// Spreads the color stops equally across the gradient. + /// + public void SpreadStops() + { + try + { + _updating = true; + for (int i = 0; i < Count; i++) + this[i].Position = MathF.Round(i / ((float) Count - 1), 3, MidpointRounding.AwayFromZero); + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// If not already the case, makes the gradient seamless by adding the first color to the end of the gradient and + /// compressing the other stops. + /// + /// If the gradient is already seamless, removes the last color and spreads the remaining stops to fill the freed + /// space. + /// + /// + public void ToggleSeamless() + { + try + { + _updating = true; + + if (IsSeamless()) + { + ColorGradientStop stopToRemove = this.Last(); + Remove(stopToRemove); + + // Uncompress the stops if there is still more than one + if (Count >= 2) + { + float multiplier = Count / (Count - 1f); + foreach (ColorGradientStop stop in this) + stop.Position = MathF.Round(Math.Min(stop.Position * multiplier, 100f), 3, MidpointRounding.AwayFromZero); + } + } + else + { + // Compress existing stops to the left + float multiplier = (Count - 1f) / Count; + foreach (ColorGradientStop stop in this) + stop.Position = MathF.Round(stop.Position * multiplier, 3, MidpointRounding.AwayFromZero); + + // Add a stop to the end that is the same color as the first stop + ColorGradientStop newStop = new(this.First().Color, 1f); + Add(newStop); + } + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Flips the stops of the gradient. + /// + public void FlipStops() + { + try + { + _updating = true; + foreach (ColorGradientStop stop in this) + stop.Position = 1 - stop.Position; + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Rotates the stops of the gradient shifting every stop over to the position of it's neighbor and wrapping around at + /// the end of the gradient. + /// + /// A boolean indicating whether or not the invert the rotation. + public void RotateStops(bool inverse) + { + try + { + _updating = true; + List stops = inverse + ? this.OrderBy(s => s.Position).ToList() + : this.OrderByDescending(s => s.Position).ToList(); + + float lastStopPosition = stops.Last().Position; + foreach (ColorGradientStop stop in stops) + (stop.Position, lastStopPosition) = (lastStopPosition, stop.Position); + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Randomizes the color gradient with the given amount of . + /// + /// The amount of stops to put into the gradient. + public void Randomize(int stops) + { + try + { + _updating = true; + + Clear(); + Random random = new(); + for (int index = 0; index < stops; index++) + { + SKColor skColor = SKColor.FromHsv(random.NextSingle() * 360, 100, 100); + float position = 1f / (stops - 1f) * index; + Add(new ColorGradientStop(skColor, position)); + } + } + finally + { + _updating = false; + Sort(); + } + } + + /// + /// Occurs when any of the stops has changed in some way + /// + public event EventHandler? StopChanged; + + internal void Sort() + { + if (_updating) + return; + + int requiredIndex = 0; + foreach (ColorGradientStop colorGradientStop in _stops.OrderBy(s => s.Position).ToList()) + { + int actualIndex = _stops.IndexOf(colorGradientStop); + if (requiredIndex != actualIndex) + { + _stops.RemoveAt(actualIndex); + _stops.Insert(requiredIndex, colorGradientStop); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, colorGradientStop, requiredIndex, actualIndex)); + } + + requiredIndex++; + } + } + + private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + Sort(); + OnStopChanged(); + } + + private void OnStopChanged() + { + StopChanged?.Invoke(this, EventArgs.Empty); + } + + #region Equality members + + /// + /// Determines whether all the stops in this gradient are equal to the stops in the given + /// gradient. + /// + /// The other gradient to compare to + protected bool Equals(ColorGradient other) + { + if (Count != other.Count) + return false; + + for (int i = 0; i < Count; i++) + if (!Equals(this[i], other[i])) + return false; + + return true; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((ColorGradient) obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + int hash = 19; + foreach (ColorGradientStop stops in this) + hash = hash * 31 + stops.GetHashCode(); + return hash; + } + } + + #endregion + + #region Implementation of IEnumerable + + /// + public IEnumerator GetEnumerator() + { + return _stops.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Implementation of ICollection + + /// + public void Add(ColorGradientStop item) + { + _stops.Add(item); + item.PropertyChanged += ItemOnPropertyChanged; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + Sort(); + } + + /// + public int Add(object? value) + { + if (value is ColorGradientStop stop) + Add(stop); + + return IndexOf(value); + } + + /// + public void Clear() + { + foreach (ColorGradientStop item in _stops) + item.PropertyChanged -= ItemOnPropertyChanged; + _stops.Clear(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + public bool Contains(object? value) + { + return _stops.Contains(value); + } + + /// + public int IndexOf(object? value) + { + return _stops.IndexOf(value); + } + + /// + public void Insert(int index, object? value) + { + if (value is ColorGradientStop stop) + Insert(index, stop); + } + + /// + public void Remove(object? value) + { + if (value is ColorGradientStop stop) + Remove(stop); + } + + /// + public bool Contains(ColorGradientStop item) + { + return _stops.Contains(item); + } + + /// + public void CopyTo(ColorGradientStop[] array, int arrayIndex) + { + _stops.CopyTo(array, arrayIndex); + } + + /// + public bool Remove(ColorGradientStop item) + { + item.PropertyChanged -= ItemOnPropertyChanged; + int index = _stops.IndexOf(item); + bool removed = _stops.Remove(item); + if (removed) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + + return removed; + } + + /// + public void CopyTo(Array array, int index) + { + _stops.CopyTo((ColorGradientStop[]) array, index); + } + + /// + public int Count => _stops.Count; + + /// + public bool IsSynchronized => false; + + /// + public object SyncRoot => this; + + /// + public bool IsReadOnly => false; + + object? IList.this[int index] + { + get => this[index]; + set => this[index] = (ColorGradientStop) value!; + } + + #endregion + + #region Implementation of IList + + /// + public int IndexOf(ColorGradientStop item) + { + return _stops.IndexOf(item); + } + + /// + public void Insert(int index, ColorGradientStop item) + { + _stops.Insert(index, item); + item.PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + } + + /// + public void RemoveAt(int index) + { + _stops[index].PropertyChanged -= ItemOnPropertyChanged; + _stops.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); + } + + /// + public bool IsFixedSize => false; + + /// + public ColorGradientStop this[int index] + { + get => _stops[index]; + set + { + ColorGradientStop? oldValue = _stops[index]; + oldValue.PropertyChanged -= ItemOnPropertyChanged; + _stops[index] = value; + _stops[index].PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); + } + } + + #endregion + + #region Implementation of INotifyCollectionChanged + + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + #endregion } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs index 242a883d3..205c772ff 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradientStop.cs @@ -1,4 +1,5 @@ -using SkiaSharp; +using System; +using SkiaSharp; namespace Artemis.Core { @@ -7,6 +8,34 @@ namespace Artemis.Core /// public class ColorGradientStop : CorePropertyChanged { + #region Equality members + + /// + protected bool Equals(ColorGradientStop other) + { + return _color.Equals(other._color) && _position.Equals(other._position); + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((ColorGradientStop) obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(_color, _position); + } + + #endregion + private SKColor _color; private float _position; diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/BaseConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/BaseConditionOperator.cs deleted file mode 100644 index 4214e14de..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/BaseConditionOperator.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a condition operator that performs a boolean operation - /// - /// To implement your own condition operator, inherit or - /// - /// - /// - public abstract class BaseConditionOperator - { - /// - /// Gets or sets the description of this logical operator - /// - public abstract string Description { get; } - - /// - /// Gets or sets the icon of this logical operator - /// - public abstract string Icon { get; } - - /// - /// Gets the plugin this condition operator belongs to - /// Note: Not set until after registering - /// - public Plugin? Plugin { get; internal set; } - - /// - /// Gets the left side type of this condition operator - /// - public abstract Type LeftSideType { get; } - - /// - /// Gets the right side type of this condition operator. May be null if the operator does not support a right side - /// - public abstract Type? RightSideType { get; } - - /// - /// Returns whether the given type is supported by the operator - /// - /// The type to check for, must be either the same or be castable to the target type - /// Which side of the operator to check, left or right - public bool SupportsType(Type type, ConditionParameterSide side) - { - if (type == null) - return true; - if (side == ConditionParameterSide.Left) - return LeftSideType.IsCastableFrom(type); - return RightSideType != null && RightSideType.IsCastableFrom(type); - } - - /// - /// Evaluates the condition with the input types being provided as objects - /// - /// This leaves the caller responsible for the types matching and - /// - /// - /// - /// The left side value, type should match - /// The right side value, type should match - /// The result of the boolean condition's evaluation - internal abstract bool InternalEvaluate(object? leftSideValue, object? rightSideValue); - } - - /// - /// Represents a side of a condition parameter - /// - public enum ConditionParameterSide - { - /// - /// The left side of a condition parameter - /// - Left, - - /// - /// The right side of a condition parameter - /// - Right - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs deleted file mode 100644 index 2134cbaf9..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/ConditionOperator.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a condition operator that performs a boolean operation using a left- and right-side - /// - public abstract class ConditionOperator : BaseConditionOperator - { - /// - public override Type LeftSideType => typeof(TLeftSide); - - /// - public override Type RightSideType => typeof(TRightSide); - - /// - /// Evaluates the operator on a and b - /// - /// The parameter on the left side of the expression - /// The parameter on the right side of the expression - public abstract bool Evaluate(TLeftSide a, TRightSide b); - - /// - internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue) - { - // TODO: Can we avoid boxing/unboxing? - TLeftSide leftSide; - if (leftSideValue != null) - { - if (leftSideValue.GetType() != typeof(TLeftSide) && leftSideValue is IConvertible) - leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide)); - else - leftSide = (TLeftSide) leftSideValue; - } - else - { - leftSide = default; - } - - TRightSide rightSide; - if (rightSideValue != null) - { - if (rightSideValue.GetType() != typeof(TRightSide) && leftSideValue is IConvertible) - rightSide = (TRightSide) Convert.ChangeType(rightSideValue, typeof(TRightSide)); - else - rightSide = (TRightSide) rightSideValue; - } - else - { - rightSide = default; - } - - return Evaluate(leftSide!, rightSide!); - } - } - - /// - /// Represents a condition operator that performs a boolean operation using only a left side - /// - public abstract class ConditionOperator : BaseConditionOperator - { - /// - public override Type LeftSideType => typeof(TLeftSide); - - /// - /// Always null, not applicable to this type of condition operator - /// - public override Type? RightSideType => null; - - /// - /// Evaluates the operator on a and b - /// - /// The parameter on the left side of the expression - public abstract bool Evaluate(TLeftSide a); - - /// - internal override bool InternalEvaluate(object? leftSideValue, object? rightSideValue) - { - // TODO: Can we avoid boxing/unboxing? - TLeftSide leftSide; - if (leftSideValue != null) - { - if (leftSideValue.GetType() != typeof(TLeftSide) && leftSideValue is IConvertible) - leftSide = (TLeftSide) Convert.ChangeType(leftSideValue, typeof(TLeftSide)); - else - leftSide = (TLeftSide) leftSideValue; - } - else - { - leftSide = default; - } - - return Evaluate(leftSide!); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs deleted file mode 100644 index d86df56a0..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPart.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Artemis.Storage.Entities.Profile.Abstract; - -namespace Artemis.Core -{ - /// - /// An abstract class for display condition parts - /// - public abstract class DataModelConditionPart : IDisposable - { - private readonly List _children = new(); - - protected DataModelConditionPart() - { - Children = new(_children); - } - - /// - /// Gets the parent of this part - /// - public DataModelConditionPart? Parent { get; internal set; } - - /// - /// Gets the children of this part - /// - public ReadOnlyCollection Children { get; } - - /// - /// Adds a child to the display condition part's collection - /// - /// - /// An optional index at which to insert the condition - public void AddChild(DataModelConditionPart dataModelConditionPart, int? index = null) - { - if (!_children.Contains(dataModelConditionPart)) - { - dataModelConditionPart.Parent = this; - if (index != null) - _children.Insert(index.Value, dataModelConditionPart); - else - _children.Add(dataModelConditionPart); - - OnChildAdded(); - } - } - - /// - /// Removes a child from the display condition part's collection - /// - /// The child to remove - public void RemoveChild(DataModelConditionPart dataModelConditionPart) - { - if (_children.Contains(dataModelConditionPart)) - { - dataModelConditionPart.Parent = null; - _children.Remove(dataModelConditionPart); - OnChildRemoved(); - } - } - - /// - /// Removes all children. You monster. - /// - public void ClearChildren() - { - while (Children.Any()) - RemoveChild(Children[0]); - } - - /// - /// Evaluates the condition part on the data model - /// - /// - public abstract bool Evaluate(); - - /// - /// Evaluates the condition part on the given target (currently only for lists) - /// - /// - /// - internal abstract bool EvaluateObject(object? target); - - internal abstract void Save(); - internal abstract DataModelConditionPartEntity GetEntity(); - - #region IDisposable - - /// - /// Disposed the condition part - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - #region Events - - /// - /// Occurs when a child-condition was added - /// - public event EventHandler? ChildAdded; - - /// - /// Occurs when a child-condition was removed - /// - public event EventHandler? ChildRemoved; - - /// - /// Invokers the event - /// - protected virtual void OnChildAdded() - { - ChildAdded?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokers the event - /// - protected virtual void OnChildRemoved() - { - ChildRemoved?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs deleted file mode 100644 index f836d9157..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/Abstract/DataModelConditionPredicate.cs +++ /dev/null @@ -1,411 +0,0 @@ -using System; -using Artemis.Storage.Entities.Profile.Abstract; -using Artemis.Storage.Entities.Profile.Conditions; -using Newtonsoft.Json; - -namespace Artemis.Core -{ - /// - /// A predicate in a data model condition using either two data model values or one data model value and a - /// static value - /// - public abstract class DataModelConditionPredicate : DataModelConditionPart - { - /// - /// Creates a new instance of the class - /// - /// - /// - /// A new empty entity - protected DataModelConditionPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType, DataModelConditionPredicateEntity entity) - { - Parent = parent; - Entity = entity; - PredicateType = predicateType; - } - - internal DataModelConditionPredicate(DataModelConditionPart parent, DataModelConditionPredicateEntity entity) - { - Parent = parent; - Entity = entity; - PredicateType = (ProfileRightSideType) entity.PredicateType; - } - - /// - /// Gets or sets the predicate type - /// - public ProfileRightSideType PredicateType { get; set; } - - /// - /// Gets the operator - /// - public BaseConditionOperator? Operator { get; protected set; } - - /// - /// Gets the path of the left property - /// - public DataModelPath? LeftPath { get; protected set; } - - /// - /// Gets the path of the right property - /// - public DataModelPath? RightPath { get; protected set; } - - /// - /// Gets the right static value, only used it is - /// - /// - public object? RightStaticValue { get; protected set; } - - internal DataModelConditionPredicateEntity Entity { get; set; } - - /// - public override string ToString() - { - if (PredicateType == ProfileRightSideType.Dynamic) - return $"[Dynamic] {LeftPath} {Operator?.Description} {RightPath}"; - return $"[Static] {LeftPath} {Operator?.Description} {RightStaticValue}"; - } - - #region IDisposable - - /// - protected override void Dispose(bool disposing) - { - ConditionOperatorStore.ConditionOperatorAdded -= ConditionOperatorStoreOnConditionOperatorAdded; - ConditionOperatorStore.ConditionOperatorRemoved -= ConditionOperatorStoreOnConditionOperatorRemoved; - - LeftPath?.Dispose(); - RightPath?.Dispose(); - - base.Dispose(disposing); - } - - #endregion - - #region Initialization - - internal void Initialize() - { - ConditionOperatorStore.ConditionOperatorAdded += ConditionOperatorStoreOnConditionOperatorAdded; - ConditionOperatorStore.ConditionOperatorRemoved += ConditionOperatorStoreOnConditionOperatorRemoved; - - InitializeLeftPath(); - - // Operator - if (Entity.OperatorPluginGuid != null) - { - BaseConditionOperator? conditionOperator = ConditionOperatorStore.Get(Entity.OperatorPluginGuid.Value, Entity.OperatorType)?.ConditionOperator; - if (conditionOperator != null) - UpdateOperator(conditionOperator); - } - - // Right side dynamic - if (PredicateType == ProfileRightSideType.Dynamic) - InitializeRightPath(); - // Right side static - else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) - { - try - { - // If the left path is not valid we cannot reliably set up the right side because the type is unknown - // Because of that wait for it to validate first - if (LeftPath != null && !LeftPath.IsValid) - { - LeftPath.PathValidated += InitializeRightSideStatic; - return; - } - if (LeftPath == null) - return; - - // Use the left side type so JSON.NET has a better idea what to do - Type leftSideType = LeftPath.GetPropertyType()!; - object? rightSideValue; - - try - { - rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType); - } - // If deserialization fails, use the type's default - catch (JsonSerializationException e) - { - DeserializationLogger.LogPredicateDeserializationFailure(this, e); - rightSideValue = Activator.CreateInstance(leftSideType); - } - - UpdateRightSideStatic(rightSideValue); - } - catch (JsonReaderException e) - { - DeserializationLogger.LogPredicateDeserializationFailure(this, e); - } - } - } - - private void InitializeRightSideStatic(object? sender, EventArgs args) - { - if (LeftPath == null) - return; - - LeftPath.PathValidated -= InitializeRightSideStatic; - - // Use the left side type so JSON.NET has a better idea what to do - Type leftSideType = LeftPath.GetPropertyType()!; - object? rightSideValue; - - try - { - rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType); - } - // If deserialization fails, use the type's default - catch (JsonSerializationException e) - { - DeserializationLogger.LogPredicateDeserializationFailure(this, e); - rightSideValue = Activator.CreateInstance(leftSideType); - } - - UpdateRightSideStatic(rightSideValue); - } - - /// - /// Initializes the left path of this condition predicate - /// - protected abstract void InitializeLeftPath(); - - /// - /// Initializes the right path of this condition predicate - /// - protected abstract void InitializeRightPath(); - - #endregion - - #region Modification - - /// - /// Updates the left side of the predicate - /// - /// The path pointing to the left side value inside the data model - public virtual void UpdateLeftSide(DataModelPath? path) - { - if (path != null && !path.IsValid) - throw new ArtemisCoreException("Cannot update left side of predicate to an invalid path"); - - LeftPath?.Dispose(); - LeftPath = path != null ? new DataModelPath(path) : null; - - ValidateOperator(); - ValidateRightSide(); - } - - /// - /// Updates the right side of the predicate, makes the predicate dynamic and re-compiles the expression - /// - /// The path pointing to the right side value inside the data model - public void UpdateRightSideDynamic(DataModelPath? path) - { - if (path != null && !path.IsValid) - throw new ArtemisCoreException("Cannot update right side of predicate to an invalid path"); - if (Operator != null && path != null && !Operator.SupportsType(path.GetPropertyType()!, ConditionParameterSide.Right)) - throw new ArtemisCoreException($"Selected operator does not support right side of type {path.GetPropertyType()!.Name}"); - - RightPath?.Dispose(); - RightPath = path != null ? new DataModelPath(path) : null; - - PredicateType = ProfileRightSideType.Dynamic; - } - - /// - /// Updates the right side of the predicate, makes the predicate static and re-compiles the expression - /// - /// The right side value to use - public void UpdateRightSideStatic(object? staticValue) - { - PredicateType = ProfileRightSideType.Static; - RightPath?.Dispose(); - RightPath = null; - - // If the operator is null simply apply the value, any validation will wait - if (Operator == null) - { - RightStaticValue = staticValue; - return; - } - - // If the operator does not support a right side, always set it to null - if (Operator.RightSideType == null) - { - RightStaticValue = null; - return; - } - - // If not null ensure the types match and if not, convert it - Type? preferredType = GetPreferredRightSideType(); - if (staticValue != null && staticValue.GetType() == preferredType || preferredType == null) - RightStaticValue = staticValue; - else if (staticValue != null) - RightStaticValue = Convert.ChangeType(staticValue, preferredType); - // If null create a default instance for value types or simply make it null for reference types - else if (preferredType.IsValueType) - RightStaticValue = Activator.CreateInstance(preferredType); - else - RightStaticValue = null; - } - - /// - /// Updates the operator of the predicate and re-compiles the expression - /// - /// - public void UpdateOperator(BaseConditionOperator? conditionOperator) - { - if (conditionOperator == null) - { - Operator = null; - return; - } - - // No need to check for compatibility without a left side, when left site does get set it will be checked again - if (LeftPath == null || !LeftPath.IsValid) - { - Operator = conditionOperator; - return; - } - - Type leftType = LeftPath.GetPropertyType()!; - // Left side can't go empty so enforce a match - if (!conditionOperator.SupportsType(leftType, ConditionParameterSide.Left)) - throw new ArtemisCoreException($"Cannot apply operator {conditionOperator.GetType().Name} to this predicate because " + - $"it does not support left side type {leftType.Name}"); - - Operator = conditionOperator; - ValidateRightSide(); - } - - /// - /// Determines the best type to use for the right side op this predicate - /// - public abstract Type? GetPreferredRightSideType(); - - private void ValidateOperator() - { - if (LeftPath == null || !LeftPath.IsValid || Operator == null) - return; - - Type leftType = LeftPath.GetPropertyType()!; - if (!Operator.SupportsType(leftType, ConditionParameterSide.Left)) - Operator = null; - } - - private void ValidateRightSide() - { - if (Operator == null) - return; - - if (PredicateType == ProfileRightSideType.Dynamic) - { - if (RightPath == null || !RightPath.IsValid) - return; - - Type rightSideType = RightPath.GetPropertyType()!; - if (!Operator.SupportsType(rightSideType, ConditionParameterSide.Right)) - UpdateRightSideDynamic(null); - } - else - { - if (RightStaticValue == null) - return; - - if (!Operator.SupportsType(RightStaticValue.GetType(), ConditionParameterSide.Right)) - UpdateRightSideStatic(null); - } - } - - #endregion - - #region Evaluation - - /// - public override bool Evaluate() - { - if (Operator == null || LeftPath == null || !LeftPath.IsValid) - return false; - - // If the operator does not support a right side, immediately evaluate with null - if (Operator.RightSideType == null) - return Operator.InternalEvaluate(LeftPath.GetValue(), null); - - // Compare with a static value - if (PredicateType == ProfileRightSideType.Static) - { - object? leftSideValue = LeftPath.GetValue(); - if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null) - return false; - - return Operator.InternalEvaluate(leftSideValue, RightStaticValue); - } - - if (RightPath == null || !RightPath.IsValid) - return false; - - // Compare with dynamic values - return Operator.InternalEvaluate(LeftPath.GetValue(), RightPath.GetValue()); - } - - /// - internal override bool EvaluateObject(object? target) - { - return false; - } - - #endregion - - #region Storage - - internal override DataModelConditionPartEntity GetEntity() - { - return Entity; - } - - internal override void Save() - { - // Don't save an invalid state - if (LeftPath != null && !LeftPath.IsValid || RightPath != null && !RightPath.IsValid) - return; - - Entity.PredicateType = (int) PredicateType; - - LeftPath?.Save(); - Entity.LeftPath = LeftPath?.Entity; - RightPath?.Save(); - Entity.RightPath = RightPath?.Entity; - - Entity.RightStaticValue = CoreJson.SerializeObject(RightStaticValue); - - if (Operator?.Plugin != null) - { - Entity.OperatorPluginGuid = Operator.Plugin.Guid; - Entity.OperatorType = Operator.GetType().Name; - } - } - - #endregion - - #region Event handlers - - private void ConditionOperatorStoreOnConditionOperatorAdded(object? sender, ConditionOperatorStoreEvent e) - { - BaseConditionOperator conditionOperator = e.Registration.ConditionOperator; - if (Entity.OperatorPluginGuid == conditionOperator.Plugin!.Guid && Entity.OperatorType == conditionOperator.GetType().Name) - UpdateOperator(conditionOperator); - } - - private void ConditionOperatorStoreOnConditionOperatorRemoved(object? sender, ConditionOperatorStoreEvent e) - { - if (e.Registration.ConditionOperator != Operator) - return; - - Operator = null; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs new file mode 100644 index 000000000..a5463dfd3 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/AlwaysOnCondition.cs @@ -0,0 +1,90 @@ +using System; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + /// + /// Represents a condition that is always true. + /// + public class AlwaysOnCondition : ICondition + { + /// + /// Creates a new instance of the class. + /// + /// The profile element this condition applies to. + public AlwaysOnCondition(RenderProfileElement profileElement) + { + ProfileElement = profileElement; + Entity = new AlwaysOnConditionEntity(); + } + + /// + /// Creates a new instance of the class. + /// + /// The entity used to store this condition. + /// The profile element this condition applies to. + public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement) + { + ProfileElement = profileElement; + Entity = alwaysOnConditionEntity; + } + + #region Implementation of IDisposable + + /// + public void Dispose() + { + } + + #endregion + + #region Implementation of IStorageModel + + /// + public void Load() + { + } + + /// + public void Save() + { + } + + #endregion + + #region Implementation of ICondition + + /// + public IConditionEntity Entity { get; } + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + public void Update() + { + if (ProfileElement.Parent is RenderProfileElement parent) + IsMet = parent.DisplayConditionMet; + else + IsMet = true; + } + + /// + public void UpdateTimeline(double deltaTime) + { + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs deleted file mode 100644 index 0ff661385..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEvent.cs +++ /dev/null @@ -1,257 +0,0 @@ -using System; -using System.Linq; -using Artemis.Storage.Entities.Profile.Abstract; -using Artemis.Storage.Entities.Profile.Conditions; - -namespace Artemis.Core -{ - /// - /// A condition that evaluates to true when an event is triggered - /// - public class DataModelConditionEvent : DataModelConditionPart - { - private bool _disposed; - private bool _reinitializing; - private IDataModelEvent? _valueChangedEvent; - - /// - /// Creates a new instance of the class - /// - /// - public DataModelConditionEvent(DataModelConditionPart parent) - { - Parent = parent; - Entity = new DataModelConditionEventEntity(); - - Initialize(); - } - - internal DataModelConditionEvent(DataModelConditionPart parent, DataModelConditionEventEntity entity) - { - Parent = parent; - Entity = entity; - - Initialize(); - } - - /// - /// Gets the path of the event property - /// - public DataModelPath? EventPath { get; private set; } - - /// - /// Gets the last time the event this condition is applied to was triggered - /// - public DateTime LastTrigger { get; private set; } - - /// - /// Gets or sets the type of argument the event provides - /// - public Type? EventArgumentType { get; private set; } - - internal DataModelConditionEventEntity Entity { get; set; } - - /// - public override bool Evaluate() - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionEvent"); - - IDataModelEvent? dataModelEvent = GetDataModelEvent(); - if (dataModelEvent == null) - return false; - dataModelEvent.Update(); - - // Only evaluate to true once every time the event has been triggered since the last evaluation - if (dataModelEvent.LastTrigger <= LastTrigger) - return false; - - LastTrigger = DateTime.Now; - - // If there is a child (root group), it must evaluate to true whenever the event triggered - if (Children.Any()) - return Children[0].EvaluateObject(dataModelEvent.LastEventArgumentsUntyped); - - // If there are no children, we always evaluate to true whenever the event triggered - return true; - } - - /// - /// Updates the event the condition is triggered by - /// - public void UpdateEvent(DataModelPath? path) - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionEvent"); - - if (path != null && !path.IsValid) - throw new ArtemisCoreException("Cannot update event to an invalid path"); - - EventPath?.Dispose(); - EventPath = path != null ? new DataModelPath(path) : null; - SubscribeToEventPath(); - CreateValueChangedEventIfNeeded(); - - // Remove the old root group that was tied to the old data model - ClearChildren(); - - if (EventPath != null) - { - EventArgumentType = GetEventArgumentType(); - // Create a new root group - AddChild(new DataModelConditionGroup(this)); - } - else - { - EventArgumentType = null; - } - - LastTrigger = GetDataModelEvent()?.LastTrigger ?? DateTime.Now; - } - - /// - /// Returns the this is triggered by - /// - /// The this is triggered by - public IDataModelEvent? GetDataModelEvent() - { - if (_valueChangedEvent != null) - return _valueChangedEvent; - return EventPath?.GetValue() as IDataModelEvent; - } - - #region IDisposable - - /// - protected override void Dispose(bool disposing) - { - _disposed = true; - - EventPath?.Dispose(); - - foreach (DataModelConditionPart child in Children) - child.Dispose(); - - base.Dispose(disposing); - } - - #endregion - - internal override bool EvaluateObject(object? target) - { - return false; - } - - internal override void Save() - { - // Don't save an invalid state - if (EventPath != null && !EventPath.IsValid) - return; - - // Target list - EventPath?.Save(); - Entity.EventPath = EventPath?.Entity; - - // Children - Entity.Children.Clear(); - Entity.Children.AddRange(Children.Select(c => c.GetEntity())); - foreach (DataModelConditionPart child in Children) - child.Save(); - } - - internal override DataModelConditionPartEntity GetEntity() - { - return Entity; - } - - internal void Initialize() - { - ClearChildren(); - - if (Entity.EventPath == null) - return; - - DataModelPath eventPath = new(null, Entity.EventPath); - EventPath = eventPath; - SubscribeToEventPath(); - CreateValueChangedEventIfNeeded(); - - EventArgumentType = GetEventArgumentType(); - // There should only be one child and it should be a group - if (Entity.Children.FirstOrDefault() is DataModelConditionGroupEntity rootGroup) - { - AddChild(new DataModelConditionGroup(this, rootGroup)); - } - else - { - Entity.Children.Clear(); - AddChild(new DataModelConditionGroup(this)); - } - - LastTrigger = GetDataModelEvent()?.LastTrigger ?? DateTime.Now; - } - - private Type? GetEventArgumentType() - { - if (EventPath == null || !EventPath.IsValid) - return null; - - if (_valueChangedEvent != null) - return _valueChangedEvent.ArgumentsType; - - // Cannot rely on EventPath.GetValue() because part of the path might be null - Type eventType = EventPath.GetPropertyType()!; - return eventType.IsGenericType ? eventType.GetGenericArguments()[0] : typeof(DataModelEventArgs); - } - - private void SubscribeToEventPath() - { - if (EventPath == null) return; - EventPath.PathValidated += EventPathOnPathValidated; - EventPath.PathInvalidated += EventPathOnPathInvalidated; - } - - private void CreateValueChangedEventIfNeeded() - { - Type? propertyType = EventPath?.GetPropertyType(); - if (propertyType == null) - return; - - if (!typeof(IDataModelEvent).IsAssignableFrom(propertyType)) - { - IDataModelEvent? instance = (IDataModelEvent?) Activator.CreateInstance(typeof(DataModelValueChangedEvent<>).MakeGenericType(propertyType), EventPath); - _valueChangedEvent = instance ?? throw new ArtemisCoreException("Failed to create a DataModelValueChangedEvent for a property changed data model event"); - } - else - { - _valueChangedEvent = null; - } - } - - #region Event handlers - - private void EventPathOnPathValidated(object? sender, EventArgs e) - { - if (_reinitializing) - return; - - _reinitializing = true; - EventPath?.Dispose(); - Initialize(); - _reinitializing = false; - } - - private void EventPathOnPathInvalidated(object? sender, EventArgs e) - { - if (_reinitializing) - return; - - _reinitializing = true; - EventPath?.Dispose(); - Initialize(); - _reinitializing = false; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEventPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEventPredicate.cs deleted file mode 100644 index c6477ba65..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionEventPredicate.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System; -using Artemis.Storage.Entities.Profile; -using Artemis.Storage.Entities.Profile.Conditions; - -namespace Artemis.Core -{ - /// - /// A predicate like evaluated inside a - /// - public class DataModelConditionEventPredicate : DataModelConditionPredicate - { - /// - /// Creates a new instance of the class - /// - /// - /// - public DataModelConditionEventPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType) - : base(parent, predicateType, new DataModelConditionEventPredicateEntity()) - { - DataModelConditionEvent = null!; - ApplyParentEvent(); - Initialize(); - } - - internal DataModelConditionEventPredicate(DataModelConditionPart parent, DataModelConditionEventPredicateEntity entity) - : base(parent, entity) - { - DataModelConditionEvent = null!; - ApplyParentEvent(); - Initialize(); - } - - /// - /// Gets the data model condition event this predicate belongs to - /// - public DataModelConditionEvent DataModelConditionEvent { get; private set; } - - private void ApplyParentEvent() - { - DataModelConditionPart? current = Parent; - while (current != null) - { - if (current is DataModelConditionEvent parentEvent) - { - DataModelConditionEvent = parentEvent; - return; - } - - current = current.Parent; - } - - if (DataModelConditionEvent == null) - throw new ArtemisCoreException("This data model condition event predicate does not belong to a data model condition event"); - } - - private object? GetEventPathValue(DataModelPath path, object? target) - { - lock (path) - { - if (!(path.Target is EventPredicateWrapperDataModel wrapper)) - throw new ArtemisCoreException("Data model condition event predicate has a path with an invalid target"); - - wrapper.UntypedArguments = target; - return path.GetValue(); - } - } - - #region Initialization - - /// - protected override void InitializeLeftPath() - { - if (Entity.LeftPath != null) - LeftPath = DataModelConditionEvent.EventArgumentType != null - ? new DataModelPath(EventPredicateWrapperDataModel.Create(DataModelConditionEvent.EventArgumentType), Entity.LeftPath) - : null; - } - - /// - protected override void InitializeRightPath() - { - if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null) - { - // Right side dynamic using event arguments - if (Entity.RightPath.WrapperType == PathWrapperType.Event) - { - RightPath = DataModelConditionEvent.EventArgumentType != null - ? new DataModelPath(EventPredicateWrapperDataModel.Create(DataModelConditionEvent.EventArgumentType), Entity.RightPath) - : null; - } - // Right side dynamic - else - RightPath = new DataModelPath(null, Entity.RightPath); - } - } - - #endregion - - #region Modification - - /// - public override Type? GetPreferredRightSideType() - { - Type? preferredType = Operator?.RightSideType; - Type? leftSideType = LeftPath?.GetPropertyType(); - if (preferredType == null) - return null; - - if (leftSideType != null && preferredType.IsAssignableFrom(leftSideType)) - preferredType = leftSideType; - - return preferredType; - } - - #endregion - - #region Evaluation - - /// - /// Not supported for event predicates, always returns false - /// - public override bool Evaluate() - { - return false; - } - - internal override bool EvaluateObject(object? target) - { - if (Operator == null || LeftPath == null || !LeftPath.IsValid) - return false; - - // If the operator does not support a right side, immediately evaluate with null - if (Operator.RightSideType == null) - return Operator.InternalEvaluate(GetEventPathValue(LeftPath, target), null); - - // Compare with a static value - if (PredicateType == ProfileRightSideType.Static) - { - object? leftSideValue = GetEventPathValue(LeftPath, target); - if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null) - return false; - - return Operator.InternalEvaluate(leftSideValue, RightStaticValue); - } - - if (RightPath == null || !RightPath.IsValid) - return false; - - // Compare with dynamic values - if (PredicateType == ProfileRightSideType.Dynamic) - { - // If the path targets a property inside the event, evaluate on the event path value instead of the right path value - if (RightPath.Target is EventPredicateWrapperDataModel) - return Operator.InternalEvaluate(GetEventPathValue(LeftPath, target), GetEventPathValue(RightPath, target)); - return Operator.InternalEvaluate(GetEventPathValue(LeftPath, target), RightPath.GetValue()); - } - - return false; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGeneralPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGeneralPredicate.cs deleted file mode 100644 index e65030a7a..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGeneralPredicate.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Artemis.Storage.Entities.Profile.Conditions; - -namespace Artemis.Core -{ - /// - /// A predicate in a data model condition using either two data model values or one data model value and a - /// static value - /// - public class DataModelConditionGeneralPredicate : DataModelConditionPredicate - { - /// - /// Creates a new instance of the class - /// - /// - /// - public DataModelConditionGeneralPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType) - : base(parent, predicateType, new DataModelConditionGeneralPredicateEntity()) - { - Initialize(); - } - - internal DataModelConditionGeneralPredicate(DataModelConditionPart parent, DataModelConditionGeneralPredicateEntity entity) - : base(parent, entity) - { - Initialize(); - } - - #region Modification - - /// - public override Type? GetPreferredRightSideType() - { - Type? preferredType = Operator?.RightSideType; - Type? leftSideType = LeftPath?.GetPropertyType(); - if (preferredType == null) - return null; - - if (leftSideType != null && preferredType.IsAssignableFrom(leftSideType)) - preferredType = leftSideType; - - return preferredType; - } - - #endregion - - #region Initialization - - /// - protected override void InitializeLeftPath() - { - if (Entity.LeftPath != null) - LeftPath = new DataModelPath(null, Entity.LeftPath); - } - - /// - protected override void InitializeRightPath() - { - if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null) - RightPath = new DataModelPath(null, Entity.RightPath); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs deleted file mode 100644 index 53dcaa0bd..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionGroup.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Artemis.Storage.Entities.Profile.Abstract; -using Artemis.Storage.Entities.Profile.Conditions; - -namespace Artemis.Core -{ - /// - /// A group containing zero to many s which it evaluates using a boolean specific - /// operator - /// - public class DataModelConditionGroup : DataModelConditionPart - { - private bool _disposed; - - /// - /// Creates a new instance of the class - /// - /// - public DataModelConditionGroup(DataModelConditionPart? parent) - { - Parent = parent; - Entity = new DataModelConditionGroupEntity(); - ChildAdded += OnChildrenChanged; - ChildRemoved += OnChildrenChanged; - } - - /// - /// Creates a new instance of the class - /// - /// - /// - public DataModelConditionGroup(DataModelConditionPart? parent, DataModelConditionGroupEntity entity) - { - Parent = parent; - Entity = entity; - BooleanOperator = (BooleanOperator) Entity.BooleanOperator; - - foreach (DataModelConditionPartEntity childEntity in Entity.Children) - { - if (childEntity is DataModelConditionGroupEntity groupEntity) - AddChild(new DataModelConditionGroup(this, groupEntity)); - else if (childEntity is DataModelConditionListEntity listEntity) - AddChild(new DataModelConditionList(this, listEntity)); - else if (childEntity is DataModelConditionEventEntity eventEntity) - AddChild(new DataModelConditionEvent(this, eventEntity)); - else if (childEntity is DataModelConditionGeneralPredicateEntity predicateEntity) - AddChild(new DataModelConditionGeneralPredicate(this, predicateEntity)); - else if (childEntity is DataModelConditionListPredicateEntity listPredicateEntity) - AddChild(new DataModelConditionListPredicate(this, listPredicateEntity)); - else if (childEntity is DataModelConditionEventPredicateEntity eventPredicateEntity) - AddChild(new DataModelConditionEventPredicate(this, eventPredicateEntity)); - } - - ContainsEvents = Children.Any(c => c is DataModelConditionEvent); - ChildAdded += OnChildrenChanged; - ChildRemoved += OnChildrenChanged; - } - - /// - /// Gets or sets the boolean operator of this group - /// - public BooleanOperator BooleanOperator { get; set; } - - /// - /// Gets whether this group contains any events - /// - public bool ContainsEvents { get; private set; } - - internal DataModelConditionGroupEntity Entity { get; set; } - - /// - public override bool Evaluate() - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionGroup"); - - // Empty groups are always true - if (Children.Count == 0) - return true; - // Groups with only one child ignore the boolean operator - if (Children.Count == 1) - return Children[0].Evaluate(); - - if (ContainsEvents) - { - bool eventTriggered = Children.Where(c => c is DataModelConditionEvent).Any(c => c.Evaluate()); - return eventTriggered && EvaluateWithOperator(Children.Where(c => !(c is DataModelConditionEvent))); - } - return EvaluateWithOperator(Children); - } - - private bool EvaluateWithOperator(IEnumerable targets) - { - return BooleanOperator switch - { - BooleanOperator.And => targets.All(c => c.Evaluate()), - BooleanOperator.Or => targets.Any(c => c.Evaluate()), - BooleanOperator.AndNot => targets.All(c => !c.Evaluate()), - BooleanOperator.OrNot => targets.Any(c => !c.Evaluate()), - _ => throw new ArgumentOutOfRangeException() - }; - } - - #region IDisposable - - /// - protected override void Dispose(bool disposing) - { - _disposed = true; - foreach (DataModelConditionPart child in Children) - child.Dispose(); - - base.Dispose(disposing); - } - - #endregion - - /// - internal override bool EvaluateObject(object? target) - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionGroup"); - - // Empty groups are always true - if (Children.Count == 0) - return true; - // Groups with only one child ignore the boolean operator - if (Children.Count == 1) - return Children[0].EvaluateObject(target); - - return BooleanOperator switch - { - BooleanOperator.And => Children.All(c => c.EvaluateObject(target)), - BooleanOperator.Or => Children.Any(c => c.EvaluateObject(target)), - BooleanOperator.AndNot => Children.All(c => !c.EvaluateObject(target)), - BooleanOperator.OrNot => Children.Any(c => !c.EvaluateObject(target)), - _ => throw new ArgumentOutOfRangeException() - }; - } - - internal override void Save() - { - Entity.BooleanOperator = (int) BooleanOperator; - - Entity.Children.Clear(); - Entity.Children.AddRange(Children.Select(c => c.GetEntity())); - foreach (DataModelConditionPart child in Children) - child.Save(); - } - - internal override DataModelConditionPartEntity GetEntity() - { - return Entity; - } - - private void OnChildrenChanged(object? sender, EventArgs e) - { - ContainsEvents = Children.Any(c => c is DataModelConditionEvent); - } - } - - /// - /// Represents a boolean operator - /// - public enum BooleanOperator - { - /// - /// All the conditions in the group should evaluate to true - /// - And, - - /// - /// Any of the conditions in the group should evaluate to true - /// - Or, - - /// - /// All the conditions in the group should evaluate to false - /// - AndNot, - - /// - /// Any of the conditions in the group should evaluate to false - /// - OrNot - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs deleted file mode 100644 index 9dd05a5e8..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionList.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Artemis.Storage.Entities.Profile.Abstract; -using Artemis.Storage.Entities.Profile.Conditions; - -namespace Artemis.Core -{ - /// - /// A condition that evaluates one or more predicates inside a list - /// - public class DataModelConditionList : DataModelConditionPart - { - private bool _disposed; - private bool _reinitializing; - - /// - /// Creates a new instance of the class - /// - /// - public DataModelConditionList(DataModelConditionPart parent) - { - Parent = parent; - Entity = new DataModelConditionListEntity(); - - Initialize(); - } - - internal DataModelConditionList(DataModelConditionPart parent, DataModelConditionListEntity entity) - { - Parent = parent; - Entity = entity; - ListOperator = (ListOperator) entity.ListOperator; - - Initialize(); - } - - /// - /// Gets or sets the list operator - /// - public ListOperator ListOperator { get; set; } - - /// - /// Gets the path of the list property - /// - public DataModelPath? ListPath { get; private set; } - - /// - /// Gets the type of the content of the list this predicate is evaluated on - /// - public Type? ListType { get; set; } - - /// - /// Gets whether the list contains primitives - /// - public bool IsPrimitiveList { get; set; } - - internal DataModelConditionListEntity Entity { get; set; } - - /// - public override bool Evaluate() - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionList"); - - if (ListPath == null || !ListPath.IsValid) - return false; - - return EvaluateObject(ListPath.GetValue()); - } - - /// - /// Updates the list the predicate is evaluated on - /// - /// The path pointing to the list inside the list - public void UpdateList(DataModelPath? path) - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionList"); - - if (path != null && !path.IsValid) - throw new ArtemisCoreException("Cannot update list to an invalid path"); - - ListPath?.Dispose(); - ListPath = path != null ? new DataModelPath(path) : null; - SubscribeToListPath(); - - // Remove the old root group that was tied to the old data model - while (Children.Any()) - RemoveChild(Children[0]); - - if (ListPath != null) - { - Type listType = ListPath.GetPropertyType()!; - ListType = listType.GetGenericEnumerableType(); - IsPrimitiveList = ListType == null || ListType.IsPrimitive || ListType.IsEnum || ListType == typeof(string); - - // Create a new root group - AddChild(new DataModelConditionGroup(this)); - } - else - { - ListType = null; - } - } - - #region IDisposable - - /// - protected override void Dispose(bool disposing) - { - _disposed = true; - - ListPath?.Dispose(); - - foreach (DataModelConditionPart child in Children) - child.Dispose(); - - base.Dispose(disposing); - } - - #endregion - - internal override bool EvaluateObject(object? target) - { - if (_disposed) - throw new ObjectDisposedException("DataModelConditionList"); - - if (!Children.Any()) - return false; - if (!(target is IEnumerable enumerable)) - return false; - - IEnumerable objectList = enumerable.Cast(); - return ListOperator switch - { - ListOperator.Any => objectList.Any(o => Children[0].EvaluateObject(o)), - ListOperator.All => objectList.All(o => Children[0].EvaluateObject(o)), - ListOperator.None => objectList.Any(o => !Children[0].EvaluateObject(o)), - ListOperator.Count => false, - _ => throw new ArgumentOutOfRangeException() - }; - } - - internal override void Save() - { - // Don't save an invalid state - if (ListPath != null && !ListPath.IsValid) - return; - - // Target list - ListPath?.Save(); - Entity.ListPath = ListPath?.Entity; - - // Operator - Entity.ListOperator = (int) ListOperator; - - // Children - Entity.Children.Clear(); - Entity.Children.AddRange(Children.Select(c => c.GetEntity())); - foreach (DataModelConditionPart child in Children) - child.Save(); - } - - internal override DataModelConditionPartEntity GetEntity() - { - return Entity; - } - - internal void Initialize() - { - while (Children.Any()) - RemoveChild(Children[0]); - - if (Entity.ListPath == null) - return; - - // Ensure the list path is valid and points to a list - DataModelPath listPath = new(null, Entity.ListPath); - Type listType = listPath.GetPropertyType()!; - // Can't check this on an invalid list, if it becomes valid later lets hope for the best - if (listPath.IsValid && !PointsToList(listPath)) - return; - - ListPath = listPath; - SubscribeToListPath(); - if (ListPath.IsValid) - { - ListType = listType.GetGenericEnumerableType(); - IsPrimitiveList = ListType == null || ListType.IsPrimitive || ListType.IsEnum || ListType == typeof(string); - } - else - { - ListType = null; - IsPrimitiveList = false; - } - - // There should only be one child and it should be a group - if (Entity.Children.FirstOrDefault() is DataModelConditionGroupEntity rootGroup) - { - AddChild(new DataModelConditionGroup(this, rootGroup)); - } - else - { - Entity.Children.Clear(); - AddChild(new DataModelConditionGroup(this)); - } - } - - private bool PointsToList(DataModelPath dataModelPath) - { - Type? type = dataModelPath.GetPropertyType(); - return type?.IsGenericEnumerable() ?? false; - } - - private void SubscribeToListPath() - { - if (ListPath == null) return; - ListPath.PathValidated += ListPathOnPathValidated; - ListPath.PathInvalidated += ListPathOnPathInvalidated; - } - - #region Event handlers - - private void ListPathOnPathValidated(object? sender, EventArgs e) - { - if (_reinitializing) - return; - - _reinitializing = true; - ListPath?.Dispose(); - Initialize(); - _reinitializing = false; - } - - private void ListPathOnPathInvalidated(object? sender, EventArgs e) - { - if (_reinitializing) - return; - - _reinitializing = true; - ListPath?.Dispose(); - Initialize(); - _reinitializing = false; - } - - #endregion - } - - /// - /// Represents a list operator - /// - public enum ListOperator - { - /// - /// Any of the list items should evaluate to true - /// - Any, - - /// - /// All of the list items should evaluate to true - /// - All, - - /// - /// None of the list items should evaluate to true - /// - None, - - /// - /// A specific amount of the list items should evaluate to true - /// - Count - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs b/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs deleted file mode 100644 index 5fdc3a8d8..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/DataModelConditionListPredicate.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using Artemis.Core.Modules; -using Artemis.Storage.Entities.Profile; -using Artemis.Storage.Entities.Profile.Conditions; - -namespace Artemis.Core -{ - /// - /// A predicate like evaluated inside a - /// - public class DataModelConditionListPredicate : DataModelConditionPredicate - { - /// - /// Creates a new instance of the class - /// - /// - /// - public DataModelConditionListPredicate(DataModelConditionPart parent, ProfileRightSideType predicateType) - : base(parent, predicateType, new DataModelConditionListPredicateEntity()) - { - DataModelConditionList = null!; - ApplyParentList(); - Initialize(); - } - - internal DataModelConditionListPredicate(DataModelConditionPart parent, DataModelConditionListPredicateEntity entity) - : base(parent, entity) - { - DataModelConditionList = null!; - ApplyParentList(); - Initialize(); - } - - /// - /// Gets the data model condition list this predicate belongs to - /// - public DataModelConditionList DataModelConditionList { get; private set; } - - private void ApplyParentList() - { - DataModelConditionPart? current = Parent; - while (current != null) - { - if (current is DataModelConditionList parentList) - { - DataModelConditionList = parentList; - return; - } - - current = current.Parent; - } - - if (DataModelConditionList == null) - throw new ArtemisCoreException("This data model condition list predicate does not belong to a data model condition list"); - } - - private object? GetListPathValue(DataModelPath path, object? target) - { - if (!(path.Target is ListPredicateWrapperDataModel wrapper)) - throw new ArtemisCoreException("Data model condition list predicate has a path with an invalid target"); - - wrapper.UntypedValue = target; - return path.GetValue(); - } - - #region Initialization - - /// - protected override void InitializeLeftPath() - { - if (Entity.LeftPath != null) - LeftPath = DataModelConditionList.ListType != null - ? new DataModelPath(ListPredicateWrapperDataModel.Create( - DataModelConditionList.ListType, - DataModelConditionList.ListPath?.GetPropertyDescription()?.ListItemName - ), Entity.LeftPath) - : null; - } - - /// - protected override void InitializeRightPath() - { - if (PredicateType == ProfileRightSideType.Dynamic && Entity.RightPath != null) - { - // Right side dynamic inside the list - if (Entity.RightPath.WrapperType == PathWrapperType.List) - { - RightPath = DataModelConditionList.ListType != null - ? new DataModelPath(ListPredicateWrapperDataModel.Create( - DataModelConditionList.ListType, - DataModelConditionList.ListPath?.GetPropertyDescription()?.ListItemName - ), Entity.RightPath) - : null; - } - // Right side dynamic - else - RightPath = new DataModelPath(null, Entity.RightPath); - } - } - - #endregion - - #region Modification - - /// - public override Type? GetPreferredRightSideType() - { - Type? preferredType = Operator?.RightSideType; - Type? leftSideType = DataModelConditionList.IsPrimitiveList - ? DataModelConditionList.ListType - : LeftPath?.GetPropertyType(); - if (preferredType == null) - return null; - - if (leftSideType != null && preferredType.IsAssignableFrom(leftSideType)) - preferredType = leftSideType; - - return preferredType; - } - - #endregion - - #region Evaluation - - /// - /// Not supported for list predicates, always returns false - /// - public override bool Evaluate() - { - return false; - } - - internal override bool EvaluateObject(object? target) - { - if (Operator == null || LeftPath == null || !LeftPath.IsValid) - return false; - - // Compare with a static value - if (PredicateType == ProfileRightSideType.Static) - { - object? leftSideValue = GetListPathValue(LeftPath, target); - if (leftSideValue != null && leftSideValue.GetType().IsValueType && RightStaticValue == null) - return false; - - return Operator.InternalEvaluate(leftSideValue, RightStaticValue); - } - - if (RightPath == null || !RightPath.IsValid) - return false; - - // Compare with dynamic values - if (PredicateType == ProfileRightSideType.Dynamic) - { - // If the path targets a property inside the list, evaluate on the list path value instead of the right path value - if (RightPath.Target is ListPredicateWrapperDataModel) - return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), GetListPathValue(RightPath, target)); - return Operator.InternalEvaluate(GetListPathValue(LeftPath, target), RightPath.GetValue()); - } - - return false; - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs new file mode 100644 index 000000000..011805946 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -0,0 +1,372 @@ +using System; +using System.Linq; +using Artemis.Core.Internal; +using Artemis.Core.VisualScripting.Internal; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core; + +/// +/// Represents a condition that is based on a +/// +public class EventCondition : CorePropertyChanged, INodeScriptCondition +{ + private readonly string _displayName; + private readonly EventConditionEntity _entity; + private IEventConditionNode _startNode; + private DataModelPath? _eventPath; + private NodeScript _script; + private bool _wasMet; + private DateTime _lastProcessedTrigger; + private object? _lastProcessedValue; + private EventOverlapMode _overlapMode; + private EventTriggerMode _triggerMode; + private EventToggleOffMode _toggleOffMode; + + /// + /// Creates a new instance of the class + /// + public EventCondition(RenderProfileElement profileElement) + { + ProfileElement = profileElement; + + _entity = new EventConditionEntity(); + _displayName = profileElement.GetType().Name; + _startNode = new EventConditionEventStartNode {X = -300}; + _script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile); + } + + internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement) + { + ProfileElement = profileElement; + + _entity = entity; + _displayName = profileElement.GetType().Name; + _startNode = new EventConditionEventStartNode(); + _script = null!; + + Load(); + } + + /// + /// Gets the script that drives the event condition + /// + public NodeScript Script + { + get => _script; + private set => SetAndNotify(ref _script, value); + } + + /// + /// Gets or sets the path to the event that drives this event condition + /// + public DataModelPath? EventPath + { + get => _eventPath; + set => SetAndNotify(ref _eventPath, value); + } + + /// + /// Gets or sets how the condition behaves when the event fires. + /// + public EventTriggerMode TriggerMode + { + get => _triggerMode; + set => SetAndNotify(ref _triggerMode, value); + } + + /// + /// Gets or sets how the condition behaves when events trigger before the timeline finishes in the + /// event trigger mode. + /// + public EventOverlapMode OverlapMode + { + get => _overlapMode; + set => SetAndNotify(ref _overlapMode, value); + } + + /// + /// Gets or sets the mode for render elements when toggling off the event when using . + /// + public EventToggleOffMode ToggleOffMode + { + get => _toggleOffMode; + set => SetAndNotify(ref _toggleOffMode, value); + } + + /// + /// Updates the event node, applying the selected event + /// + public void UpdateEventNode() + { + if (EventPath == null) + return; + + Type? pathType = EventPath.GetPropertyType(); + if (pathType == null) + return; + + // Create an event node if the path type is a data model event + if (pathType.IsAssignableTo(typeof(IDataModelEvent))) + { + EventConditionEventStartNode eventNode; + // Ensure the start node is an event node + if (_startNode is not EventConditionEventStartNode node) + { + eventNode = new EventConditionEventStartNode(); + ReplaceStartNode(eventNode); + _startNode = eventNode; + } + else + eventNode = node; + + IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent; + eventNode.CreatePins(dataModelEvent); + } + // Create a value changed node if the path type is a regular value + else + { + // Ensure the start nod is a value changed node + EventConditionValueChangedStartNode valueChangedNode; + // Ensure the start node is an event node + if (_startNode is not EventConditionValueChangedStartNode node) + { + valueChangedNode = new EventConditionValueChangedStartNode(); + ReplaceStartNode(valueChangedNode); + } + else + valueChangedNode = node; + + valueChangedNode.UpdateOutputPins(EventPath); + } + Script.Save(); + } + + private void ReplaceStartNode(IEventConditionNode newStartNode) + { + if (Script.Nodes.Contains(_startNode)) + Script.RemoveNode(_startNode); + + _startNode = newStartNode; + if (!Script.Nodes.Contains(_startNode)) + Script.AddNode(_startNode); + } + + /// + /// Gets the start node of the event script, if any + /// + /// The start node of the event script, if any. + public INode GetStartNode() + { + return _startNode; + } + + private bool Evaluate() + { + if (EventPath == null) + return false; + + object? value = EventPath.GetValue(); + if (_startNode is EventConditionEventStartNode) + { + if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger) + return false; + + _lastProcessedTrigger = dataModelEvent.LastTrigger; + } + else if (_startNode is EventConditionValueChangedStartNode valueChangedNode) + { + if (Equals(value, _lastProcessedValue)) + return false; + + valueChangedNode.UpdateValues(value, _lastProcessedValue); + _lastProcessedValue = value; + } + + if (!Script.ExitNodeConnected) + return true; + + Script.Run(); + return Script.Result; + } + + /// + public IConditionEntity Entity => _entity; + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + public void Update() + { + _wasMet = IsMet; + if (TriggerMode == EventTriggerMode.Toggle) + { + if (Evaluate()) + IsMet = !IsMet; + } + else + { + IsMet = Evaluate(); + } + } + + /// + public void UpdateTimeline(double deltaTime) + { + if (TriggerMode == EventTriggerMode.Toggle) + { + if (IsMet && !_wasMet) + ProfileElement.Timeline.JumpToStart(); + if (!IsMet && _wasMet && ToggleOffMode == EventToggleOffMode.SkipToEnd) + ProfileElement.Timeline.JumpToEndSegment(); + } + else + { + if (IsMet && ProfileElement.Timeline.IsFinished) + { + ProfileElement.Timeline.JumpToStart(); + } + else if (IsMet) + { + if (OverlapMode == EventOverlapMode.Restart) + ProfileElement.Timeline.JumpToStart(); + else if (OverlapMode == EventOverlapMode.Copy && ProfileElement is Layer layer && layer.Parent is not Layer) + layer.CreateRenderCopy(10); + } + } + + // Stick to mean segment in toggle mode for as long as the condition is met + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), TriggerMode == EventTriggerMode.Toggle && IsMet); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, TriggerMode == EventTriggerMode.Toggle && position > ProfileElement.Timeline.Length); + } + + /// + public void Dispose() + { + Script?.Dispose(); + EventPath?.Dispose(); + } + + #region Storage + + /// + public void Load() + { + TriggerMode = (EventTriggerMode) _entity.TriggerMode; + OverlapMode = (EventOverlapMode) _entity.OverlapMode; + ToggleOffMode = (EventToggleOffMode) _entity.ToggleOffMode; + + if (_entity.EventPath != null) + EventPath = new DataModelPath(_entity.EventPath); + + Script = _entity.Script != null + ? new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile) + : new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile); + UpdateEventNode(); + } + + /// + public void Save() + { + _entity.TriggerMode = (int) TriggerMode; + _entity.OverlapMode = (int) OverlapMode; + _entity.ToggleOffMode = (int) ToggleOffMode; + + // If the exit node isn't connected and there are only the start- and exit node, don't save the script + if (!Script.ExitNodeConnected && Script.Nodes.Count() <= 2) + { + _entity.Script = null; + } + else + { + Script.Save(); + _entity.Script = Script.Entity; + } + + EventPath?.Save(); + _entity.EventPath = EventPath?.Entity; + } + + /// + public INodeScript NodeScript => Script; + + /// + public void LoadNodeScript() + { + Script.Load(); + + // The load action may have created an event node, use that one over the one we have here + INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == EventConditionEventStartNode.NodeId || n.Id == EventConditionValueChangedStartNode.NodeId); + if (existingEventNode != null) + _startNode = (IEventConditionNode) existingEventNode; + + UpdateEventNode(); + Script.LoadConnections(); + } + + #endregion +} + +/// +/// Represents a mode for render elements to start their timeline when display conditions events are fired. +/// +public enum EventTriggerMode +{ + /// + /// Play the timeline once. + /// + Play, + + /// + /// Toggle repeating the timeline. + /// + Toggle +} + +/// +/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before +/// the timeline finishes. +/// +public enum EventOverlapMode +{ + /// + /// Stop the current run and restart the timeline + /// + Restart, + + /// + /// Play another copy of the timeline on top of the current run + /// + Copy, + + /// + /// Ignore subsequent event fires until the timeline finishes + /// + Ignore +} + +/// +/// Represents a mode for render elements when toggling off the event when using . +/// +public enum EventToggleOffMode +{ + /// + /// When the event toggles the condition off, finish the the current run of the main timeline + /// + Finish, + + /// + /// When the event toggles the condition off, skip to the end segment of the timeline + /// + SkipToEnd +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs new file mode 100644 index 000000000..bddd6867b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/ICondition.cs @@ -0,0 +1,57 @@ +using System; +using Artemis.Storage.Entities.Profile.Abstract; + +namespace Artemis.Core; + +/// +/// Represents a condition applied to a +/// +public interface ICondition : IDisposable, IStorageModel +{ + /// + /// Gets the entity used to store this condition + /// + public IConditionEntity Entity { get; } + + /// + /// Gets the profile element this condition applies to + /// + public RenderProfileElement ProfileElement { get; } + + /// + /// Gets a boolean indicating whether the condition is currently met + /// + + bool IsMet { get; } + + /// + /// Updates the condition + /// + void Update(); + + /// + /// Updates the timeline according to the provided as the display condition sees fit. + /// + void UpdateTimeline(double deltaTime); + + /// + /// Overrides the timeline to the provided as the display condition sees fit. + /// + void OverrideTimeline(TimeSpan position); +} + +/// +/// Represents a condition applied to a using a +/// +public interface INodeScriptCondition : ICondition +{ + /// + /// Gets the node script of this node script condition + /// + INodeScript? NodeScript { get; } + + /// + /// Loads the node script this node script condition uses + /// + void LoadNodeScript(); +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs new file mode 100644 index 000000000..459e24e19 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/PlayOnceCondition.cs @@ -0,0 +1,90 @@ +using System; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + /// + /// Represents a condition that plays once when its script evaluates to . + /// + public class PlayOnceCondition : ICondition + { + /// + /// Creates a new instance of the class. + /// + /// The profile element this condition applies to. + public PlayOnceCondition(RenderProfileElement profileElement) + { + ProfileElement = profileElement; + Entity = new PlayOnceConditionEntity(); + } + + /// + /// Creates a new instance of the class. + /// + /// The entity used to store this condition. + /// The profile element this condition applies to. + public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement) + { + ProfileElement = profileElement; + Entity = entity; + } + + #region Implementation of IDisposable + + /// + public void Dispose() + { + } + + #endregion + + #region Implementation of IStorageModel + + /// + public void Load() + { + } + + /// + public void Save() + { + } + + #endregion + + #region Implementation of ICondition + + /// + public IConditionEntity Entity { get; } + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + public void Update() + { + if (ProfileElement.Parent is RenderProfileElement parent) + IsMet = parent.DisplayConditionMet; + else + IsMet = true; + } + + /// + public void UpdateTimeline(double deltaTime) + { + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, false); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs new file mode 100644 index 000000000..8ab4abd96 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/StaticCondition.cs @@ -0,0 +1,193 @@ +using System; +using System.Linq; +using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; + +namespace Artemis.Core +{ + /// + /// Represents a condition that is based on a data model value + /// + public class StaticCondition : CorePropertyChanged, INodeScriptCondition + { + private readonly string _displayName; + private readonly StaticConditionEntity _entity; + private StaticPlayMode _playMode; + private StaticStopMode _stopMode; + private bool _wasMet; + + /// + /// Creates a new instance of the class + /// + public StaticCondition(RenderProfileElement profileElement) + { + _entity = new StaticConditionEntity(); + _displayName = profileElement.GetType().Name; + + ProfileElement = profileElement; + Script = new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", profileElement.Profile); + } + + internal StaticCondition(StaticConditionEntity entity, RenderProfileElement profileElement) + { + _entity = entity; + _displayName = profileElement.GetType().Name; + + ProfileElement = profileElement; + Script = null!; + + Load(); + } + + /// + /// Gets the script that drives the static condition + /// + public NodeScript Script { get; private set; } + + /// + public IConditionEntity Entity => _entity; + + /// + public RenderProfileElement ProfileElement { get; } + + /// + public bool IsMet { get; private set; } + + /// + /// Gets or sets the mode in which the render element starts its timeline when display conditions are met + /// + public StaticPlayMode PlayMode + { + get => _playMode; + set => SetAndNotify(ref _playMode, value); + } + + /// + /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met + /// + public StaticStopMode StopMode + { + get => _stopMode; + set => SetAndNotify(ref _stopMode, value); + } + + /// + public void Update() + { + _wasMet = IsMet; + + // No need to run the script if the parent isn't met anyway + bool parentConditionMet = ProfileElement.Parent is not RenderProfileElement renderProfileElement || renderProfileElement.DisplayConditionMet; + if (!parentConditionMet) + { + IsMet = false; + return; + } + + if (!Script.ExitNodeConnected) + IsMet = true; + else + { + Script.Run(); + IsMet = Script.Result; + } + } + + /// + public void UpdateTimeline(double deltaTime) + { + if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished) + ProfileElement.Timeline.JumpToStart(); + else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd) + ProfileElement.Timeline.JumpToEndSegment(); + + ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), PlayMode == StaticPlayMode.Repeat && IsMet); + } + + /// + public void OverrideTimeline(TimeSpan position) + { + ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length); + } + + /// + public void Dispose() + { + Script.Dispose(); + } + + #region Storage + + /// + public void Load() + { + PlayMode = (StaticPlayMode) _entity.PlayMode; + StopMode = (StaticStopMode) _entity.StopMode; + + Script = _entity.Script != null + ? new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile) + : new NodeScript($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", ProfileElement.Profile); + } + + /// + public void Save() + { + _entity.PlayMode = (int) PlayMode; + _entity.StopMode = (int) StopMode; + + // If the exit node isn't connected and there is only the exit node, don't save the script + if (!Script.ExitNodeConnected && Script.Nodes.Count() == 1) + { + _entity.Script = null; + } + else + { + Script.Save(); + _entity.Script = Script.Entity; + } + } + + /// + public INodeScript? NodeScript => Script; + + /// + public void LoadNodeScript() + { + Script.Load(); + } + + #endregion + } + + /// + /// Represents a mode for render elements to start their timeline when display conditions are met + /// + public enum StaticPlayMode + { + /// + /// Continue repeating the main segment of the timeline while the condition is met + /// + Repeat, + + /// + /// Only play the timeline once when the condition is met + /// + Once + } + + /// + /// Represents a mode for render elements to stop their timeline when display conditions are no longer met + /// + public enum StaticStopMode + { + /// + /// When conditions are no longer met, finish the the current run of the main timeline + /// + Finish, + + /// + /// When conditions are no longer met, skip to the end segment of the timeline + /// + SkipToEnd + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs deleted file mode 100644 index 7b30c20f9..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/EventPredicateWrapperDataModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Artemis.Core.Modules; - -namespace Artemis.Core -{ - internal class EventPredicateWrapperDataModel : EventPredicateWrapperDataModel - { - [DataModelProperty(Name = "Event arguments", Description = "The arguments provided when the event triggers")] - public T Arguments => (UntypedArguments is T typedArguments ? typedArguments : default)!; - } - - /// - /// Represents a datamodel that wraps the event arguments of an event - /// - public abstract class EventPredicateWrapperDataModel : DataModel - { - internal EventPredicateWrapperDataModel() - { - Module = Constants.CorePluginFeature; - } - - /// - /// Gets the last arguments of this event as an object - /// - [DataModelIgnore] - public object? UntypedArguments { get; internal set; } - - /// - /// Creates a new instance of the class - /// - public static EventPredicateWrapperDataModel Create(Type type) - { - object? instance = Activator.CreateInstance(typeof(EventPredicateWrapperDataModel<>).MakeGenericType(type)); - if (instance == null) - throw new ArtemisCoreException($"Failed to create an instance of EventPredicateWrapperDataModel for type {type.Name}"); - - return (EventPredicateWrapperDataModel) instance; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs b/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs deleted file mode 100644 index 69a2fe087..000000000 --- a/src/Artemis.Core/Models/Profile/Conditions/Wrappers/ListPredicateWrapperDataModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Reflection; -using Artemis.Core.Modules; - -namespace Artemis.Core -{ - internal class ListPredicateWrapperDataModel : ListPredicateWrapperDataModel - { - public T Value => (UntypedValue is T typedValue ? typedValue : default)!; - } - - /// - /// Represents a datamodel that wraps a value in a list - /// - public abstract class ListPredicateWrapperDataModel : DataModel - { - internal ListPredicateWrapperDataModel() - { - Module = Constants.CorePluginFeature; - } - - /// - /// Gets or sets the value of this list as an object - /// - [DataModelIgnore] - public object? UntypedValue { get; set; } - - /// - /// Gets or sets the name of the list item - /// - [DataModelIgnore] - public string? ItemName { get; set; } - - #region Overrides of DataModel - - /// - public override DataModelPropertyAttribute? GetPropertyDescription(PropertyInfo propertyInfo) - { - if (!string.IsNullOrWhiteSpace(ItemName)) - return new DataModelPropertyAttribute {Name = ItemName}; - return base.GetPropertyDescription(propertyInfo); - } - - #endregion - - /// - /// Creates a new instance of the class - /// - public static ListPredicateWrapperDataModel Create(Type type, string? name = null) - { - ListPredicateWrapperDataModel? instance = Activator.CreateInstance(typeof(ListPredicateWrapperDataModel<>).MakeGenericType(type)) as ListPredicateWrapperDataModel; - if (instance == null) - throw new ArtemisCoreException($"Failed to create an instance of ListPredicateWrapperDataModel for type {type.Name}"); - - instance.ItemName = name; - return instance; - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs index 6b560ee52..d4bdc6d13 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBinding.cs @@ -1,67 +1,47 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Artemis.Storage.Entities.Profile.DataBindings; namespace Artemis.Core { /// - public class DataBinding : IDataBinding + public class DataBinding : IDataBinding { - private TProperty _currentValue = default!; + private readonly List _properties = new(); private bool _disposed; - private TimeSpan _easingProgress; - private TProperty _lastAppliedValue = default!; - private TProperty _previousValue = default!; - private bool _reapplyValue; + private bool _isEnabled; + private DataBindingNodeScript _script; - internal DataBinding(DataBindingRegistration dataBindingRegistration) + internal DataBinding(LayerProperty layerProperty) { - LayerProperty = dataBindingRegistration.LayerProperty; - Entity = new DataBindingEntity(); + LayerProperty = layerProperty; + + Entity = new DataBindingEntity(); + _script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); - ApplyRegistration(dataBindingRegistration); Save(); - ApplyDataBindingMode(); } internal DataBinding(LayerProperty layerProperty, DataBindingEntity entity) { LayerProperty = layerProperty; + Entity = entity; + _script = new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); // Load will add children so be initialized before that Load(); - ApplyDataBindingMode(); } - /// - /// Gets the data binding registration this data binding is based upon - /// - public DataBindingRegistration? Registration { get; private set; } - /// /// Gets the layer property this data binding targets /// public LayerProperty LayerProperty { get; } - /// - /// Gets the converter used to apply this data binding to the - /// - public DataBindingConverter? Converter { get; private set; } - - /// - /// Gets the data binding mode - /// - public IDataBindingMode? DataBindingMode { get; private set; } - - /// - /// Gets or sets the easing time of the data binding - /// - public TimeSpan EasingTime { get; set; } - - /// - /// Gets ors ets the easing function of the data binding - /// - public Easings.Functions EasingFunction { get; set; } + /// + public INodeScript Script => _script; /// /// Gets the data binding entity this data binding uses for persistent storage @@ -69,38 +49,52 @@ namespace Artemis.Core public DataBindingEntity Entity { get; } /// - /// Gets the current value of the data binding + /// Updates the pending values of this data binding /// - /// The base value of the property the data binding is applied to - /// - public TProperty GetValue(TProperty baseValue) + public void Update() { if (_disposed) throw new ObjectDisposedException("DataBinding"); - if (Converter == null || DataBindingMode == null) - return baseValue; + if (!IsEnabled) + return; - TProperty value = DataBindingMode.GetValue(baseValue); + // TODO: Update the 'base value' node - // If no easing is to be applied simple return whatever the current value is - if (EasingTime == TimeSpan.Zero || !Converter.SupportsInterpolate) - return value; - - // If the value changed, update the current and previous values used for easing - if (!Equals(value, _currentValue)) - ResetEasing(value); - - // Apply interpolation between the previous and current value - return GetInterpolatedValue(); + Script.Run(); } /// - /// Returns the type of the target property of this data binding + /// Registers a data binding property so that is available to the data binding system /// - public Type? GetTargetType() + /// The type of the layer property + /// The function to call to get the value of the property + /// The action to call to set the value of the property + /// The display name of the data binding property + public DataBindingProperty RegisterDataBindingProperty(Func getter, Action setter, string displayName) { - return Registration?.Getter.Method.ReturnType; + if (_disposed) + throw new ObjectDisposedException("DataBinding"); + if (Properties.Any(d => d.DisplayName == displayName)) + throw new ArtemisCoreException($"A data binding property named '{displayName}' is already registered."); + + DataBindingProperty property = new(getter, setter, displayName); + _properties.Add(property); + + OnDataBindingPropertyRegistered(); + return property; + } + + /// + /// Removes all data binding properties so they are no longer available to the data binding system + /// + public void ClearDataBindingProperties() + { + if (_disposed) + throw new ObjectDisposedException("LayerProperty"); + + _properties.Clear(); + OnDataBindingPropertiesCleared(); } /// @@ -115,103 +109,80 @@ namespace Artemis.Core if (disposing) { _disposed = true; + _isEnabled = false; - if (Registration != null) - Registration.DataBinding = null; - DataBindingMode?.Dispose(); + Script.Dispose(); } } - private void ResetEasing(TProperty value) - { - _previousValue = GetInterpolatedValue(); - _currentValue = value; - _easingProgress = TimeSpan.Zero; - } - - private void ApplyRegistration(DataBindingRegistration dataBindingRegistration) - { - if (dataBindingRegistration == null) - throw new ArgumentNullException(nameof(dataBindingRegistration)); - - dataBindingRegistration.DataBinding = this; - Converter = dataBindingRegistration.Converter; - Registration = dataBindingRegistration; - - if (GetTargetType()!.IsValueType) - { - if (_currentValue == null) - _currentValue = default!; - if (_previousValue == null) - _previousValue = default!; - } - - Converter?.Initialize(this); - } - - private TProperty GetInterpolatedValue() - { - if (_easingProgress == EasingTime || Converter == null || !Converter.SupportsInterpolate) - return _currentValue; - - double easingAmount = _easingProgress.TotalSeconds / EasingTime.TotalSeconds; - return Converter.Interpolate(_previousValue, _currentValue, Easings.Interpolate(easingAmount, EasingFunction)); - } - /// - /// Updates the smoothing progress of the data binding + /// Invokes the event /// - /// The timeline to apply during update - public void Update(Timeline timeline) + protected virtual void OnDataBindingPropertyRegistered() { - // Don't update data bindings if there is no delta, otherwise this creates an inconsistency between - // data bindings with easing and data bindings without easing (the ones with easing will seemingly not update) - if (timeline.Delta == TimeSpan.Zero || timeline.IsOverridden) - return; + DataBindingPropertyRegistered?.Invoke(this, new DataBindingEventArgs(this)); + } - UpdateWithDelta(timeline.Delta); + /// + /// Invokes the event + /// + protected virtual void OnDataBindingPropertiesCleared() + { + DataBindingPropertiesCleared?.Invoke(this, new DataBindingEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingEnabled(DataBindingEventArgs e) + { + DataBindingEnabled?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingDisabled(DataBindingEventArgs e) + { + DataBindingDisabled?.Invoke(this, e); + } + + private string GetScriptName() + { + return LayerProperty.PropertyDescription.Name ?? LayerProperty.Path; } /// - public void UpdateWithDelta(TimeSpan delta) + public ILayerProperty BaseLayerProperty => LayerProperty; + + /// + public bool IsEnabled { - if (_disposed) - throw new ObjectDisposedException("DataBinding"); + get => _isEnabled; + set + { + _isEnabled = value; - // Data bindings cannot go back in time like brushes - if (delta < TimeSpan.Zero) - delta = TimeSpan.Zero; - - _easingProgress = _easingProgress.Add(delta); - if (_easingProgress > EasingTime) - _easingProgress = EasingTime; - - // Tell Apply() to apply a new value next call - _reapplyValue = false; + if (_isEnabled) + OnDataBindingEnabled(new DataBindingEventArgs(this)); + else + OnDataBindingDisabled(new DataBindingEventArgs(this)); + } } + /// + public ReadOnlyCollection Properties => _properties.AsReadOnly(); + /// public void Apply() { if (_disposed) throw new ObjectDisposedException("DataBinding"); - if (Converter == null) + if (!IsEnabled) return; - // If Update() has not been called, reapply the previous value - if (_reapplyValue) - { - Converter.ApplyValue(_lastAppliedValue); - return; - } - - TProperty converterValue = Converter.GetValue(); - TProperty value = GetValue(converterValue); - Converter.ApplyValue(value); - - _lastAppliedValue = value; - _reapplyValue = true; + _script.DataBindingExitNode.ApplyToDataBinding(); } /// @@ -221,56 +192,17 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - #region Mode management + /// + public event EventHandler? DataBindingPropertyRegistered; - /// - /// Changes the data binding mode of the data binding to the specified - /// - public void ChangeDataBindingMode(DataBindingModeType dataBindingMode) - { - switch (dataBindingMode) - { - case DataBindingModeType.Direct: - Entity.DataBindingMode = new DirectDataBindingEntity(); - break; - case DataBindingModeType.Conditional: - Entity.DataBindingMode = new ConditionalDataBindingEntity(); - break; - default: - Entity.DataBindingMode = null; - break; - } + /// + public event EventHandler? DataBindingPropertiesCleared; - ApplyDataBindingMode(); - } + /// + public event EventHandler? DataBindingEnabled; - /// - /// Replaces the current data binding mode with one based on the provided data binding mode entity - /// - /// The data binding mode entity to base the new data binding mode upon - public void ApplyDataBindingEntity(IDataBindingModeEntity dataBindingModeEntity) - { - Entity.DataBindingMode = dataBindingModeEntity; - ApplyDataBindingMode(); - } - - private void ApplyDataBindingMode() - { - DataBindingMode?.Dispose(); - DataBindingMode = null; - - switch (Entity.DataBindingMode) - { - case DirectDataBindingEntity directDataBindingEntity: - DataBindingMode = new DirectDataBinding(this, directDataBindingEntity); - break; - case ConditionalDataBindingEntity conditionalDataBindingEntity: - DataBindingMode = new ConditionalDataBinding(this, conditionalDataBindingEntity); - break; - } - } - - #endregion + /// + public event EventHandler? DataBindingDisabled; #region Storage @@ -280,15 +212,16 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("DataBinding"); - // General - DataBindingRegistration? registration = LayerProperty.GetDataBindingRegistration(Entity.Identifier); - if (registration != null) - ApplyRegistration(registration); + IsEnabled = Entity.IsEnabled; + } - EasingTime = Entity.EasingTime; - EasingFunction = (Easings.Functions) Entity.EasingFunction; - - DataBindingMode?.Load(); + /// + public void LoadNodeScript() + { + _script.Dispose(); + _script = Entity.NodeScript != null + ? new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, Entity.NodeScript, LayerProperty.ProfileElement.Profile) + : new DataBindingNodeScript(GetScriptName(), "The value to put into the data binding", this, LayerProperty.ProfileElement.Profile); } /// @@ -297,40 +230,16 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("DataBinding"); - if (!LayerProperty.Entity.DataBindingEntities.Contains(Entity)) - LayerProperty.Entity.DataBindingEntities.Add(Entity); - - // Don't save an invalid state - if (Registration != null) - Entity.Identifier = Registration.DisplayName; - - Entity.EasingTime = EasingTime; - Entity.EasingFunction = (int) EasingFunction; - - DataBindingMode?.Save(); + Entity.IsEnabled = IsEnabled; + if (_script.ExitNodeConnected || _script.Nodes.Count() > 1) + { + _script.Save(); + Entity.NodeScript = _script.Entity; + } + else + Entity.NodeScript = null; } #endregion } - - /// - /// A mode that determines how the data binding is applied to the layer property - /// - public enum DataBindingModeType - { - /// - /// Disables the data binding - /// - None, - - /// - /// Replaces the layer property value with the data binding value - /// - Direct, - - /// - /// Replaces the layer property value with the data binding value - /// - Conditional - } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs deleted file mode 100644 index 2b3d7eb5a..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingConverter.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a data binding converter that acts as the bridge between a - /// and a - /// - public abstract class DataBindingConverter : IDataBindingConverter - { - /// - /// Gets the data binding this converter is applied to - /// - public DataBinding? DataBinding { get; private set; } - - /// - /// Gets whether or not this data binding converter supports the method - /// - public bool SupportsSum { get; protected set; } - - /// - /// Gets whether or not this data binding converter supports the method - /// - public bool SupportsInterpolate { get; protected set; } - - /// - /// Returns the sum of and - /// - public abstract TProperty Sum(TProperty a, TProperty b); - - /// - /// Returns the the interpolated value between and on a scale (generally) - /// between 0.0 and 1.0 defined by the - /// Note: The progress may go be negative or go beyond 1.0 depending on the easing method used - /// - /// The value to interpolate away from - /// The value to interpolate towards - /// The progress of the interpolation between 0.0 and 1.0 - /// - public abstract TProperty Interpolate(TProperty a, TProperty b, double progress); - - /// - /// Applies the to the layer property - /// - /// - public virtual void ApplyValue(TProperty value) - { - if (DataBinding?.Registration == null) - throw new ArtemisCoreException("Data binding converter is not yet initialized"); - DataBinding.Registration.Setter(value); - } - - /// - /// Returns the current base value of the data binding - /// - public virtual TProperty GetValue() - { - if (DataBinding?.Registration == null) - throw new ArtemisCoreException("Data binding converter is not yet initialized"); - return DataBinding.Registration.Getter(); - } - - /// - /// Converts the provided object to a type of - /// - public virtual TProperty ConvertFromObject(object? source) - { - return (TProperty) Convert.ChangeType(source, typeof(TProperty))!; - } - - /// - /// Called when the data binding converter has been initialized and the is available - /// - protected virtual void OnInitialized() - { - } - - internal void Initialize(DataBinding dataBinding) - { - if (dataBinding.Registration == null) - throw new ArtemisCoreException("Cannot initialize a data binding converter for a data binding without a registration"); - - DataBinding = dataBinding; - OnInitialized(); - } - - /// - public Type SupportedType => typeof(TProperty); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs new file mode 100644 index 000000000..7c09381fd --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs @@ -0,0 +1,63 @@ +using System; + +namespace Artemis.Core +{ + /// + public class DataBindingProperty : IDataBindingProperty + { + internal DataBindingProperty(Func getter, Action setter, string displayName) + { + Getter = getter ?? throw new ArgumentNullException(nameof(getter)); + Setter = setter ?? throw new ArgumentNullException(nameof(setter)); + DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); + } + + /// + /// Gets the function to call to get the value of the property + /// + public Func Getter { get; } + + /// + /// Gets the action to call to set the value of the property + /// + public Action Setter { get; } + + /// + public string DisplayName { get; } + + /// + public Type ValueType => typeof(TProperty); + + /// + public object? GetValue() + { + return Getter(); + } + + /// + public void SetValue(object? value) + { + // Numeric has a bunch of conversion, this seems the cheapest way to use them :) + switch (value) + { + case TProperty match: + Setter(match); + break; + case Numeric numeric when Setter is Action floatSetter: + floatSetter(numeric); + break; + case Numeric numeric when Setter is Action intSetter: + intSetter(numeric); + break; + case Numeric numeric when Setter is Action doubleSetter: + doubleSetter(numeric); + break; + case Numeric numeric when Setter is Action byteSetter: + byteSetter(numeric); + break; + default: + throw new ArgumentException("Value must match the type of the data binding registration", nameof(value)); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs deleted file mode 100644 index ab50c44c9..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingRegistration.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using Artemis.Storage.Entities.Profile.DataBindings; - -namespace Artemis.Core -{ - /// - public class DataBindingRegistration : IDataBindingRegistration - { - internal DataBindingRegistration(LayerProperty layerProperty, DataBindingConverter converter, - Func getter, Action setter, string displayName) - { - LayerProperty = layerProperty ?? throw new ArgumentNullException(nameof(layerProperty)); - Converter = converter ?? throw new ArgumentNullException(nameof(converter)); - Getter = getter ?? throw new ArgumentNullException(nameof(getter)); - Setter = setter ?? throw new ArgumentNullException(nameof(setter)); - DisplayName = displayName ?? throw new ArgumentNullException(nameof(displayName)); - } - - /// - /// Gets the layer property this registration was made on - /// - public LayerProperty LayerProperty { get; } - - /// - /// Gets the converter that's used by the data binding - /// - public DataBindingConverter Converter { get; } - - /// - /// Gets the function to call to get the value of the property - /// - public Func Getter { get; } - - /// - /// Gets the action to call to set the value of the property - /// - public Action Setter { get; } - - /// - public string DisplayName { get; } - - /// - /// Gets the data binding created using this registration - /// - public DataBinding? DataBinding { get; internal set; } - - /// - public IDataBinding? GetDataBinding() - { - return DataBinding; - } - - /// - public IDataBinding? CreateDataBinding() - { - if (DataBinding != null) - return DataBinding; - - DataBindingEntity? dataBinding = LayerProperty.Entity.DataBindingEntities.FirstOrDefault(e => e.Identifier == DisplayName); - if (dataBinding == null) - return null; - - DataBinding = new DataBinding(LayerProperty, dataBinding); - return DataBinding; - } - - /// - public void ClearDataBinding() - { - if (DataBinding == null) - return; - - // The related entity is left behind, just in case the data binding is added back later - LayerProperty.DisableDataBinding(DataBinding); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs index 1f54ca146..8c56aa298 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBinding.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.ObjectModel; using Artemis.Core.Modules; namespace Artemis.Core @@ -7,17 +8,56 @@ namespace Artemis.Core /// Represents a data binding that binds a certain to a value inside a /// /// - public interface IDataBinding : IStorageModel, IUpdateModel, IDisposable + public interface IDataBinding : IStorageModel, IDisposable { /// - /// Updates the smoothing progress of the data binding and recalculates the value next call + /// Gets the layer property the data binding is applied to /// - /// The delta to apply during update - void UpdateWithDelta(TimeSpan delta); + ILayerProperty BaseLayerProperty { get; } /// - /// Applies the data binding to the layer property + /// Gets the script used to populate the data binding + /// + INodeScript Script { get; } + + /// + /// Gets a list of sub-properties this data binding applies to + /// + ReadOnlyCollection Properties { get; } + + /// + /// Gets a boolean indicating whether the data binding is enabled or not + /// + bool IsEnabled { get; set; } + + /// + /// Applies the pending value of the data binding to the property /// void Apply(); + + /// + /// If the data binding is enabled, loads the node script for that data binding + /// + void LoadNodeScript(); + + /// + /// Occurs when a data binding property has been added + /// + public event EventHandler? DataBindingPropertyRegistered; + + /// + /// Occurs when all data binding properties have been removed + /// + public event EventHandler? DataBindingPropertiesCleared; + + /// + /// Occurs when a data binding has been enabled + /// + public event EventHandler? DataBindingEnabled; + + /// + /// Occurs when a data binding has been disabled + /// + public event EventHandler? DataBindingDisabled; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs deleted file mode 100644 index 9f207133f..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a data binding converter that acts as the bridge between a - /// and a - /// - public interface IDataBindingConverter - { - /// - /// Gets the type this converter supports - /// - public Type SupportedType { get; } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs new file mode 100644 index 000000000..c6f299766 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs @@ -0,0 +1,32 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents a data binding registration + /// + public interface IDataBindingProperty + { + /// + /// Gets or sets the display name of the data binding registration + /// + string DisplayName { get; } + + /// + /// Gets the type of the value this data binding registration points to + /// + Type ValueType { get; } + + /// + /// Gets the value of the property this registration points to + /// + /// A value matching the type of + object? GetValue(); + + /// + /// Sets the value of the property this registration points to + /// + /// A value matching the type of + void SetValue(object? value); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs deleted file mode 100644 index c1321d4c3..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingRegistration.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Artemis.Core -{ - /// - /// Represents a data binding registration - /// - public interface IDataBindingRegistration - { - /// - /// Gets or sets the display name of the data binding registration - /// - string DisplayName { get; } - - /// - /// Returns the data binding applied using this registration - /// - public IDataBinding? GetDataBinding(); - - /// - /// If found, creates a data binding from storage - /// - /// - IDataBinding? CreateDataBinding(); - - /// - /// If present, removes the current data binding - /// - void ClearDataBinding(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs deleted file mode 100644 index d5d50125d..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/ConditionalDataBinding.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Artemis.Storage.Entities.Profile.DataBindings; - -namespace Artemis.Core -{ - /// - /// Represents a data binding mode that applies a value depending on conditions - /// - public class ConditionalDataBinding : IDataBindingMode - { - private readonly List> _conditions = new(); - private bool _disposed; - - internal ConditionalDataBinding(DataBinding dataBinding, ConditionalDataBindingEntity entity) - { - DataBinding = dataBinding; - Entity = entity; - Conditions = new(_conditions); - Load(); - } - - /// - /// Gets a list of conditions applied to this data binding - /// - public ReadOnlyCollection> Conditions { get; } - - internal ConditionalDataBindingEntity Entity { get; } - - /// - public DataBinding DataBinding { get; } - - /// - public TProperty GetValue(TProperty baseValue) - { - if (_disposed) - throw new ObjectDisposedException("ConditionalDataBinding"); - - DataBindingCondition? condition = Conditions.FirstOrDefault(c => c.Evaluate()); - return condition == null ? baseValue : condition.Value; - } - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - - foreach (DataBindingCondition dataBindingCondition in Conditions) - dataBindingCondition.Dispose(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - #region Values - - /// - /// Adds a condition to the conditional data binding's collection - /// - /// The newly created - public DataBindingCondition AddCondition() - { - if (_disposed) - throw new ObjectDisposedException("ConditionalDataBinding"); - - DataBindingCondition condition = new(this); - _conditions.Add(condition); - - ApplyOrder(); - OnConditionsUpdated(); - - return condition; - } - - /// - /// Removes a condition from the conditional data binding's collection and disposes it - /// - /// - public void RemoveCondition(DataBindingCondition condition) - { - if (_disposed) - throw new ObjectDisposedException("ConditionalDataBinding"); - if (!_conditions.Contains(condition)) - return; - - _conditions.Remove(condition); - condition.Dispose(); - - ApplyOrder(); - OnConditionsUpdated(); - } - - /// - /// Applies the current order of conditions to the collection - /// - public void ApplyOrder() - { - if (_disposed) - throw new ObjectDisposedException("ConditionalDataBinding"); - - _conditions.Sort((a, b) => a.Order.CompareTo(b.Order)); - for (int index = 0; index < _conditions.Count; index++) - { - DataBindingCondition condition = _conditions[index]; - condition.Order = index + 1; - } - } - - #endregion - - #region Storage - - /// - public void Load() - { - foreach (DataBindingConditionEntity dataBindingConditionEntity in Entity.Values) - _conditions.Add(new DataBindingCondition(this, dataBindingConditionEntity)); - - ApplyOrder(); - } - - /// - public void Save() - { - Entity.Values.Clear(); - foreach (DataBindingCondition dataBindingCondition in Conditions) - dataBindingCondition.Save(); - } - - #endregion - - #region Events - - /// - /// Occurs when a condition is added or removed - /// - public event EventHandler? ConditionsUpdated; - - /// - /// Invokes the event - /// - protected virtual void OnConditionsUpdated() - { - ConditionsUpdated?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/DataBindingCondition.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/DataBindingCondition.cs deleted file mode 100644 index daa22c4fa..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/DataBindingCondition.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using Artemis.Storage.Entities.Profile.DataBindings; - -namespace Artemis.Core -{ - /// - public class DataBindingCondition : IDataBindingCondition - { - private bool _disposed; - - /// - /// Creates a new instance of the class - /// - /// The conditional data binding this condition is applied too - internal DataBindingCondition(ConditionalDataBinding conditionalDataBinding) - { - ConditionalDataBinding = conditionalDataBinding ?? throw new ArgumentNullException(nameof(conditionalDataBinding)); - Order = conditionalDataBinding.Conditions.Count + 1; - Condition = new DataModelConditionGroup(null); - Value = default!; - - Entity = new DataBindingConditionEntity(); - Save(); - } - - internal DataBindingCondition(ConditionalDataBinding conditionalDataBinding, DataBindingConditionEntity entity) - { - ConditionalDataBinding = conditionalDataBinding ?? throw new ArgumentNullException(nameof(conditionalDataBinding)); - Entity = entity; - Condition = null!; - Value = default!; - - Load(); - } - - /// - /// Gets the conditional data binding this condition is applied to - /// - public ConditionalDataBinding ConditionalDataBinding { get; } - - /// - /// Gets or sets the position at which the modifier appears on the data binding - /// - public int Order { get; set; } - - /// - /// Gets or sets the value to be applied when the condition is met - /// - public TProperty Value { get; set; } - - /// - /// Gets the root group of the condition that must be met - /// - public DataModelConditionGroup Condition { get; private set; } - - internal DataBindingConditionEntity Entity { get; set; } - - /// - public bool Evaluate() - { - return Condition.Evaluate(); - } - - /// - public void Save() - { - if (_disposed) - throw new ObjectDisposedException("DataBindingCondition"); - - if (!ConditionalDataBinding.Entity.Values.Contains(Entity)) - ConditionalDataBinding.Entity.Values.Add(Entity); - - Entity.Condition = Condition.Entity; - Condition.Save(); - - Entity.Value = CoreJson.SerializeObject(Value); - Entity.Order = Order; - } - - /// - public void Load() - { - if (_disposed) - throw new ObjectDisposedException("DataBindingCondition"); - - Condition = Entity.Condition != null - ? new DataModelConditionGroup(null, Entity.Condition) - : new DataModelConditionGroup(null); - - Value = (Entity.Value == null ? default : CoreJson.DeserializeObject(Entity.Value))!; - Order = Entity.Order; - } - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - Condition.Dispose(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/IDataBindingCondition.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/IDataBindingCondition.cs deleted file mode 100644 index 26ad9a9f2..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Conditional/IDataBindingCondition.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a condition and a value inside a - /// - public interface IDataBindingCondition : IStorageModel, IDisposable - { - /// - /// Evaluates the condition - /// - bool Evaluate(); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/BaseDataBindingModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/BaseDataBindingModifierType.cs deleted file mode 100644 index 2d85efb96..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/BaseDataBindingModifierType.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// A modifier that changes the source value of a data binding in some way - /// - /// To implement your own condition operator, inherit or - /// - /// - /// - public abstract class BaseDataBindingModifierType - { - /// - /// Gets the plugin this data binding modifier belongs to - /// Note: Not set until after registering - /// - public Plugin? Plugin { get; internal set; } - - /// - /// Gets the value type of this modifier type - /// - public abstract Type ValueType { get; } - - /// - /// Gets the parameter type of this modifier type. May be null if the modifier type does not support a parameter - /// - public abstract Type? ParameterType { get; } - - /// - /// Gets the name of this modifier - /// - public abstract string Name { get; } - - /// - /// Gets or sets the icon of this modifier - /// - public abstract string? Icon { get; } - - /// - /// Gets the description of this modifier - /// - public virtual string? Description => null; - - /// - /// Gets the category of this modifier - /// - public virtual string? Category => null; - - /// - /// Returns whether the given type is supported by the modifier - /// - /// The type to check for, must be either the same or be castable to the target type - /// Which part of the modifier to check, the value or the parameter - public bool SupportsType(Type type, ModifierTypePart part) - { - if (type == null) - return true; - if (part == ModifierTypePart.Value) - return ValueType.IsCastableFrom(type); - return ParameterType != null && ParameterType.IsCastableFrom(type); - } - - /// - /// Applies the modifier to the provided current value - /// - /// This leaves the caller responsible for the types matching and - /// - /// - /// - /// The current value before modification, type should match - /// - /// The parameter to use for the modification, type should match - /// - /// The modified value, with a type of - internal abstract object? InternalApply(object? currentValue, object? parameterValue); - } - - /// - /// Represents a part of a modifier type - /// - public enum ModifierTypePart - { - /// - /// The value part of a modifier, backed by - /// - Value, - - /// - /// The parameter part of a modifier, backed by - /// - Parameter - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs deleted file mode 100644 index 1b11249c9..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifier.cs +++ /dev/null @@ -1,324 +0,0 @@ -using System; -using Artemis.Storage.Entities.Profile.DataBindings; -using Newtonsoft.Json; - -namespace Artemis.Core -{ - /// - public class DataBindingModifier : IDataBindingModifier - { - private bool _disposed; - - internal DataBindingModifier(DirectDataBinding directDataBinding, ProfileRightSideType parameterType) - { - DirectDataBinding = directDataBinding ?? throw new ArgumentNullException(nameof(directDataBinding)); - Order = directDataBinding.Modifiers.Count + 1; - ParameterType = parameterType; - Entity = new DataBindingModifierEntity(); - Initialize(); - Save(); - } - - internal DataBindingModifier(DirectDataBinding directDataBinding, DataBindingModifierEntity entity) - { - DirectDataBinding = directDataBinding ?? throw new ArgumentNullException(nameof(directDataBinding)); - Entity = entity; - Load(); - Initialize(); - } - - /// - /// Gets the type of modifier that is being applied - /// - public BaseDataBindingModifierType? ModifierType { get; private set; } - - /// - /// Gets the direct data binding this modifier is applied to - /// - public DirectDataBinding DirectDataBinding { get; } - - /// - /// Gets the type of the parameter, can either be dynamic (based on a data model value) or static - /// - public ProfileRightSideType ParameterType { get; set; } - - /// - /// Gets or sets the position at which the modifier appears on the data binding - /// - public int Order { get; set; } - - /// - /// Gets the path of the parameter property - /// - public DataModelPath? ParameterPath { get; set; } - - /// - /// Gets the parameter static value, only used it is - /// - /// - public object? ParameterStaticValue { get; private set; } - - internal DataBindingModifierEntity Entity { get; set; } - - /// - /// Applies the modifier to the provided value - /// - /// The value to apply the modifier to, should be of the same type as the data binding target - /// The modified value - public object? Apply(object? currentValue) - { - if (_disposed) - throw new ObjectDisposedException("DataBindingModifier"); - - if (ModifierType == null) - return currentValue; - - if (ModifierType.ParameterType == null) - return ModifierType.InternalApply(currentValue, null); - - if (ParameterType == ProfileRightSideType.Dynamic && ParameterPath != null && ParameterPath.IsValid) - { - object? value = ParameterPath.GetValue(); - return ModifierType.InternalApply(currentValue, value); - } - - if (ParameterType == ProfileRightSideType.Static) - return ModifierType.InternalApply(currentValue, ParameterStaticValue); - - return currentValue; - } - - /// - /// Updates the modifier type of the modifier and re-compiles the expression - /// - /// - public void UpdateModifierType(BaseDataBindingModifierType? modifierType) - { - if (_disposed) - throw new ObjectDisposedException("DataBindingModifier"); - - if (modifierType == null) - { - ModifierType = null; - return; - } - - Type? targetType = DirectDataBinding.DataBinding.GetTargetType(); - if (targetType != null && !modifierType.SupportsType(targetType, ModifierTypePart.Value)) - throw new ArtemisCoreException($"Cannot apply modifier type {modifierType.GetType().Name} to this modifier because " + - $"it does not support this data binding's type {targetType.Name}"); - - ModifierType = modifierType; - ValidateParameter(); - } - - /// - /// Updates the parameter of the modifier and makes the modifier dynamic - /// - /// The path pointing to the parameter - public void UpdateParameterDynamic(DataModelPath? path) - { - if (_disposed) - throw new ObjectDisposedException("DataBindingModifier"); - - if (path != null && !path.IsValid) - throw new ArtemisCoreException("Cannot update parameter of data binding modifier to an invalid path"); - - ParameterPath?.Dispose(); - ParameterPath = path != null ? new DataModelPath(path) : null; - - ParameterType = ProfileRightSideType.Dynamic; - } - - /// - /// Updates the parameter of the modifier, makes the modifier static and re-compiles the expression - /// - /// The static value to use as a parameter - public void UpdateParameterStatic(object? staticValue) - { - if (_disposed) - throw new ObjectDisposedException("DataBindingModifier"); - - ParameterType = ProfileRightSideType.Static; - ParameterPath?.Dispose(); - ParameterPath = null; - - Type? parameterType = ModifierType?.ParameterType ?? DirectDataBinding.DataBinding.GetTargetType(); - - // If not null ensure the types match and if not, convert it - if (parameterType == null || staticValue != null && staticValue.GetType() == parameterType) - ParameterStaticValue = staticValue; - else if (staticValue != null) - ParameterStaticValue = Convert.ChangeType(staticValue, parameterType); - // If null create a default instance for value types or simply make it null for reference types - else if (parameterType.IsValueType) - ParameterStaticValue = Activator.CreateInstance(parameterType); - else - ParameterStaticValue = null; - } - - private void ValidateParameter() - { - if (ModifierType == null) - return; - - if (ParameterType == ProfileRightSideType.Dynamic) - { - if (ParameterPath == null || !ParameterPath.IsValid) - return; - - Type parameterType = ParameterPath.GetPropertyType()!; - if (!ModifierType.SupportsType(parameterType, ModifierTypePart.Parameter)) - UpdateParameterDynamic(null); - } - else - { - if (ParameterStaticValue == null) - return; - - if (!ModifierType.SupportsType(ParameterStaticValue.GetType(), ModifierTypePart.Parameter)) - UpdateParameterStatic(null); - } - } - - private void Initialize() - { - DataBindingModifierTypeStore.DataBindingModifierAdded += DataBindingModifierTypeStoreOnDataBindingModifierAdded; - DataBindingModifierTypeStore.DataBindingModifierRemoved += DataBindingModifierTypeStoreOnDataBindingModifierRemoved; - - // Modifier type - if (Entity.ModifierTypePluginGuid != null && ModifierType == null) - { - BaseDataBindingModifierType? modifierType = DataBindingModifierTypeStore.Get(Entity.ModifierTypePluginGuid.Value, Entity.ModifierType)?.DataBindingModifierType; - if (modifierType != null) - UpdateModifierType(modifierType); - } - - // Dynamic parameter - if (ParameterType == ProfileRightSideType.Dynamic && Entity.ParameterPath != null) - { - ParameterPath = new DataModelPath(null, Entity.ParameterPath); - } - // Static parameter - else if (ParameterType == ProfileRightSideType.Static && Entity.ParameterStaticValue != null) - { - // Use the target type so JSON.NET has a better idea what to do - Type? parameterType = ModifierType?.ParameterType ?? DirectDataBinding.DataBinding.GetTargetType(); - object? staticValue = null; - - try - { - staticValue = parameterType != null - ? CoreJson.DeserializeObject(Entity.ParameterStaticValue, parameterType) - : CoreJson.DeserializeObject(Entity.ParameterStaticValue); - } - // If deserialization fails, use the type's default - catch (JsonSerializationException e) - { - DeserializationLogger.LogModifierDeserializationFailure(GetType().Name, e); - if (parameterType != null) - staticValue = Activator.CreateInstance(parameterType); - } - - UpdateParameterStatic(staticValue); - } - } - - /// - public void Save() - { - if (_disposed) - throw new ObjectDisposedException("DataBindingModifier"); - - // Don't save an invalid state - if (ParameterPath != null && !ParameterPath.IsValid) - return; - - if (!DirectDataBinding.Entity.Modifiers.Contains(Entity)) - DirectDataBinding.Entity.Modifiers.Add(Entity); - - // Modifier - if (ModifierType?.Plugin != null) - { - Entity.ModifierType = ModifierType.GetType().Name; - Entity.ModifierTypePluginGuid = ModifierType.Plugin.Guid; - } - - // General - Entity.Order = Order; - Entity.ParameterType = (int) ParameterType; - - // Parameter - ParameterPath?.Save(); - Entity.ParameterPath = ParameterPath?.Entity; - - Entity.ParameterStaticValue = CoreJson.SerializeObject(ParameterStaticValue); - } - - /// - public void Load() - { - if (_disposed) - throw new ObjectDisposedException("DataBindingModifier"); - - // Modifier type is done during Initialize - - // General - Order = Entity.Order; - ParameterType = (ProfileRightSideType) Entity.ParameterType; - - // Parameter is done during initialize - } - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - - DataBindingModifierTypeStore.DataBindingModifierAdded -= DataBindingModifierTypeStoreOnDataBindingModifierAdded; - DataBindingModifierTypeStore.DataBindingModifierRemoved -= DataBindingModifierTypeStoreOnDataBindingModifierRemoved; - - ParameterPath?.Dispose(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - #region Event handlers - - private void DataBindingModifierTypeStoreOnDataBindingModifierAdded(object? sender, DataBindingModifierTypeStoreEvent e) - { - if (ModifierType != null) - return; - - BaseDataBindingModifierType modifierType = e.TypeRegistration.DataBindingModifierType; - if (modifierType.Plugin!.Guid == Entity.ModifierTypePluginGuid && modifierType.GetType().Name == Entity.ModifierType) - UpdateModifierType(modifierType); - } - - private void DataBindingModifierTypeStoreOnDataBindingModifierRemoved(object? sender, DataBindingModifierTypeStoreEvent e) - { - if (e.TypeRegistration.DataBindingModifierType == ModifierType) - UpdateModifierType(null); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifierType.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifierType.cs deleted file mode 100644 index c09cd8fcc..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/DataBindingModifierType.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// A modifier that changes the source value of a data binding in some way using a parameter - /// - public abstract class DataBindingModifierType : BaseDataBindingModifierType - { - /// - public override Type ValueType => typeof(TValue); - - /// - public override Type ParameterType => typeof(TParameter); - - /// - /// Called whenever the modifier must apply to a specific value - /// - /// - /// The current value before modification - /// - /// - /// The parameter to use for the modification - /// - /// The modified value> - public abstract TValue Apply(TValue currentValue, TParameter parameterValue); - - /// - internal override object? InternalApply(object? currentValue, object? parameterValue) - { - // TODO: Can we avoid boxing/unboxing? - TValue current; - if (currentValue != null) - current = (TValue) Convert.ChangeType(currentValue, typeof(TValue)); - else - current = default; - - TParameter parameter; - if (parameterValue != null) - parameter = (TParameter) Convert.ChangeType(parameterValue, typeof(TParameter)); - else - parameter = default; - - return Apply(current!, parameter!); - } - } - - /// - /// A modifier that changes the source value of a data binding in some way - /// - public abstract class DataBindingModifierType : BaseDataBindingModifierType - { - /// - public override Type ValueType => typeof(TValue); - - /// - public override Type? ParameterType => null; - - /// - /// Called whenever the modifier must apply to a specific value - /// - /// - /// The current value before modification - /// - /// The modified value - public abstract TValue Apply(TValue currentValue); - - /// - internal override object? InternalApply(object? currentValue, object? parameterValue) - { - // TODO: Can we avoid boxing/unboxing? - TValue current; - if (currentValue != null) - current = (TValue) Convert.ChangeType(currentValue, typeof(TValue)); - else - current = default; - - return Apply(current!); - } - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/IDataBindingModifier.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/IDataBindingModifier.cs deleted file mode 100644 index 6f920c0a1..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/Direct/IDataBindingModifier.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Modifies a data model value in a way defined by the modifier type - /// - public interface IDataBindingModifier : IStorageModel, IDisposable - { - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/DirectDataBinding.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/DirectDataBinding.cs deleted file mode 100644 index 5ed0c6d67..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/DirectDataBinding.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Artemis.Storage.Entities.Profile.DataBindings; - -namespace Artemis.Core -{ - /// - /// Represents a data binding mode that directly applies a data model value to a data binding - /// - public class DirectDataBinding : IDataBindingMode - { - private readonly List> _modifiers = new(); - private bool _disposed; - - internal DirectDataBinding(DataBinding dataBinding, DirectDataBindingEntity entity) - { - DataBinding = dataBinding; - Entity = entity; - Modifiers = new(_modifiers); - Load(); - } - - /// - /// Gets the path of the source property - /// - public DataModelPath? SourcePath { get; private set; } - - /// - /// Gets a list of modifiers applied to this data binding - /// - public ReadOnlyCollection> Modifiers { get; } - - internal DirectDataBindingEntity Entity { get; } - - /// - public DataBinding DataBinding { get; } - - /// - public TProperty GetValue(TProperty baseValue) - { - if (_disposed) - throw new ObjectDisposedException("DirectDataBinding"); - - if (SourcePath == null || !SourcePath.IsValid || DataBinding.Converter == null) - return baseValue; - - object? dataBindingValue = SourcePath.GetValue(); - foreach (DataBindingModifier dataBindingModifier in Modifiers) - dataBindingValue = dataBindingModifier.Apply(dataBindingValue); - - return DataBinding.Converter.ConvertFromObject(dataBindingValue); - } - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - - foreach (DataBindingModifier dataBindingModifier in Modifiers) - dataBindingModifier.Dispose(); - - SourcePath?.Dispose(); - } - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - #endregion - - #region Storage - - /// - public void Load() - { - // Source - if (Entity.SourcePath != null) - SourcePath = new DataModelPath(null, Entity.SourcePath); - - // Modifiers - foreach (DataBindingModifierEntity dataBindingModifierEntity in Entity.Modifiers) - _modifiers.Add(new DataBindingModifier(this, dataBindingModifierEntity)); - - ApplyOrder(); - } - - /// - public void Save() - { - // Don't save an invalid state - if (SourcePath != null && !SourcePath.IsValid) - return; - - SourcePath?.Save(); - Entity.SourcePath = SourcePath?.Entity; - - // Modifiers - Entity.Modifiers.Clear(); - foreach (DataBindingModifier dataBindingModifier in Modifiers) - dataBindingModifier.Save(); - } - - #endregion - - #region Source - - /// - /// Returns the type of the source property of this data binding - /// - public Type? GetSourceType() - { - return SourcePath?.GetPropertyType(); - } - - /// - /// Updates the source of the data binding - /// - /// The path pointing to the source - public void UpdateSource(DataModelPath? path) - { - if (_disposed) - throw new ObjectDisposedException("DirectDataBinding"); - - if (path != null && !path.IsValid) - throw new ArtemisCoreException("Cannot update source of data binding to an invalid path"); - - SourcePath?.Dispose(); - SourcePath = path != null ? new DataModelPath(path) : null; - } - - #endregion - - #region Modifiers - - /// - /// Adds a modifier to the direct data binding's collection - /// - /// The type of the parameter, can either be dynamic (based on a data model value) or static - public DataBindingModifier AddModifier(ProfileRightSideType type) - { - if (_disposed) - throw new ObjectDisposedException("DirectDataBinding"); - - DataBindingModifier modifier = new(this, type); - _modifiers.Add(modifier); - - ApplyOrder(); - OnModifiersUpdated(); - - return modifier; - } - - /// - /// Removes a modifier from the direct data binding's collection and disposes it - /// - public void RemoveModifier(DataBindingModifier modifier) - { - if (_disposed) - throw new ObjectDisposedException("DirectDataBinding"); - if (!_modifiers.Contains(modifier)) - return; - - _modifiers.Remove(modifier); - modifier.Dispose(); - - ApplyOrder(); - OnModifiersUpdated(); - } - - /// - /// Applies the current order of conditions to the collection - /// - public void ApplyOrder() - { - _modifiers.Sort((a, b) => a.Order.CompareTo(b.Order)); - for (int index = 0; index < _modifiers.Count; index++) - { - DataBindingModifier modifier = _modifiers[index]; - modifier.Order = index + 1; - } - } - - #endregion - - #region Events - - /// - /// Occurs when a modifier is added or removed - /// - public event EventHandler? ModifiersUpdated; - - /// - /// Invokes the event - /// - protected virtual void OnModifiersUpdated() - { - ModifiersUpdated?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/Modes/IDataBindingMode.cs b/src/Artemis.Core/Models/Profile/DataBindings/Modes/IDataBindingMode.cs deleted file mode 100644 index e46a2aee9..000000000 --- a/src/Artemis.Core/Models/Profile/DataBindings/Modes/IDataBindingMode.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Artemis.Core -{ - /// - /// Represents a data binding mode - /// - public interface IDataBindingMode : IStorageModel, IDisposable - { - /// - /// Gets the data binding this mode is applied to - /// - DataBinding DataBinding { get; } - - /// - /// Gets the current value of the data binding - /// - /// The base value of the property the data binding is applied to - /// - TProperty GetValue(TProperty baseValue); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs index baa7eac6c..6aaaf0ee9 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPath.cs @@ -72,9 +72,12 @@ namespace Artemis.Core SubscribeToDataModelStore(); } - internal DataModelPath(DataModel? target, DataModelPathEntity entity) + /// + /// Creates a new instance of the class based on a + /// + /// + public DataModelPath(DataModelPathEntity entity) { - Target = target; Path = entity.Path; Entity = entity; @@ -110,7 +113,10 @@ namespace Artemis.Core /// public IReadOnlyCollection Segments => _segments.ToList().AsReadOnly(); - internal DataModelPathEntity Entity { get; } + /// + /// Gets the entity used for persistent storage + /// + public DataModelPathEntity Entity { get; } internal Func? Accessor { get; private set; } @@ -173,6 +179,52 @@ namespace Artemis.Core return string.IsNullOrWhiteSpace(Path) ? "this" : Path; } + /// + /// Occurs whenever the path becomes invalid + /// + public event EventHandler? PathInvalidated; + + /// + /// Occurs whenever the path becomes valid + /// + public event EventHandler? PathValidated; + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + + DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; + DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + + Invalidate(); + } + } + + /// + /// Invokes the event + /// + protected virtual void OnPathValidated() + { + PathValidated?.Invoke(this, EventArgs.Empty); + } + + /// + /// Invokes the event + /// + protected virtual void OnPathInvalidated() + { + PathInvalidated?.Invoke(this, EventArgs.Empty); + } + internal void Invalidate() { Target?.RemoveDataModelPath(this); @@ -262,27 +314,24 @@ namespace Artemis.Core DataModelStore.DataModelAdded += DataModelStoreOnDataModelAdded; DataModelStore.DataModelRemoved += DataModelStoreOnDataModelRemoved; } - - #region IDisposable - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) + private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) { - if (disposing) - { - _disposed = true; + if (e.Registration.DataModel.Module.Id != Entity.DataModelId) + return; - DataModelStore.DataModelAdded -= DataModelStoreOnDataModelAdded; - DataModelStore.DataModelRemoved -= DataModelStoreOnDataModelRemoved; + Invalidate(); + Target = e.Registration.DataModel; + Initialize(); + } - Invalidate(); - } + private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) + { + if (e.Registration.DataModel.Module.Id != Entity.DataModelId) + return; + + Invalidate(); + Target = null; } /// @@ -292,8 +341,6 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - #endregion - #region Storage /// @@ -314,68 +361,33 @@ namespace Artemis.Core Entity.Path = Path; Entity.DataModelId = DataModelId; + } - Entity.WrapperType = Target switch - { - ListPredicateWrapperDataModel _ => PathWrapperType.List, - EventPredicateWrapperDataModel _ => PathWrapperType.Event, - _ => PathWrapperType.None - }; + #region Equality members + + /// > + protected bool Equals(DataModelPath other) + { + return ReferenceEquals(Target, other.Target) && Path == other.Path; + } + + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((DataModelPath) obj); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(Target, Path); } #endregion - #region Event handlers - - private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) - { - if (e.Registration.DataModel.Module.Id != Entity.DataModelId) - return; - - Invalidate(); - Target = e.Registration.DataModel; - Initialize(); - } - - private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) - { - if (e.Registration.DataModel.Module.Id != Entity.DataModelId) - return; - - Invalidate(); - Target = null; - } - - #endregion - - #region Events - - /// - /// Occurs whenever the path becomes invalid - /// - public event EventHandler? PathInvalidated; - - /// - /// Occurs whenever the path becomes valid - /// - public event EventHandler? PathValidated; - - /// - /// Invokes the event - /// - protected virtual void OnPathValidated() - { - PathValidated?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the event - /// - protected virtual void OnPathInvalidated() - { - PathInvalidated?.Invoke(this, EventArgs.Empty); - } - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs index 5c70eae3f..19c1d87ab 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/DataModelPathSegment.cs @@ -295,7 +295,10 @@ namespace Artemis.Core private void DynamicChildOnDynamicChildAdded(object? sender, DynamicDataModelChildEventArgs e) { if (e.Key == Identifier) + { + DataModelPath.Invalidate(); DataModelPath.Initialize(); + } } private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e) diff --git a/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs index 4357dc3c6..4ab9d271c 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/IDataModelEvent.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Artemis.Core { /// - /// Represents a data model event that can trigger s. + /// Represents an event that is part of a data model /// public interface IDataModelEvent { diff --git a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs deleted file mode 100644 index 9bb880516..000000000 --- a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Artemis.Core -{ - internal class DataModelValueChangedEvent : IDataModelEvent - { - public DataModelValueChangedEvent(DataModelPath path) - { - Path = path; - } - - public DataModelPath Path { get; } - public T? LastValue { get; private set; } - public T? CurrentValue { get; private set; } - public DateTime LastTrigger { get; private set; } - public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger; - public int TriggerCount { get; private set; } - public Type ArgumentsType { get; } = typeof(DataModelValueChangedEventArgs); - public string TriggerPastParticiple => "changed"; - public bool TrackHistory { get; set; } = false; - public DataModelEventArgs? LastEventArgumentsUntyped { get; private set; } - public List EventArgumentsHistoryUntyped { get; } = new(); - - public void Update() - { - if (!Path.IsValid) - return; - - object? value = Path.GetValue(); - if (value != null) - CurrentValue = (T?) value; - else - CurrentValue = default; - - if (!Equals(LastValue, CurrentValue)) - Trigger(); - - LastValue = CurrentValue; - } - - public void Reset() - { - TriggerCount = 0; - } - - private void Trigger() - { - LastEventArgumentsUntyped = new DataModelValueChangedEventArgs(CurrentValue, LastValue); - LastTrigger = DateTime.Now; - TriggerCount++; - - OnEventTriggered(); - } - - #region Events - - public event EventHandler? EventTriggered; - - internal virtual void OnEventTriggered() - { - EventTriggered?.Invoke(this, EventArgs.Empty); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEventArgs.cs b/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEventArgs.cs deleted file mode 100644 index b25976a61..000000000 --- a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Artemis.Core.Modules; - -namespace Artemis.Core -{ - internal class DataModelValueChangedEventArgs : DataModelEventArgs - { - public DataModelValueChangedEventArgs(T? currentValue, T? previousValue) - { - CurrentValue = currentValue; - PreviousValue = previousValue; - } - - [DataModelProperty(Description = "The current value of the property")] - public T? CurrentValue { get; } - [DataModelProperty(Description = "The previous value of the property")] - public T? PreviousValue { get; } - } -} \ 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 7ef74f1d1..36f107598 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -22,16 +22,13 @@ namespace Artemis.Core /// /// The parent of the folder /// The name of the folder - public Folder(ProfileElement parent, string name) : base(parent.Profile) + public Folder(ProfileElement parent, string name) : base(parent, parent.Profile) { FolderEntity = new FolderEntity(); EntityId = Guid.NewGuid(); - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Profile = Parent.Profile; Name = name; - - Parent.AddChild(this); } /// @@ -40,13 +37,12 @@ namespace Artemis.Core /// The profile the folder belongs to /// The parent of the folder /// The entity of the folder - public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent.Profile) + public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile) { FolderEntity = folderEntity; EntityId = folderEntity.Id; Profile = profile; - Parent = parent; Name = folderEntity.Name; IsExpanded = folderEntity.IsExpanded; Suspended = folderEntity.Suspended; @@ -98,6 +94,12 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Folder"); + if (Timeline.IsOverridden) + { + Timeline.ClearOverride(); + return; + } + UpdateDisplayCondition(); UpdateTimeline(deltaTime); @@ -106,6 +108,9 @@ namespace Artemis.Core else if (Timeline.IsFinished) Disable(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + foreach (ProfileElement child in Children) child.Update(deltaTime); } @@ -119,6 +124,9 @@ namespace Artemis.Core Timeline.JumpToStart(); else Timeline.JumpToEnd(); + + foreach (ProfileElement child in Children) + child.Reset(); } /// @@ -168,7 +176,7 @@ namespace Artemis.Core #region Rendering /// - public override void Render(SKCanvas canvas, SKPointI basePosition) + public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus) { if (Disposed) throw new ObjectDisposedException("Folder"); @@ -180,41 +188,45 @@ namespace Artemis.Core // No point rendering if all children are disabled if (!Children.Any(c => c is RenderProfileElement {Enabled: true})) return; + + // If the editor focus is on this folder, discard further focus for children to effectively focus the entire folder and all descendants + if (editorFocus == this) + editorFocus = null; - lock (Timeline) + SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; + try { - 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) { - SKRectI rendererBounds = SKRectI.Create(0, 0, Bounds.Width, Bounds.Height); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + if (!baseLayerEffect.Suspended) 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) - return; + // No point rendering if the alpha was set to zero by one of the effects + if (layerPaint.Color.Alpha == 0) + return; - canvas.SaveLayer(layerPaint); - canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); + canvas.SaveLayer(layerPaint); + canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); - // Iterate the children in reverse because the first layer must be rendered last to end up on top - for (int index = Children.Count - 1; index > -1; index--) - Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top)); + // Iterate the children in reverse because the first layer must be rendered last to end up on top + for (int index = Children.Count - 1; index > -1; index--) + Children[index].Render(canvas, new SKPointI(Bounds.Left, Bounds.Top), editorFocus); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + if (!baseLayerEffect.Suspended) baseLayerEffect.InternalPostProcess(canvas, rendererBounds, layerPaint); } - finally - { - canvas.Restore(); - layerPaint.DisposeSelfAndProperties(); - } - - Timeline.ClearDelta(); } + finally + { + canvas.Restore(); + layerPaint.DisposeSelfAndProperties(); + } + + Timeline.ClearDelta(); } #endregion @@ -222,30 +234,22 @@ namespace Artemis.Core /// public override void Enable() { - if (Enabled) - return; - + // No checks here, effects will do their own checks to ensure they never enable twice + // Also not enabling children, they'll enable themselves during their own Update foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.InternalEnable(); - foreach (ProfileElement profileElement in Children) - { - if (profileElement is RenderProfileElement renderProfileElement && renderProfileElement.ShouldBeEnabled) - renderProfileElement.Enable(); - } - Enabled = true; } /// public override void Disable() { - if (!Enabled) - return; - + // No checks here, effects will do their own checks to ensure they never disable twice foreach (BaseLayerEffect baseLayerEffect in LayerEffects) baseLayerEffect.InternalDisable(); + // Disabling children since their Update won't get called with their parent disabled foreach (ProfileElement profileElement in Children) { if (profileElement is RenderProfileElement renderProfileElement) @@ -255,6 +259,14 @@ namespace Artemis.Core Enabled = false; } + /// + public override void OverrideTimelineAndApply(TimeSpan position) + { + DisplayCondition.OverrideTimeline(position); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); ; + } + /// /// Occurs when a property affecting the rendering properties of this folder has been updated /// @@ -295,7 +307,6 @@ namespace Artemis.Core internal override void Load() { - ExpandedPropertyGroups.AddRange(FolderEntity.ExpandedPropertyGroups); Reset(); // Load child folders @@ -306,7 +317,7 @@ namespace Artemis.Core ChildrenList.Add(new Layer(Profile, this, childLayer)); // Ensure order integrity, should be unnecessary but no one is perfect specially me - ChildrenList.Sort((a,b) => a.Order.CompareTo(b.Order)); + ChildrenList.Sort((a, b) => a.Order.CompareTo(b.Order)); for (int index = 0; index < ChildrenList.Count; index++) ChildrenList[index].Order = index + 1; @@ -327,8 +338,6 @@ namespace Artemis.Core FolderEntity.Suspended = Suspended; FolderEntity.ProfileId = Profile.EntityId; - FolderEntity.ExpandedPropertyGroups.Clear(); - FolderEntity.ExpandedPropertyGroups.AddRange(ExpandedPropertyGroups); SaveRenderElement(); } diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index dfb0a68dc..7f2be3fb5 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -4,7 +4,6 @@ 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; @@ -17,6 +16,7 @@ namespace Artemis.Core /// public sealed class Layer : RenderProfileElement { + private readonly List _renderCopies; private LayerGeneralProperties _general; private BaseLayerBrush? _layerBrush; private LayerShape? _layerShape; @@ -29,16 +29,17 @@ namespace Artemis.Core /// /// The parent of the layer /// The name of the layer - public Layer(ProfileElement parent, string name) : base(parent.Profile) + public Layer(ProfileElement parent, string name) : base(parent, parent.Profile) { LayerEntity = new LayerEntity(); EntityId = Guid.NewGuid(); - Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Profile = Parent.Profile; Name = name; Suspended = false; + // TODO: move to top + _renderCopies = new List(); _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -47,7 +48,6 @@ namespace Artemis.Core Adapter = new LayerAdapter(this); Initialize(); - Parent.AddChild(this, 0); } /// @@ -56,7 +56,7 @@ namespace Artemis.Core /// The profile the layer belongs to /// The parent of the layer /// The entity of the layer - public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent.Profile) + public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent, parent.Profile) { LayerEntity = layerEntity; EntityId = layerEntity.Id; @@ -64,6 +64,8 @@ namespace Artemis.Core Profile = profile; Parent = parent; + // TODO: move to top + _renderCopies = new List(); _general = new LayerGeneralProperties(); _transform = new LayerTransformProperties(); @@ -75,6 +77,37 @@ namespace Artemis.Core Initialize(); } + /// + /// Creates a new instance of the class by copying the provided . + /// + /// The layer to copy + private Layer(Layer source) : base(source, source.Profile) + { + LayerEntity = source.LayerEntity; + + Profile = source.Profile; + Parent = source; + + // TODO: move to top + _renderCopies = new List(); + _general = new LayerGeneralProperties(); + _transform = new LayerTransformProperties(); + + _leds = new List(); + Leds = new ReadOnlyCollection(_leds); + + Adapter = new LayerAdapter(this); + Load(); + Initialize(); + + Timeline.JumpToStart(); + AddLeds(source.Leds); + Enable(); + + // After loading using the source entity create a new entity so the next call to Save won't mess with the source, just in case. + LayerEntity = new LayerEntity(); + } + /// /// A collection of all the LEDs this layer is assigned to. /// @@ -97,7 +130,7 @@ namespace Artemis.Core /// /// Gets the general properties of the layer /// - [PropertyGroupDescription(Name = "General", Description = "A collection of general properties")] + [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")] public LayerGeneralProperties General { get => _general; @@ -107,7 +140,7 @@ namespace Artemis.Core /// /// Gets the transform properties of the layer /// - [PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")] + [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")] public LayerTransformProperties Transform { get => _transform; @@ -147,10 +180,8 @@ 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 +202,19 @@ namespace Artemis.Core /// public event EventHandler? LayerBrushUpdated; + #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 + /// protected override void Dispose(bool disposing) { @@ -200,19 +244,20 @@ namespace Artemis.Core LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; // Layers have two hardcoded property groups, instantiate them - Attribute generalAttribute = Attribute.GetCustomAttribute( + PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( GetType().GetProperty(nameof(General))!, typeof(PropertyGroupDescriptionAttribute) )!; - Attribute transformAttribute = Attribute.GetCustomAttribute( + PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute( GetType().GetProperty(nameof(Transform))!, typeof(PropertyGroupDescriptionAttribute) )!; - General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; - General.Initialize(this, "General.", Constants.CorePluginFeature); - Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute; - Transform.Initialize(this, "Transform.", Constants.CorePluginFeature); + LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier}; + LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier}; + + General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup); + Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup); General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; ApplyShapeType(); @@ -233,8 +278,7 @@ namespace Artemis.Core return; LayerBrushReference? current = General.BrushReference.CurrentValue; - if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && - e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) ActivateLayerBrush(); } @@ -252,7 +296,6 @@ namespace Artemis.Core Suspended = LayerEntity.Suspended; Order = LayerEntity.Order; - ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); LoadRenderElement(); Adapter.Load(); } @@ -269,12 +312,16 @@ namespace Artemis.Core LayerEntity.Suspended = Suspended; LayerEntity.Name = Name; LayerEntity.ProfileId = Profile.EntityId; - LayerEntity.ExpandedPropertyGroups.Clear(); - LayerEntity.ExpandedPropertyGroups.AddRange(ExpandedPropertyGroups); General.ApplyToEntity(); Transform.ApplyToEntity(); - LayerBrush?.BaseProperties?.ApplyToEntity(); + + // Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush + if (LayerBrush != null) + { + LayerBrush.Save(); + LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity; + } // LEDs LayerEntity.Leds.Clear(); @@ -324,109 +371,80 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Layer"); + if (Timeline.IsOverridden) + { + Timeline.ClearOverride(); + return; + } + UpdateDisplayCondition(); UpdateTimeline(deltaTime); if (ShouldBeEnabled) Enable(); - else if (Timeline.IsFinished) + else if (Suspended || (Timeline.IsFinished && !_renderCopies.Any())) Disable(); + + if (Timeline.Delta == TimeSpan.Zero) + return; + + General.Update(Timeline); + Transform.Update(Timeline); + LayerBrush?.InternalUpdate(Timeline); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalUpdate(Timeline); + } + + // Remove render copies that finished their timeline and update the rest + for (int index = 0; index < _renderCopies.Count; index++) + { + Layer child = _renderCopies[index]; + if (!child.Timeline.IsFinished) + { + child.Update(deltaTime); + } + else + { + _renderCopies.Remove(child); + child.Dispose(); + index--; + } + } } /// - public override void Reset() - { - UpdateDisplayCondition(); - - if (DisplayConditionMet) - Timeline.JumpToStart(); - else - Timeline.JumpToEnd(); - } - - /// - public override void Render(SKCanvas canvas, SKPointI basePosition) + public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus) { if (Disposed) throw new ObjectDisposedException("Layer"); - // Ensure the layer is ready - if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized) + if (editorFocus != null && editorFocus != this) return; + + RenderLayer(canvas, basePosition); + RenderCopies(canvas, basePosition); + } + + private void RenderLayer(SKCanvas canvas, SKPointI basePosition) + { + // Ensure the layer is ready + if (!Enabled || Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized || !Leds.Any()) + return; + // Ensure the brush is ready if (LayerBrush == null || LayerBrush?.BaseProperties?.PropertiesInitialized == false) return; - RenderTimeline(Timeline, canvas, basePosition); - foreach (Timeline extraTimeline in Timeline.ExtraTimelines.ToList()) - RenderTimeline(extraTimeline, canvas, basePosition); - Timeline.ClearDelta(); - } - - /// - public override void Enable() - { - if (Enabled) - return; - - 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; - } - - /// - public override void Disable() - { - if (!Enabled) - return; - - LayerBrush?.InternalDisable(); - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.InternalDisable(); - - Enabled = false; - } - - private void ApplyTimeline(Timeline timeline) - { - if (timeline.Delta == TimeSpan.Zero) - return; - - General.Update(timeline); - Transform.Update(timeline); - LayerBrush?.InternalUpdate(timeline); - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalUpdate(timeline); - } - - private void RenderTimeline(Timeline timeline, SKCanvas canvas, SKPointI basePosition) - { - if (Path == null || LayerBrush == null) - throw new ArtemisCoreException("The layer is not yet ready for rendering"); - - if (!Leds.Any() || timeline.IsFinished) - return; - - ApplyTimeline(timeline); - - if (LayerBrush?.BrushType != LayerBrushType.Regular) + if (Timeline.IsFinished || LayerBrush?.BrushType != LayerBrushType.Regular) return; SKPaint layerPaint = new() {FilterQuality = SKFilterQuality.Low}; try { - canvas.Save(); + using SKAutoCanvasRestore _ = new(canvas); canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); using SKPath clipPath = new(Path); clipPath.Transform(SKMatrix.CreateTranslation(Bounds.Left * -1, Bounds.Top * -1)); @@ -472,37 +490,89 @@ namespace Artemis.Core } finally { - canvas.Restore(); layerPaint.DisposeSelfAndProperties(); } + + Timeline.ClearDelta(); + } + + private void RenderCopies(SKCanvas canvas, SKPointI basePosition) + { + for (int i = _renderCopies.Count - 1; i >= 0; i--) + _renderCopies[i].Render(canvas, basePosition, null); + } + + /// + public override void Enable() + { + // No checks here, the brush and effects will do their own checks to ensure they never enable twice + 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; + } + + /// + public override void Disable() + { + // No checks here, the brush and effects will do their own checks to ensure they never disable twice + LayerBrush?.InternalDisable(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.InternalDisable(); + + Enabled = false; + } + + /// + public override void OverrideTimelineAndApply(TimeSpan position) + { + DisplayCondition.OverrideTimeline(position); + + General.Update(Timeline); + Transform.Update(Timeline); + LayerBrush?.InternalUpdate(Timeline); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) + baseLayerEffect.InternalUpdate(Timeline); + } + + /// + public override void Reset() + { + UpdateDisplayCondition(); + + if (DisplayConditionMet) + Timeline.JumpToStart(); + else + Timeline.JumpToEnd(); + + while (_renderCopies.Any()) + { + _renderCopies[0].Dispose(); + _renderCopies.RemoveAt(0); + } } - private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) + /// + /// Creates a copy of this layer and renders it alongside this layer for as long as its timeline lasts. + /// + /// The total maximum of render copies to keep + public void CreateRenderCopy(int max) { - if (LayerBrush == null) - throw new ArtemisCoreException("The layer is not yet ready for rendering"); + if (_renderCopies.Count >= max) + return; - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended)) - baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); - - try - { - canvas.SaveLayer(layerPaint); - 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.InternalPostProcess(canvas, bounds, layerPaint); - } - - finally - { - canvas.Restore(); - } + Layer copy = new(this); + _renderCopies.Add(copy); } internal void CalculateRenderProperties() @@ -511,7 +581,9 @@ namespace Artemis.Core throw new ObjectDisposedException("Layer"); if (!Leds.Any()) + { Path = new SKPath(); + } else { SKPath path = new() {FillType = SKPathFillType.Winding}; @@ -532,28 +604,60 @@ namespace Artemis.Core OnRenderPropertiesUpdated(); } - internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased) + internal SKPoint GetLayerAnchorPosition(bool applyTranslation, bool zeroBased, SKRect? customBounds = null) { if (Disposed) throw new ObjectDisposedException("Layer"); + SKRect bounds = customBounds ?? Bounds; SKPoint positionProperty = Transform.Position.CurrentValue; - // Start at the center of the shape - SKPoint position = zeroBased - ? new SKPointI(Bounds.MidX - Bounds.Left, Bounds.MidY - Bounds.Top) - : new SKPointI(Bounds.MidX, Bounds.MidY); + // Start at the top left of the shape + SKPoint position = zeroBased ? new SKPoint(0, 0) : new SKPoint(bounds.Left, bounds.Top); // Apply translation if (applyTranslation) { - position.X += positionProperty.X * Bounds.Width; - position.Y += positionProperty.Y * Bounds.Height; + position.X += positionProperty.X * bounds.Width; + position.Y += positionProperty.Y * bounds.Height; } return position; } + private void DelegateRendering(SKCanvas canvas, SKPath renderPath, SKRect bounds, SKPaint layerPaint) + { + if (LayerBrush == null) + throw new ArtemisCoreException("The layer is not yet ready for rendering"); + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalPreProcess(canvas, bounds, layerPaint); + } + + try + { + canvas.SaveLayer(layerPaint); + 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) + { + if (!baseLayerEffect.Suspended) + baseLayerEffect.InternalPostProcess(canvas, bounds, layerPaint); + } + } + finally + { + canvas.Restore(); + } + } + /// /// Creates a transformation matrix that applies the current transformation settings /// @@ -564,8 +668,9 @@ namespace Artemis.Core /// Whether translation should be included /// Whether the scale should be included /// Whether the rotation should be included + /// Optional custom bounds to base the anchor on /// The transformation matrix containing the current transformation settings - public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation) + public SKMatrix GetTransformMatrix(bool zeroBased, bool includeTranslation, bool includeScale, bool includeRotation, SKRect? customBounds = null) { if (Disposed) throw new ObjectDisposedException("Layer"); @@ -573,15 +678,16 @@ namespace Artemis.Core if (Path == null) return SKMatrix.Empty; + SKRect bounds = customBounds ?? Bounds; SKSize sizeProperty = Transform.Scale.CurrentValue; float rotationProperty = Transform.Rotation.CurrentValue; - SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased); + SKPoint anchorPosition = GetLayerAnchorPosition(true, zeroBased, bounds); SKPoint anchorProperty = Transform.AnchorPoint.CurrentValue; - // Translation originates from the unscaled center of the shape and is tied to the anchor - float x = anchorPosition.X - (zeroBased ? Bounds.MidX - Bounds.Left : Bounds.MidX) - anchorProperty.X * Bounds.Width; - float y = anchorPosition.Y - (zeroBased ? Bounds.MidY - Bounds.Top : Bounds.MidY) - anchorProperty.Y * Bounds.Height; + // Translation originates from the top left of the shape and is tied to the anchor + float x = anchorPosition.X - (zeroBased ? 0 : bounds.Left) - anchorProperty.X * bounds.Width; + float y = anchorPosition.Y - (zeroBased ? 0 : bounds.Top) - anchorProperty.Y * bounds.Height; SKMatrix transform = SKMatrix.Empty; @@ -690,53 +796,46 @@ namespace Artemis.Core #region Brush management /// - /// Changes the current layer brush to the brush described in the provided + /// Changes the current layer brush to the provided layer brush and activates it /// - public void ChangeLayerBrush(LayerBrushDescriptor descriptor) + public void ChangeLayerBrush(BaseLayerBrush? layerBrush) { - if (descriptor == null) - throw new ArgumentNullException(nameof(descriptor)); + BaseLayerBrush? oldLayerBrush = LayerBrush; + + General.BrushReference.SetCurrentValue(layerBrush != null ? new LayerBrushReference(layerBrush.Descriptor) : null, null); + LayerBrush = layerBrush; + + oldLayerBrush?.InternalDisable(); if (LayerBrush != null) - { - BaseLayerBrush brush = LayerBrush; - LayerBrush = null; - brush.Dispose(); - } - - // Ensure the brush reference matches the brush - LayerBrushReference? current = General.BrushReference.BaseValue; - if (!descriptor.MatchesLayerBrushReference(current)) - General.BrushReference.BaseValue = new LayerBrushReference(descriptor); - - ActivateLayerBrush(); - } - - /// - /// Removes the current layer brush from the layer - /// - public void RemoveLayerBrush() - { - if (LayerBrush == null) - return; - - BaseLayerBrush brush = LayerBrush; - DeactivateLayerBrush(); - LayerEntity.PropertyEntities.RemoveAll(p => p.FeatureId == brush.ProviderId && p.Path.StartsWith("LayerBrush.")); + ActivateLayerBrush(); + else + OnLayerBrushUpdated(); } internal void ActivateLayerBrush() { try { - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (current == null) + if (LayerBrush == null) + { + // If the brush is null, try to instantiate it + LayerBrushReference? brushReference = General.BrushReference.CurrentValue; + if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null) + ChangeLayerBrush(LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush)); + // If that's not possible there's nothing to do return; + } - LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null - ? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor - : null; - descriptor?.CreateInstance(this); + General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + Transform.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; + if (LayerBrush != null) + { + if (!LayerBrush.Enabled) + LayerBrush.InternalEnable(); + LayerBrush?.Update(0); + } OnLayerBrushUpdated(); ClearBrokenState("Failed to initialize layer brush"); @@ -752,27 +851,14 @@ namespace Artemis.Core if (LayerBrush == null) return; - BaseLayerBrush brush = LayerBrush; + BaseLayerBrush? brush = LayerBrush; LayerBrush = null; - brush.Dispose(); + brush?.Dispose(); OnLayerBrushUpdated(); } #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/LayerAdapter.cs b/src/Artemis.Core/Models/Profile/LayerAdapter.cs index b60d901f9..aa8ceeb5c 100644 --- a/src/Artemis.Core/Models/Profile/LayerAdapter.cs +++ b/src/Artemis.Core/Models/Profile/LayerAdapter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.AdaptionHints; @@ -12,10 +13,13 @@ namespace Artemis.Core /// public class LayerAdapter : IStorageModel { + private readonly List _adaptionHints; + internal LayerAdapter(Layer layer) { + _adaptionHints = new List(); Layer = layer; - AdaptionHints = new List(); + AdaptionHints = new ReadOnlyCollection(_adaptionHints); } /// @@ -26,7 +30,7 @@ namespace Artemis.Core /// /// Gets or sets a list containing the adaption hints used by this adapter /// - public List AdaptionHints { get; set; } + public ReadOnlyCollection AdaptionHints { get; set; } /// /// Modifies the layer, adapting it to the provided @@ -73,7 +77,7 @@ namespace Artemis.Core if (devices.All(DoesLayerCoverDevice)) { DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All}; - AdaptionHints.Add(hint); + Add(hint); newHints.Add(hint); } else @@ -88,7 +92,7 @@ namespace Artemis.Core if (DoesLayerCoverDevice(device)) { DeviceAdaptionHint hint = new() {DeviceType = device.DeviceType}; - AdaptionHints.Add(hint); + Add(hint); newHints.Add(hint); } } @@ -103,7 +107,7 @@ namespace Artemis.Core if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice)) { CategoryAdaptionHint hint = new() {Category = deviceCategory}; - AdaptionHints.Add(hint); + Add(hint); newHints.Add(hint); } } @@ -117,25 +121,57 @@ namespace Artemis.Core return device.Leds.All(l => Layer.Leds.Contains(l)); } + /// + /// Adds an adaption hint to the adapter. + /// + /// The adaption hint to add. + public void Add(IAdaptionHint adaptionHint) + { + if (_adaptionHints.Contains(adaptionHint)) + return; + + _adaptionHints.Add(adaptionHint); + AdapterHintAdded?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint)); + } + + /// + /// Removes the first occurrence of a specific adaption hint from the adapter. + /// + /// The adaption hint to remove. + public void Remove(IAdaptionHint adaptionHint) + { + if (_adaptionHints.Remove(adaptionHint)) + AdapterHintRemoved?.Invoke(this, new LayerAdapterHintEventArgs(adaptionHint)); + } + + /// + /// Removes all adaption hints from the adapter. + /// + public void Clear() + { + while (_adaptionHints.Any()) + Remove(_adaptionHints.First()); + } + #region Implementation of IStorageModel /// public void Load() { - AdaptionHints.Clear(); + _adaptionHints.Clear(); // Kind of meh. // This leaves the adapter responsible for finding the right hint for the right entity, but it's gotta be done somewhere.. foreach (IAdaptionHintEntity hintEntity in Layer.LayerEntity.AdaptionHints) switch (hintEntity) { case DeviceAdaptionHintEntity entity: - AdaptionHints.Add(new DeviceAdaptionHint(entity)); + Add(new DeviceAdaptionHint(entity)); break; case CategoryAdaptionHintEntity entity: - AdaptionHints.Add(new CategoryAdaptionHint(entity)); + Add(new CategoryAdaptionHint(entity)); break; case KeyboardSectionAdaptionHintEntity entity: - AdaptionHints.Add(new KeyboardSectionAdaptionHint(entity)); + Add(new KeyboardSectionAdaptionHint(entity)); break; } } @@ -149,5 +185,15 @@ namespace Artemis.Core } #endregion + + /// + /// Occurs whenever a new adapter hint is added to the adapter. + /// + public event EventHandler? AdapterHintAdded; + + /// + /// Occurs whenever an adapter hint is removed from the adapter. + /// + public event EventHandler? AdapterHintRemoved; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs new file mode 100644 index 000000000..5e1b6abb7 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerEffectPropertyGroup.cs @@ -0,0 +1,25 @@ +namespace Artemis.Core +{ + /// + /// Represents a property group on a layer + /// + /// Note: You cannot initialize property groups yourself. If properly placed and annotated, the Artemis core will + /// initialize these for you. + /// + /// + public abstract class LayerEffectPropertyGroup : LayerPropertyGroup + { + /// + /// Whether or not this layer effect is enabled + /// + [PropertyDescription(Name = "Enabled", Description = "Whether or not this layer effect is enabled")] + public BoolLayerProperty IsEnabled { get; set; } = null!; + + internal void InitializeIsEnabled() + { + IsEnabled.DefaultValue = true; + if (!IsEnabled.IsLoadedFromStorage) + IsEnabled.SetCurrentValue(true); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs index e4b63a567..b3b58b7bb 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyDescriptionAttribute.cs @@ -7,6 +7,11 @@ namespace Artemis.Core /// public class PropertyDescriptionAttribute : Attribute { + /// + /// The identifier of this property used for storage, if not set one will be generated property name in code + /// + public string? Identifier { get; set; } + /// /// The user-friendly name for this property, shown in the UI /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs index 98cdb74db..4049688b6 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/Attributes/PropertyGroupDescriptionAttribute.cs @@ -8,12 +8,17 @@ namespace Artemis.Core public class PropertyGroupDescriptionAttribute : Attribute { /// - /// The user-friendly name for this property, shown in the UI. + /// The identifier of this property group used for storage, if not set one will be generated based on the group name in code /// - public string? Name { get; set; } + public string? Identifier { get; set; } /// - /// The user-friendly description for this property, shown in the UI. + /// The user-friendly name for this property group, shown in the UI. + /// + public string? Name { get; set; } + + /// + /// The user-friendly description for this property group, shown in the UI. /// public string? Description { get; set; } } diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs b/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs index 2e696d7af..3a990297c 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/FloatRange.cs @@ -5,18 +5,10 @@ namespace Artemis.Core /// /// Represents a range between two single-precision floating point numbers /// - public class FloatRange + public readonly struct FloatRange { private readonly Random _rand; - /// - /// Creates a new instance of the class - /// - public FloatRange() - { - _rand = new Random(); - } - /// /// Creates a new instance of the class /// @@ -31,14 +23,14 @@ namespace Artemis.Core } /// - /// Gets or sets the start value of the range + /// Gets the start value of the range /// - public float Start { get; set; } + public float Start { get; } /// - /// Gets or sets the end value of the range + /// Gets the end value of the range /// - public float End { get; set; } + public float End { get; } /// /// Determines whether the given value is in this range diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index dcfcdeeda..4ad645a94 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.ObjectModel; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -18,16 +18,46 @@ namespace Artemis.Core /// PropertyDescriptionAttribute PropertyDescription { get; } + /// + /// Gets the profile element (such as layer or folder) this property is applied to + /// + RenderProfileElement ProfileElement { get; } + /// /// The parent group of this layer property, set after construction /// LayerPropertyGroup LayerPropertyGroup { get; } /// - /// Gets the unique path of the property on the layer + /// Gets or sets whether the property is hidden in the UI + /// + public bool IsHidden { get; set; } + + /// + /// Gets the data binding of this property + /// + IDataBinding BaseDataBinding { get; } + + /// + /// Gets a boolean indicating whether the layer has any data binding properties + /// + public bool HasDataBinding { get; } + + /// + /// Gets a boolean indicating whether data bindings are supported on this type of property + /// + public bool DataBindingsSupported { get; } + + /// + /// Gets the unique path of the property on the render element /// string Path { get; } + /// + /// Gets a read-only list of all the keyframes on this layer property + /// + ReadOnlyCollection UntypedKeyframes { get; } + /// /// Gets the type of the property /// @@ -45,19 +75,14 @@ namespace Artemis.Core /// /// /// - void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description); /// - /// Returns a list off all data binding registrations + /// Attempts to create a keyframe for this property from the provided entity /// - List GetAllDataBindingRegistrations(); - - /// - /// Attempts to load and add the provided keyframe entity to the layer property - /// - /// The entity representing the keyframe to add + /// The entity representing the keyframe to create /// If succeeded the resulting keyframe, otherwise - ILayerPropertyKeyframe? AddKeyframeEntity(KeyframeEntity keyframeEntity); + ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity); /// /// Overrides the property value with the default value @@ -70,6 +95,26 @@ namespace Artemis.Core /// The timeline to apply to the property void Update(Timeline timeline); + + /// + /// Updates just the data binding instead of the entire layer + /// + void UpdateDataBinding(); + + /// + /// Removes a keyframe from the layer property without knowing it's type. + /// Prefer . + /// + /// + void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe); + + /// + /// Adds a keyframe to the layer property without knowing it's type. + /// Prefer . + /// + /// + void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe); + /// /// Occurs when the layer property is disposed /// @@ -98,31 +143,11 @@ namespace Artemis.Core /// /// Occurs when a new keyframe was added to the layer property /// - public event EventHandler? KeyframeAdded; + public event EventHandler? KeyframeAdded; /// /// Occurs when a keyframe was removed from the layer property /// - public event EventHandler? KeyframeRemoved; - - /// - /// Occurs when a data binding property has been added - /// - public event EventHandler? DataBindingPropertyRegistered; - - /// - /// Occurs when all data binding properties have been removed - /// - public event EventHandler? DataBindingPropertiesCleared; - - /// - /// Occurs when a data binding has been enabled - /// - public event EventHandler? DataBindingEnabled; - - /// - /// Occurs when a data binding has been disabled - /// - public event EventHandler? DataBindingDisabled; + public event EventHandler? KeyframeRemoved; } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs index bcf182c27..d236e297c 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Artemis.Storage.Entities.Profile; namespace Artemis.Core @@ -6,7 +7,7 @@ namespace Artemis.Core /// /// Represents a keyframe on a containing a value and a timestamp /// - public interface ILayerPropertyKeyframe + public interface ILayerPropertyKeyframe : INotifyPropertyChanged { /// /// Gets an untyped reference to the layer property of this keyframe @@ -14,12 +15,12 @@ namespace Artemis.Core ILayerProperty UntypedLayerProperty { get; } /// - /// The position of this keyframe in the timeline + /// Gets or sets the position of this keyframe in the timeline /// TimeSpan Position { get; set; } /// - /// The easing function applied on the value of the keyframe + /// Gets or sets the easing function applied on the value of the keyframe /// Easings.Functions EasingFunction { get; set; } @@ -32,5 +33,12 @@ namespace Artemis.Core /// Removes the keyframe from the layer property /// void Remove(); + + /// + /// Creates a copy of this keyframe. + /// Note: The copied keyframe is not added to the layer property. + /// + /// The resulting copy + ILayerPropertyKeyframe CreateCopy(); } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs b/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs index e93e6a349..8fe97c53d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/IntRange.cs @@ -5,18 +5,10 @@ namespace Artemis.Core /// /// Represents a range between two signed integers /// - public class IntRange + public readonly struct IntRange { private readonly Random _rand; - - /// - /// Creates a new instance of the class - /// - public IntRange() - { - _rand = new Random(); - } - + /// /// Creates a new instance of the class /// @@ -31,14 +23,14 @@ namespace Artemis.Core } /// - /// Gets or sets the start value of the range + /// Gets the start value of the range /// - public int Start { get; set; } + public int Start { get; } /// - /// Gets or sets the end value of the range + /// Gets the end value of the range /// - public int End { get; set; } + public int End { get; } /// /// Determines whether the given value is in this range diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 6bcf8f8b2..b419263f5 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -27,9 +27,10 @@ namespace Artemis.Core // These are set right after construction to keep the constructor (and inherited constructs) clean ProfileElement = null!; LayerPropertyGroup = null!; - Path = null!; Entity = null!; PropertyDescription = null!; + DataBinding = null!; + Path = ""; CurrentValue = default!; DefaultValue = default!; @@ -44,7 +45,13 @@ namespace Artemis.Core _baseValue = default!; _keyframes = new List>(); - Keyframes = new(_keyframes); + Keyframes = new ReadOnlyCollection>(_keyframes); + } + + /// + public override string ToString() + { + return $"{Path} - {CurrentValue} ({PropertyType})"; } /// @@ -58,93 +65,10 @@ namespace Artemis.Core { _disposed = true; - foreach (IDataBinding dataBinding in _dataBindings) - dataBinding.Dispose(); - + DataBinding.Dispose(); Disposed?.Invoke(this, EventArgs.Empty); } - /// - /// Invokes the event - /// - protected virtual void OnUpdated() - { - Updated?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnCurrentValueSet() - { - CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); - LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframesToggled() - { - KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframeAdded() - { - KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframeRemoved() - { - KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertyRegistered() - { - DataBindingPropertyRegistered?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertiesCleared() - { - DataBindingPropertiesCleared?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingEnabled(LayerPropertyEventArgs e) - { - DataBindingEnabled?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingDisabled(LayerPropertyEventArgs e) - { - DataBindingDisabled?.Invoke(this, e); - } - /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } @@ -163,11 +87,38 @@ namespace Artemis.Core CurrentValue = BaseValue; UpdateKeyframes(timeline); - UpdateDataBindings(timeline); + UpdateDataBinding(); + + // UpdateDataBinding called OnUpdated() + } + + /// + public void UpdateDataBinding() + { + DataBinding.Update(); + DataBinding.Apply(); OnUpdated(); } + /// + public void RemoveUntypedKeyframe(ILayerPropertyKeyframe keyframe) + { + if (keyframe is not LayerPropertyKeyframe typedKeyframe) + throw new ArtemisCoreException($"Can't remove a keyframe that is not of type {typeof(T).FullName}."); + + RemoveKeyframe(typedKeyframe); + } + + /// + public void AddUntypedKeyframe(ILayerPropertyKeyframe keyframe) + { + if (keyframe is not LayerPropertyKeyframe typedKeyframe) + throw new ArtemisCoreException($"Can't add a keyframe that is not of type {typeof(T).FullName}."); + + AddKeyframe(typedKeyframe); + } + /// public void Dispose() { @@ -175,46 +126,11 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - /// - public event EventHandler? Disposed; - - /// - public event EventHandler? Updated; - - /// - public event EventHandler? CurrentValueSet; - - /// - public event EventHandler? VisibilityChanged; - - /// - public event EventHandler? KeyframesToggled; - - /// - public event EventHandler? KeyframeAdded; - - /// - public event EventHandler? KeyframeRemoved; - - /// - public event EventHandler? DataBindingPropertyRegistered; - - /// - public event EventHandler? DataBindingPropertiesCleared; - - /// - public event EventHandler? DataBindingEnabled; - - /// - public event EventHandler? DataBindingDisabled; - #region Hierarchy private bool _isHidden; - /// - /// Gets or sets whether the property is hidden in the UI - /// + /// public bool IsHidden { get => _isHidden; @@ -225,13 +141,11 @@ namespace Artemis.Core } } - /// - /// Gets the profile element (such as layer or folder) this property is applied to - /// - public RenderProfileElement ProfileElement { get; internal set; } + /// + public RenderProfileElement ProfileElement { get; private set; } /// - public LayerPropertyGroup LayerPropertyGroup { get; internal set; } + public LayerPropertyGroup LayerPropertyGroup { get; private set; } #endregion @@ -286,29 +200,33 @@ namespace Artemis.Core /// An optional time to set the value add, if provided and property is using keyframes the value will be set to an new /// or existing keyframe. /// - public void SetCurrentValue(T value, TimeSpan? time) + /// The keyframe if one was created or updated. + public LayerPropertyKeyframe? SetCurrentValue(T value, TimeSpan? time = null) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); + LayerPropertyKeyframe? keyframe = null; if (time == null || !KeyframesEnabled || !KeyframesSupported) - { BaseValue = value; - } else { // If on a keyframe, update the keyframe - LayerPropertyKeyframe? currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); + keyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value); // Create a new keyframe if none found - if (currentKeyframe == null) - AddKeyframe(new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this)); + if (keyframe == null) + { + keyframe = new LayerPropertyKeyframe(value, time.Value, Easings.Functions.Linear, this); + AddKeyframe(keyframe); + } else - currentKeyframe.Value = value; + keyframe.Value = value; } // Force an update so that the base value is applied to the current value and // keyframes/data bindings are applied using the new base value ReapplyUpdate(); + return keyframe; } /// @@ -317,12 +235,23 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("LayerProperty"); - string json = CoreJson.SerializeObject(DefaultValue, true); + if (DefaultValue == null) + return; + KeyframesEnabled = false; - SetCurrentValue(CoreJson.DeserializeObject(json)!, null); + + // For value types there's no need to make a copy + if (DefaultValue.GetType().IsValueType) + SetCurrentValue(DefaultValue); + // Reference types make a deep clone (ab)using JSON + else + { + string json = CoreJson.SerializeObject(DefaultValue, true); + SetCurrentValue(CoreJson.DeserializeObject(json)!); + } } - private void ReapplyUpdate() + internal void ReapplyUpdate() { // Create a timeline with the same position but a delta of zero Timeline temporaryTimeline = new(); @@ -343,7 +272,7 @@ namespace Artemis.Core /// /// Gets whether keyframes are supported on this type of property /// - public bool KeyframesSupported { get; protected internal set; } = true; + public bool KeyframesSupported { get; protected set; } = true; /// /// Gets or sets whether keyframes are enabled on this property, has no effect if is @@ -356,7 +285,9 @@ namespace Artemis.Core { if (_keyframesEnabled == value) return; _keyframesEnabled = value; + ReapplyUpdate(); OnKeyframesToggled(); + OnPropertyChanged(nameof(KeyframesEnabled)); } } @@ -366,6 +297,9 @@ namespace Artemis.Core /// public ReadOnlyCollection> Keyframes { get; } + /// + public ReadOnlyCollection UntypedKeyframes => new(Keyframes.Cast().ToList()); + /// /// Gets the current keyframe in the timeline according to the current progress /// @@ -396,23 +330,29 @@ namespace Artemis.Core KeyframesEnabled = true; SortKeyframes(); - OnKeyframeAdded(); + ReapplyUpdate(); + OnKeyframeAdded(keyframe); } /// - public ILayerPropertyKeyframe? AddKeyframeEntity(KeyframeEntity keyframeEntity) + public ILayerPropertyKeyframe? CreateKeyframeFromEntity(KeyframeEntity keyframeEntity) { if (keyframeEntity.Position > ProfileElement.Timeline.Length) return null; - T? value = CoreJson.DeserializeObject(keyframeEntity.Value); - if (value == null) - return null; - LayerPropertyKeyframe keyframe = new( - CoreJson.DeserializeObject(keyframeEntity.Value)!, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction, this - ); - AddKeyframe(keyframe); - return keyframe; + try + { + T? value = CoreJson.DeserializeObject(keyframeEntity.Value); + if (value == null) + return null; + + LayerPropertyKeyframe keyframe = new(value, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction, this); + return keyframe; + } + catch (JsonException) + { + return null; + } } /// @@ -428,8 +368,10 @@ namespace Artemis.Core return; _keyframes.Remove(keyframe); + SortKeyframes(); - OnKeyframeRemoved(); + ReapplyUpdate(); + OnKeyframeRemoved(keyframe); } /// @@ -481,137 +423,19 @@ namespace Artemis.Core #region Data bindings - // ReSharper disable InconsistentNaming - internal readonly List _dataBindingRegistrations = new(); - - internal readonly List _dataBindings = new(); - // ReSharper restore InconsistentNaming - /// - /// Gets whether data bindings are supported on this type of property + /// Gets the data binding of this property /// - public bool DataBindingsSupported { get; protected internal set; } = true; + public DataBinding DataBinding { get; private set; } - /// - /// Gets whether the layer has any active data bindings - /// - public bool HasDataBinding => GetAllDataBindingRegistrations().Any(r => r.GetDataBinding() != null); + /// + public bool DataBindingsSupported => DataBinding.Properties.Any(); - /// - /// Gets a data binding registration by the display name used to register it - /// Note: The expression must exactly match the one used to register the data binding - /// - public DataBindingRegistration? GetDataBindingRegistration(string identifier) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); + /// + public IDataBinding BaseDataBinding => DataBinding; - IDataBindingRegistration? match = _dataBindingRegistrations.FirstOrDefault(r => r is DataBindingRegistration registration && - registration.DisplayName == identifier); - return (DataBindingRegistration?) match; - } - - /// - /// Gets a list containing all data binding registrations of this layer property - /// - /// A list containing all data binding registrations of this layer property - public List GetAllDataBindingRegistrations() - { - return _dataBindingRegistrations; - } - - /// - /// Registers a data binding property so that is available to the data binding system - /// - /// The type of the layer property - /// The function to call to get the value of the property - /// The action to call to set the value of the property - /// The converter to use while applying the data binding - /// The display name of the data binding property - public DataBindingRegistration RegisterDataBindingProperty(Func getter, Action setter, DataBindingConverter converter, - string displayName) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - if (_dataBindingRegistrations.Any(d => d.DisplayName == displayName)) - throw new ArtemisCoreException($"A databinding property named '{displayName}' is already registered."); - - DataBindingRegistration registration = new(this, converter, getter, setter, displayName); - _dataBindingRegistrations.Add(registration); - - // If not yet initialized, load the data binding related to the registration if available - if (_isInitialized) - { - IDataBinding? dataBinding = registration.CreateDataBinding(); - if (dataBinding != null) - _dataBindings.Add(dataBinding); - } - - OnDataBindingPropertyRegistered(); - return registration; - } - - /// - /// Removes all data binding properties so they are no longer available to the data binding system - /// - public void ClearDataBindingProperties() - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) - dataBindingRegistration.ClearDataBinding(); - _dataBindingRegistrations.Clear(); - - OnDataBindingPropertiesCleared(); - } - - /// - /// Enables a data binding for the provided - /// - /// The newly created data binding - public DataBinding EnableDataBinding(DataBindingRegistration dataBindingRegistration) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - if (dataBindingRegistration.LayerProperty != this) - throw new ArtemisCoreException("Cannot enable a data binding using a data binding registration of a different layer property"); - if (dataBindingRegistration.DataBinding != null) - throw new ArtemisCoreException("Provided data binding registration already has an enabled data binding"); - - DataBinding dataBinding = new(dataBindingRegistration); - _dataBindings.Add(dataBinding); - - OnDataBindingEnabled(new LayerPropertyEventArgs(dataBinding.LayerProperty)); - return dataBinding; - } - - /// - /// Disables the provided data binding - /// - /// The data binding to remove - public void DisableDataBinding(DataBinding dataBinding) - { - if (_disposed) - throw new ObjectDisposedException("LayerProperty"); - - _dataBindings.Remove(dataBinding); - - if (dataBinding.Registration != null) - dataBinding.Registration.DataBinding = null; - dataBinding.Dispose(); - OnDataBindingDisabled(new LayerPropertyEventArgs(dataBinding.LayerProperty)); - } - - private void UpdateDataBindings(Timeline timeline) - { - foreach (IDataBinding dataBinding in _dataBindings) - { - dataBinding.Update(timeline); - dataBinding.Apply(); - } - } + /// + public bool HasDataBinding => DataBinding.IsEnabled; #endregion @@ -682,23 +506,29 @@ namespace Artemis.Core internal PropertyEntity Entity { get; set; } /// - public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path) + public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description) { if (_disposed) throw new ObjectDisposedException("LayerProperty"); + if (description.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); + _isInitialized = true; ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); - Path = path; Entity = entity ?? throw new ArgumentNullException(nameof(entity)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); IsLoadedFromStorage = fromStorage; + DataBinding = Entity.DataBinding != null ? new DataBinding(this, Entity.DataBinding) : new DataBinding(this); if (PropertyDescription.DisableKeyframes) KeyframesSupported = false; + // Create the path to this property by walking up the tree + Path = LayerPropertyGroup.Path + "." + description.Identifier; + OnInitialize(); } @@ -731,20 +561,18 @@ namespace Artemis.Core try { foreach (KeyframeEntity keyframeEntity in Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length)) - AddKeyframeEntity(keyframeEntity); + { + LayerPropertyKeyframe? keyframe = CreateKeyframeFromEntity(keyframeEntity) as LayerPropertyKeyframe; + if (keyframe != null) + AddKeyframe(keyframe); + } } catch (JsonException) { // ignored for now } - _dataBindings.Clear(); - foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) - { - IDataBinding? dataBinding = dataBindingRegistration.CreateDataBinding(); - if (dataBinding != null) - _dataBindings.Add(dataBinding); - } + DataBinding.Load(); } /// @@ -763,9 +591,8 @@ namespace Artemis.Core Entity.KeyframeEntities.Clear(); Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); - Entity.DataBindingEntities.Clear(); - foreach (IDataBinding dataBinding in _dataBindings) - dataBinding.Save(); + DataBinding.Save(); + Entity.DataBinding = DataBinding.Entity; } /// @@ -776,5 +603,81 @@ namespace Artemis.Core } #endregion + + #region Events + + /// + public event EventHandler? Disposed; + + /// + public event EventHandler? Updated; + + /// + public event EventHandler? CurrentValueSet; + + /// + public event EventHandler? VisibilityChanged; + + /// + public event EventHandler? KeyframesToggled; + + /// + public event EventHandler? KeyframeAdded; + + /// + public event EventHandler? KeyframeRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnUpdated() + { + Updated?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnCurrentValueSet() + { + CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); + LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframesToggled() + { + KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + /// + protected virtual void OnKeyframeAdded(ILayerPropertyKeyframe keyframe) + { + KeyframeAdded?.Invoke(this, new LayerPropertyKeyframeEventArgs(keyframe)); + } + + /// + /// Invokes the event + /// + /// + protected virtual void OnKeyframeRemoved(ILayerPropertyKeyframe keyframe) + { + KeyframeRemoved?.Invoke(this, new LayerPropertyKeyframeEventArgs(keyframe)); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index a7cd776a6..4a889f613 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -57,6 +57,7 @@ namespace Artemis.Core { SetAndNotify(ref _position, value); LayerProperty.SortKeyframes(); + LayerProperty.ReapplyUpdate(); } } @@ -66,7 +67,7 @@ namespace Artemis.Core /// public KeyframeEntity GetKeyframeEntity() { - return new() + return new KeyframeEntity { Value = CoreJson.SerializeObject(Value), Position = Position, @@ -79,5 +80,11 @@ namespace Artemis.Core { LayerProperty.RemoveKeyframe(this); } + + /// + public ILayerPropertyKeyframe CreateCopy() + { + return new LayerPropertyKeyframe(Value, Position, EasingFunction, LayerProperty); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs new file mode 100644 index 000000000..906c5058b --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyPreview.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; + +namespace Artemis.Core; + +/// +/// Represents a container for a preview value of a that can be used to update and +/// discard a temporary value. +/// +/// The value type of the layer property. +public sealed class LayerPropertyPreview : IDisposable +{ + /// + /// Creates a new instance of the class. + /// + /// The layer property to apply the preview value to. + /// The time in the timeline at which the preview is applied. + public LayerPropertyPreview(LayerProperty layerProperty, TimeSpan time) + { + Property = layerProperty; + Time = time; + OriginalKeyframe = layerProperty.Keyframes.FirstOrDefault(k => k.Position == time); + OriginalValue = OriginalKeyframe != null ? OriginalKeyframe.Value : layerProperty.CurrentValue; + PreviewValue = OriginalValue; + } + + /// + /// Gets the property this preview applies to. + /// + public LayerProperty Property { get; } + + /// + /// Gets the original keyframe of the property at the time the preview was created. + /// + public LayerPropertyKeyframe? OriginalKeyframe { get; } + + /// + /// Gets the original value of the property at the time the preview was created. + /// + public T OriginalValue { get; } + + /// + /// Gets the time in the timeline at which the preview is applied. + /// + public TimeSpan Time { get; } + + /// + /// Gets the keyframe that was created to preview the value. + /// + public LayerPropertyKeyframe? PreviewKeyframe { get; private set; } + + /// + /// Gets the preview value. + /// + public T? PreviewValue { get; private set; } + + /// + /// Updates the layer property to the given , keeping track of the original state of the + /// property. + /// + /// The value to preview. + public void Preview(T value) + { + PreviewValue = value; + PreviewKeyframe = Property.SetCurrentValue(value, Time); + } + + /// + /// Discard the preview value and restores the original state of the property. The returned boolean can be used to + /// determine whether the preview value was different from the original value. + /// + /// if any changes where discarded; otherwise . + public bool DiscardPreview() + { + if (PreviewKeyframe != null && OriginalKeyframe == null) + { + Property.RemoveKeyframe(PreviewKeyframe); + return true; + } + + Property.SetCurrentValue(OriginalValue, Time); + return !Equals(OriginalValue, PreviewValue); ; + } + + /// + /// Discard the preview value and restores the original state of the property. The returned boolean can be used to + /// determine whether the preview value was different from the original value. + /// + public void Dispose() + { + DiscardPreview(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index 00658528d..637a1ea6e 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -30,59 +30,43 @@ namespace Artemis.Core protected LayerPropertyGroup() { // These are set right after construction to keep the constructor (and inherited constructs) clean - GroupDescription = null!; - Feature = null!; ProfileElement = null!; - Path = null!; + GroupDescription = null!; + Path = ""; _layerProperties = new List(); _layerPropertyGroups = new List(); - LayerProperties = new(_layerProperties); - LayerPropertyGroups = new(_layerPropertyGroups); + LayerProperties = new ReadOnlyCollection(_layerProperties); + LayerPropertyGroups = new ReadOnlyCollection(_layerPropertyGroups); } - /// - /// Gets the description of this group - /// - public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; } - - /// - /// Gets the plugin feature this group is associated with - /// - public PluginFeature Feature { get; set; } - /// /// Gets the profile element (such as layer or folder) this group is associated with /// - public RenderProfileElement ProfileElement { get; internal set; } + public RenderProfileElement ProfileElement { get; private set; } + + /// + /// Gets the description of this group + /// + public PropertyGroupDescriptionAttribute GroupDescription { get; private set; } /// /// The parent group of this group /// - [LayerPropertyIgnore] + [LayerPropertyIgnore] // Ignore the parent when selecting child groups public LayerPropertyGroup? Parent { get; internal set; } /// - /// The path of this property group + /// Gets the unique path of the property on the render element /// - public string Path { get; internal set; } + public string Path { get; private set; } /// /// Gets whether this property groups properties are all initialized /// public bool PropertiesInitialized { get; private set; } - /// - /// The layer brush this property group belongs to - /// - public BaseLayerBrush? LayerBrush { get; internal set; } - - /// - /// The layer effect this property group belongs to - /// - public BaseLayerEffect? LayerEffect { get; internal set; } - /// /// Gets or sets whether the property is hidden in the UI /// @@ -96,6 +80,11 @@ namespace Artemis.Core } } + /// + /// Gets the entity this property group uses for persistent storage + /// + public PropertyGroupEntity? PropertyGroupEntity { get; internal set; } + /// /// A list of all layer properties in this group /// @@ -194,17 +183,20 @@ namespace Artemis.Core } } - internal void Initialize(RenderProfileElement profileElement, string path, PluginFeature feature) + internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity) { - if (path == null) throw new ArgumentNullException(nameof(path)); + if (groupDescription.Identifier == null) + throw new ArtemisCoreException("Can't initialize a property group without an identifier"); // Doubt this will happen but let's make sure if (PropertiesInitialized) throw new ArtemisCoreException("Layer property group already initialized, wut"); - Feature = feature ?? throw new ArgumentNullException(nameof(feature)); - ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); - Path = path.TrimEnd('.'); + ProfileElement = profileElement; + Parent = parent; + GroupDescription = groupDescription; + PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier}; + Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier; // Get all properties implementing ILayerProperty or LayerPropertyGroup foreach (PropertyInfo propertyInfo in GetType().GetProperties()) @@ -240,14 +232,18 @@ namespace Artemis.Core internal void ApplyToEntity() { - if (!PropertiesInitialized) + if (!PropertiesInitialized || PropertyGroupEntity == null) return; foreach (ILayerProperty layerProperty in LayerProperties) layerProperty.Save(); + PropertyGroupEntity.PropertyGroups.Clear(); foreach (LayerPropertyGroup layerPropertyGroup in LayerPropertyGroups) + { layerPropertyGroup.ApplyToEntity(); + PropertyGroupEntity.PropertyGroups.Add(layerPropertyGroup.PropertyGroupEntity); + } } internal void Update(Timeline timeline) @@ -258,6 +254,15 @@ namespace Artemis.Core layerPropertyGroup.Update(timeline); } + internal void MoveLayerProperty(ILayerProperty layerProperty, int index) + { + if (!_layerProperties.Contains(layerProperty)) + return; + + _layerProperties.Remove(layerProperty); + _layerProperties.Insert(index, layerProperty); + } + internal virtual void OnVisibilityChanged() { VisibilityChanged?.Invoke(this, EventArgs.Empty); @@ -271,61 +276,68 @@ namespace Artemis.Core private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { - string path = $"{Path}.{propertyInfo.Name}"; - - if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path}"); - - if (!(Activator.CreateInstance(propertyInfo.PropertyType, true) is ILayerProperty instance)) - throw new ArtemisPluginException($"Failed to create instance of layer property at {path}"); - - // Ensure the description has a name, if not this is a good point to set it based on the property info + // Ensure the description has an identifier and name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyDescription.Identifier)) + propertyDescription.Identifier = propertyInfo.Name; if (string.IsNullOrWhiteSpace(propertyDescription.Name)) propertyDescription.Name = propertyInfo.Name.Humanize(); - PropertyEntity entity = GetPropertyEntity(ProfileElement, path, out bool fromStorage); - instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription, path); + if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}"); + if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance) + throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}"); + + PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage); + instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription); propertyInfo.SetValue(this, instance); + _layerProperties.Add(instance); } private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription) { - string path = Path + "."; - - if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) - throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup"); - - if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance)) - throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}"); - - // Ensure the description has a name, if not this is a good point to set it based on the property info + // Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info + if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier)) + propertyGroupDescription.Identifier = propertyInfo.Name; if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name)) propertyGroupDescription.Name = propertyInfo.Name.Humanize(); - instance.Parent = this; - instance.GroupDescription = propertyGroupDescription; - instance.LayerBrush = LayerBrush; - instance.LayerEffect = LayerEffect; - instance.Initialize(ProfileElement, $"{path}{propertyInfo.Name}.", Feature); + if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}"); + if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance)) + throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}"); + + PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier); + instance.Initialize(ProfileElement, this, propertyGroupDescription, entity); propertyInfo.SetValue(this, instance); _layerPropertyGroups.Add(instance); } - private PropertyEntity GetPropertyEntity(RenderProfileElement profileElement, string path, out bool fromStorage) + private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage) { - PropertyEntity? entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.FeatureId == Feature.Id && p.Path == path); + if (PropertyGroupEntity == null) + throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup"); + + PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier); fromStorage = entity != null; if (entity == null) { - entity = new PropertyEntity {FeatureId = Feature.Id, Path = path}; - profileElement.RenderElementEntity.PropertyEntities.Add(entity); + entity = new PropertyEntity {Identifier = identifier}; + PropertyGroupEntity.Properties.Add(entity); } return entity; } + private PropertyGroupEntity GetPropertyGroupEntity(string identifier) + { + if (PropertyGroupEntity == null) + throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup"); + + return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity() {Identifier = identifier}; + } + /// public void Dispose() { diff --git a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs index 9a78aba1c..5e2447c2e 100644 --- a/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs +++ b/src/Artemis.Core/Models/Profile/LayerTransformProperties.cs @@ -12,13 +12,13 @@ namespace Artemis.Core /// /// The point at which the shape is attached to its position /// - [PropertyDescription(Description = "The point at which the shape is attached to its position", InputStepSize = 0.001f)] + [PropertyDescription(Description = "The point at which the shape is attached to its position", InputAffix = "%", InputStepSize = 0.001f)] public SKPointLayerProperty AnchorPoint { get; set; } /// /// The position of the shape /// - [PropertyDescription(Description = "The position of the shape", InputStepSize = 0.001f)] + [PropertyDescription(Description = "The position of the shape", InputAffix = "%", InputStepSize = 0.001f)] public SKPointLayerProperty Position { get; set; } /// @@ -30,19 +30,21 @@ namespace Artemis.Core /// /// The rotation of the shape in degree /// - [PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°")] + [PropertyDescription(Description = "The rotation of the shape in degrees", InputAffix = "°", InputStepSize = 0.5f)] public FloatLayerProperty Rotation { get; set; } /// /// The opacity of the shape /// - [PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f)] + [PropertyDescription(Description = "The opacity of the shape", InputAffix = "%", MinInputValue = 0f, MaxInputValue = 100f, InputStepSize = 0.1f)] public FloatLayerProperty Opacity { get; set; } /// protected override void PopulateDefaults() { Scale.DefaultValue = new SKSize(100, 100); + AnchorPoint.DefaultValue = new SKPoint(0.5f, 0.5f); + Position.DefaultValue = new SKPoint(0.5f, 0.5f); Opacity.DefaultValue = 100; } diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index c0c3a2528..eaa4abce4 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; @@ -15,19 +16,22 @@ namespace Artemis.Core private readonly object _lock = new(); private bool _isFreshImport; private ProfileElement? _lastSelectedProfileElement; + private readonly ObservableCollection _scripts; + private readonly ObservableCollection _scriptConfigurations; internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { + _scripts = new ObservableCollection(); + _scriptConfigurations = new ObservableCollection(); + Configuration = configuration; Profile = this; ProfileEntity = profileEntity; EntityId = profileEntity.Id; - Scripts = new List(); - ScriptConfigurations = new List(); - UndoStack = new MaxStack(20); - RedoStack = new MaxStack(20); Exceptions = new List(); + Scripts = new ReadOnlyObservableCollection(_scripts); + ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations); Load(); } @@ -40,13 +44,12 @@ namespace Artemis.Core /// /// Gets a collection of all active scripts assigned to this profile /// - public List Scripts { get; } + public ReadOnlyObservableCollection Scripts { get; } /// /// Gets a collection of all script configurations assigned to this profile /// - public List ScriptConfigurations { get; } - + 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 @@ -76,8 +79,6 @@ namespace Artemis.Core /// public ProfileEntity ProfileEntity { get; internal set; } - internal MaxStack UndoStack { get; set; } - internal MaxStack RedoStack { get; set; } internal List Exceptions { get; } /// @@ -100,7 +101,7 @@ namespace Artemis.Core } /// - public override void Render(SKCanvas canvas, SKPointI basePosition) + public override void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus) { lock (_lock) { @@ -111,7 +112,7 @@ namespace Artemis.Core profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds); foreach (ProfileElement profileElement in Children) - profileElement.Render(canvas, basePosition); + profileElement.Render(canvas, basePosition, editorFocus); foreach (ProfileScript profileScript in Scripts) profileScript.OnProfileRendered(canvas, canvas.LocalClipBounds); @@ -170,8 +171,8 @@ namespace Artemis.Core if (!disposing) return; - while (Scripts.Count > 1) - Scripts[0].Dispose(); + while (Scripts.Count > 0) + RemoveScript(Scripts[0]); foreach (ProfileElement profileElement in Children) profileElement.Dispose(); @@ -197,28 +198,72 @@ namespace Artemis.Core // Populate the profile starting at the root, the rest is populated recursively FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId); if (rootFolder == null) - { - Folder _ = new(this, "Root folder"); - } + AddChild(new Folder(this, "Root folder")); else - { AddChild(new Folder(this, this, rootFolder)); - } } + List renderElements = GetAllRenderElements(); + if (ProfileEntity.LastSelectedProfileElement != Guid.Empty) - { - LastSelectedProfileElement = GetAllFolders().FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); - if (LastSelectedProfileElement == null) - LastSelectedProfileElement = GetAllLayers().FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); - } + LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement); else LastSelectedProfileElement = null; - foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) - scriptConfiguration.Script?.Dispose(); - ScriptConfigurations.Clear(); - ScriptConfigurations.AddRange(ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))); + 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() @@ -253,10 +298,7 @@ namespace Artemis.Core /// 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; + return GetAllRenderElements().SelectMany(folders => folders.GetBrokenHierarchy()); } #endregion diff --git a/src/Artemis.Core/Models/Profile/ProfileCategory.cs b/src/Artemis.Core/Models/Profile/ProfileCategory.cs index c00671f56..6ecba4ae2 100644 --- a/src/Artemis.Core/Models/Profile/ProfileCategory.cs +++ b/src/Artemis.Core/Models/Profile/ProfileCategory.cs @@ -24,14 +24,14 @@ namespace Artemis.Core { _name = name; Entity = new ProfileCategoryEntity(); - ProfileConfigurations = new(_profileConfigurations); + ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); } internal ProfileCategory(ProfileCategoryEntity entity) { _name = null!; Entity = entity; - ProfileConfigurations = new(_profileConfigurations); + ProfileConfigurations = new ReadOnlyCollection(_profileConfigurations); Load(); } @@ -98,7 +98,10 @@ namespace Artemis.Core configuration.Category.RemoveProfileConfiguration(configuration); if (targetIndex != null) - _profileConfigurations.Insert(Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count), configuration); + { + targetIndex = Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count); + _profileConfigurations.Insert(targetIndex.Value, configuration); + } else _profileConfigurations.Add(configuration); configuration.Category = this; @@ -116,7 +119,8 @@ namespace Artemis.Core internal void RemoveProfileConfiguration(ProfileConfiguration configuration) { - if (!_profileConfigurations.Remove(configuration)) return; + if (!_profileConfigurations.Remove(configuration)) + return; for (int index = 0; index < _profileConfigurations.Count; index++) _profileConfigurations[index].Order = index; diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 74bbfc0af..83a131739 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Text.RegularExpressions; using SkiaSharp; namespace Artemis.Core @@ -11,6 +12,7 @@ namespace Artemis.Core /// public abstract class ProfileElement : BreakableModel, IDisposable { + internal readonly List ChildrenList; private Guid _entityId; private string? _name; private int _order; @@ -18,13 +20,11 @@ namespace Artemis.Core private Profile _profile; private bool _suspended; - internal readonly List ChildrenList; - internal ProfileElement(Profile profile) { _profile = profile; ChildrenList = new List(); - Children = new(ChildrenList); + Children = new ReadOnlyCollection(ChildrenList); } /// @@ -91,6 +91,13 @@ namespace Artemis.Core /// public bool Disposed { get; protected set; } + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => Name ?? GetType().Name; + + #endregion + /// /// Updates the element /// @@ -98,9 +105,12 @@ namespace Artemis.Core public abstract void Update(double deltaTime); /// - /// Renders the element + /// Renders the element /// - public abstract void Render(SKCanvas canvas, SKPointI basePosition); + /// The canvas to render upon. + /// The base position to use to translate relative positions to absolute positions. + /// An optional element to focus on while rendering (other elements will not render). + public abstract void Render(SKCanvas canvas, SKPointI basePosition, ProfileElement? editorFocus); /// /// Resets the internal state of the element @@ -113,12 +123,76 @@ namespace Artemis.Core return $"{nameof(EntityId)}: {EntityId}, {nameof(Order)}: {Order}, {nameof(Name)}: {Name}"; } - #region Overrides of BreakableModel + /// + /// Occurs when a child was added to the list + /// + public event EventHandler? ChildAdded; + + /// + /// Occurs when a child was removed from the list + /// + public event EventHandler? ChildRemoved; + + /// + /// Occurs when a child was added to the list of this element or any of it's descendents. + /// + public event EventHandler? DescendentAdded; + + /// + /// Occurs when a child was removed from the list of this element or any of it's descendents. + /// + public event EventHandler? DescendentRemoved; + + /// + /// Invokes the event + /// + protected virtual void OnChildAdded(ProfileElement child) + { + ChildAdded?.Invoke(this, new ProfileElementEventArgs(child)); + } + + /// + /// Invokes the event + /// + protected virtual void OnChildRemoved(ProfileElement child) + { + ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDescendentAdded(ProfileElement child) + { + DescendentAdded?.Invoke(this, new ProfileElementEventArgs(child)); + Parent?.OnDescendentAdded(child); + } + + /// + /// Invokes the event + /// + protected virtual void OnDescendentRemoved(ProfileElement child) + { + DescendentRemoved?.Invoke(this, new ProfileElementEventArgs(child)); + Parent?.OnDescendentRemoved(child); + } + + /// + /// Disposes the profile element + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + } + } /// - public override string BrokenDisplayName => Name ?? GetType().Name; - - #endregion + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } #region Hierarchy @@ -132,6 +206,14 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException(GetType().Name); + ProfileElement? current = this; + while (current != null) + { + if (ReferenceEquals(child, this)) + throw new ArtemisCoreException("Cannot make an element a child of itself"); + current = current.Parent; + } + lock (ChildrenList) { if (ChildrenList.Contains(child)) @@ -139,7 +221,9 @@ namespace Artemis.Core // Add to the end of the list if (order == null) + { ChildrenList.Add(child); + } // Insert at the given index else { @@ -154,7 +238,8 @@ namespace Artemis.Core StreamlineOrder(); } - OnChildAdded(); + OnChildAdded(child); + OnDescendentAdded(child); } /// @@ -174,13 +259,35 @@ namespace Artemis.Core child.Parent = null; } - OnChildRemoved(); + OnChildRemoved(child); + OnDescendentRemoved(child); } private void StreamlineOrder() { for (int index = 0; index < ChildrenList.Count; index++) - ChildrenList[index].Order = index; + ChildrenList[index].Order = index + 1; + } + + /// + /// Returns a flattened list of all child render elements + /// + /// + public List GetAllRenderElements() + { + if (Disposed) + throw new ObjectDisposedException(GetType().Name); + + List elements = new(); + foreach (RenderProfileElement childElement in Children.Where(c => c is RenderProfileElement).Cast()) + { + // Add all folders in this element + elements.Add(childElement); + // Add all folders in folders inside this element + elements.AddRange(childElement.GetAllRenderElements()); + } + + return elements; } /// @@ -225,6 +332,44 @@ namespace Artemis.Core return layers; } + /// + /// Returns a name for a new layer according to any other layers with a default name similar to creating new folders in + /// Explorer + /// + /// The resulting name i.e. New layer or New layer (2) + public string GetNewLayerName(string baseName = "New layer") + { + if (!Children.Any(c => c is Layer && c.Name == baseName)) + return baseName; + + int current = 2; + while (true) + { + if (Children.Where(c => c is Layer).All(c => c.Name != $"{baseName} ({current})")) + return $"{baseName} ({current})"; + current++; + } + } + + /// + /// Returns a name for a new folder according to any other folders with a default name similar to creating new folders + /// in Explorer + /// + /// The resulting name i.e. New folder or New folder (2) + public string GetNewFolderName(string baseName = "New folder") + { + if (!Children.Any(c => c is Folder && c.Name == baseName)) + return baseName; + + int current = 2; + while (true) + { + if (Children.Where(c => c is Folder).All(c => c.Name != $"{baseName} ({current})")) + return $"{baseName} ({current})"; + current++; + } + } + #endregion #region Storage @@ -233,56 +378,5 @@ namespace Artemis.Core internal abstract void Save(); #endregion - - #region IDisposable - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the profile element - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - } - } - - #endregion - - #region Events - - /// - /// Occurs when a child was added to the list - /// - public event EventHandler? ChildAdded; - - /// - /// Occurs when a child was removed from the list - /// - public event EventHandler? ChildRemoved; - - /// - /// Invokes the event - /// - protected virtual void OnChildAdded() - { - ChildAdded?.Invoke(this, EventArgs.Empty); - } - - /// - /// Invokes the event - /// - protected virtual void OnChildRemoved() - { - ChildRemoved?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs index b518ec1f7..fe3d1017b 100644 --- a/src/Artemis.Core/Models/Profile/RenderProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/RenderProfileElement.cs @@ -4,437 +4,393 @@ using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects.Placeholder; -using Artemis.Core.Properties; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.Conditions; using SkiaSharp; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents an element of a that has advanced rendering capabilities +/// +public abstract class RenderProfileElement : ProfileElement { - /// - /// Represents an element of a that has advanced rendering capabilities - /// - public abstract class RenderProfileElement : ProfileElement + private SKRectI _bounds; + private SKPath? _path; + + internal RenderProfileElement(ProfileElement parent, Profile profile) : base(profile) { - private SKRectI _bounds; - private SKPath? _path; - - internal RenderProfileElement(Profile profile) : base(profile) - { - Timeline = new Timeline(); - ExpandedPropertyGroups = new List(); - LayerEffectsList = new List(); - LayerEffects = new(LayerEffectsList); - - LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; - LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; - } - - /// - /// Gets a boolean indicating whether this render element and its layers/brushes are enabled - /// - public bool Enabled { get; protected set; } - - /// - /// Gets a boolean indicating whether this render element and its layers/brushes should be enabled - /// - public abstract bool ShouldBeEnabled { get; } - - /// - /// Creates a list of all layer properties present on this render element - /// - /// A list of all layer properties present on this render element - public abstract List GetAllLayerProperties(); - - /// - /// Occurs when a layer effect has been added or removed to this render element - /// - public event EventHandler? LayerEffectsUpdated; - - /// - protected override void Dispose(bool disposing) - { - LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded; - LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved; - - foreach (BaseLayerEffect baseLayerEffect in LayerEffects) - baseLayerEffect.Dispose(); - - DisplayCondition?.Dispose(); - - base.Dispose(disposing); - } - - internal void LoadRenderElement() - { - DisplayCondition = RenderElementEntity.DisplayCondition != null - ? new DataModelConditionGroup(null, RenderElementEntity.DisplayCondition) - : new DataModelConditionGroup(null); - - Timeline = RenderElementEntity.Timeline != null - ? new Timeline(RenderElementEntity.Timeline) - : new Timeline(); - - ActivateEffects(); - } - - internal void SaveRenderElement() - { - RenderElementEntity.LayerEffects.Clear(); - foreach (BaseLayerEffect layerEffect in LayerEffects) - { - LayerEffectEntity layerEffectEntity = new() - { - Id = layerEffect.EntityId, - ProviderId = layerEffect.Descriptor?.PlaceholderFor ?? layerEffect.ProviderId, - EffectType = layerEffect.GetEffectTypeName(), - Name = layerEffect.Name, - Suspended = layerEffect.Suspended, - HasBeenRenamed = layerEffect.HasBeenRenamed, - Order = layerEffect.Order - }; - RenderElementEntity.LayerEffects.Add(layerEffectEntity); - layerEffect.BaseProperties?.ApplyToEntity(); - } - - // Conditions - RenderElementEntity.DisplayCondition = DisplayCondition?.Entity; - DisplayCondition?.Save(); - - // Timeline - RenderElementEntity.Timeline = Timeline?.Entity; - Timeline?.Save(); - } - - internal void OnLayerEffectsUpdated() - { - LayerEffectsUpdated?.Invoke(this, EventArgs.Empty); - } - - #region Timeline - - /// - /// Gets the timeline associated with this render element - /// - public Timeline Timeline { get; private set; } - - /// - /// Updates the according to the provided and current display - /// condition status - /// - public void UpdateTimeline(double deltaTime) - { - // The play mode dictates whether to stick to the main segment unless the display conditions contains events - bool stickToMainSegment = (Timeline.PlayMode == TimelinePlayMode.Repeat || Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) && DisplayConditionMet; - if (DisplayCondition != null && DisplayCondition.ContainsEvents && Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle) - stickToMainSegment = false; - - Timeline.Update(TimeSpan.FromSeconds(deltaTime), stickToMainSegment); - } - - #endregion - - #region Properties - - internal abstract RenderElementEntity RenderElementEntity { get; } - - /// - /// Gets the parent of this element - /// - public new ProfileElement? Parent - { - get => base.Parent; - internal set - { - base.Parent = value; - OnPropertyChanged(nameof(Parent)); - } - } - - /// - /// Gets the path containing all the LEDs this entity is applied to, any rendering outside the entity Path is - /// clipped. - /// - public SKPath? Path - { - get => _path; - protected set - { - SetAndNotify(ref _path, value); - // I can't really be sure about the performance impact of calling Bounds often but - // SkiaSharp calls SkiaApi.sk_path_get_bounds (Handle, &rect); which sounds expensive - Bounds = SKRectI.Round(value?.Bounds ?? SKRect.Empty); - } - } - - /// - /// The bounds of this entity - /// - public SKRectI Bounds - { - get => _bounds; - private set => SetAndNotify(ref _bounds, value); - } - - - #region Property group expansion - - internal List ExpandedPropertyGroups; - - /// - /// Determines whether the provided property group is expanded - /// - /// The property group to check - /// A boolean indicating whether the provided property group is expanded - public bool IsPropertyGroupExpanded(LayerPropertyGroup layerPropertyGroup) - { - return ExpandedPropertyGroups.Contains(layerPropertyGroup.Path); - } - - /// - /// Expands or collapses the provided property group - /// - /// The group to expand or collapse - /// Whether to expand or collapse the property group - public void SetPropertyGroupExpanded(LayerPropertyGroup layerPropertyGroup, bool expanded) - { - if (!expanded && IsPropertyGroupExpanded(layerPropertyGroup)) - ExpandedPropertyGroups.Remove(layerPropertyGroup.Path); - else if (expanded && !IsPropertyGroupExpanded(layerPropertyGroup)) - ExpandedPropertyGroups.Add(layerPropertyGroup.Path); - } - - #endregion - - #endregion - - #region State - - /// - /// Enables the render element and its brushes and effects - /// - public abstract void Disable(); - - /// - /// Disables the render element and its brushes and effects - /// - public abstract void Enable(); - - #endregion - - #region Effect management - - internal readonly List LayerEffectsList; - - /// - /// Gets a read-only collection of the layer effects on this entity - /// - public ReadOnlyCollection LayerEffects { get; } - - /// - /// Adds a the layer effect described inthe provided - /// - public void AddLayerEffect(LayerEffectDescriptor descriptor) - { - if (descriptor == null) - throw new ArgumentNullException(nameof(descriptor)); - - LayerEffectEntity entity = new() - { - Id = Guid.NewGuid(), - Suspended = false, - Order = LayerEffects.Count + 1 - }; - descriptor.CreateInstance(this, entity); - - OrderEffects(); - OnLayerEffectsUpdated(); - } - - /// - /// Removes the provided layer - /// - /// - public void RemoveLayerEffect([NotNull] BaseLayerEffect effect) - { - if (effect == null) throw new ArgumentNullException(nameof(effect)); - - // Remove the effect from the layer and dispose it - LayerEffectsList.Remove(effect); - effect.Dispose(); - - // Update the order on the remaining effects - OrderEffects(); - OnLayerEffectsUpdated(); - } - - private void OrderEffects() - { - int index = 0; - foreach (BaseLayerEffect baseLayerEffect in LayerEffects.OrderBy(e => e.Order)) - { - baseLayerEffect.Order = index + 1; - index++; - } - - LayerEffectsList.Sort((a, b) => a.Order.CompareTo(b.Order)); - } - - internal void ActivateEffects() - { - foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects) - { - // If there is a non-placeholder existing effect, skip this entity - BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.EntityId == layerEffectEntity.Id); - if (existing != null && existing.Descriptor.PlaceholderFor == null) - continue; - - LayerEffectDescriptor? descriptor = LayerEffectStore.Get(layerEffectEntity.ProviderId, layerEffectEntity.EffectType)?.LayerEffectDescriptor; - if (descriptor != null) - { - // If a descriptor is found but there is an existing placeholder, remove the placeholder - if (existing != null) - { - LayerEffectsList.Remove(existing); - existing.Dispose(); - } - - // Create an instance with the descriptor - descriptor.CreateInstance(this, layerEffectEntity); - } - else if (existing == null) - { - // If no descriptor was found and there was no existing placeholder, create a placeholder - descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffectEntity.ProviderId); - descriptor.CreateInstance(this, layerEffectEntity); - } - } - - OrderEffects(); - } - - - internal void ActivateLayerEffect(BaseLayerEffect layerEffect) - { - LayerEffectsList.Add(layerEffect); - OnLayerEffectsUpdated(); - } - - private void LayerEffectStoreOnLayerEffectRemoved(object? sender, LayerEffectStoreEvent e) - { - // If effects provided by the plugin are on the element, replace them with placeholders - List pluginEffects = LayerEffectsList.Where(ef => ef.ProviderId == e.Registration.PluginFeature.Id).ToList(); - foreach (BaseLayerEffect pluginEffect in pluginEffects) - { - LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.EntityId); - LayerEffectsList.Remove(pluginEffect); - pluginEffect.Dispose(); - - LayerEffectDescriptor descriptor = PlaceholderLayerEffectDescriptor.Create(pluginEffect.ProviderId); - descriptor.CreateInstance(this, entity); - } - } - - private void LayerEffectStoreOnLayerEffectAdded(object? sender, LayerEffectStoreEvent e) - { - if (RenderElementEntity.LayerEffects.Any(ef => ef.ProviderId == e.Registration.PluginFeature.Id)) - ActivateEffects(); - } - - #endregion - - #region Conditions - - /// - /// Gets whether the display conditions applied to this layer where met or not during last update - /// Always true if the layer has no display conditions - /// - public bool DisplayConditionMet - { - get => _displayConditionMet; - protected set => SetAndNotify(ref _displayConditionMet, value); - } - - private DataModelConditionGroup? _displayCondition; - private bool _displayConditionMet; - private bool _toggledOnByEvent = false; - - /// - /// Gets or sets the root display condition group - /// - public DataModelConditionGroup? DisplayCondition - { - get => _displayCondition; - set => SetAndNotify(ref _displayCondition, value); - } - - /// - /// Evaluates the display conditions on this element and applies any required changes to the - /// - public void UpdateDisplayCondition() - { - if (Suspended) - { - DisplayConditionMet = false; - return; - } - - if (DisplayCondition == null) - { - DisplayConditionMet = true; - return; - } - - if (Timeline.EventOverlapMode != TimeLineEventOverlapMode.Toggle) - _toggledOnByEvent = false; - - bool conditionMet = DisplayCondition.Evaluate(); - if (Parent is RenderProfileElement parent && !parent.DisplayConditionMet) - conditionMet = false; - - if (!DisplayCondition.ContainsEvents) - { - // Regular conditions reset the timeline whenever their condition is met and was not met before that - if (conditionMet && !DisplayConditionMet && Timeline.IsFinished) - Timeline.JumpToStart(); - // If regular conditions are no longer met, jump to the end segment if stop mode requires it - if (!conditionMet && Timeline.StopMode == TimelineStopMode.SkipToEnd) - Timeline.JumpToEndSegment(); - } - else if (conditionMet) - { - if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle) - { - _toggledOnByEvent = !_toggledOnByEvent; - if (_toggledOnByEvent) - Timeline.JumpToStart(); - } - else - { - // Event conditions reset if the timeline finished - if (Timeline.IsFinished) - { - Timeline.JumpToStart(); - } - // and otherwise apply their overlap mode - else - { - if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Restart) - Timeline.JumpToStart(); - else if (Timeline.EventOverlapMode == TimeLineEventOverlapMode.Copy) - Timeline.AddExtraTimeline(); - // The third option is ignore which is handled below: - - // done - } - } - } - - DisplayConditionMet = Timeline.EventOverlapMode == TimeLineEventOverlapMode.Toggle - ? _toggledOnByEvent - : conditionMet; - } - - #endregion + _layerEffects = new List(); + _displayCondition = new AlwaysOnCondition(this); + Timeline = new Timeline(); + LayerEffects = new ReadOnlyCollection(_layerEffects); + Parent = parent ?? throw new ArgumentNullException(nameof(parent)); + + LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; + LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; } + + /// + /// Gets a boolean indicating whether this render element and its layers/brushes are enabled + /// + public bool Enabled { get; protected set; } + + /// + /// Gets a boolean indicating whether this render element and its layers/brushes should be enabled + /// + public abstract bool ShouldBeEnabled { get; } + + /// + /// Creates a list of all layer properties present on this render element + /// + /// A list of all layer properties present on this render element + public abstract List GetAllLayerProperties(); + + /// + /// Occurs when a layer effect has been added or removed to this render element + /// + public event EventHandler? LayerEffectsUpdated; + + /// + /// Overrides the main timeline to the specified time and clears any extra time lines + /// + /// The position to set the timeline to + public abstract void OverrideTimelineAndApply(TimeSpan position); + + /// + protected override void Dispose(bool disposing) + { + LayerEffectStore.LayerEffectAdded -= LayerEffectStoreOnLayerEffectAdded; + LayerEffectStore.LayerEffectRemoved -= LayerEffectStoreOnLayerEffectRemoved; + + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + baseLayerEffect.Dispose(); + + if (DisplayCondition is IDisposable disposable) + disposable.Dispose(); + + base.Dispose(disposing); + } + + internal void LoadRenderElement() + { + Timeline = RenderElementEntity.Timeline != null + ? new Timeline(RenderElementEntity.Timeline) + : new Timeline(); + + DisplayCondition = RenderElementEntity.DisplayCondition switch + { + AlwaysOnConditionEntity entity => new AlwaysOnCondition(entity, this), + PlayOnceConditionEntity entity => new PlayOnceCondition(entity, this), + StaticConditionEntity entity => new StaticCondition(entity, this), + EventConditionEntity entity => new EventCondition(entity, this), + _ => DisplayCondition + }; + + LoadLayerEffects(); + } + + internal void SaveRenderElement() + { + RenderElementEntity.LayerEffects.Clear(); + foreach (BaseLayerEffect baseLayerEffect in LayerEffects) + { + baseLayerEffect.Save(); + RenderElementEntity.LayerEffects.Add(baseLayerEffect.LayerEffectEntity); + } + + // Condition + DisplayCondition?.Save(); + RenderElementEntity.DisplayCondition = DisplayCondition?.Entity; + + // Timeline + RenderElementEntity.Timeline = Timeline?.Entity; + Timeline?.Save(); + } + + internal void LoadNodeScript() + { + if (DisplayCondition is INodeScriptCondition scriptCondition) + scriptCondition.LoadNodeScript(); + + foreach (ILayerProperty layerProperty in GetAllLayerProperties()) + layerProperty.BaseDataBinding.LoadNodeScript(); + } + + private void OnLayerEffectsUpdated() + { + LayerEffectsUpdated?.Invoke(this, EventArgs.Empty); + } + + #region Timeline + + /// + /// Gets the timeline associated with this render element + /// + public Timeline Timeline { get; private set; } + + /// + /// Updates the according to the provided and current display + /// condition + /// + protected void UpdateTimeline(double deltaTime) + { + DisplayCondition.UpdateTimeline(deltaTime); + } + + #endregion + + #region Properties + + internal abstract RenderElementEntity RenderElementEntity { get; } + + /// + /// Gets the parent of this element + /// + public new ProfileElement Parent + { + get => base.Parent!; + internal set + { + base.Parent = value; + OnPropertyChanged(nameof(Parent)); + } + } + + /// + /// Gets the path containing all the LEDs this entity is applied to, any rendering outside the entity Path is + /// clipped. + /// + public SKPath? Path + { + get => _path; + protected set + { + SetAndNotify(ref _path, value); + // I can't really be sure about the performance impact of calling Bounds often but + // SkiaSharp calls SkiaApi.sk_path_get_bounds (Handle, &rect); which sounds expensive + Bounds = SKRectI.Round(value?.Bounds ?? SKRect.Empty); + } + } + + /// + /// The bounds of this entity + /// + public SKRectI Bounds + { + get => _bounds; + private set => SetAndNotify(ref _bounds, value); + } + + #endregion + + #region State + + /// + /// Enables the render element and its brushes and effects + /// + public abstract void Disable(); + + /// + /// Disables the render element and its brushes and effects + /// + public abstract void Enable(); + + #endregion + + #region Effect management + + private readonly List _layerEffects; + + /// + /// Gets a read-only collection of the layer effects on this entity + /// + public ReadOnlyCollection LayerEffects { get; } + + /// + /// Adds a the provided layer effect to the render profile element + /// + /// The effect to add. + public void AddLayerEffect(BaseLayerEffect layerEffect) + { + if (layerEffect == null) + throw new ArgumentNullException(nameof(layerEffect)); + + // Ensure something needs to be done + if (_layerEffects.Contains(layerEffect)) + return; + + // Make sure the layer effect is tied to this element + layerEffect.ProfileElement = this; + _layerEffects.Add(layerEffect); + + // Update the order on the effects + OrderEffects(); + OnLayerEffectsUpdated(); + } + + /// + /// Removes the provided layer effect. + /// + /// The effect to remove. + public void RemoveLayerEffect(BaseLayerEffect layerEffect) + { + if (layerEffect == null) + throw new ArgumentNullException(nameof(layerEffect)); + + // Ensure something needs to be done + if (!_layerEffects.Contains(layerEffect)) + return; + + // Remove the effect from the layer + _layerEffects.Remove(layerEffect); + + // Update the order on the remaining effects + OrderEffects(); + OnLayerEffectsUpdated(); + } + + private void LoadLayerEffects() + { + foreach (BaseLayerEffect baseLayerEffect in _layerEffects) + baseLayerEffect.Dispose(); + _layerEffects.Clear(); + + foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects.OrderBy(e => e.Order)) + LoadLayerEffect(layerEffectEntity); + } + + private void LoadLayerEffect(LayerEffectEntity layerEffectEntity) + { + LayerEffectDescriptor? descriptor = LayerEffectStore.Get(layerEffectEntity.ProviderId, layerEffectEntity.EffectType)?.LayerEffectDescriptor; + BaseLayerEffect layerEffect; + // Create an instance with the descriptor + if (descriptor != null) + { + layerEffect = descriptor.CreateInstance(this, layerEffectEntity); + } + // If no descriptor was found and there was no existing placeholder, create a placeholder + else + { + descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffectEntity.ProviderId); + layerEffect = descriptor.CreateInstance(this, layerEffectEntity); + } + + _layerEffects.Add(layerEffect); + } + + private void ReplaceLayerEffectWithPlaceholder(BaseLayerEffect layerEffect) + { + int index = _layerEffects.IndexOf(layerEffect); + if (index == -1) + return; + + LayerEffectDescriptor descriptor = PlaceholderLayerEffectDescriptor.Create(layerEffect.ProviderId); + BaseLayerEffect placeholder = descriptor.CreateInstance(this, layerEffect.LayerEffectEntity); + _layerEffects[index] = placeholder; + layerEffect.Dispose(); + OnLayerEffectsUpdated(); + } + + private void ReplacePlaceholderWithLayerEffect(PlaceholderLayerEffect placeholder) + { + int index = _layerEffects.IndexOf(placeholder); + if (index == -1) + return; + + LayerEffectDescriptor? descriptor = LayerEffectStore.Get(placeholder.OriginalEntity.ProviderId, placeholder.PlaceholderFor)?.LayerEffectDescriptor; + if (descriptor == null) + throw new ArtemisCoreException("Can't replace a placeholder effect because the real effect isn't available."); + + BaseLayerEffect layerEffect = descriptor.CreateInstance(this, placeholder.OriginalEntity); + _layerEffects[index] = layerEffect; + placeholder.Dispose(); + OnLayerEffectsUpdated(); + } + + private void OrderEffects() + { + int index = 0; + foreach (BaseLayerEffect baseLayerEffect in LayerEffects.OrderBy(e => e.Order)) + { + baseLayerEffect.Order = index + 1; + index++; + } + + _layerEffects.Sort((a, b) => a.Order.CompareTo(b.Order)); + } + + private void LayerEffectStoreOnLayerEffectRemoved(object? sender, LayerEffectStoreEvent e) + { + // Find effects that just got disabled and replace them with placeholders + List affectedLayerEffects = _layerEffects.Where(ef => ef.ProviderId == e.Registration.PluginFeature.Id).ToList(); + + if (!affectedLayerEffects.Any()) + return; + + foreach (BaseLayerEffect baseLayerEffect in affectedLayerEffects) + ReplaceLayerEffectWithPlaceholder(baseLayerEffect); + OnLayerEffectsUpdated(); + } + + private void LayerEffectStoreOnLayerEffectAdded(object? sender, LayerEffectStoreEvent e) + { + // Find placeholders that just got enabled and replace them with real effects + List affectedPlaceholders = LayerEffects + .Where(l => l is PlaceholderLayerEffect ph && ph.OriginalEntity.ProviderId == e.Registration.PluginFeature.Id) + .Cast() + .ToList(); + + if (!affectedPlaceholders.Any()) + return; + + foreach (PlaceholderLayerEffect placeholderLayerEffect in affectedPlaceholders) + ReplacePlaceholderWithLayerEffect(placeholderLayerEffect); + OnLayerEffectsUpdated(); + } + + #endregion + + #region Conditions + + /// + /// Gets whether the display conditions applied to this layer where met or not during last update + /// Always true if the layer has no display conditions + /// + public bool DisplayConditionMet + { + get => _displayConditionMet; + private set => SetAndNotify(ref _displayConditionMet, value); + } + + private bool _displayConditionMet; + + /// + /// Gets or sets the display condition used to determine whether this element is active or not + /// + public ICondition DisplayCondition + { + get => _displayCondition; + set => SetAndNotify(ref _displayCondition, value); + } + + private ICondition _displayCondition; + + /// + /// Evaluates the display conditions on this element and applies any required changes to the + /// + public void UpdateDisplayCondition() + { + if (Suspended) + { + DisplayConditionMet = false; + return; + } + + DisplayCondition.Update(); + DisplayConditionMet = DisplayCondition.IsMet; + } + + #endregion } diff --git a/src/Artemis.Core/Models/Profile/Timeline.cs b/src/Artemis.Core/Models/Profile/Timeline.cs index 5e29dc86e..016ea1ecb 100644 --- a/src/Artemis.Core/Models/Profile/Timeline.cs +++ b/src/Artemis.Core/Models/Profile/Timeline.cs @@ -2,528 +2,393 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Threading; using Artemis.Storage.Entities.Profile; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a timeline used by profile elements +/// +public class Timeline : CorePropertyChanged, IStorageModel { + private readonly object _lock = new(); + /// - /// Represents a timeline used by profile elements + /// Creates a new instance of the class /// - public class Timeline : CorePropertyChanged, IStorageModel + public Timeline() { - private const int MaxExtraTimelines = 15; - private readonly object _lock = new (); + Entity = new TimelineEntity(); + MainSegmentLength = TimeSpan.FromSeconds(5); - /// - /// Creates a new instance of the class - /// - public Timeline() + Save(); + } + + internal Timeline(TimelineEntity entity) + { + Entity = entity; + + Load(); + } + + /// + public override string ToString() + { + return $"Progress: {Position}/{Length} - delta: {Delta}"; + } + + #region Properties + + private TimeSpan _position; + private TimeSpan _lastDelta; + + private TimeSpan _startSegmentLength; + private TimeSpan _mainSegmentLength; + private TimeSpan _endSegmentLength; + private TimeSpan _lastOverride; + + /// + /// Gets the current position of the timeline + /// + public TimeSpan Position + { + get => _position; + private set => SetAndNotify(ref _position, value); + } + + /// + /// Gets the cumulative delta of all calls to that took place after the last call to + /// + /// + public TimeSpan Delta + { + get => _lastDelta; + private set => SetAndNotify(ref _lastDelta, value); + } + + + + /// + /// Gets a boolean indicating whether the timeline has finished its run + /// + public bool IsFinished => Position > Length; + + /// + /// Gets a boolean indicating whether the timeline progress has been overridden + /// + public bool IsOverridden { get; private set; } + + #region Segments + + /// + /// Gets the total length of this timeline + /// + public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength; + + /// + /// Gets or sets the length of the start segment + /// + public TimeSpan StartSegmentLength + { + get => _startSegmentLength; + set { - Entity = new TimelineEntity(); - MainSegmentLength = TimeSpan.FromSeconds(5); - - _extraTimelines = new List(); - ExtraTimelines = new(_extraTimelines); - - Save(); - } - - internal Timeline(TimelineEntity entity) - { - Entity = entity; - _extraTimelines = new List(); - ExtraTimelines = new(_extraTimelines); - - Load(); - } - - private Timeline(Timeline parent) - { - Entity = new TimelineEntity(); - Parent = parent; - StartSegmentLength = Parent.StartSegmentLength; - MainSegmentLength = Parent.MainSegmentLength; - EndSegmentLength = Parent.EndSegmentLength; - - _extraTimelines = new List(); - ExtraTimelines = new(_extraTimelines); - } - - /// - public override string ToString() - { - return $"Progress: {Position}/{Length} - delta: {Delta}"; - } - - #region Extra timelines - - /// - /// Adds an extra timeline to this timeline - /// - public void AddExtraTimeline() - { - _extraTimelines.Add(new Timeline(this)); - if (_extraTimelines.Count > MaxExtraTimelines) - _extraTimelines.RemoveAt(0); - } - - /// - /// Removes all extra timelines from this timeline - /// - public void ClearExtraTimelines() - { - _extraTimelines.Clear(); - } - - #endregion - - #region Properties - - private TimeSpan _position; - private TimeSpan _lastDelta; - private TimeLineEventOverlapMode _eventOverlapMode; - private TimelinePlayMode _playMode; - private TimelineStopMode _stopMode; - private readonly List _extraTimelines; - private TimeSpan _startSegmentLength; - private TimeSpan _mainSegmentLength; - private TimeSpan _endSegmentLength; - - /// - /// Gets the parent this timeline is an extra timeline of - /// - public Timeline? Parent { get; } - - /// - /// Gets the current position of the timeline - /// - public TimeSpan Position - { - get => _position; - private set => SetAndNotify(ref _position, value); - } - - /// - /// Gets the cumulative delta of all calls to that took place after the last call to - /// - /// - /// Note: If this is an extra timeline is always equal to - /// - /// - public TimeSpan Delta - { - get => Parent == null ? _lastDelta : DeltaToParent; - private set => SetAndNotify(ref _lastDelta, value); - } - - /// - /// Gets the delta to this timeline's - /// - public TimeSpan DeltaToParent => Parent != null ? Position - Parent.Position : TimeSpan.Zero; - - /// - /// Gets or sets the mode in which the render element starts its timeline when display conditions are met - /// - public TimelinePlayMode PlayMode - { - get => _playMode; - set => SetAndNotify(ref _playMode, value); - } - - /// - /// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met - /// - public TimelineStopMode StopMode - { - get => _stopMode; - set => SetAndNotify(ref _stopMode, value); - } - - /// - /// Gets or sets the mode in which the render element responds to display condition events being fired before the - /// timeline finished - /// - public TimeLineEventOverlapMode EventOverlapMode - { - get => _eventOverlapMode; - set => SetAndNotify(ref _eventOverlapMode, value); - } - - /// - /// Gets a list of extra copies of the timeline applied to this timeline - /// - public ReadOnlyCollection ExtraTimelines { get; } - - /// - /// Gets a boolean indicating whether the timeline has finished its run - /// - public bool IsFinished => Position > Length && !ExtraTimelines.Any(); - - /// - /// Gets a boolean indicating whether the timeline progress has been overridden - /// - public bool IsOverridden { get; private set; } - - #region Segments - - /// - /// Gets the total length of this timeline - /// - public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength; - - /// - /// Gets or sets the length of the start segment - /// - public TimeSpan StartSegmentLength - { - get => _startSegmentLength; - set - { - if (SetAndNotify(ref _startSegmentLength, value)) - NotifySegmentShiftAt(TimelineSegment.Start, false); - } - } - - /// - /// Gets or sets the length of the main segment - /// - public TimeSpan MainSegmentLength - { - get => _mainSegmentLength; - set - { - if (SetAndNotify(ref _mainSegmentLength, value)) - NotifySegmentShiftAt(TimelineSegment.Main, false); - } - } - - /// - /// Gets or sets the length of the end segment - /// - public TimeSpan EndSegmentLength - { - get => _endSegmentLength; - set - { - if (SetAndNotify(ref _endSegmentLength, value)) - NotifySegmentShiftAt(TimelineSegment.End, false); - } - } - - /// - /// Gets or sets the start position of the main segment - /// - public TimeSpan MainSegmentStartPosition - { - get => StartSegmentEndPosition; - set - { - StartSegmentEndPosition = value; - NotifySegmentShiftAt(TimelineSegment.Main, true); - } - } - - /// - /// Gets or sets the end position of the end segment - /// - public TimeSpan EndSegmentStartPosition - { - get => MainSegmentEndPosition; - set - { - MainSegmentEndPosition = value; - NotifySegmentShiftAt(TimelineSegment.End, true); - } - } - - /// - /// Gets or sets the end position of the start segment - /// - public TimeSpan StartSegmentEndPosition - { - get => StartSegmentLength; - set - { - StartSegmentLength = value; + if (SetAndNotify(ref _startSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.Start, false); - } } + } - /// - /// Gets or sets the end position of the main segment - /// - public TimeSpan MainSegmentEndPosition + /// + /// Gets or sets the length of the main segment + /// + public TimeSpan MainSegmentLength + { + get => _mainSegmentLength; + set { - get => StartSegmentEndPosition + MainSegmentLength; - set - { - MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero; + if (SetAndNotify(ref _mainSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.Main, false); - } } + } - /// - /// Gets or sets the end position of the end segment - /// - public TimeSpan EndSegmentEndPosition + /// + /// Gets or sets the length of the end segment + /// + public TimeSpan EndSegmentLength + { + get => _endSegmentLength; + set { - get => MainSegmentEndPosition + EndSegmentLength; - set - { - EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero; + if (SetAndNotify(ref _endSegmentLength, value)) NotifySegmentShiftAt(TimelineSegment.End, false); - } } - - internal TimelineEntity Entity { get; set; } - - /// - /// Notifies the right segments in a way that I don't have to think about it - /// - /// The segment that was updated - /// Whether the start point of the was updated - private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated) - { - if (segment <= TimelineSegment.End) - { - if (startUpdated || segment < TimelineSegment.End) - OnPropertyChanged(nameof(EndSegmentStartPosition)); - OnPropertyChanged(nameof(EndSegmentEndPosition)); - } - - if (segment <= TimelineSegment.Main) - { - if (startUpdated || segment < TimelineSegment.Main) - OnPropertyChanged(nameof(MainSegmentStartPosition)); - OnPropertyChanged(nameof(MainSegmentEndPosition)); - } - - if (segment <= TimelineSegment.Start) OnPropertyChanged(nameof(StartSegmentEndPosition)); - - OnPropertyChanged(nameof(Length)); - } - - #endregion - - #endregion - - #region Updating - - private TimeSpan _lastOverridePosition; - - /// - /// Updates the timeline, applying the provided to the - /// - /// The amount of time to apply to the position - /// Whether to stick to the main segment, wrapping around if needed - public void Update(TimeSpan delta, bool stickToMainSegment) - { - lock (_lock) - { - Delta += delta; - Position += delta; - - IsOverridden = false; - _lastOverridePosition = Position; - - if (stickToMainSegment && Position > MainSegmentEndPosition) - { - // If the main segment has no length, simply stick to the start of the segment - if (MainSegmentLength == TimeSpan.Zero) - Position = MainSegmentStartPosition; - // Ensure wrapping back around retains the delta time - else - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); - } - - _extraTimelines.RemoveAll(t => t.IsFinished); - foreach (Timeline extraTimeline in _extraTimelines) - extraTimeline.Update(delta, false); - } - } - - /// - /// Moves the position of the timeline backwards to the very start of the timeline - /// - public void JumpToStart() - { - lock (_lock) - { - if (Position == TimeSpan.Zero) - return; - - Delta = TimeSpan.Zero - Position; - Position = TimeSpan.Zero; - } - } - - /// - /// Moves the position of the timeline forwards to the beginning of the end segment - /// - public void JumpToEndSegment() - { - lock (_lock) - { - if (Position >= EndSegmentStartPosition) - return; - - Delta = EndSegmentStartPosition - Position; - Position = EndSegmentStartPosition; - } - } - - /// - /// Moves the position of the timeline forwards to the very end of the timeline - /// - public void JumpToEnd() - { - lock (_lock) - { - if (Position >= EndSegmentEndPosition) - return; - - Delta = EndSegmentEndPosition - Position; - Position = EndSegmentEndPosition; - } - } - - /// - /// Overrides the to the specified time and clears any extra time lines - /// - /// The position to set the timeline to - /// Whether to stick to the main segment, wrapping around if needed - public void Override(TimeSpan position, bool stickToMainSegment) - { - lock (_lock) - { - Delta += position - _lastOverridePosition; - Position = position; - - IsOverridden = true; - _lastOverridePosition = position; - - if (stickToMainSegment && Position >= MainSegmentStartPosition) - { - bool atSegmentStart = Position == MainSegmentStartPosition; - if (MainSegmentLength > TimeSpan.Zero) - { - Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); - // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor - // is actually at the start of the segment - if (Position == MainSegmentStartPosition && !atSegmentStart) - Position = MainSegmentEndPosition; - } - else - Position = MainSegmentStartPosition; - } - - _extraTimelines.Clear(); - } - } - - /// - /// Sets the to - /// - public void ClearDelta() - { - lock (_lock) - { - Delta = TimeSpan.Zero; - } - } - - #endregion - - #region Storage - - /// - public void Load() - { - StartSegmentLength = Entity.StartSegmentLength; - MainSegmentLength = Entity.MainSegmentLength; - EndSegmentLength = Entity.EndSegmentLength; - PlayMode = (TimelinePlayMode) Entity.PlayMode; - StopMode = (TimelineStopMode) Entity.StopMode; - EventOverlapMode = (TimeLineEventOverlapMode) Entity.EventOverlapMode; - } - - /// - public void Save() - { - Entity.StartSegmentLength = StartSegmentLength; - Entity.MainSegmentLength = MainSegmentLength; - Entity.EndSegmentLength = EndSegmentLength; - Entity.PlayMode = (int) PlayMode; - Entity.StopMode = (int) StopMode; - Entity.EventOverlapMode = (int) EventOverlapMode; - } - - #endregion - } - - internal enum TimelineSegment - { - Start, - Main, - End } /// - /// Represents a mode for render elements to start their timeline when display conditions are met + /// Gets or sets the start position of the main segment /// - public enum TimelinePlayMode + public TimeSpan MainSegmentStartPosition { - /// - /// Continue repeating the main segment of the timeline while the condition is met - /// - Repeat, - - /// - /// Only play the timeline once when the condition is met - /// - Once + get => StartSegmentEndPosition; + set + { + StartSegmentEndPosition = value; + NotifySegmentShiftAt(TimelineSegment.Main, true); + } } /// - /// Represents a mode for render elements to stop their timeline when display conditions are no longer met + /// Gets or sets the end position of the end segment /// - public enum TimelineStopMode + public TimeSpan EndSegmentStartPosition { - /// - /// When conditions are no longer met, finish the the current run of the main timeline - /// - Finish, - - /// - /// When conditions are no longer met, skip to the end segment of the timeline - /// - SkipToEnd + get => MainSegmentEndPosition; + set + { + MainSegmentEndPosition = value; + NotifySegmentShiftAt(TimelineSegment.End, true); + } } /// - /// Represents a mode for render elements to start their timeline when display conditions events are fired + /// Gets or sets the end position of the start segment /// - public enum TimeLineEventOverlapMode + public TimeSpan StartSegmentEndPosition { - /// - /// Stop the current run and restart the timeline - /// - Restart, - - /// - /// Ignore subsequent event fires until the timeline finishes - /// - Ignore, - - /// - /// Play another copy of the timeline on top of the current run - /// - Copy, - - /// - /// Repeat the timeline until the event fires again - /// - Toggle + get => StartSegmentLength; + set + { + StartSegmentLength = value; + NotifySegmentShiftAt(TimelineSegment.Start, false); + } } -} \ No newline at end of file + + /// + /// Gets or sets the end position of the main segment + /// + public TimeSpan MainSegmentEndPosition + { + get => StartSegmentEndPosition + MainSegmentLength; + set + { + MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero; + NotifySegmentShiftAt(TimelineSegment.Main, false); + } + } + + /// + /// Gets or sets the end position of the end segment + /// + public TimeSpan EndSegmentEndPosition + { + get => MainSegmentEndPosition + EndSegmentLength; + set + { + EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero; + NotifySegmentShiftAt(TimelineSegment.End, false); + } + } + + internal TimelineEntity Entity { get; set; } + + /// + /// Notifies the right segments in a way that I don't have to think about it + /// + /// The segment that was updated + /// Whether the start point of the was updated + private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated) + { + if (segment <= TimelineSegment.End) + { + if (startUpdated || segment < TimelineSegment.End) + OnPropertyChanged(nameof(EndSegmentStartPosition)); + OnPropertyChanged(nameof(EndSegmentEndPosition)); + } + + if (segment <= TimelineSegment.Main) + { + if (startUpdated || segment < TimelineSegment.Main) + OnPropertyChanged(nameof(MainSegmentStartPosition)); + OnPropertyChanged(nameof(MainSegmentEndPosition)); + } + + if (segment <= TimelineSegment.Start) + OnPropertyChanged(nameof(StartSegmentEndPosition)); + + OnPropertyChanged(nameof(Length)); + OnTimelineChanged(); + } + + /// + /// Occurs when changes have been made to any of the segments of the timeline. + /// + public event EventHandler? TimelineChanged; + + private void OnTimelineChanged() + { + TimelineChanged?.Invoke(this, EventArgs.Empty); + } + + #endregion + + #endregion + + #region Updating + + /// + /// Updates the timeline, applying the provided to the + /// + /// The amount of time to apply to the position + /// Whether to stick to the main segment, wrapping around if needed + public void Update(TimeSpan delta, bool stickToMainSegment) + { + lock (_lock) + { + if (IsOverridden) + throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first."); + + Delta += delta; + Position += delta; + + if (!stickToMainSegment || Position <= MainSegmentEndPosition) + return; + + // If the main segment has no length, simply stick to the start of the segment + if (MainSegmentLength == TimeSpan.Zero) + Position = MainSegmentStartPosition; + // Ensure wrapping back around retains the delta time + else + Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); + } + } + + /// + /// Moves the position of the timeline backwards to the very start of the timeline + /// + public void JumpToStart() + { + lock (_lock) + { + if (Position == TimeSpan.Zero) + return; + + Delta = TimeSpan.Zero - Position; + Position = TimeSpan.Zero; + } + } + + /// + /// Moves the position of the timeline forwards to the beginning of the end segment + /// + public void JumpToEndSegment() + { + lock (_lock) + { + if (Position >= EndSegmentStartPosition) + return; + + Delta = EndSegmentStartPosition - Position; + Position = EndSegmentStartPosition; + } + } + + /// + /// Moves the position of the timeline forwards to the very end of the timeline + /// + public void JumpToEnd() + { + lock (_lock) + { + if (Position >= EndSegmentEndPosition) + return; + + Delta = EndSegmentEndPosition - Position; + Position = EndSegmentEndPosition; + } + } + + /// + /// Overrides the to the specified time + /// + /// The position to set the timeline to + /// Whether to stick to the main segment, wrapping around if needed + internal void Override(TimeSpan position, bool stickToMainSegment) + { + lock (_lock) + { + if (_lastOverride == TimeSpan.Zero) + Delta = Position - position; + else + Delta = position - _lastOverride; + + Position = position; + IsOverridden = true; + _lastOverride = position; + + if (!stickToMainSegment || Position < MainSegmentStartPosition) + return; + + bool atSegmentStart = Position >= MainSegmentStartPosition; + if (MainSegmentLength > TimeSpan.Zero) + { + Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds); + // If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor + // is actually at the start of the segment + if (Position == MainSegmentStartPosition && !atSegmentStart) + Position = MainSegmentEndPosition; + } + else + { + Position = MainSegmentStartPosition; + } + } + } + + internal void ClearOverride() + { + IsOverridden = false; + _lastOverride = TimeSpan.Zero; + } + + /// + /// Sets the to + /// + public void ClearDelta() + { + lock (_lock) + { + Delta = TimeSpan.Zero; + } + } + + #endregion + + #region Storage + + /// + public void Load() + { + StartSegmentLength = Entity.StartSegmentLength; + MainSegmentLength = Entity.MainSegmentLength; + EndSegmentLength = Entity.EndSegmentLength; + + JumpToEnd(); + } + + /// + public void Save() + { + Entity.StartSegmentLength = StartSegmentLength; + Entity.MainSegmentLength = MainSegmentLength; + Entity.EndSegmentLength = EndSegmentLength; + } + + #endregion +} + +internal enum TimelineSegment +{ + Start, + Main, + End +} diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationHotkey.cs b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs similarity index 85% rename from src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationHotkey.cs rename to src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs index 0a735f875..aaa5485e6 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationHotkey.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/Hotkey.cs @@ -6,18 +6,18 @@ namespace Artemis.Core /// /// Represents a key or combination of keys that changes the suspension status of a /// - public class ProfileConfigurationHotkey : CorePropertyChanged, IStorageModel + public class Hotkey : CorePropertyChanged, IStorageModel { /// - /// Creates a new instance of + /// Creates a new instance of /// - public ProfileConfigurationHotkey() + public Hotkey() { Entity = new ProfileConfigurationHotkeyEntity(); } - internal ProfileConfigurationHotkey(ProfileConfigurationHotkeyEntity entity) + internal Hotkey(ProfileConfigurationHotkeyEntity entity) { Entity = entity; Load(); diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index 21e1a3157..f993e3ae5 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -9,17 +9,23 @@ namespace Artemis.Core /// /// Represents the configuration of a profile, contained in a /// - public class ProfileConfiguration : CorePropertyChanged, IStorageModel, IDisposable + public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable { - private ProfileCategory _category; private bool _disposed; - private bool _isMissingModule; - private bool _isSuspended; - private Module? _module; private string _name; private int _order; + private bool _isSuspended; + private bool _isMissingModule; + private ProfileCategory _category; + private ProfileConfigurationHotkeyMode _hotkeyMode; + private Hotkey? _enableHotkey; + private Hotkey? _disableHotkey; + private ActivationBehaviour _activationBehaviour; + private bool _activationConditionMet; + private bool _isBeingEdited; private Profile? _profile; + private Module? _module; internal ProfileConfiguration(ProfileCategory category, string name, string icon) { @@ -27,7 +33,9 @@ namespace Artemis.Core _category = category; Entity = new ProfileConfigurationEntity(); - Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon}; + Icon = new ProfileConfigurationIcon(Entity); + Icon.SetIconByName(icon); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); } internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity) @@ -38,6 +46,8 @@ namespace Artemis.Core Entity = entity; Icon = new ProfileConfigurationIcon(Entity); + ActivationCondition = new NodeScript("Activate profile", "Whether or not the profile should be active", this); + Load(); } @@ -87,31 +97,60 @@ namespace Artemis.Core internal set => SetAndNotify(ref _category, value); } - /// - /// Gets the icon configuration - /// - public ProfileConfigurationIcon Icon { get; } - /// /// Gets or sets the used to determine hotkey behaviour /// - public ProfileConfigurationHotkeyMode HotkeyMode { get; set; } + public ProfileConfigurationHotkeyMode HotkeyMode + { + get => _hotkeyMode; + set => SetAndNotify(ref _hotkeyMode, value); + } /// /// Gets or sets the hotkey used to enable or toggle the profile /// - public ProfileConfigurationHotkey? EnableHotkey { get; set; } + public Hotkey? EnableHotkey + { + get => _enableHotkey; + set => SetAndNotify(ref _enableHotkey, value); + } /// /// Gets or sets the hotkey used to disable the profile /// - public ProfileConfigurationHotkey? DisableHotkey { get; set; } + public Hotkey? DisableHotkey + { + get => _disableHotkey; + set => SetAndNotify(ref _disableHotkey, value); + } /// - /// Gets the ID of the profile of this profile configuration + /// Gets or sets the behaviour of when this profile is activated /// - public Guid ProfileId => Entity.ProfileId; + public ActivationBehaviour ActivationBehaviour + { + get => _activationBehaviour; + set => SetAndNotify(ref _activationBehaviour, value); + } + /// + /// Gets a boolean indicating whether the activation conditions where met during the last call + /// + public bool ActivationConditionMet + { + get => _activationConditionMet; + private set => SetAndNotify(ref _activationConditionMet, value); + } + + /// + /// Gets or sets a boolean indicating whether this profile configuration is being edited + /// + public bool IsBeingEdited + { + get => _isBeingEdited; + set => SetAndNotify(ref _isBeingEdited, value); + } + /// /// Gets the profile of this profile configuration /// @@ -121,17 +160,6 @@ namespace Artemis.Core internal set => SetAndNotify(ref _profile, value); } - /// - /// Gets or sets the behaviour of when this profile is activated - /// - public ActivationBehaviour ActivationBehaviour { get; set; } - - /// - /// Gets the data model condition that must evaluate to for this profile to be activated - /// alongside any activation requirements of the , if set - /// - public DataModelConditionGroup? ActivationCondition { get; set; } - /// /// Gets or sets the module this profile uses /// @@ -140,25 +168,31 @@ namespace Artemis.Core get => _module; set { - _module = value; + SetAndNotify(ref _module, value); IsMissingModule = false; } } - + /// - /// Gets a boolean indicating whether the activation conditions where met during the last call + /// Gets the icon configuration /// - public bool ActivationConditionMet { get; private set; } - + public ProfileConfigurationIcon Icon { get; } + /// - /// Gets or sets a boolean indicating whether this profile configuration is being edited + /// Gets the data model condition that must evaluate to for this profile to be activated + /// alongside any activation requirements of the , if set /// - public bool IsBeingEdited { get; set; } + public NodeScript ActivationCondition { get; } /// /// Gets the entity used by this profile config /// public ProfileConfigurationEntity Entity { get; } + + /// + /// Gets the ID of the profile of this profile configuration + /// + public Guid ProfileId => Entity.ProfileId; /// /// Updates this configurations activation condition status @@ -168,7 +202,13 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("ProfileConfiguration"); - ActivationConditionMet = ActivationCondition == null || ActivationCondition.Evaluate(); + if (!ActivationCondition.ExitNodeConnected) + ActivationConditionMet = true; + else + { + ActivationCondition.Run(); + ActivationConditionMet = ActivationCondition.Result; + } } /// @@ -210,7 +250,7 @@ namespace Artemis.Core public void Dispose() { _disposed = true; - ActivationCondition?.Dispose(); + ActivationCondition.Dispose(); } #endregion @@ -231,10 +271,11 @@ namespace Artemis.Core Icon.Load(); - ActivationCondition = Entity.ActivationCondition != null ? new DataModelConditionGroup(null, Entity.ActivationCondition) : null; + if (Entity.ActivationCondition != null) + ActivationCondition.LoadFromEntity(Entity.ActivationCondition); - EnableHotkey = Entity.EnableHotkey != null ? new ProfileConfigurationHotkey(Entity.EnableHotkey) : null; - DisableHotkey = Entity.DisableHotkey != null ? new ProfileConfigurationHotkey(Entity.DisableHotkey) : null; + EnableHotkey = Entity.EnableHotkey != null ? new Hotkey(Entity.EnableHotkey) : null; + DisableHotkey = Entity.DisableHotkey != null ? new Hotkey(Entity.DisableHotkey) : null; } /// @@ -252,8 +293,8 @@ namespace Artemis.Core Icon.Save(); - ActivationCondition?.Save(); - Entity.ActivationCondition = ActivationCondition?.Entity; + ActivationCondition.Save(); + Entity.ActivationCondition = ActivationCondition.Entity; EnableHotkey?.Save(); Entity.EnableHotkey = EnableHotkey?.Entity; @@ -265,6 +306,13 @@ namespace Artemis.Core } #endregion + + #region Overrides of BreakableModel + + /// + public override string BrokenDisplayName => "Profile Configuration"; + + #endregion } /// diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs index c899cdc01..20ea8f088 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using Artemis.Core.JsonConverters; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -8,7 +9,7 @@ namespace Artemis.Core /// /// A model that can be used to serialize a profile configuration, it's profile and it's icon /// - public class ProfileConfigurationExportModel + public class ProfileConfigurationExportModel : IDisposable { /// /// Gets or sets the storage entity of the profile configuration @@ -26,5 +27,11 @@ namespace Artemis.Core /// [JsonConverter(typeof(StreamConverter))] public Stream? ProfileImage { get; set; } + + /// + public void Dispose() + { + ProfileImage?.Dispose(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs index b188852bf..d76bf7495 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.IO; using Artemis.Storage.Entities.Profile; @@ -10,9 +11,11 @@ namespace Artemis.Core public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel { private readonly ProfileConfigurationEntity _entity; - private Stream? _fileIcon; + private Stream? _iconStream; private ProfileConfigurationIconType _iconType; - private string? _materialIcon; + private string? _iconName; + private string? _originalFileName; + private bool _fill; internal ProfileConfigurationIcon(ProfileConfigurationEntity entity) { @@ -20,31 +23,108 @@ namespace Artemis.Core } /// - /// Gets or sets the type of icon this profile configuration uses + /// Gets the type of icon this profile configuration uses /// public ProfileConfigurationIconType IconType { get => _iconType; - set => SetAndNotify(ref _iconType, value); + private set => SetAndNotify(ref _iconType, value); } /// - /// Gets or sets the icon if it is a Material icon + /// Gets the name of the icon if is /// - public string? MaterialIcon + public string? IconName { - get => _materialIcon; - set => SetAndNotify(ref _materialIcon, value); + get => _iconName; + private set => SetAndNotify(ref _iconName, value); } /// - /// Gets or sets a stream containing the icon if it is bitmap or SVG + /// Gets the original file name of the icon (if applicable) /// - /// - public Stream? FileIcon + public string? OriginalFileName { - get => _fileIcon; - set => SetAndNotify(ref _fileIcon, value); + get => _originalFileName; + private set => SetAndNotify(ref _originalFileName, value); + } + + /// + /// Gets or sets a boolean indicating whether or not this icon should be filled. + /// + public bool Fill + { + get => _fill; + set => SetAndNotify(ref _fill, value); + } + + /// + /// Updates the to the provided value and changes the is + /// + /// + /// The name of the icon + public void SetIconByName(string iconName) + { + if (iconName == null) throw new ArgumentNullException(nameof(iconName)); + + _iconStream?.Dispose(); + IconName = iconName; + OriginalFileName = null; + IconType = ProfileConfigurationIconType.MaterialIcon; + + OnIconUpdated(); + } + + /// + /// Updates the stream returned by to the provided stream + /// + /// The original file name backing the stream, should include the extension + /// The stream to copy + public void SetIconByStream(string originalFileName, Stream stream) + { + if (originalFileName == null) throw new ArgumentNullException(nameof(originalFileName)); + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + _iconStream?.Dispose(); + _iconStream = new MemoryStream(); + stream.Seek(0, SeekOrigin.Begin); + stream.CopyTo(_iconStream); + _iconStream.Seek(0, SeekOrigin.Begin); + + IconName = null; + OriginalFileName = originalFileName; + IconType = ProfileConfigurationIconType.BitmapImage; + OnIconUpdated(); + } + + /// + /// Creates a copy of the stream containing the icon + /// + /// A stream containing the icon + public Stream? GetIconStream() + { + if (_iconStream == null) + return null; + + MemoryStream stream = new(); + _iconStream.CopyTo(stream); + + stream.Seek(0, SeekOrigin.Begin); + _iconStream.Seek(0, SeekOrigin.Begin); + return stream; + } + + /// + /// Occurs when the icon was updated + /// + public event EventHandler? IconUpdated; + + /// + /// Invokes the event + /// + protected virtual void OnIconUpdated() + { + IconUpdated?.Invoke(this, EventArgs.Empty); } #region Implementation of IStorageModel @@ -53,14 +133,20 @@ namespace Artemis.Core public void Load() { IconType = (ProfileConfigurationIconType) _entity.IconType; - MaterialIcon = _entity.MaterialIcon; + Fill = _entity.IconFill; + if (IconType != ProfileConfigurationIconType.MaterialIcon) + return; + + IconName = _entity.MaterialIcon; + OnIconUpdated(); } /// public void Save() { _entity.IconType = (int) IconType; - _entity.MaterialIcon = MaterialIcon; + _entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null; + _entity.IconFill = Fill; } #endregion @@ -80,10 +166,5 @@ namespace Artemis.Core /// A bitmap image icon /// [Description("Bitmap Image")] BitmapImage, - - /// - /// An SVG image icon - /// - [Description("SVG Image")] SvgImage } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 8c6dc49db..aa8991159 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -451,7 +451,7 @@ namespace Artemis.Core }); DeviceEntity.InputMappings.Clear(); - foreach (var (original, mapped) in InputMappings) + foreach ((ArtemisLed? original, ArtemisLed? mapped) in InputMappings) DeviceEntity.InputMappings.Add(new InputMappingEntity {OriginalLedId = (int) original.RgbLed.Id, MappedLedId = (int) mapped.RgbLed.Id}); DeviceEntity.Categories.Clear(); diff --git a/src/Artemis.Core/Ninject/CoreModule.cs b/src/Artemis.Core/Ninject/CoreModule.cs index 323cfbcce..5c572b62c 100644 --- a/src/Artemis.Core/Ninject/CoreModule.cs +++ b/src/Artemis.Core/Ninject/CoreModule.cs @@ -1,5 +1,4 @@ -using System.IO; -using Artemis.Core.Services; +using Artemis.Core.Services; using Artemis.Storage; using Artemis.Storage.Migrations.Interfaces; using Artemis.Storage.Repositories.Interfaces; @@ -79,7 +78,7 @@ namespace Artemis.Core.Ninject private bool HasAccessToProtectedService(IRequest r) { - return r.ParentRequest != null && !r.ParentRequest.Service.Assembly.Location.StartsWith(Path.Combine(Constants.DataFolder, "plugins")); + return r.ParentRequest != null && !r.ParentRequest.Service.Assembly.Location.StartsWith(Constants.PluginsFolder); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Ninject/LoggerProvider.cs b/src/Artemis.Core/Ninject/LoggerProvider.cs index 64f5efcf3..0f76c85a2 100644 --- a/src/Artemis.Core/Ninject/LoggerProvider.cs +++ b/src/Artemis.Core/Ninject/LoggerProvider.cs @@ -13,7 +13,7 @@ namespace Artemis.Core.Ninject private static readonly ILogger Logger = new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.File(Path.Combine(Constants.DataFolder, "logs", "Artemis log-.log"), + .WriteTo.File(Path.Combine(Constants.LogsFolder, "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/Ninject/PluginModule.cs b/src/Artemis.Core/Ninject/PluginModule.cs index f6909993b..8dfed2f48 100644 --- a/src/Artemis.Core/Ninject/PluginModule.cs +++ b/src/Artemis.Core/Ninject/PluginModule.cs @@ -22,7 +22,7 @@ namespace Artemis.Core.Ninject Kernel.Components.Remove(); - Kernel.Bind().ToConstant(Plugin); + Kernel.Bind().ToConstant(Plugin).InTransientScope(); // Bind plugin service interfaces Kernel.Bind(x => diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index abb2e1ea3..abafb8031 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -88,7 +88,7 @@ namespace Artemis.Core.DeviceProviders /// The resulting Artemis layout public virtual ArtemisLayout LoadUserLayout(ArtemisDevice device) { - string layoutDir = Path.Combine(Constants.DataFolder, "user layouts"); + string layoutDir = Constants.LayoutsFolder; string filePath = Path.Combine( layoutDir, device.RgbDevice.DeviceInfo.Manufacturer, diff --git a/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs b/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs index 27736b149..5d179d0d7 100644 --- a/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs +++ b/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs @@ -1,9 +1,15 @@ -namespace Artemis.Core +using System; + +namespace Artemis.Core { /// /// Represents a configuration dialog for a /// public interface IPluginConfigurationDialog { + /// + /// The type of view model the tab contains + /// + Type Type { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index 40a37b09d..24d54ae82 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.LayerBrushes @@ -24,6 +26,7 @@ namespace Artemis.Core.LayerBrushes // Both are set right after construction to keep the constructor of inherited classes clean _layer = null!; _descriptor = null!; + LayerBrushEntity = null!; } /// @@ -35,6 +38,11 @@ namespace Artemis.Core.LayerBrushes internal set => SetAndNotify(ref _layer, value); } + /// + /// Gets the brush entity this brush uses for persistent storage + /// + public LayerBrushEntity LayerBrushEntity { get; internal set; } + /// /// Gets the descriptor of this brush /// @@ -185,6 +193,13 @@ namespace Artemis.Core.LayerBrushes public override string BrokenDisplayName => Descriptor.DisplayName; #endregion + + internal void Save() + { + // No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change + BaseProperties?.ApplyToEntity(); + LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 4225b0455..52b6c3ab4 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,11 +1,12 @@ using System; +using Artemis.Storage.Entities.Profile; namespace Artemis.Core.LayerBrushes { /// /// For internal use only, please use or or instead /// - public abstract class PropertiesLayerBrush : BaseLayerBrush where T : LayerPropertyGroup + public abstract class PropertiesLayerBrush : BaseLayerBrush where T : LayerPropertyGroup, new() { private T _properties = null!; @@ -34,13 +35,10 @@ namespace Artemis.Core.LayerBrushes internal void InitializeProperties() { - Properties = Activator.CreateInstance(); - Properties.GroupDescription = new PropertyGroupDescriptionAttribute {Name = Descriptor.DisplayName, Description = Descriptor.Description}; - Properties.LayerBrush = this; - Properties.Initialize(Layer, "LayerBrush.", Descriptor.Provider); + Properties = new T(); + PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description}; + Properties.Initialize(Layer, null, groupDescription, LayerBrushEntity.PropertyGroup); PropertiesInitialized = true; - - EnableLayerBrush(); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs index 9e69380d0..4db8f176e 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrush.cs @@ -6,7 +6,7 @@ namespace Artemis.Core.LayerBrushes /// Represents a brush that renders on a layer /// /// The type of brush properties - public abstract class LayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup + public abstract class LayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup, new() { /// /// Creates a new instance of the class diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs index 6fb91ca71..b2fad30cc 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushDescriptor.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; using Ninject; namespace Artemis.Core.LayerBrushes @@ -57,23 +58,18 @@ namespace Artemis.Core.LayerBrushes /// /// Creates an instance of the described brush and applies it to the layer /// - internal void CreateInstance(Layer layer) + public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity) { - if (layer == null) throw new ArgumentNullException(nameof(layer)); - if (layer.LayerBrush != null) - throw new ArtemisCoreException("Layer already has an instantiated layer brush"); + if (layer == null) + throw new ArgumentNullException(nameof(layer)); BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); brush.Layer = layer; brush.Descriptor = this; + brush.LayerBrushEntity = entity ?? new LayerBrushEntity { ProviderId = Provider.Id, BrushType = LayerBrushType.FullName }; + brush.Initialize(); - brush.Update(0); - - layer.LayerBrush = brush; - layer.OnLayerBrushUpdated(); - - if (layer.ShouldBeEnabled) - brush.InternalEnable(); + return brush; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs index 25f72b7ac..43a200fc5 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/LayerBrushProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Text.RegularExpressions; namespace Artemis.Core.LayerBrushes { @@ -17,7 +18,7 @@ namespace Artemis.Core.LayerBrushes protected LayerBrushProvider() { _layerBrushDescriptors = new List(); - LayerBrushDescriptors = new(_layerBrushDescriptors); + LayerBrushDescriptors = new ReadOnlyCollection(_layerBrushDescriptors); Disabled += OnDisabled; } @@ -42,7 +43,7 @@ namespace Artemis.Core.LayerBrushes if (!IsEnabled) throw new ArtemisPluginException(Plugin, "Can only add a layer brush descriptor when the plugin is enabled"); - if (icon.ToLower().EndsWith(".svg")) + if (icon.Contains('.')) icon = Plugin.ResolveRelativePath(icon); LayerBrushDescriptor descriptor = new(displayName, description, icon, typeof(T), this); _layerBrushDescriptors.Add(descriptor); diff --git a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs index 1f1948808..cfaeb5a48 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/PerLedLayerBrush.cs @@ -1,4 +1,5 @@ -using SkiaSharp; +using System; +using SkiaSharp; namespace Artemis.Core.LayerBrushes { @@ -6,7 +7,7 @@ namespace Artemis.Core.LayerBrushes /// Represents a brush that renders on a per-layer basis /// /// The type of brush properties - public abstract class PerLedLayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup + public abstract class PerLedLayerBrush : PropertiesLayerBrush where T : LayerPropertyGroup, new() { /// /// Creates a new instance of the class @@ -35,7 +36,6 @@ namespace Artemis.Core.LayerBrushes canvas.SetMatrix(canvas.TotalMatrix.PreConcat(Layer.GetTransformMatrix(true, false, false, true).Invert())); using SKPath pointsPath = new(); - using SKPaint ledPaint = new(); foreach (ArtemisLed artemisLed in Layer.Leds) { pointsPath.AddPoly(new[] @@ -53,24 +53,25 @@ namespace Artemis.Core.LayerBrushes TryOrBreak(() => { + + 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); - + paint.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 ); - - canvas.DrawRect(ledRectangle, ledPaint); + + canvas.DrawRect(ledRectangle, paint); } }, "Failed to render"); } diff --git a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs index ce32edfd0..bbd355e2c 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Internal/BaseLayerEffect.cs @@ -1,4 +1,5 @@ using System; +using Artemis.Storage.Entities.Profile; using SkiaSharp; namespace Artemis.Core.LayerEffects @@ -6,12 +7,10 @@ namespace Artemis.Core.LayerEffects /// /// For internal use only, please use instead /// - public abstract class BaseLayerEffect : BreakableModel, IDisposable + public abstract class BaseLayerEffect : BreakableModel, IDisposable, IStorageModel { private ILayerEffectConfigurationDialog? _configurationDialog; private LayerEffectDescriptor _descriptor; - private bool _suspended; - private Guid _entityId; private bool _hasBeenRenamed; private string _name; private int _order; @@ -24,16 +23,13 @@ namespace Artemis.Core.LayerEffects _profileElement = null!; _descriptor = null!; _name = null!; + LayerEffectEntity = null!; } /// - /// Gets the unique ID of this effect + /// Gets the /// - public Guid EntityId - { - get => _entityId; - internal set => SetAndNotify(ref _entityId, value); - } + public LayerEffectEntity LayerEffectEntity { get; internal set; } /// /// Gets the profile element (such as layer or folder) this effect is applied to @@ -53,15 +49,6 @@ namespace Artemis.Core.LayerEffects set => SetAndNotify(ref _name, value); } - /// - /// Gets or sets the suspended state, if suspended the effect is skipped in render and update - /// - public bool Suspended - { - get => _suspended; - set => SetAndNotify(ref _suspended, value); - } - /// /// Gets or sets whether the effect has been renamed by the user, if true consider refraining from changing the name /// programatically @@ -107,15 +94,18 @@ namespace Artemis.Core.LayerEffects /// /// Gets a reference to the layer property group without knowing it's type /// - public virtual LayerPropertyGroup? BaseProperties => null; - - internal string PropertyRootPath => $"LayerEffect.{EntityId}.{GetType().Name}."; + public virtual LayerEffectPropertyGroup? BaseProperties => null; /// /// Gets a boolean indicating whether the layer effect is enabled or not /// public bool Enabled { get; private set; } + /// + /// Gets a boolean indicating whether the layer effect is suspended or not + /// + public bool Suspended => BaseProperties is not {PropertiesInitialized: true} || !BaseProperties.IsEnabled; + #region IDisposable /// @@ -227,5 +217,26 @@ namespace Artemis.Core.LayerEffects public override string BrokenDisplayName => Name; #endregion + + /// + public void Load() + { + HasBeenRenamed = LayerEffectEntity.HasBeenRenamed; + Name = HasBeenRenamed ? LayerEffectEntity.Name : Descriptor.DisplayName; + Order = LayerEffectEntity.Order; + } + + /// + public void Save() + { + LayerEffectEntity.ProviderId = Descriptor.Provider.Id; + LayerEffectEntity.EffectType = GetType().FullName; + LayerEffectEntity.Name = Name; + LayerEffectEntity.HasBeenRenamed = HasBeenRenamed; + LayerEffectEntity.Order = Order; + + BaseProperties?.ApplyToEntity(); + LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs index 3a1b03e62..87259d44b 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffect.cs @@ -6,7 +6,7 @@ namespace Artemis.Core.LayerEffects /// Represents an effect that applies preprocessing and/or postprocessing to a layer /// /// - public abstract class LayerEffect : BaseLayerEffect where T : LayerPropertyGroup + public abstract class LayerEffect : BaseLayerEffect where T : LayerEffectPropertyGroup, new() { private T _properties = null!; @@ -16,7 +16,7 @@ namespace Artemis.Core.LayerEffects public bool PropertiesInitialized { get; internal set; } /// - public override LayerPropertyGroup BaseProperties => Properties; + public override LayerEffectPropertyGroup BaseProperties => Properties; /// /// Gets the properties of this effect. @@ -32,20 +32,22 @@ namespace Artemis.Core.LayerEffects } internal set => _properties = value; } - - internal void InitializeProperties() - { - Properties = Activator.CreateInstance(); - Properties.LayerEffect = this; - Properties.Initialize(ProfileElement, PropertyRootPath, Descriptor.Provider); - PropertiesInitialized = true; - - EnableLayerEffect(); - } - + internal override void Initialize() { InitializeProperties(); } + + private void InitializeProperties() + { + Properties = new T(); + Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute {Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup); + + // Initialize will call PopulateDefaults but that is for plugin developers so can't rely on that to default IsEnabled to true + Properties.InitializeIsEnabled(); + PropertiesInitialized = true; + + EnableLayerEffect(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs index f724f367a..5c605de8d 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectDescriptor.cs @@ -1,102 +1,112 @@ using System; -using System.Linq; using Artemis.Core.LayerEffects.Placeholder; using Artemis.Storage.Entities.Profile; using Ninject; -namespace Artemis.Core.LayerEffects +namespace Artemis.Core.LayerEffects; + +/// +/// A class that describes a layer effect +/// +public class LayerEffectDescriptor { - /// - /// A class that describes a layer effect - /// - public class LayerEffectDescriptor + internal LayerEffectDescriptor(string displayName, string description, string icon, Type layerEffectType, LayerEffectProvider provider) { - internal LayerEffectDescriptor(string displayName, string description, string icon, Type? layerEffectType, LayerEffectProvider provider) + DisplayName = displayName; + Description = description; + Icon = icon; + LayerEffectType = layerEffectType ?? throw new ArgumentNullException(nameof(layerEffectType)); + Provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + internal LayerEffectDescriptor(string placeholderFor, LayerEffectProvider provider) + { + PlaceholderFor = placeholderFor ?? throw new ArgumentNullException(nameof(placeholderFor)); + Provider = provider ?? throw new ArgumentNullException(nameof(provider)); + DisplayName = "Missing effect"; + Description = "This effect could not be loaded"; + Icon = "FileQuestion"; + } + + /// + /// The name that is displayed in the UI + /// + public string DisplayName { get; } + + /// + /// The description that is displayed in the UI + /// + public string Description { get; } + + /// + /// The Material icon to display in the UI, a full reference can be found + /// here + /// + public string Icon { get; } + + /// + /// The type of the layer effect + /// + public Type? LayerEffectType { get; } + + /// + /// The plugin that provided this + /// + public LayerEffectProvider Provider { get; } + + /// + /// Gets the GUID this descriptor is acting as a placeholder for. If null, this descriptor is not a placeholder + /// + public string? PlaceholderFor { get; } + + /// + /// Creates an instance of the described effect and applies it to the render element + /// + public BaseLayerEffect CreateInstance(RenderProfileElement renderElement, LayerEffectEntity? entity) + { + if (PlaceholderFor != null) { - DisplayName = displayName; - Description = description; - Icon = icon; - LayerEffectType = layerEffectType; - Provider = provider; + if (entity == null) + throw new ArtemisCoreException("Cannot create a placeholder for a layer effect that wasn't loaded from an entity"); + + return CreatePlaceHolderInstance(renderElement, entity); } - /// - /// The name that is displayed in the UI - /// - public string DisplayName { get; } + if (LayerEffectType == null) + throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); - /// - /// The description that is displayed in the UI - /// - public string Description { get; } - - /// - /// The Material icon to display in the UI, a full reference can be found - /// here - /// - public string Icon { get; } - - /// - /// The type of the layer effect - /// - public Type? LayerEffectType { get; } - - /// - /// The plugin that provided this - /// - public LayerEffectProvider Provider { get; } - - /// - /// Gets the GUID this descriptor is acting as a placeholder for. If null, this descriptor is not a placeholder - /// - public string? PlaceholderFor { get; internal set; } - - /// - /// Creates an instance of the described effect and applies it to the render element - /// - internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType); + effect.ProfileElement = renderElement; + effect.Descriptor = this; + if (entity != null) { - // Skip effects already on the element - if (renderElement.LayerEffects.Any(e => e.EntityId == entity.Id)) - return; - - if (PlaceholderFor != null) - { - CreatePlaceHolderInstance(renderElement, entity); - return; - } - - if (LayerEffectType == null) - throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType"); - - BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType); - effect.ProfileElement = renderElement; - effect.EntityId = entity.Id; - effect.Order = entity.Order; - effect.Name = entity.Name; - effect.Suspended = entity.Suspended; - effect.Descriptor = this; - + effect.LayerEffectEntity = entity; + effect.Load(); effect.Initialize(); - effect.Update(0); - - renderElement.ActivateLayerEffect(effect); } - - private void CreatePlaceHolderInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + else { - if (PlaceholderFor == null) - throw new ArtemisCoreException("Cannot create a placeholder instance using a layer effect descriptor that is not a placeholder for anything"); - PlaceholderLayerEffect effect = new(entity, PlaceholderFor) - { - ProfileElement = renderElement, - Descriptor = this - }; + effect.LayerEffectEntity = new LayerEffectEntity(); + effect.Name = DisplayName; effect.Initialize(); - renderElement.ActivateLayerEffect(effect); - - if (renderElement.ShouldBeEnabled) - effect.InternalEnable(); + effect.Save(); } + + return effect; + } + + private BaseLayerEffect CreatePlaceHolderInstance(RenderProfileElement renderElement, LayerEffectEntity entity) + { + if (PlaceholderFor == null) + throw new ArtemisCoreException("Cannot create a placeholder instance using a layer effect descriptor that is not a placeholder for anything"); + + PlaceholderLayerEffect effect = new(entity, PlaceholderFor) + { + ProfileElement = renderElement, + Descriptor = this + }; + effect.Initialize(); + + return effect; } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs index a35eb156f..d777ad622 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/LayerEffectProvider.cs @@ -18,7 +18,7 @@ namespace Artemis.Core.LayerEffects protected LayerEffectProvider() { _layerEffectDescriptors = new List(); - LayerEffectDescriptors = new(_layerEffectDescriptors); + LayerEffectDescriptors = new ReadOnlyCollection(_layerEffectDescriptors); Disabled += OnDisabled; } @@ -43,7 +43,7 @@ namespace Artemis.Core.LayerEffects if (!IsEnabled) throw new ArtemisPluginFeatureException(this, "Can only add a layer effect descriptor when the plugin is enabled"); - if (icon.ToLower().EndsWith(".svg")) + if (icon.Contains('.')) icon = Plugin.ResolveRelativePath(icon); LayerEffectDescriptor descriptor = new(displayName, description, icon, typeof(T), this); _layerEffectDescriptors.Add(descriptor); diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs index 07f57491b..c9b187088 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffect.cs @@ -13,10 +13,9 @@ namespace Artemis.Core.LayerEffects.Placeholder OriginalEntity = originalEntity; PlaceholderFor = placeholderFor; - EntityId = OriginalEntity.Id; + LayerEffectEntity = originalEntity; Order = OriginalEntity.Order; Name = OriginalEntity.Name; - Suspended = OriginalEntity.Suspended; HasBeenRenamed = OriginalEntity.HasBeenRenamed; } @@ -58,7 +57,7 @@ namespace Artemis.Core.LayerEffects.Placeholder /// /// This is in place so that the UI has something to show /// - internal class PlaceholderProperties : LayerPropertyGroup + internal class PlaceholderProperties : LayerEffectPropertyGroup { protected override void PopulateDefaults() { diff --git a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs index 374340bb1..a5496d986 100644 --- a/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs +++ b/src/Artemis.Core/Plugins/LayerEffects/Placeholder/PlaceholderLayerEffectDescriptor.cs @@ -4,11 +4,7 @@ { public static LayerEffectDescriptor Create(string missingProviderId) { - LayerEffectDescriptor descriptor = new("Missing effect", "This effect could not be loaded", "FileQuestion", null, Constants.EffectPlaceholderPlugin) - { - PlaceholderFor = missingProviderId - }; - + LayerEffectDescriptor descriptor = new(missingProviderId, Constants.EffectPlaceholderPlugin); return descriptor; } } diff --git a/src/Artemis.Core/Plugins/Modules/DataModel.cs b/src/Artemis.Core/Plugins/Modules/DataModel.cs index 507584c24..ea1cacd6d 100644 --- a/src/Artemis.Core/Plugins/Modules/DataModel.cs +++ b/src/Artemis.Core/Plugins/Modules/DataModel.cs @@ -27,8 +27,8 @@ namespace Artemis.Core.Modules Module = null!; DataModelDescription = null!; - ActivePaths = new(_activePaths); - DynamicChildren = new(_dynamicChildren); + ActivePaths = new ReadOnlyCollection(_activePaths); + DynamicChildren = new ReadOnlyDictionary(_dynamicChildren); } /// @@ -285,34 +285,43 @@ namespace Artemis.Core.Modules internal bool IsPropertyInUse(string path, bool includeChildren) { path = path.ToUpperInvariant(); - return includeChildren - ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) - : _activePathsHashSet.Contains(path); + lock (_activePaths) + { + return includeChildren + ? _activePathsHashSet.Any(p => p.StartsWith(path, StringComparison.Ordinal)) + : _activePathsHashSet.Contains(path); + } } internal void AddDataModelPath(DataModelPath path) { - if (_activePaths.Contains(path)) - return; + lock (_activePaths) + { + if (_activePaths.Contains(path)) + return; - _activePaths.Add(path); + _activePaths.Add(path); - // Add to the hashset if this is the first path pointing - string hashPath = path.Path.ToUpperInvariant(); - if (!_activePathsHashSet.Contains(hashPath)) - _activePathsHashSet.Add(hashPath); + // Add to the hashset if this is the first path pointing + string hashPath = path.Path.ToUpperInvariant(); + if (!_activePathsHashSet.Contains(hashPath)) + _activePathsHashSet.Add(hashPath); + } OnActivePathAdded(new DataModelPathEventArgs(path)); } internal void RemoveDataModelPath(DataModelPath path) { - if (!_activePaths.Remove(path)) - return; + lock (_activePaths) + { + if (!_activePaths.Remove(path)) + return; - // Remove from the hashset if this was the last path pointing there - if (_activePaths.All(p => p.Path != path.Path)) - _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); + // Remove from the hashset if this was the last path pointing there + if (_activePaths.All(p => p.Path != path.Path)) + _activePathsHashSet.Remove(path.Path.ToUpperInvariant()); + } OnActivePathRemoved(new DataModelPathEventArgs(path)); } diff --git a/src/Artemis.Core/Plugins/Modules/Module.cs b/src/Artemis.Core/Plugins/Modules/Module.cs index c42e378a4..5b62963a2 100644 --- a/src/Artemis.Core/Plugins/Modules/Module.cs +++ b/src/Artemis.Core/Plugins/Modules/Module.cs @@ -12,7 +12,7 @@ namespace Artemis.Core.Modules /// /// Allows you to add new data to the Artemis data model /// - public abstract class Module : Module where T : DataModel + public abstract class Module : Module where T : DataModel, new() { /// /// The data model driving this module @@ -79,7 +79,7 @@ namespace Artemis.Core.Modules internal override void InternalEnable() { - DataModel = Activator.CreateInstance(); + DataModel = new T(); DataModel.Module = this; DataModel.DataModelDescription = GetDataModelDescription(); base.InternalEnable(); @@ -114,17 +114,20 @@ namespace Artemis.Core.Modules private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new(); private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new(); - protected Module() - { - DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths); - HiddenProperties = new(HiddenPropertiesList); - } - /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// protected internal readonly List HiddenPropertiesList = new(); + /// + /// The base constructor of the class. + /// + protected Module() + { + DefaultProfilePaths = new ReadOnlyCollection<(DefaultCategoryName, string)>(_defaultProfilePaths); + HiddenProperties = new ReadOnlyCollection(HiddenPropertiesList); + } + /// /// Gets a read only collection of default profile paths /// @@ -237,7 +240,7 @@ namespace Artemis.Core.Modules /// public virtual DataModelPropertyAttribute GetDataModelDescription() { - return new() {Name = Info.Name, Description = Info.Description}; + return new DataModelPropertyAttribute {Name = Info.Name, Description = Info.Description}; } /// @@ -307,7 +310,7 @@ namespace Artemis.Core.Modules /// internal override void InternalEnable() { - foreach ((DefaultCategoryName categoryName, var path) in _pendingDefaultProfilePaths) + foreach ((DefaultCategoryName categoryName, string? path) in _pendingDefaultProfilePaths) AddDefaultProfile(categoryName, path); _pendingDefaultProfilePaths.Clear(); diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index c82eab146..e7e463238 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; +using Artemis.Core.DeviceProviders; using Artemis.Storage.Entities.Plugins; using McMaster.NETCore.Plugins; using Ninject; @@ -16,6 +17,7 @@ namespace Artemis.Core /// public class Plugin : CorePropertyChanged, IDisposable { + private readonly bool _loadedFromStorage; private readonly List _features; private readonly List _profilers; @@ -25,14 +27,15 @@ namespace Artemis.Core { Info = info; Directory = directory; - Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; + Entity = pluginEntity ?? new PluginEntity {Id = Guid}; Info.Plugin = this; + _loadedFromStorage = pluginEntity != null; _features = new List(); _profilers = new List(); - Features = new(_features); - Profilers = new(_profilers); + Features = new ReadOnlyCollection(_features); + Profilers = new ReadOnlyCollection(_profilers); } /// @@ -309,6 +312,27 @@ namespace Artemis.Core { return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); } + + internal void AutoEnableIfNew() + { + if (_loadedFromStorage) + return; + + // Enabled is preset to true if the plugin meets the following criteria + // - Requires no admin rights + // - No always-enabled device providers + // - Either has no prerequisites or they are all met + Entity.IsEnabled = !Info.RequiresAdmin && + Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) && + Info.ArePrerequisitesMet(); + + if (!Entity.IsEnabled) + return; + + // Also auto-enable any non-device provider feature + foreach (PluginFeatureInfo pluginFeatureInfo in Features) + pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); + } /// public void Dispose() @@ -316,5 +340,7 @@ namespace Artemis.Core Dispose(true); GC.SuppressFinalize(this); } + + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs index 40863ce25..e761b7a3e 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs @@ -26,6 +26,7 @@ namespace Artemis.Core /// /// Marks the feature to always be enabled as long as the plugin is enabled + /// Note: always if this is the plugin's only feature /// public bool AlwaysEnabled { get; set; } } diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 62b4a6c5c..ed9064f70 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -91,7 +91,7 @@ namespace Artemis.Core } /// - /// The name of the plugin + /// The name of the feature /// [JsonProperty(Required = Required.Always)] public string Name @@ -101,7 +101,7 @@ namespace Artemis.Core } /// - /// A short description of the plugin + /// A short description of the feature /// [JsonProperty] public string? Description @@ -122,10 +122,11 @@ namespace Artemis.Core } /// - /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled + /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. + /// Note: always if this is the plugin's only feature /// [JsonProperty] - public bool AlwaysEnabled { get; } + public bool AlwaysEnabled { get; internal set; } /// /// Gets a boolean indicating whether the feature is enabled in persistent storage @@ -151,7 +152,7 @@ namespace Artemis.Core { if (Icon == null) return null; - return Icon.EndsWith(".svg") ? Plugin.ResolveRelativePath(Icon) : Icon; + return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; } } @@ -166,10 +167,13 @@ namespace Artemis.Core /// public List Prerequisites { get; } = new(); + /// + public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); + /// public bool ArePrerequisitesMet() { - return Prerequisites.All(p => p.IsMet()); + return PlatformPrerequisites.All(p => p.IsMet()); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 7358c3ad5..e0850b3a0 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -24,7 +24,7 @@ namespace Artemis.Core private Plugin _plugin = null!; private Version _version = null!; private bool _requiresAdmin; - + private PluginPlatform? _platforms; internal PluginInfo() { @@ -142,7 +142,17 @@ namespace Artemis.Core get => _requiresAdmin; internal set => SetAndNotify(ref _requiresAdmin, value); } - + + /// + /// Gets + /// + [JsonProperty] + public PluginPlatform? Platforms + { + get => _platforms; + internal set => _platforms = value; + } + /// /// Gets the plugin this info is associated with /// @@ -161,9 +171,14 @@ namespace Artemis.Core { if (Icon == null) return null; - return Icon.EndsWith(".svg") ? Plugin.ResolveRelativePath(Icon) : Icon; + return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; } } + + /// + /// Gets a boolean indicating whether this plugin is compatible with the current operating system + /// + public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem(); internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; @@ -175,11 +190,14 @@ namespace Artemis.Core /// public List Prerequisites { get; } = new(); + + /// + public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); /// public bool ArePrerequisitesMet() { - return Prerequisites.All(p => p.IsMet()); + return PlatformPrerequisites.All(p => p.IsMet()); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginPlatform.cs b/src/Artemis.Core/Plugins/PluginPlatform.cs new file mode 100644 index 000000000..fa07d2646 --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginPlatform.cs @@ -0,0 +1,42 @@ +using System; +using Newtonsoft.Json; + +namespace Artemis.Core; + +/// +/// Specifies OS platforms a plugin may support. +/// +[Flags] +[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] +public enum PluginPlatform +{ + /// The Windows platform. + Windows = 1, + + /// The Linux platform. + Linux = 2, + + /// The OSX platform. + // ReSharper disable once InconsistentNaming + OSX = 4 +} + +internal static class PluginPlatformExtensions +{ + /// + /// Determines whether the provided platform matches the current operating system. + /// + internal static bool MatchesCurrentOperatingSystem(this PluginPlatform? platform) + { + if (platform == null) + return true; + + if (OperatingSystem.IsWindows()) + return platform.Value.HasFlag(PluginPlatform.Windows); + if (OperatingSystem.IsLinux()) + return platform.Value.HasFlag(PluginPlatform.Linux); + if (OperatingSystem.IsMacOS()) + return platform.Value.HasFlag(PluginPlatform.OSX); + return false; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs index 5401455ed..6fa0d25cd 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; namespace Artemis.Core { @@ -12,6 +11,11 @@ namespace Artemis.Core /// Gets a list of prerequisites for this plugin /// List Prerequisites { get; } + + /// + /// Gets a list of prerequisites of the current platform for this plugin + /// + IEnumerable PlatformPrerequisites { get; } /// /// Determines whether the prerequisites of this plugin are met diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index 3adc6fac1..f4bc30b79 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -22,6 +22,11 @@ namespace Artemis.Core /// public abstract string Description { get; } + /// + /// Gets or sets the platform(s) this prerequisite applies to. + /// + public PluginPlatform? Platform { get; protected set; } + /// /// Gets a list of actions to execute when is called /// @@ -91,6 +96,14 @@ namespace Artemis.Core /// if the prerequisite is met; otherwise public abstract bool IsMet(); + /// + /// Determines whether this prerequisite applies to the current operating system. + /// + public bool AppliesToPlatform() + { + return Platform.MatchesCurrentOperatingSystem(); + } + /// /// Called before installation starts /// diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs index b09f076b9..033379a6f 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -36,6 +38,11 @@ namespace Artemis.Core /// public string Target { get; } + /// + /// Gets or sets an optional list of files to extract, if all files will be extracted. + /// + public List? FilesToExtract { get; set; } + /// public override async Task Execute(CancellationToken cancellationToken) { @@ -50,10 +57,15 @@ namespace Artemis.Core { ZipArchive archive = new(fileStream); long count = 0; - foreach (ZipArchiveEntry entry in archive.Entries) + + List entries = new(archive.Entries); + if (FilesToExtract != null) + entries = entries.Where(e => FilesToExtract.Contains(e.FullName)).ToList(); + + foreach (ZipArchiveEntry entry in entries) { await using Stream unzippedEntryStream = entry.Open(); - Progress.Report((count, archive.Entries.Count)); + Progress.Report((count, entries.Count)); if (entry.Length > 0) { string path = Path.Combine(Target, entry.FullName); diff --git a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs index eed3a391d..a6d16fef6 100644 --- a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs +++ b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs @@ -70,7 +70,7 @@ namespace Artemis.Core /// public TimeSpan GetLast() { - return new(_last); + return new TimeSpan(_last); } /// diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs index ecc953eb5..f93fd0413 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs @@ -18,11 +18,13 @@ namespace Artemis.Core.ScriptingProviders /// /// Creates a new instance of the class /// - public ScriptConfiguration(ScriptingProvider provider, string name) + public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType) { _scriptingProviderId = provider.Id; _name = name; Entity = new ScriptConfigurationEntity(); + PendingScriptContent = provider.GetDefaultScriptContent(scriptType); + ScriptContent = PendingScriptContent; } internal ScriptConfiguration(ScriptConfigurationEntity entity) diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs index 0ae720943..cb69e82ad 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs @@ -44,9 +44,12 @@ namespace Artemis.Core.ScriptingProviders /// public abstract class ScriptingProvider : PluginFeature { + /// + /// The base constructor of the class + /// protected ScriptingProvider() { - Scripts = new(InternalScripts); + Scripts = new ReadOnlyCollection