diff --git a/src/Artemis.Core/Artemis.Core.csproj b/src/Artemis.Core/Artemis.Core.csproj index 8b5c4bc83..17b364307 100644 --- a/src/Artemis.Core/Artemis.Core.csproj +++ b/src/Artemis.Core/Artemis.Core.csproj @@ -80,4 +80,10 @@ 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 f99fab4de..daa70a5ef 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -1,5 +1,8 @@  True + True + True + True True True True @@ -53,6 +56,9 @@ True True True + True + True + True True True True diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 4c22e8728..6f8f1c27d 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -62,8 +62,8 @@ namespace Artemis.Core /// public static readonly Plugin CorePlugin = new(CorePluginInfo, new DirectoryInfo(ApplicationFolder), null); - internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin}; - internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin}; + internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")}; + internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")}; internal static JsonSerializerSettings JsonConvertSettings = new() { diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 1 LED headset.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 1 LED headset.xml new file mode 100644 index 000000000..bc0ebc8ef --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 1 LED headset.xml @@ -0,0 +1,25 @@ + + + 1 LEDs headset + The default layout for headsets with 1 LED + SpoinkyNL & Aureshion + Headset + Artemis + Artemis 1 LED headset + 242 + 233 + 1 + 1 + + + M0.073,0.002 L0.073,0.015 L0.293,0.937 L0.3,0.946 L0.275,0.969 L0.048,0.025z M0.925,0.001 L0.925,0.012 L0.706,0.938 L0.697,0.941 L0.729,0.97 L0.95,0.021z + 15 + 125 + 212 + 108 + + + + Headset.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 2 LEDs headset.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 2 LEDs headset.xml new file mode 100644 index 000000000..00a153c9b --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 2 LEDs headset.xml @@ -0,0 +1,32 @@ + + + 2 LEDs headset + The default layout for headsets with 2 LEDs + SpoinkyNL & Aureshion + Headset + Artemis + Artemis 2 LEDs headset + 242 + 233 + 1 + 1 + + + M0.246,0 L0.24,0.02 L0.974,0.938 L0.999,0.941 L0.916,0.965 L0.17,0.023z + 15 + 125 + 64 + 108 + + + M0.741,-0.003 L0.755,0.012 L0.026,0.937 L0.003,0.939 L0.084,0.965 L0.833,0.027z + 163 + ~ + 64 + 108 + + + + Headset.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 4 LEDs headset.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 4 LEDs headset.xml new file mode 100644 index 000000000..7764b9abc --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Artemis 4 LEDs headset.xml @@ -0,0 +1,46 @@ + + + 4 LEDs headset + The default layout for headsets with 4 LEDs + SpoinkyNL & Aureshion + Headset + Artemis + Artemis 4 LEDs headset + 242 + 233 + 1 + 1 + + + M0.241,0.001 L0.231,0.021 L0.62,1 L0.523,1 L0.153,0.056z + 15 + 125 + 64 + 54 + + + M0.62,0 L0.968,0.87 L0.992,0.886 L0.903,0.943 L0.523,0z + ~ + + + 64 + 54 + + + M0.748,0 L0.752,0.027 L0.377,1 L0.468,1 L0.837,0.053z + 163 + 125 + 64 + 54 + + + M0.377,0 L0.468,0 L0.09,0.935 L0,0.88 L0.032,0.863z + ~ + + + 64 + 54 + + + + Headset.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Headset.png b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Headset.png new file mode 100644 index 000000000..2925ef002 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Headset/Headset.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis L keyboard-ANSI.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis L keyboard-ANSI.xml new file mode 100644 index 000000000..8c61a0f4d --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis L keyboard-ANSI.xml @@ -0,0 +1,956 @@ + + + Artemis large keyboard layout + The default layout for full sized keyboards + SpoinkyNL + Keyboard + Artemis + Artemis L keyboard + 441 + 144 + + + 7 + 18 + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + 39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.5 + 1 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 7 + ~ + 1.75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.25 + + + + + + + + +67 + + + + + + + + + + + + + + + + + + + + + + 7 + + + 2.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.75 + + + + + + + + +24 + + + + + + + + +24 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 7 + ~ + 1.5 + + + + + + + + + + + + + + + 1.25 + + + + + + + + 6.5 + + + + + + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + 1.5 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + 2 + + + + + + + + + + + + + + + 2 + 0 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 2 + 139 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + + 361 + 10 + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + + L.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis L keyboard-ISO.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis L keyboard-ISO.xml new file mode 100644 index 000000000..fe5f59f05 --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis L keyboard-ISO.xml @@ -0,0 +1,964 @@ + + + Artemis large keyboard layout + The default layout for full sized keyboards + SpoinkyNL + Keyboard + Artemis + Artemis L keyboard + 441 + 144 + + + 7 + 18 + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + 39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M0,0 L0,0.5 L0.16666666666,0.5 L0.16666666666,1 L1,1 L1,0 Z + 1.5 + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 7 + ~ + 1.75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + +90.75 + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.75 + + + + + + + + +24 + + + + + + + + +24 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 7 + ~ + 1.5 + + + + + + + + + + + + + + + 1.25 + + + + + + + + 6.5 + + + + + + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + 1.5 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + 2 + + + + + + + + + + + + + + + 2 + 0 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 2 + 139 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + + 361 + 10 + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + + L.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis TKL keyboard-ANSI.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis TKL keyboard-ANSI.xml new file mode 100644 index 000000000..fd0683ee6 --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis TKL keyboard-ANSI.xml @@ -0,0 +1,800 @@ + + + Artemis tenkeyless keyboard layout + The default layout for tenkeyless keyboards + SpoinkyNL + Keyboard + Artemis + Artemis TKL keyboard + 360 + 144 + + + 7 + 19 + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + 42 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.5 + 1 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.25 + + + + + + + + 7 + + + 2.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.75 + + + + + + + + +24 + + + + + + + + 7 + + + 1.5 + + + + + + + + + + + + + + + 1.25 + + + + + + + + 6.5 + + + + + + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + 1.5 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 9 + 0 + 1 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 9 + 139 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + + 221 + 7 + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + + TKL.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis TKL keyboard-ISO.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis TKL keyboard-ISO.xml new file mode 100644 index 000000000..ef4f1d732 --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis TKL keyboard-ISO.xml @@ -0,0 +1,808 @@ + + + Artemis tenkeyless keyboard layout + The default layout for tenkeyless keyboards + SpoinkyNL + Keyboard + Artemis + Artemis TKL keyboard + 360 + 144 + + + 7 + 19 + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + 42 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M0,0 L0,0.5 L0.16666666666,0.5 L0.16666666666,1 L1,1 L1,0 Z + 1.5 + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 7 + + + 1.75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + 7 + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.75 + + + + + + + + +24 + + + + + + + + 7 + + + 1.5 + + + + + + + + + + + + + + + 1.25 + + + + + + + + 6.5 + + + + + + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + 1.5 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 9 + 0 + 1 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 9 + 139 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + + 221 + 7 + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + + TKL.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis XL keyboard-ANSI.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis XL keyboard-ANSI.xml new file mode 100644 index 000000000..21e0f8bfb --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis XL keyboard-ANSI.xml @@ -0,0 +1,1016 @@ + + + Artemis XL keyboard layout + The default layout for keyboards with programmable keys + SpoinkyNL + Keyboard + Artemis + Artemis XL keyboard + 464 + 144 + + + 30 + 16 + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 30 + 39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + + 1.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.5 + 1 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 30 + ~ + 1.75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.25 + + + + + + + + +67 + + + + + + + + + + + + + + + + + + + + + + 30 + + + 2.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.75 + + + + + + + + +24 + + + + + + + + +24 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 30 + ~ + 1.5 + + + + + + + + + + + + + + + 1.25 + + + + + + + + 6.5 + + + + + + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + 1.5 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + 2 + + + + + + + + + + + + + + + 7 + 16 + + + + + + + + 7 + + + + + + + + + + 7 + +2 + + + + + + + + 7 + + + + + + + + + + 7 + +2 + + + + + + + + 7 + + + + + + + + + + 5 + 0 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 5 + 139 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + + 384 + 10 + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + + XL.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis XL keyboard-ISO.xml b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis XL keyboard-ISO.xml new file mode 100644 index 000000000..4da97caa7 --- /dev/null +++ b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Artemis XL keyboard-ISO.xml @@ -0,0 +1,1024 @@ + + + Artemis XL keyboard layout + The default layout for keyboards with programmable keys + SpoinkyNL + Keyboard + Artemis + Artemis XL keyboard + 464 + 144 + + + 30 + 16 + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +12.667 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 30 + 39 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + + 1.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M0,0 L0,0.5 L0.16666666666,0.5 L0.16666666666,1 L1,1 L1,0 Z + 1.5 + 2 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 30 + ~ + 1.75 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + +90.75 + + + + + + + + + + + + + + + + + + + + + + 30 + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2.75 + + + + + + + + +24 + + + + + + + + +24 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + 30 + ~ + 1.5 + + + + + + + + + + + + + + + 1.25 + + + + + + + + 6.5 + + + + + + + + 1.25 + + + + + + + + + + + + + + + + + + + + + + 1.5 + + + + + + + + +5 + + + + + + + + + + + + + + + + + + + + + + +5 + 2 + + + + + + + + + + + + + + + 7 + 16 + + + + + + + + 7 + + + + + + + + + + 7 + +2 + + + + + + + + 7 + + + + + + + + + + 7 + +2 + + + + + + + + 7 + + + + + + + + + + 5 + 0 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 5 + 139 + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + 0.25 + + + + 384 + 10 + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + 0.75 + 0.5 + + + + XL.png + + \ No newline at end of file diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/0_BracketRight.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/0_BracketRight.png new file mode 100644 index 000000000..48a040795 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/0_BracketRight.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/1_ExclamationMark.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/1_ExclamationMark.png new file mode 100644 index 000000000..b9d6c96ec Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/1_ExclamationMark.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/2_At.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/2_At.png new file mode 100644 index 000000000..c00bfbe80 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/2_At.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/3_Hash.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/3_Hash.png new file mode 100644 index 000000000..e0bb1468e Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/3_Hash.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/4_Dollar.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/4_Dollar.png new file mode 100644 index 000000000..a65a4071d Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/4_Dollar.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/5_Percent.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/5_Percent.png new file mode 100644 index 000000000..3065eb11d Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/5_Percent.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/6_Circumflex.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/6_Circumflex.png new file mode 100644 index 000000000..3a9b2bb3a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/6_Circumflex.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/7_Ampersand.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/7_Ampersand.png new file mode 100644 index 000000000..137f74aa7 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/7_Ampersand.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/8_Asterisk.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/8_Asterisk.png new file mode 100644 index 000000000..f8b6b2331 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/8_Asterisk.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/9_BracketRight.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/9_BracketRight.png new file mode 100644 index 000000000..b97abfbf7 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/9_BracketRight.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/A.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/A.png new file mode 100644 index 000000000..7ecd4d362 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/A.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/AccentGrave_Tilde.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/AccentGrave_Tilde.png new file mode 100644 index 000000000..83f3d7dfb Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/AccentGrave_Tilde.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Alt.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Alt.png new file mode 100644 index 000000000..bbc50eda3 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Alt.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Apostrophe_QuotationMark.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Apostrophe_QuotationMark.png new file mode 100644 index 000000000..c3e77a855 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Apostrophe_QuotationMark.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Asterisk.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Asterisk.png new file mode 100644 index 000000000..df3c69164 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Asterisk.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/B.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/B.png new file mode 100644 index 000000000..e9fb32356 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/B.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Backspace.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Backspace.png new file mode 100644 index 000000000..8b5c8f6ec Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Backspace.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/BlackslashIso.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/BlackslashIso.png new file mode 100644 index 000000000..7d0af6756 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/BlackslashIso.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Blackslash_Pipe_Wide.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Blackslash_Pipe_Wide.png new file mode 100644 index 000000000..9759ff65f Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Blackslash_Pipe_Wide.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/C.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/C.png new file mode 100644 index 000000000..38d4e98fc Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/C.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CapsLock.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CapsLock.png new file mode 100644 index 000000000..cf2bc9435 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CapsLock.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretDown.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretDown.png new file mode 100644 index 000000000..1b340b639 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretDown.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretLeft.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretLeft.png new file mode 100644 index 000000000..63cdba35f Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretLeft.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretRight.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretRight.png new file mode 100644 index 000000000..e0b8b4574 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretRight.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretUp.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretUp.png new file mode 100644 index 000000000..d1852b4b0 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/CaretUp.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Ctrl.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Ctrl.png new file mode 100644 index 000000000..3e41272b4 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Ctrl.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/D.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/D.png new file mode 100644 index 000000000..fe58c8e9a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/D.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Delete.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Delete.png new file mode 100644 index 000000000..588db4ad9 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Delete.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Dot_Del.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Dot_Del.png new file mode 100644 index 000000000..a0f8191b7 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Dot_Del.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/E.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/E.png new file mode 100644 index 000000000..b5eb03c71 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/E.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/End.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/End.png new file mode 100644 index 000000000..cb970aa04 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/End.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/EnterAnsi.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/EnterAnsi.png new file mode 100644 index 000000000..84ecded86 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/EnterAnsi.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/EnterIso.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/EnterIso.png new file mode 100644 index 000000000..4b7b9da00 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/EnterIso.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Equals_Plus.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Equals_Plus.png new file mode 100644 index 000000000..586c04aef Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Equals_Plus.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Escape.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Escape.png new file mode 100644 index 000000000..1af00b8d2 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Escape.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F.png new file mode 100644 index 000000000..eb1919efb Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F1.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F1.png new file mode 100644 index 000000000..4baea9a7a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F1.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F10.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F10.png new file mode 100644 index 000000000..6026250c7 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F10.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F11.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F11.png new file mode 100644 index 000000000..f6a01502d Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F11.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F12.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F12.png new file mode 100644 index 000000000..4c85ebb0b Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F12.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F2.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F2.png new file mode 100644 index 000000000..d3ad5e562 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F2.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F3.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F3.png new file mode 100644 index 000000000..d34d22ec3 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F3.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F4.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F4.png new file mode 100644 index 000000000..bcbfb1814 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F4.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F5.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F5.png new file mode 100644 index 000000000..80ad70b92 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F5.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F6.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F6.png new file mode 100644 index 000000000..92636fca7 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F6.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F7.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F7.png new file mode 100644 index 000000000..a154bad30 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F7.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F8.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F8.png new file mode 100644 index 000000000..69538a0ee Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F8.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F9.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F9.png new file mode 100644 index 000000000..f0724beef Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/F9.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/G.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/G.png new file mode 100644 index 000000000..7fcd7312f Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/G.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/GreaterThan_Dot.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/GreaterThan_Dot.png new file mode 100644 index 000000000..6857bf6c0 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/GreaterThan_Dot.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Gui.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Gui.png new file mode 100644 index 000000000..03129b609 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Gui.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/H.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/H.png new file mode 100644 index 000000000..142fc4e58 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/H.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Home.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Home.png new file mode 100644 index 000000000..f13d87472 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Home.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Hyphen_Underscore.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Hyphen_Underscore.png new file mode 100644 index 000000000..9e0d04aa6 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Hyphen_Underscore.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/I.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/I.png new file mode 100644 index 000000000..1ffd1a1ce Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/I.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Insert.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Insert.png new file mode 100644 index 000000000..74d2da7d9 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Insert.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/J.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/J.png new file mode 100644 index 000000000..e2cd8f150 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/J.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/K.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/K.png new file mode 100644 index 000000000..6052850df Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/K.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/L.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/L.png new file mode 100644 index 000000000..fe858d869 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/L.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/LessThan_Comma.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/LessThan_Comma.png new file mode 100644 index 000000000..3c2cd0146 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/LessThan_Comma.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M.png new file mode 100644 index 000000000..e446ae051 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M1.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M1.png new file mode 100644 index 000000000..8b48a5f1e Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M1.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M2.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M2.png new file mode 100644 index 000000000..3fd6ec16a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M2.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M3.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M3.png new file mode 100644 index 000000000..3a7e68434 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M3.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M4.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M4.png new file mode 100644 index 000000000..ea1c7989d Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M4.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M5.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M5.png new file mode 100644 index 000000000..a626b0c6a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M5.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M6.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M6.png new file mode 100644 index 000000000..7bf0dd173 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/M6.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Menu.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Menu.png new file mode 100644 index 000000000..e7277d409 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Menu.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Minus.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Minus.png new file mode 100644 index 000000000..86cc37aea Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Minus.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/N.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/N.png new file mode 100644 index 000000000..34bf833f1 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/N.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num0_Ins.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num0_Ins.png new file mode 100644 index 000000000..4e97d7d7e Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num0_Ins.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num1.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num1.png new file mode 100644 index 000000000..c14017561 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num1.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num2.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num2.png new file mode 100644 index 000000000..29043d8d5 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num2.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num3.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num3.png new file mode 100644 index 000000000..e0e2a4c3b Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num3.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num4.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num4.png new file mode 100644 index 000000000..99ea9a204 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num4.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num5.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num5.png new file mode 100644 index 000000000..d01b5d948 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num5.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num6.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num6.png new file mode 100644 index 000000000..eee9cab3d Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num6.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num7.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num7.png new file mode 100644 index 000000000..ba2ef84e1 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num7.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num8.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num8.png new file mode 100644 index 000000000..2d3cbb3bf Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num8.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num9.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num9.png new file mode 100644 index 000000000..6c108c649 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Num9.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumEnter.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumEnter.png new file mode 100644 index 000000000..505aa6275 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumEnter.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumLock.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumLock.png new file mode 100644 index 000000000..70f7543f7 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumLock.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumPlus.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumPlus.png new file mode 100644 index 000000000..268410cf2 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/NumPlus.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/O.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/O.png new file mode 100644 index 000000000..31cdb61d0 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/O.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/P.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/P.png new file mode 100644 index 000000000..b8549ee86 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/P.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PageDown.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PageDown.png new file mode 100644 index 000000000..38d3473f1 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PageDown.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PageUp.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PageUp.png new file mode 100644 index 000000000..7165692b8 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PageUp.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PauseBreak.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PauseBreak.png new file mode 100644 index 000000000..012678088 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PauseBreak.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PrintScreen.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PrintScreen.png new file mode 100644 index 000000000..172ff12c9 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/PrintScreen.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Q.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Q.png new file mode 100644 index 000000000..cacff5049 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Q.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/QuestionMark_Slash.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/QuestionMark_Slash.png new file mode 100644 index 000000000..96bfa9058 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/QuestionMark_Slash.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/R.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/R.png new file mode 100644 index 000000000..d7eb69e4d Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/R.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/S.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/S.png new file mode 100644 index 000000000..54cf3b8c0 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/S.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/ScrollLock.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/ScrollLock.png new file mode 100644 index 000000000..0bb90635b Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/ScrollLock.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Semicolon_Colon.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Semicolon_Colon.png new file mode 100644 index 000000000..2c478d313 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Semicolon_Colon.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Shift.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Shift.png new file mode 100644 index 000000000..a36a93b0c Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Shift.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/ShiftBig.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/ShiftBig.png new file mode 100644 index 000000000..66b8e0231 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/ShiftBig.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Slash.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Slash.png new file mode 100644 index 000000000..93a46e6c2 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Slash.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Space.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Space.png new file mode 100644 index 000000000..3c5fab49a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Space.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/SquareBracketLeft_CurlyBracketLeft.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/SquareBracketLeft_CurlyBracketLeft.png new file mode 100644 index 000000000..f242b6896 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/SquareBracketLeft_CurlyBracketLeft.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/SquareBracketRight_CurlyBracketRight.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/SquareBracketRight_CurlyBracketRight.png new file mode 100644 index 000000000..2dd2e6053 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/SquareBracketRight_CurlyBracketRight.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/T.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/T.png new file mode 100644 index 000000000..bf3abb57b Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/T.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Tab.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Tab.png new file mode 100644 index 000000000..deb103414 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Tab.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/U.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/U.png new file mode 100644 index 000000000..ebc2814c9 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/U.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/V.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/V.png new file mode 100644 index 000000000..c4aded6a2 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/V.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/W.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/W.png new file mode 100644 index 000000000..cb22657c6 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/W.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/X.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/X.png new file mode 100644 index 000000000..d457eeb80 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/X.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Y.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Y.png new file mode 100644 index 000000000..f12dd786c Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Y.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Z.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Z.png new file mode 100644 index 000000000..4fff012f6 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/Keycaps/Z.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/L.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/L.png new file mode 100644 index 000000000..c6287290a Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/L.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/TKL.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/TKL.png new file mode 100644 index 000000000..64797cd07 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/TKL.png differ diff --git a/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/XL.png b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/XL.png new file mode 100644 index 000000000..9f999d407 Binary files /dev/null and b/src/Artemis.Core/DefaultLayouts/Artemis/Keyboard/XL.png differ diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs new file mode 100644 index 000000000..e2f3a850d --- /dev/null +++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs @@ -0,0 +1,30 @@ +using System; + +namespace Artemis.Core +{ + /// + /// An exception thrown when a plugin prerequisite-related error occurs + /// + public class ArtemisPluginPrerequisiteException : Exception + { + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject) + { + Subject = subject; + } + + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message) + { + Subject = subject; + } + + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner) + { + Subject = subject; + } + + /// + /// Gets the subject the error is related to + /// + public IPrerequisitesSubject Subject { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/StreamExtensions.cs b/src/Artemis.Core/Extensions/StreamExtensions.cs new file mode 100644 index 000000000..a6b350fd2 --- /dev/null +++ b/src/Artemis.Core/Extensions/StreamExtensions.cs @@ -0,0 +1,134 @@ +// Based on: https://www.codeproject.com/Tips/5274597/An-Improved-Stream-CopyToAsync-that-Reports-Progre +// The MIT License +// +// Copyright (c) 2020 honey the codewitch +// +// Permission is hereby granted, free of charge, +// to any person obtaining a copy of this software and +// associated documentation files (the "Software"), to +// deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom +// the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + internal static class StreamExtensions + { + private const int DefaultBufferSize = 81920; + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// The size of the copy block buffer + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static async Task CopyToAsync( + this Stream source, + long sourceLength, + Stream destination, + int bufferSize, + IProgress<(long, long)> progress, + CancellationToken cancellationToken) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new ArgumentException("Has to be readable", nameof(source)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new ArgumentException("Has to be writable", nameof(destination)); + if (bufferSize <= 0) + bufferSize = DefaultBufferSize; + + byte[] buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report((totalBytesRead, sourceLength)); + } + + progress?.Report((totalBytesRead, sourceLength)); + cancellationToken.ThrowIfCancellationRequested(); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken) + { + return CopyToAsync(source, sourceLength, destination, 0, progress, cancellationToken); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The destination to copy to + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken) + { + return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// An implementation for reporting progress + /// A task representing the operation + public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress) + { + return CopyToAsync(source, sourceLength, destination, 0, progress, default); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The destination to copy to + /// An implementation for reporting progress + /// A task representing the operation + public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress) + { + return CopyToAsync(source, 0L, destination, 0, progress, default); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs new file mode 100644 index 000000000..1e31bb371 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/CategoryAdaptionHint.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.AdaptionHints; + +namespace Artemis.Core +{ + /// + /// Represents a hint that adapts layers to a certain category of devices + /// + public class CategoryAdaptionHint : IAdaptionHint + { + public CategoryAdaptionHint() + { + } + + internal CategoryAdaptionHint(CategoryAdaptionHintEntity entity) + { + Category = (DeviceCategory) entity.Category; + Skip = entity.Skip; + LimitAmount = entity.LimitAmount; + Amount = entity.Amount; + } + + /// + /// Gets or sets the category of devices LEDs will be applied to + /// + public DeviceCategory Category { get; set; } + + /// + /// Gets or sets the amount of devices to skip + /// + public int Skip { get; set; } + + /// + /// Gets or sets a boolean indicating whether a limited amount of devices should be used + /// + public bool LimitAmount { get; set; } + + /// + /// Gets or sets the amount of devices to limit to if is + /// + public int Amount { get; set; } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + IEnumerable matches = devices + .Where(d => d.Categories.Contains(Category)) + .OrderBy(d => d.Rectangle.Top) + .ThenBy(d => d.Rectangle.Left) + .Skip(Skip); + if (LimitAmount) + matches = matches.Take(Amount); + + foreach (ArtemisDevice artemisDevice in matches) + layer.AddLeds(artemisDevice.Leds); + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new CategoryAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, Category = (int) Category, Skip = Skip}; + } + + #endregion + + /// + public override string ToString() + { + return $"Category adaption - {nameof(Category)}: {Category}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs new file mode 100644 index 000000000..b51e68547 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.AdaptionHints; +using RGB.NET.Core; + +namespace Artemis.Core +{ + /// + /// Represents a hint that adapts layers to a certain type of devices + /// + public class DeviceAdaptionHint : IAdaptionHint + { + public DeviceAdaptionHint() + { + } + + internal DeviceAdaptionHint(DeviceAdaptionHintEntity entity) + { + DeviceType = (RGBDeviceType) entity.DeviceType; + Skip = entity.Skip; + LimitAmount = entity.LimitAmount; + Amount = entity.Amount; + } + + /// + /// Gets or sets the type of devices LEDs will be applied to + /// + public RGBDeviceType DeviceType { get; set; } + + /// + /// Gets or sets the amount of devices to skip + /// + public int Skip { get; set; } + + /// + /// Gets or sets a boolean indicating whether a limited amount of devices should be used + /// + public bool LimitAmount { get; set; } + + /// + /// Gets or sets the amount of devices to limit to if is + /// + public int Amount { get; set; } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + IEnumerable matches = devices + .Where(d => DeviceType == RGBDeviceType.All || d.DeviceType == DeviceType) + .OrderBy(d => d.Rectangle.Top) + .ThenBy(d => d.Rectangle.Left) + .Skip(Skip); + if (LimitAmount) + matches = matches.Take(Amount); + + foreach (ArtemisDevice artemisDevice in matches) + layer.AddLeds(artemisDevice.Leds); + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new DeviceAdaptionHintEntity {Amount = Amount, LimitAmount = LimitAmount, DeviceType = (int) DeviceType, Skip = Skip}; + } + + /// + public override string ToString() + { + return $"Device adaption - {nameof(DeviceType)}: {DeviceType}, {nameof(Skip)}: {Skip}, {nameof(LimitAmount)}: {LimitAmount}, {nameof(Amount)}: {Amount}"; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs new file mode 100644 index 000000000..48c3dc29a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/IAdaptionHint.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Artemis.Storage.Entities.Profile.AdaptionHints; + +namespace Artemis.Core +{ + /// + /// Represents an adaption hint that's used to adapt a layer to a set of devices + /// + public interface IAdaptionHint + { + /// + /// Applies the adaptive action to the provided layer + /// + /// The layer to adapt + /// The devices to adapt the layer for + void Apply(Layer layer, List devices); + + /// + /// Returns an adaption hint entry for this adaption hint used for persistent storage + /// + IAdaptionHintEntity GetEntry(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs new file mode 100644 index 000000000..2a68be352 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/KeyboardSectionAdaptionHint.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile.AdaptionHints; +using RGB.NET.Core; + +namespace Artemis.Core +{ + /// + /// Represents a hint that adapts layers to a certain region of keyboards + /// + public class KeyboardSectionAdaptionHint : IAdaptionHint + { + private static readonly Dictionary> RegionLedIds = new() + { + {KeyboardSection.MacroKeys, Enum.GetValues().Where(l => l >= LedId.Keyboard_Programmable1 && l <= LedId.Keyboard_Programmable32).ToList()}, + {KeyboardSection.LedStrips, Enum.GetValues().Where(l => l >= LedId.LedStripe1 && l <= LedId.LedStripe128).ToList()}, + {KeyboardSection.Extra, Enum.GetValues().Where(l => l >= LedId.Keyboard_Custom1 && l <= LedId.Keyboard_Custom64).ToList()} + }; + + public KeyboardSectionAdaptionHint() + { + } + + internal KeyboardSectionAdaptionHint(KeyboardSectionAdaptionHintEntity entity) + { + Section = (KeyboardSection) entity.Section; + } + + /// + /// Gets or sets the section this hint will apply LEDs to + /// + public KeyboardSection Section { get; set; } + + #region Implementation of IAdaptionHint + + /// + public void Apply(Layer layer, List devices) + { + // Only keyboards should have the LEDs we care about + foreach (ArtemisDevice keyboard in devices.Where(d => d.DeviceType == RGBDeviceType.Keyboard)) + { + List ledIds = RegionLedIds[Section]; + layer.AddLeds(keyboard.Leds.Where(l => ledIds.Contains(l.RgbLed.Id))); + } + } + + /// + public IAdaptionHintEntity GetEntry() + { + return new KeyboardSectionAdaptionHintEntity {Section = (int) Section}; + } + + /// + public override string ToString() + { + return $"Keyboard section adaption - {nameof(Section)}: {Section}"; + } + + #endregion + } + + /// + /// Represents a section of LEDs on a keyboard + /// + public enum KeyboardSection + { + /// + /// A region containing the macro keys of a keyboard + /// + MacroKeys, + + /// + /// A region containing the LED strips of a keyboard + /// + LedStrips, + + /// + /// A region containing extra non-standard LEDs of a keyboard + /// + Extra + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs index fcf52f32b..9bb880516 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs @@ -24,6 +24,9 @@ namespace Artemis.Core public void Update() { + if (!Path.IsValid) + return; + object? value = Path.GetValue(); if (value != null) CurrentValue = (T?) value; diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index c7a17bc46..01754e172 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -7,6 +7,7 @@ using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile.Abstract; +using RGB.NET.Core; using SkiaSharp; namespace Artemis.Core @@ -42,6 +43,7 @@ namespace Artemis.Core _leds = new List(); + Adapter = new LayerAdapter(this); Initialize(); Parent.AddChild(this, 0); } @@ -64,6 +66,7 @@ namespace Artemis.Core _leds = new List(); + Adapter = new LayerAdapter(this); Load(); Initialize(); } @@ -121,6 +124,11 @@ namespace Artemis.Core /// public LayerEntity LayerEntity { get; internal set; } + /// + /// Gets the layer adapter that can be used to adapt this layer to a different set of devices + /// + public LayerAdapter Adapter { get; } + /// public override bool ShouldBeEnabled => !Suspended && DisplayConditionMet; @@ -147,7 +155,15 @@ namespace Artemis.Core return $"[Layer] {nameof(Name)}: {Name}, {nameof(Order)}: {Order}"; } - #region IDisposable + /// + /// Occurs when a property affecting the rendering properties of this layer has been updated + /// + public event EventHandler? RenderPropertiesUpdated; + + /// + /// Occurs when the layer brush of this layer has been updated + /// + public event EventHandler? LayerBrushUpdated; /// protected override void Dispose(bool disposing) @@ -162,7 +178,10 @@ namespace Artemis.Core base.Dispose(disposing); } - #endregion + internal void OnLayerBrushUpdated() + { + LayerBrushUpdated?.Invoke(this, EventArgs.Empty); + } private void Initialize() { @@ -190,6 +209,28 @@ namespace Artemis.Core Reset(); } + private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) + { + if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) + DeactivateLayerBrush(); + } + + private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) + { + if (LayerBrush != null || !General.PropertiesInitialized) + return; + + LayerBrushReference? current = General.BrushReference.CurrentValue; + if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && + e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) + ActivateLayerBrush(); + } + + private void OnRenderPropertiesUpdated() + { + RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); + } + #region Storage internal override void Load() @@ -201,6 +242,7 @@ namespace Artemis.Core ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); LoadRenderElement(); + Adapter.Load(); } internal override void Save() @@ -229,11 +271,15 @@ namespace Artemis.Core LedEntity ledEntity = new() { DeviceIdentifier = artemisLed.Device.Identifier, - LedName = artemisLed.RgbLed.Id.ToString() + LedName = artemisLed.RgbLed.Id.ToString(), + PhysicalLayout = artemisLed.Device.DeviceType == RGBDeviceType.Keyboard ? (int) artemisLed.Device.PhysicalLayout : null }; LayerEntity.Leds.Add(ledEntity); } + // Adaption hints + Adapter.Save(); + SaveRenderElement(); } @@ -575,7 +621,7 @@ namespace Artemis.Core if (Disposed) throw new ObjectDisposedException("Layer"); - _leds.AddRange(leds); + _leds.AddRange(leds.Except(_leds)); CalculateRenderProperties(); } @@ -692,51 +738,6 @@ namespace Artemis.Core } #endregion - - #region Event handlers - - private void LayerBrushStoreOnLayerBrushRemoved(object? sender, LayerBrushStoreEvent e) - { - if (LayerBrush?.Descriptor == e.Registration.LayerBrushDescriptor) - DeactivateLayerBrush(); - } - - private void LayerBrushStoreOnLayerBrushAdded(object? sender, LayerBrushStoreEvent e) - { - if (LayerBrush != null || !General.PropertiesInitialized) - return; - - LayerBrushReference? current = General.BrushReference.CurrentValue; - if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && - e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType) - ActivateLayerBrush(); - } - - #endregion - - #region Events - - /// - /// Occurs when a property affecting the rendering properties of this layer has been updated - /// - public event EventHandler? RenderPropertiesUpdated; - - /// - /// Occurs when the layer brush of this layer has been updated - /// - public event EventHandler? LayerBrushUpdated; - - private void OnRenderPropertiesUpdated() - { - RenderPropertiesUpdated?.Invoke(this, EventArgs.Empty); - } - - internal void OnLayerBrushUpdated() - { - LayerBrushUpdated?.Invoke(this, EventArgs.Empty); - } - - #endregion } /// diff --git a/src/Artemis.Core/Models/Profile/LayerAdapter.cs b/src/Artemis.Core/Models/Profile/LayerAdapter.cs new file mode 100644 index 000000000..b60d901f9 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerAdapter.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Entities.Profile.AdaptionHints; +using RGB.NET.Core; + +namespace Artemis.Core +{ + /// + /// Represents an adapter that adapts a layer to a certain set of devices using s + /// + public class LayerAdapter : IStorageModel + { + internal LayerAdapter(Layer layer) + { + Layer = layer; + AdaptionHints = new List(); + } + + /// + /// Gets the layer this adapter can adapt + /// + public Layer Layer { get; } + + /// + /// Gets or sets a list containing the adaption hints used by this adapter + /// + public List AdaptionHints { get; set; } + + /// + /// Modifies the layer, adapting it to the provided + /// + /// The devices to adapt the layer to + public void Adapt(List devices) + { + // Use adaption hints if provided + if (AdaptionHints.Any()) + { + foreach (IAdaptionHint adaptionHint in AdaptionHints) + adaptionHint.Apply(Layer, devices); + } + // If there are no hints, try to find matching LEDs anyway + else + { + List availableLeds = devices.SelectMany(d => d.Leds).ToList(); + List usedLeds = new(); + + foreach (LedEntity ledEntity in Layer.LayerEntity.Leds) + { + // TODO: If this is a keyboard LED and the layouts don't match, convert it before looking for it on the devices + + LedId ledId = Enum.Parse(ledEntity.LedName); + ArtemisLed? led = availableLeds.FirstOrDefault(l => l.RgbLed.Id == ledId); + + if (led != null) + { + availableLeds.Remove(led); + usedLeds.Add(led); + } + } + + Layer.AddLeds(usedLeds); + } + } + + /// + /// Automatically determine hints for this layer + /// + public List DetermineHints(IEnumerable devices) + { + List newHints = new(); + if (devices.All(DoesLayerCoverDevice)) + { + DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } + else + { + // Any fully covered device will add a device adaption hint for that type + foreach (IGrouping deviceLeds in Layer.Leds.GroupBy(l => l.Device)) + { + ArtemisDevice device = deviceLeds.Key; + // If there is already an adaption hint for this type, don't add another + if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.DeviceType)) + continue; + if (DoesLayerCoverDevice(device)) + { + DeviceAdaptionHint hint = new() {DeviceType = device.DeviceType}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } + } + + // Any fully covered category will add a category adaption hint for its category + foreach (DeviceCategory deviceCategory in Enum.GetValues()) + { + if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory)) + continue; + + List categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList(); + if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice)) + { + CategoryAdaptionHint hint = new() {Category = deviceCategory}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } + } + } + + return newHints; + } + + private bool DoesLayerCoverDevice(ArtemisDevice device) + { + return device.Leds.All(l => Layer.Leds.Contains(l)); + } + + #region Implementation of IStorageModel + + /// + public void Load() + { + 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)); + break; + case CategoryAdaptionHintEntity entity: + AdaptionHints.Add(new CategoryAdaptionHint(entity)); + break; + case KeyboardSectionAdaptionHintEntity entity: + AdaptionHints.Add(new KeyboardSectionAdaptionHint(entity)); + break; + } + } + + /// + public void Save() + { + Layer.LayerEntity.AdaptionHints.Clear(); + foreach (IAdaptionHint adaptionHint in AdaptionHints) + Layer.LayerEntity.AdaptionHints.Add(adaptionHint.GetEntry()); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index e5733c8e2..d9ad40690 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -15,7 +15,7 @@ namespace Artemis.Core /// /// /// The type of property encapsulated in this layer property - public class LayerProperty : ILayerProperty + public class LayerProperty : CorePropertyChanged, ILayerProperty { private bool _disposed; @@ -265,6 +265,7 @@ namespace Artemis.Core _baseValue = value; ReapplyUpdate(); + OnPropertyChanged(nameof(BaseValue)); } } diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 09e1dc62e..8bb05cb45 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -14,6 +14,7 @@ namespace Artemis.Core { private readonly object _lock = new(); private bool _isActivated; + private bool _isFreshImport; internal Profile(ProfileModule module, string name) : base(null!) { @@ -57,6 +58,20 @@ namespace Artemis.Core private set => SetAndNotify(ref _isActivated, value); } + /// + /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it + /// since import + /// + /// Note: As long as this is , profile adaption will be performed on load and any surface + /// changes + /// + /// + public bool IsFreshImport + { + get => _isFreshImport; + set => SetAndNotify(ref _isFreshImport, value); + } + /// /// Gets the profile entity this profile uses for persistent storage /// @@ -134,6 +149,16 @@ namespace Artemis.Core layer.PopulateLeds(devices); } + /// + /// Occurs when the profile has been activated. + /// + public event EventHandler? Activated; + + /// + /// Occurs when the profile is being deactivated. + /// + public event EventHandler? Deactivated; + /// protected override void Dispose(bool disposing) { @@ -156,6 +181,7 @@ namespace Artemis.Core throw new ObjectDisposedException("Profile"); Name = ProfileEntity.Name; + IsFreshImport = ProfileEntity.IsFreshImport; lock (ChildrenList) { @@ -171,9 +197,7 @@ namespace Artemis.Core Folder _ = new(this, "Root folder"); } else - { AddChild(new Folder(this, this, rootFolder)); - } } } @@ -186,6 +210,7 @@ namespace Artemis.Core ProfileEntity.ModuleId = Module.Id; ProfileEntity.Name = Name; ProfileEntity.IsActive = IsActivated; + ProfileEntity.IsFreshImport = IsFreshImport; foreach (ProfileElement profileElement in Children) profileElement.Save(); @@ -196,7 +221,7 @@ namespace Artemis.Core ProfileEntity.Layers.Clear(); ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity)); } - + internal void Activate(IEnumerable devices) { lock (_lock) @@ -212,18 +237,6 @@ namespace Artemis.Core } } - #region Events - - /// - /// Occurs when the profile has been activated. - /// - public event EventHandler? Activated; - - /// - /// Occurs when the profile is being deactivated. - /// - public event EventHandler? Deactivated; - private void OnActivated() { Activated?.Invoke(this, EventArgs.Empty); @@ -233,7 +246,5 @@ namespace Artemis.Core { Deactivated?.Invoke(this, EventArgs.Empty); } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs b/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs index b59e963c8..7e5d776e5 100644 --- a/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs +++ b/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs @@ -12,7 +12,7 @@ namespace Artemis.Core internal ProfileDescriptor(ProfileModule profileModule, ProfileEntity profileEntity) { ProfileModule = profileModule; - + Id = profileEntity.Id; Name = profileEntity.Name; IsLastActiveProfile = profileEntity.IsActive; @@ -37,6 +37,5 @@ namespace Artemis.Core /// Gets a boolean indicating whether this was the last active profile /// public bool IsLastActiveProfile { get; } - } } \ 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 ca1ae6094..7f4e8dc3d 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -6,7 +6,6 @@ using Artemis.Core.DeviceProviders; using Artemis.Core.Services; using Artemis.Storage.Entities.Surface; using RGB.NET.Core; -using RGB.NET.Layout; using SkiaSharp; namespace Artemis.Core @@ -36,10 +35,12 @@ namespace Artemis.Core InputIdentifiers = new List(); InputMappings = new Dictionary(); + Categories = new HashSet(); UpdateLeds(); ApplyKeyboardLayout(); ApplyToEntity(); + ApplyDefaultCategories(); CalculateRenderProperties(); } @@ -52,6 +53,7 @@ namespace Artemis.Core InputIdentifiers = new List(); InputMappings = new Dictionary(); + Categories = new HashSet(); foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers) InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier)); @@ -88,6 +90,11 @@ namespace Artemis.Core /// public IRGBDevice RgbDevice { get; } + /// + /// Gets the device type of the ArtemisDevice + /// + public RGBDeviceType DeviceType => RgbDevice.DeviceInfo.DeviceType; + /// /// Gets the device provider that provided this device /// @@ -114,6 +121,11 @@ namespace Artemis.Core /// public Dictionary InputMappings { get; } + /// + /// Gets a list containing the categories of this device + /// + public HashSet Categories { get; } + /// /// Gets or sets the X-position of the device /// @@ -343,6 +355,46 @@ namespace Artemis.Core /// public event EventHandler? DeviceUpdated; + /// + /// Applies the default categories for this device to the list + /// + public void ApplyDefaultCategories() + { + switch (RgbDevice.DeviceInfo.DeviceType) + { + case RGBDeviceType.Keyboard: + case RGBDeviceType.Mouse: + case RGBDeviceType.Headset: + case RGBDeviceType.Mousepad: + case RGBDeviceType.HeadsetStand: + case RGBDeviceType.Keypad: + if (!Categories.Contains(DeviceCategory.Peripherals)) + Categories.Add(DeviceCategory.Peripherals); + break; + case RGBDeviceType.Mainboard: + case RGBDeviceType.GraphicsCard: + case RGBDeviceType.DRAM: + case RGBDeviceType.Fan: + case RGBDeviceType.LedStripe: + case RGBDeviceType.Cooler: + if (!Categories.Contains(DeviceCategory.Case)) + Categories.Add(DeviceCategory.Case); + break; + case RGBDeviceType.Speaker: + if (!Categories.Contains(DeviceCategory.Desk)) + Categories.Add(DeviceCategory.Desk); + break; + case RGBDeviceType.Monitor: + if (!Categories.Contains(DeviceCategory.Monitor)) + Categories.Add(DeviceCategory.Monitor); + break; + case RGBDeviceType.LedMatrix: + if (!Categories.Contains(DeviceCategory.Room)) + Categories.Add(DeviceCategory.Room); + break; + } + } + /// /// Invokes the event /// @@ -373,7 +425,8 @@ namespace Artemis.Core "set to true because the device provider does not support it"); if (layout.IsValid) - layout.RgbLayout!.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); + layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); + UpdateLeds(); @@ -390,17 +443,19 @@ namespace Artemis.Core DeviceEntity.InputIdentifiers.Clear(); foreach (ArtemisDeviceInputIdentifier identifier in InputIdentifiers) - { DeviceEntity.InputIdentifiers.Add(new DeviceInputIdentifierEntity { InputProvider = identifier.InputProvider, Identifier = identifier.Identifier }); - } DeviceEntity.InputMappings.Clear(); foreach (var (original, mapped) in InputMappings) DeviceEntity.InputMappings.Add(new InputMappingEntity {OriginalLedId = (int) original.RgbLed.Id, MappedLedId = (int) mapped.RgbLed.Id}); + + DeviceEntity.Categories.Clear(); + foreach (DeviceCategory deviceCategory in Categories) + DeviceEntity.Categories.Add((int) deviceCategory); } internal void ApplyToRgbDevice() @@ -420,6 +475,12 @@ namespace Artemis.Core if (!RgbDevice.ColorCorrections.Any()) RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this)); + Categories.Clear(); + foreach (int deviceEntityCategory in DeviceEntity.Categories) + Categories.Add((DeviceCategory) deviceEntityCategory); + if (!Categories.Any()) + ApplyDefaultCategories(); + CalculateRenderProperties(); OnDeviceUpdated(); } @@ -472,4 +533,35 @@ namespace Artemis.Core LogicalLayout = DeviceEntity.LogicalLayout; } } + + /// + /// Represents a device category + /// + public enum DeviceCategory + { + /// + /// A device used to light up (part of) the desk + /// + Desk, + + /// + /// A device attached or embedded into the monitor + /// + Monitor, + + /// + /// A device placed or embedded into the case + /// + Case, + + /// + /// A device used to light up (part of) the room + /// + Room, + + /// + /// A peripheral + /// + Peripherals + } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs index 7ff43d6ef..2df270e5b 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using RGB.NET.Core; using RGB.NET.Layout; namespace Artemis.Core @@ -65,6 +66,44 @@ namespace Artemis.Core /// public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!; + /// + /// Applies the layout to the provided device + /// + public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false) + { + device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height)); + device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData; + + HashSet ledIds = new(); + foreach (ILedLayout layoutLed in RgbLayout.Leds) + { + if (Enum.TryParse(layoutLed.Id, true, out LedId ledId)) + { + ledIds.Add(ledId); + + Led? led = device[ledId]; + if (led == null && createMissingLeds) + led = device.AddLed(ledId, new Point(), new Size()); + + if (led != null) + { + led.Location = new Point(MathF.Round(layoutLed.X), MathF.Round(layoutLed.Y)); + led.Size = new Size(MathF.Round(layoutLed.Width), MathF.Round(layoutLed.Height)); + led.Shape = layoutLed.Shape; + led.ShapeData = layoutLed.ShapeData; + led.LayoutMetadata = layoutLed.CustomData; + } + } + } + + if (removeExcessiveLeds) + { + List ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList(); + foreach (LedId led in ledsToRemove) + device.RemoveLed(led); + } + } + internal void ApplyDevice(ArtemisDevice artemisDevice) { Device = artemisDevice; @@ -107,6 +146,58 @@ namespace Artemis.Core else Image = null; } + + internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device) + { + string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts\\Artemis"); + if (device.DeviceType == RGBDeviceType.Keyboard) + { + // XL layout is defined by its programmable macro keys + if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32)) + { + if (device.PhysicalLayout == KeyboardLayoutType.ANSI) + return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis XL keyboard-ANSI.xml", LayoutSource.Default); + return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis XL keyboard-ISO.xml", LayoutSource.Default); + } + + // L layout is defined by its numpad + if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete)) + { + if (device.PhysicalLayout == KeyboardLayoutType.ANSI) + return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis L keyboard-ANSI.xml", LayoutSource.Default); + return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis L keyboard-ISO.xml", LayoutSource.Default); + } + + // No numpad will result in TKL + if (device.PhysicalLayout == KeyboardLayoutType.ANSI) + return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis TKL keyboard-ANSI.xml", LayoutSource.Default); + return new ArtemisLayout(layoutFolder + "\\Keyboard\\Artemis TKL keyboard-ISO.xml", LayoutSource.Default); + } + + // if (device.DeviceType == RGBDeviceType.Mouse) + // { + // if (device.Leds.Count == 1) + // { + // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) + // return new ArtemisLayout(layoutFolder + "\\Mouse\\1 LED mouse logo.xml", LayoutSource.Default); + // return new ArtemisLayout(layoutFolder + "\\Mouse\\1 LED mouse.xml", LayoutSource.Default); + // } + // if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo)) + // return new ArtemisLayout(layoutFolder + "\\Mouse\\4 LED mouse logo.xml", LayoutSource.Default); + // return new ArtemisLayout(layoutFolder + "\\Mouse\\4 LED mouse.xml", LayoutSource.Default); + // } + + if (device.DeviceType == RGBDeviceType.Headset) + { + if (device.Leds.Count == 1) + return new ArtemisLayout(layoutFolder + "\\Headset\\Artemis 1 LED headset.xml", LayoutSource.Default); + if (device.Leds.Count == 2) + return new ArtemisLayout(layoutFolder + "\\Headset\\Artemis 2 LED headset.xml", LayoutSource.Default); + return new ArtemisLayout(layoutFolder + "\\Headset\\Artemis 4 LED headset.xml", LayoutSource.Default); + } + + return null; + } } /// diff --git a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs index 83649847a..099fba242 100644 --- a/src/Artemis.Core/Plugins/DataModelPluginFeature.cs +++ b/src/Artemis.Core/Plugins/DataModelPluginFeature.cs @@ -8,40 +8,6 @@ namespace Artemis.Core /// public abstract class DataModelPluginFeature : PluginFeature { - /// - /// Registers a timed update that whenever the plugin is enabled calls the provided at the - /// provided - /// - /// - /// The interval at which the update should occur - /// - /// The action to call every time the interval has passed. The delta time parameter represents the - /// time passed since the last update in seconds - /// - /// The resulting plugin update registration which can be used to stop the update - public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action) - { - if (action == null) - throw new ArgumentNullException(nameof(action)); - return new TimedUpdateRegistration(this, interval, action); - } - - /// - /// Registers a timed update that whenever the plugin is enabled calls the provided at the - /// provided - /// - /// - /// The interval at which the update should occur - /// - /// The async action to call every time the interval has passed. The delta time parameter - /// represents the time passed since the last update in seconds - /// - /// The resulting plugin update registration - public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction) - { - if (asyncAction == null) - throw new ArgumentNullException(nameof(asyncAction)); - return new TimedUpdateRegistration(this, interval, asyncAction); - } + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index f41e9c145..fc540ff96 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -74,7 +74,7 @@ namespace Artemis.Core.DeviceProviders string filePath = Path.Combine( layoutDir, device.RgbDevice.DeviceInfo.Manufacturer, - device.RgbDevice.DeviceInfo.DeviceType.ToString(), + device.DeviceType.ToString(), device.GetLayoutFileName() ); return new ArtemisLayout(filePath, LayoutSource.Plugin); @@ -91,7 +91,7 @@ namespace Artemis.Core.DeviceProviders string filePath = Path.Combine( layoutDir, device.RgbDevice.DeviceInfo.Manufacturer, - device.RgbDevice.DeviceInfo.DeviceType.ToString(), + device.DeviceType.ToString(), device.GetLayoutFileName() ); return new ArtemisLayout(filePath, LayoutSource.User); diff --git a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs b/src/Artemis.Core/Plugins/IPluginBootstrapper.cs deleted file mode 100644 index af4b607d7..000000000 --- a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Artemis.Core -{ - /// - /// An optional entry point for your plugin - /// - public interface IPluginBootstrapper - { - /// - /// Called when the plugin is activated - /// - /// The plugin instance of your plugin - void Enable(Plugin plugin); - - /// - /// Called when the plugin is deactivated or when Artemis shuts down - /// - /// The plugin instance of your plugin - void Disable(Plugin plugin); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index b2f6536ba..081959517 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Artemis.Core.DataModelExpansions; +using Artemis.Core.Services; +using Artemis.Storage.Entities.Profile; +using Newtonsoft.Json; using SkiaSharp; namespace Artemis.Core.Modules @@ -91,11 +95,16 @@ namespace Artemis.Core.Modules /// public abstract class ProfileModule : Module { + private readonly List _defaultProfilePaths = new(); + private readonly List _pendingDefaultProfilePaths = new(); + private readonly List _defaultProfiles = new(); + private readonly object _lock = new(); + /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// protected internal readonly List HiddenPropertiesList = new(); - private readonly object _lock = new(); + /// /// Creates a new instance of the class @@ -130,6 +139,11 @@ namespace Artemis.Core.Modules /// public bool AnimatingProfileChange { get; private set; } + /// + /// Gets a list of default profiles, to add a new default profile use + /// + internal ReadOnlyCollection DefaultProfiles => _defaultProfiles.AsReadOnly(); + /// /// Called after the profile has updated /// @@ -148,6 +162,72 @@ namespace Artemis.Core.Modules { } + /// + /// Occurs when the has changed + /// + public event EventHandler? ActiveProfileChanged; + + /// + /// Adds a default profile by reading it from the file found at the provided path + /// + /// A path pointing towards a profile file. May be relative to the plugin directory. + /// + /// if the default profile was added; if it was not because it is + /// already in the list. + /// + protected bool AddDefaultProfile(string file) + { + // It can be null if the plugin has not loaded yet... + if (Plugin == null!) + { + if (_pendingDefaultProfilePaths.Contains(file)) + return false; + _pendingDefaultProfilePaths.Add(file); + return true; + } + + if (!Path.IsPathRooted(file)) + file = Plugin.ResolveRelativePath(file); + + if (_defaultProfilePaths.Contains(file)) + return false; + _defaultProfilePaths.Add(file); + + // Ensure the file exists + if (!File.Exists(file)) + throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}."); + // Deserialize and make sure that succeeded + ProfileEntity? profileEntity = JsonConvert.DeserializeObject(File.ReadAllText(file), ProfileService.ExportSettings); + if (profileEntity == null) + throw new ArtemisPluginFeatureException(this, $"Failed to deserialize default profile at {file}."); + // Ensure the profile ID is unique + if (_defaultProfiles.Any(d => d.Id == profileEntity.Id)) + throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {profileEntity.Id} already in use."); + + profileEntity.IsFreshImport = true; + profileEntity.IsActive = false; + _defaultProfiles.Add(profileEntity); + + return true; + } + + /// + /// Invokes the event + /// + protected virtual void OnActiveProfileChanged() + { + ActiveProfileChanged?.Invoke(this, EventArgs.Empty); + } + + internal override void InternalEnable() + { + foreach (string pendingDefaultProfile in _pendingDefaultProfilePaths) + AddDefaultProfile(pendingDefaultProfile); + _pendingDefaultProfilePaths.Clear(); + + base.InternalEnable(); + } + internal override void InternalUpdate(double deltaTime) { StartUpdateMeasure(); @@ -245,22 +325,5 @@ namespace Artemis.Core.Modules base.Deactivate(isDeactivateOverride); Activate(isActivateOverride); } - - #region Events - - /// - /// Occurs when the has changed - /// - public event EventHandler? ActiveProfileChanged; - - /// - /// Invokes the event - /// - protected virtual void OnActiveProfileChanged() - { - ActiveProfileChanged?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index da4e3f7aa..bb6235ac0 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -17,6 +17,7 @@ namespace Artemis.Core public class Plugin : CorePropertyChanged, IDisposable { private readonly List _features; + private readonly List _profilers; private bool _isEnabled; @@ -25,8 +26,10 @@ namespace Artemis.Core Info = info; Directory = directory; Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; + Info.Plugin = this; _features = new List(); + _profilers = new List(); } /// @@ -63,6 +66,8 @@ namespace Artemis.Core /// public ReadOnlyCollection Features => _features.AsReadOnly(); + public ReadOnlyCollection Profilers => _profilers.AsReadOnly(); + /// /// The assembly the plugin code lives in /// @@ -71,7 +76,7 @@ namespace Artemis.Core /// /// Gets the plugin bootstrapper /// - public IPluginBootstrapper? Bootstrapper { get; internal set; } + public PluginBootstrapper? Bootstrapper { get; internal set; } /// /// The Ninject kernel of the plugin @@ -114,6 +119,42 @@ namespace Artemis.Core return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; } + /// + /// Looks up the feature info the feature of type + /// + /// The type of feature to find + /// Feature info of the feature + public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature + { + // This should be a safe assumption because any type of PluginFeature is registered and added + return _features.First(i => i.FeatureType == typeof(T)); + } + + /// + /// Gets a profiler with the provided , if it does not yet exist it will be created. + /// + /// The name of the profiler + /// A new or existing profiler with the provided + public Profiler GetProfiler(string name) + { + Profiler? profiler = _profilers.FirstOrDefault(p => p.Name == name); + if (profiler != null) + return profiler; + + profiler = new Profiler(this, name); + _profilers.Add(profiler); + return profiler; + } + + /// + /// Removes a profiler from the plugin + /// + /// The profiler to remove + public void RemoveProfiler(Profiler profiler) + { + _profilers.Remove(profiler); + } + /// public override string ToString() { @@ -235,12 +276,12 @@ namespace Artemis.Core if (enable) { - Bootstrapper?.Enable(this); + Bootstrapper?.OnPluginEnabled(this); OnEnabled(); } else { - Bootstrapper?.Disable(this); + Bootstrapper?.OnPluginDisabled(this); OnDisabled(); } } diff --git a/src/Artemis.Core/Plugins/PluginBootstrapper.cs b/src/Artemis.Core/Plugins/PluginBootstrapper.cs new file mode 100644 index 000000000..0d41ad36d --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginBootstrapper.cs @@ -0,0 +1,100 @@ +namespace Artemis.Core +{ + /// + /// An optional entry point for your plugin + /// + public abstract class PluginBootstrapper + { + private Plugin? _plugin; + + /// + /// Called when the plugin is loaded + /// + /// + public virtual void OnPluginLoaded(Plugin plugin) + { + } + + /// + /// Called when the plugin is activated + /// + /// The plugin instance of your plugin + public virtual void OnPluginEnabled(Plugin plugin) + { + } + + /// + /// Called when the plugin is deactivated or when Artemis shuts down + /// + /// The plugin instance of your plugin + public virtual void OnPluginDisabled(Plugin plugin) + { + } + + /// + /// Adds the provided prerequisite to the plugin. + /// + /// The prerequisite to add + public void AddPluginPrerequisite(PluginPrerequisite prerequisite) + { + // TODO: We can keep track of them and add them after load, same goes for the others + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + + if (!_plugin.Info.Prerequisites.Contains(prerequisite)) + _plugin.Info.Prerequisites.Add(prerequisite); + } + + /// + /// Removes the provided prerequisite from the plugin. + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite) + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + + return _plugin.Info.Prerequisites.Remove(prerequisite); + } + + /// + /// Adds the provided prerequisite to the feature of type . + /// + /// The prerequisite to add + public void AddFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + + PluginFeatureInfo info = _plugin.GetFeatureInfo(); + if (!info.Prerequisites.Contains(prerequisite)) + info.Prerequisites.Add(prerequisite); + } + + /// + /// Removes the provided prerequisite from the feature of type . + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemoveFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + + return _plugin.GetFeatureInfo().Prerequisites.Remove(prerequisite); + } + + internal void InternalOnPluginLoaded(Plugin plugin) + { + _plugin = plugin; + OnPluginLoaded(plugin); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index 8b9943a0a..983f20438 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Artemis.Storage.Entities.Plugins; @@ -11,8 +10,6 @@ namespace Artemis.Core /// public abstract class PluginFeature : CorePropertyChanged, IDisposable { - private readonly Stopwatch _renderStopwatch = new(); - private readonly Stopwatch _updateStopwatch = new(); private bool _isEnabled; private Exception? _loadException; @@ -26,6 +23,11 @@ namespace Artemis.Core /// public Plugin Plugin { get; internal set; } = null!; // Will be set right after construction + /// + /// Gets the profiler that can be used to take profiling measurements + /// + public Profiler Profiler { get; internal set; } = null!; // Will be set right after construction + /// /// Gets whether the plugin is enabled /// @@ -49,16 +51,6 @@ namespace Artemis.Core /// public string Id => $"{GetType().FullName}-{Plugin.Guid.ToString().Substring(0, 8)}"; // Not as unique as a GUID but good enough and stays readable - /// - /// Gets the last measured update time of the feature - /// - public TimeSpan UpdateTime { get; private set; } - - /// - /// Gets the last measured render time of the feature - /// - public TimeSpan RenderTime { get; private set; } - internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction /// @@ -111,24 +103,22 @@ namespace Artemis.Core internal void StartUpdateMeasure() { - _updateStopwatch.Start(); + Profiler.StartMeasurement("Update"); } internal void StopUpdateMeasure() { - UpdateTime = _updateStopwatch.Elapsed; - _updateStopwatch.Reset(); + Profiler.StopMeasurement("Update"); } internal void StartRenderMeasure() { - _renderStopwatch.Start(); + Profiler.StartMeasurement("Render"); } internal void StopRenderMeasure() { - RenderTime = _renderStopwatch.Elapsed; - _renderStopwatch.Reset(); + Profiler.StopMeasurement("Render"); } internal void SetEnabled(bool enable, bool isAutoEnable = false) @@ -241,5 +231,47 @@ namespace Artemis.Core } #endregion + + #region Timed updates + + /// + /// Registers a timed update that whenever the plugin is enabled calls the provided at the + /// provided + /// + /// + /// The interval at which the update should occur + /// + /// The action to call every time the interval has passed. The delta time parameter represents the + /// time passed since the last update in seconds + /// + /// An optional name used in exceptions and profiling + /// The resulting plugin update registration which can be used to stop the update + public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Action action, string? name = null) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + return new TimedUpdateRegistration(this, interval, action, name); + } + + /// + /// Registers a timed update that whenever the plugin is enabled calls the provided at the + /// provided + /// + /// + /// The interval at which the update should occur + /// + /// The async action to call every time the interval has passed. The delta time parameter + /// represents the time passed since the last update in seconds + /// + /// An optional name used in exceptions and profiling + /// The resulting plugin update registration + public TimedUpdateRegistration AddTimedUpdate(TimeSpan interval, Func asyncAction, string? name = null) + { + if (asyncAction == null) + throw new ArgumentNullException(nameof(asyncAction)); + return new TimedUpdateRegistration(this, interval, asyncAction, name); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 47f34cbaa..6806db862 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; +using System.Linq; using Artemis.Core.DataModelExpansions; using Artemis.Core.DeviceProviders; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.Modules; +using Artemis.Storage.Entities.Plugins; using Humanizer; using Newtonsoft.Json; @@ -13,23 +16,24 @@ namespace Artemis.Core /// Represents basic info about a plugin feature and contains a reference to the instance of said feature /// [JsonObject(MemberSerialization.OptIn)] - public class PluginFeatureInfo : CorePropertyChanged + public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject { private string? _description; private string? _icon; private PluginFeature? _instance; private string _name = null!; - internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureAttribute? attribute) + internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute) { Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType)); + Entity = pluginFeatureEntity; Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title); Description = attribute?.Description; Icon = attribute?.Icon; AlwaysEnabled = attribute?.AlwaysEnabled ?? false; - + if (Icon != null) return; if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType)) Icon = "TableAdd"; @@ -46,7 +50,7 @@ namespace Artemis.Core else Icon = "Plugin"; } - + internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance) { if (instance == null) throw new ArgumentNullException(nameof(instance)); @@ -119,6 +123,11 @@ namespace Artemis.Core [JsonProperty] public bool AlwaysEnabled { get; } + /// + /// Gets a boolean indicating whether the feature is enabled in persistent storage + /// + public bool EnabledInStorage => Entity.IsEnabled; + /// /// Gets the feature this info is associated with /// @@ -128,6 +137,14 @@ namespace Artemis.Core internal set => SetAndNotify(ref _instance, value); } + /// + public List Prerequisites { get; } = new(); + + /// + public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); + + internal PluginFeatureEntity Entity { get; } + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index fcbeacd07..5037b186b 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using Newtonsoft.Json; namespace Artemis.Core @@ -8,17 +10,21 @@ namespace Artemis.Core /// Represents basic info about a plugin and contains a reference to the instance of said plugin /// [JsonObject(MemberSerialization.OptIn)] - public class PluginInfo : CorePropertyChanged + public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject { - private bool _autoEnableFeatures = true; - private string? _description; private Guid _guid; + private string? _description; + private string? _author; + private Uri? _website; + private Uri? _repository; private string? _icon; private string _main = null!; + private bool _autoEnableFeatures = true; private string _name = null!; private Plugin _plugin = null!; - private bool _requiresAdmin; private Version _version = null!; + private bool _requiresAdmin; + internal PluginInfo() { @@ -54,10 +60,39 @@ namespace Artemis.Core set => SetAndNotify(ref _description, value); } + /// + /// Gets or sets the author of this plugin + /// + [JsonProperty] + public string? Author + { + get => _author; + set => SetAndNotify(ref _author, value); + } + + /// + /// Gets or sets the website of this plugin or its author + /// + [JsonProperty] + public Uri? Website + { + get => _website; + set => SetAndNotify(ref _website, value); + } + + /// + /// Gets or sets the repository of this plugin + /// + [JsonProperty] + public Uri? Repository + { + get => _repository; + set => SetAndNotify(ref _repository, value); + } + /// /// The plugins display icon that's shown in the settings see for - /// available - /// icons + /// available icons /// [JsonProperty] public string? Icon @@ -107,7 +142,7 @@ namespace Artemis.Core get => _requiresAdmin; internal set => SetAndNotify(ref _requiresAdmin, value); } - + /// /// Gets the plugin this info is associated with /// @@ -117,10 +152,21 @@ namespace Artemis.Core internal set => SetAndNotify(ref _plugin, value); } + internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}"; + /// public override string ToString() { return $"{Name} v{Version} - {Guid}"; } + + /// + public List Prerequisites { get; } = new(); + + /// + public bool ArePrerequisitesMet() + { + return Prerequisites.All(p => p.IsMet()); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs new file mode 100644 index 000000000..5401455ed --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core +{ + /// + /// Represents a type that has prerequisites + /// + public interface IPrerequisitesSubject + { + /// + /// Gets a list of prerequisites for this plugin + /// + List Prerequisites { get; } + + /// + /// Determines whether the prerequisites of this plugin are met + /// + bool ArePrerequisitesMet(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs new file mode 100644 index 000000000..57c4850a7 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a prerequisite for a or + /// + public abstract class PluginPrerequisite : CorePropertyChanged + { + private PluginPrerequisiteAction? _currentAction; + + /// + /// Gets the name of the prerequisite + /// + public abstract string Name { get; } + + /// + /// Gets the description of the prerequisite + /// + public abstract string Description { get; } + + /// + /// [NYI] Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges + /// + public abstract bool RequiresElevation { get; } + + /// + /// Gets a list of actions to execute when is called + /// + public abstract List InstallActions { get; } + + /// + /// Gets a list of actions to execute when is called + /// + public abstract List UninstallActions { get; } + + /// + /// Gets or sets the action currently being executed + /// + public PluginPrerequisiteAction? CurrentAction + { + get => _currentAction; + private set => SetAndNotify(ref _currentAction, value); + } + + /// + /// Execute all install actions + /// + public async Task Install(CancellationToken cancellationToken) + { + try + { + OnInstallStarting(); + foreach (PluginPrerequisiteAction installAction in InstallActions) + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentAction = installAction; + await installAction.Execute(cancellationToken); + } + } + finally + { + CurrentAction = null; + OnInstallFinished(); + } + } + + /// + /// Execute all uninstall actions + /// + public async Task Uninstall(CancellationToken cancellationToken) + { + try + { + OnUninstallStarting(); + foreach (PluginPrerequisiteAction uninstallAction in UninstallActions) + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentAction = uninstallAction; + await uninstallAction.Execute(cancellationToken); + } + } + finally + { + CurrentAction = null; + OnUninstallFinished(); + } + } + + /// + /// Called to determine whether the prerequisite is met + /// + /// if the prerequisite is met; otherwise + public abstract bool IsMet(); + + /// + /// Called before installation starts + /// + protected virtual void OnInstallStarting() + { + } + + /// + /// Called after installation finishes + /// + protected virtual void OnInstallFinished() + { + } + + /// + /// Called before uninstall starts + /// + protected virtual void OnUninstallStarting() + { + } + + /// + /// Called after uninstall finished + /// + protected virtual void OnUninstallFinished() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs new file mode 100644 index 000000000..90d9a787e --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents an action that must be taken to install or uninstall a plugin prerequisite + /// + public abstract class PluginPrerequisiteAction : CorePropertyChanged + { + private bool _progressIndeterminate; + private bool _showProgressBar; + private bool _showSubProgressBar; + private string? _status; + private bool _subProgressIndeterminate; + + /// + /// The base constructor for all plugin prerequisite actions + /// + /// The name of the action + protected PluginPrerequisiteAction(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + #region Implementation of IPluginPrerequisiteAction + + /// + /// Gets the name of the action + /// + public string Name { get; } + + /// + /// Gets or sets the status of the action + /// + public string? Status + { + get => _status; + set => SetAndNotify(ref _status, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress is indeterminate or not + /// + public bool ProgressIndeterminate + { + get => _progressIndeterminate; + set => SetAndNotify(ref _progressIndeterminate, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress is indeterminate or not + /// + public bool SubProgressIndeterminate + { + get => _subProgressIndeterminate; + set => SetAndNotify(ref _subProgressIndeterminate, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress bar should be shown + /// + public bool ShowProgressBar + { + get => _showProgressBar; + set => SetAndNotify(ref _showProgressBar, value); + } + + /// + /// Gets or sets a boolean indicating whether the sub progress bar should be shown + /// + public bool ShowSubProgressBar + { + get => _showSubProgressBar; + set => SetAndNotify(ref _showSubProgressBar, value); + } + + /// + /// Gets or sets the progress of the action (0 to 100) + /// + public PrerequisiteActionProgress Progress { get; } = new(); + + /// + /// Gets or sets the sub progress of the action + /// + public PrerequisiteActionProgress SubProgress { get; } = new(); + + /// + /// Called when the action must execute + /// + public abstract Task Execute(CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs new file mode 100644 index 000000000..6602567e0 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Humanizer; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class CopyFolderAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The source folder to copy + /// The target folder to copy to (will be created if needed) + public CopyFolderAction(string name, string source, string target) : base(name) + { + Source = source; + Target = target; + + ShowProgressBar = true; + ShowSubProgressBar = true; + } + + /// + /// Gets the source directory + /// + public string Source { get; } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + DirectoryInfo source = new(Source); + DirectoryInfo target = new(Target); + + if (!source.Exists) + throw new ArtemisCoreException($"The source directory at '{source}' was not found."); + + int filesCopied = 0; + FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories); + + foreach (FileInfo fileInfo in files) + { + string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName); + string outputDir = Path.GetDirectoryName(outputPath)!; + Utilities.CreateAccessibleDirectory(outputDir); + + void SubProgressOnProgressReported(object? sender, EventArgs e) + { + if (SubProgress.ProgressPerSecond != 0) + Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; + else + Status = $"Copying {fileInfo.Name}"; + } + + Progress.Report((filesCopied, files.Length)); + SubProgress.ProgressReported += SubProgressOnProgressReported; + + await using FileStream sourceStream = fileInfo.OpenRead(); + await using FileStream destinationStream = File.Create(outputPath); + + await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken); + + filesCopied++; + SubProgress.ProgressReported -= SubProgressOnProgressReported; + } + + Progress.Report((filesCopied, files.Length)); + Status = $"Finished copying {filesCopied} file(s)"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs new file mode 100644 index 000000000..e6324bf8f --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that deletes a file + /// + public class DeleteFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target folder to delete recursively + public DeleteFileAction(string name, string target) : base(name) + { + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => + { + if (File.Exists(Target)) + File.Delete(Target); + }, cancellationToken); + + ShowProgressBar = false; + Status = $"Removed {Target}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs new file mode 100644 index 000000000..62b4dfc41 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that recursively deletes a folder + /// + public class DeleteFolderAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target folder to delete recursively + public DeleteFolderAction(string name, string target) : base(name) + { + if (Enum.GetValues().Select(Environment.GetFolderPath).Contains(target)) + throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose."); + + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => + { + if (Directory.Exists(Target)) + Directory.Delete(Target, true); + }, cancellationToken); + + ShowProgressBar = false; + Status = $"Removed {Target}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs new file mode 100644 index 000000000..0707abfd6 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Humanizer; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that downloads a file + /// + public class DownloadFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The source URL to download + /// The target file to save as (will be created if needed) + public DownloadFileAction(string name, string url, string fileName) : base(name) + { + Url = url ?? throw new ArgumentNullException(nameof(url)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + + ShowProgressBar = true; + } + + /// + /// Gets the source URL to download + /// + public string Url { get; } + + /// + /// Gets the target file to save as (will be created if needed) + /// + public string FileName { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + using HttpClient client = new(); + await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate); + + void ProgressOnProgressReported(object? sender, EventArgs e) + { + if (Progress.ProgressPerSecond != 0) + Status = $"Downloading {Url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; + else + Status = $"Downloading {Url}"; + } + + Progress.ProgressReported += ProgressOnProgressReported; + + // Get the http headers first to examine the content length + using HttpResponseMessage response = await client.GetAsync(Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken); + long? contentLength = response.Content.Headers.ContentLength; + + // Ignore progress reporting when no progress reporter was + // passed or when the content length is unknown + if (!contentLength.HasValue) + { + ProgressIndeterminate = true; + await download.CopyToAsync(destinationStream, Progress, cancellationToken); + ProgressIndeterminate = false; + } + else + { + ProgressIndeterminate = false; + await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + + Progress.ProgressReported -= ProgressOnProgressReported; + Progress.Report((1, 1)); + Status = "Finished downloading"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs new file mode 100644 index 000000000..fcfc4db19 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs @@ -0,0 +1,112 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that executes a file + /// + public class ExecuteFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of + /// + /// The name of the action + /// The target file to execute + /// A set of command-line arguments to use when starting the application + /// A boolean indicating whether the action should wait for the process to exit + /// A boolean indicating whether the file should run with administrator privileges (does not require ) + public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Arguments = arguments; + WaitForExit = waitForExit; + Elevate = elevate; + } + + /// + /// Gets the target file to execute + /// + public string FileName { get; } + + /// + /// Gets a set of command-line arguments to use when starting the application + /// + public string? Arguments { get; } + + /// + /// Gets a boolean indicating whether the action should wait for the process to exit + /// + public bool WaitForExit { get; } + + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + if (WaitForExit) + { + Status = $"Running {FileName} and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; + + int result = await RunProcessAsync(FileName, Arguments, Elevate); + + Status = $"{FileName} exited with code {result}"; + } + else + { + Status = $"Running {FileName}"; + Process process = new() + { + StartInfo = {FileName = FileName, Arguments = Arguments!}, + EnableRaisingEvents = true + }; + process.Start(); + } + } + + private static Task RunProcessAsync(string fileName, string? arguments, bool elevate) + { + TaskCompletionSource tcs = new(); + + Process process = new() + { + StartInfo = + { + FileName = fileName, + Arguments = arguments!, + Verb = elevate ? "RunAs" : "", + UseShellExecute = elevate + }, + EnableRaisingEvents = true + }; + + process.Exited += (_, _) => + { + tcs.SetResult(process.ExitCode); + process.Dispose(); + }; + + try + { + process.Start(); + } + catch (Win32Exception e) + { + if (!elevate || e.NativeErrorCode != 0x4c7) + throw; + tcs.SetResult(-1); + } + + + return tcs.Task; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs new file mode 100644 index 000000000..b09f076b9 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that extracts a ZIP file + /// + public class ExtractArchiveAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of . + /// + /// The name of the action + /// The ZIP file to extract + /// The folder into which to extract the file + public ExtractArchiveAction(string name, string fileName, string target) : base(name) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + ShowProgressBar = true; + } + + /// + /// Gets the file to extract + /// + public string FileName { get; } + + /// + /// Gets the folder into which to extract the file + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + using HttpClient client = new(); + + ShowSubProgressBar = true; + Status = $"Extracting {FileName}"; + + Utilities.CreateAccessibleDirectory(Target); + + await using (FileStream fileStream = new(FileName, FileMode.Open)) + { + ZipArchive archive = new(fileStream); + long count = 0; + foreach (ZipArchiveEntry entry in archive.Entries) + { + await using Stream unzippedEntryStream = entry.Open(); + Progress.Report((count, archive.Entries.Count)); + if (entry.Length > 0) + { + string path = Path.Combine(Target, entry.FullName); + CreateDirectoryForFile(path); + await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate); + await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken); + } + + count++; + } + } + + Progress.Report((1, 1)); + ShowSubProgressBar = false; + Status = "Finished extracting"; + } + + private static void CreateDirectoryForFile(string path) + { + string? directory = Path.GetDirectoryName(path); + if (directory == null) + throw new ArtemisCoreException($"Failed to get directory from path {path}"); + + Utilities.CreateAccessibleDirectory(directory); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs new file mode 100644 index 000000000..92c7d0294 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class WriteBytesToFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteBytesToFileAction(string name, string target, byte[] content) : base(name) + { + Target = target; + ByteContent = content ?? throw new ArgumentNullException(nameof(content)); + } + + /// + /// Gets or sets the target file + /// + public string Target { get; } + + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; + + /// + /// Gets the bytes that will be written + /// + public byte[] ByteContent { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); + + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; + + if (!Append && File.Exists(Target)) + File.Delete(Target); + + await using Stream fileStream = File.OpenWrite(Target); + await using MemoryStream sourceStream = new(ByteContent); + await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken); + + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs new file mode 100644 index 000000000..e4e6a9875 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class WriteStringToFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteStringToFileAction(string name, string target, string content) : base(name) + { + Target = target; + Content = content ?? throw new ArgumentNullException(nameof(content)); + + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target file + /// + public string Target { get; } + + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; + + /// + /// Gets the string that will be written + /// + public string Content { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); + + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; + + if (Append) + await File.AppendAllTextAsync(Target, Content, cancellationToken); + else + await File.WriteAllTextAsync(Target, Content, cancellationToken); + + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs new file mode 100644 index 000000000..96b48bde3 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs @@ -0,0 +1,91 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents progress on a plugin prerequisite action + /// + public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)> + { + private long _current; + private DateTime _lastReport; + private double _percentage; + private double _progressPerSecond; + private long _total; + private long _lastReportValue; + + /// + /// The current amount + /// + public long Current + { + get => _current; + set => SetAndNotify(ref _current, value); + } + + /// + /// The total amount + /// + public long Total + { + get => _total; + set => SetAndNotify(ref _total, value); + } + + /// + /// The percentage + /// + public double Percentage + { + get => _percentage; + set => SetAndNotify(ref _percentage, value); + } + + /// + /// Gets or sets the progress per second + /// + public double ProgressPerSecond + { + get => _progressPerSecond; + set => SetAndNotify(ref _progressPerSecond, value); + } + + #region Implementation of IProgress + + /// + public void Report((long, long) value) + { + (long newCurrent, long newTotal) = value; + + TimeSpan timePassed = DateTime.Now - _lastReport; + if (timePassed >= TimeSpan.FromSeconds(1)) + { + ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2)); + _lastReportValue = newCurrent; + _lastReport = DateTime.Now; + } + + Current = newCurrent; + Total = newTotal; + Percentage = Math.Round((double) Current / Total * 100.0, 2); + + OnProgressReported(); + } + + #endregion + + #region Events + + /// + /// Occurs when progress has been reported + /// + public event EventHandler? ProgressReported; + + protected virtual void OnProgressReported() + { + ProgressReported?.Invoke(this, EventArgs.Empty); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Profiling/Profiler.cs b/src/Artemis.Core/Plugins/Profiling/Profiler.cs new file mode 100644 index 000000000..c424c19f1 --- /dev/null +++ b/src/Artemis.Core/Plugins/Profiling/Profiler.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Artemis.Core +{ + /// + /// Represents a profiler that can measure time between calls distinguished by identifiers + /// + public class Profiler + { + internal Profiler(Plugin plugin, string name) + { + Plugin = plugin; + Name = name; + } + + /// + /// Gets the plugin this profiler belongs to + /// + public Plugin Plugin { get; } + + /// + /// Gets the name of this profiler + /// + public string Name { get; } + + + /// + /// Gets a dictionary containing measurements by their identifiers + /// + public Dictionary Measurements { get; set; } = new(); + + /// + /// Starts measuring time for the provided + /// + /// A unique identifier for this measurement + public void StartMeasurement(string identifier) + { + lock (Measurements) + { + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + { + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); + } + + measurement.Start(); + } + } + + /// + /// Stops measuring time for the provided + /// + /// A unique identifier for this measurement + /// The number of ticks that passed since the call with the same identifier + public long StopMeasurement(string identifier) + { + long lockRequestedAt = Stopwatch.GetTimestamp(); + lock (Measurements) + { + if (!Measurements.TryGetValue(identifier, out ProfilingMeasurement? measurement)) + { + measurement = new ProfilingMeasurement(identifier); + Measurements.Add(identifier, measurement); + } + + return measurement.Stop(Stopwatch.GetTimestamp() - lockRequestedAt); + } + } + + /// + /// Clears measurements with the the provided + /// + /// + public void ClearMeasurements(string identifier) + { + lock (Measurements) + { + Measurements.Remove(identifier); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs new file mode 100644 index 000000000..eed3a391d --- /dev/null +++ b/src/Artemis.Core/Plugins/Profiling/ProfilingMeasurement.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Artemis.Core +{ + /// + /// Represents a set of profiling measurements + /// + public class ProfilingMeasurement + { + private bool _filledArray; + private int _index; + private long _last; + private bool _open; + private long _start; + + internal ProfilingMeasurement(string identifier) + { + Identifier = identifier; + } + + /// + /// Gets the unique identifier of this measurement + /// + public string Identifier { get; } + + /// + /// Gets the last 1000 measurements + /// + public long[] Measurements { get; } = new long[1000]; + + /// + /// Starts measuring time until is called + /// + public void Start() + { + _start = Stopwatch.GetTimestamp(); + _open = true; + } + + /// + /// Stops measuring time and stores the time passed in the list + /// + /// An optional correction in ticks to subtract from the measurement + /// The time passed since the last call + public long Stop(long correction = 0) + { + if (!_open) + return 0; + + long difference = Stopwatch.GetTimestamp() - _start - correction; + _open = false; + Measurements[_index] = difference; + + _index++; + if (_index >= 1000) + { + _filledArray = true; + _index = 0; + } + + _last = difference; + return difference; + } + + /// + /// Gets the last measured time + /// + public TimeSpan GetLast() + { + return new(_last); + } + + /// + /// Gets the average time of the last 1000 measurements + /// + public TimeSpan GetAverage() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + return _filledArray + ? new TimeSpan((long) Measurements.Average(m => m)) + : new TimeSpan((long) Measurements.Take(_index).Average(m => m)); + } + + /// + /// Gets the min time of the last 1000 measurements + /// + public TimeSpan GetMin() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + return _filledArray + ? new TimeSpan(Measurements.Min()) + : new TimeSpan(Measurements.Take(_index).Min()); + } + + /// + /// Gets the max time of the last 1000 measurements + /// + public TimeSpan GetMax() + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + return _filledArray + ? new TimeSpan(Measurements.Max()) + : new TimeSpan(Measurements.Take(_index).Max()); + } + + /// + /// Gets the nth percentile of the last 1000 measurements + /// + public TimeSpan GetPercentile(double percentile) + { + if (!_filledArray && _index == 0) + return TimeSpan.Zero; + + long[] collection = _filledArray + ? Measurements.OrderBy(l => l).ToArray() + : Measurements.Take(_index).OrderBy(l => l).ToArray(); + + return new TimeSpan((long) Percentile(collection, percentile)); + } + + private static double Percentile(long[] elements, double percentile) + { + Array.Sort(elements); + double realIndex = percentile * (elements.Length - 1); + int index = (int) realIndex; + double frac = realIndex - index; + if (index + 1 < elements.Length) + return elements[index] * (1 - frac) + elements[index + 1] * frac; + return elements[index]; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs index f93225481..39d5ad8cb 100644 --- a/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs +++ b/src/Artemis.Core/Plugins/TimedUpdateRegistration.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; using Artemis.Core.Services; +using Humanizer; using Ninject; using Serilog; @@ -13,19 +14,20 @@ namespace Artemis.Core /// public class TimedUpdateRegistration : IDisposable { - private DateTime _lastEvent; - private Timer? _timer; - private bool _disposed; private readonly object _lock = new(); - private ILogger _logger; + private bool _disposed; + private DateTime _lastEvent; + private readonly ILogger _logger; + private Timer? _timer; - internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action action) + internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Action action, string? name) { _logger = CoreService.Kernel.Get(); Feature = feature; Interval = interval; Action = action; + Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}"; Feature.Enabled += FeatureOnEnabled; Feature.Disabled += FeatureOnDisabled; @@ -33,13 +35,14 @@ namespace Artemis.Core Start(); } - internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction) + internal TimedUpdateRegistration(PluginFeature feature, TimeSpan interval, Func asyncAction, string? name) { _logger = CoreService.Kernel.Get(); - + Feature = feature; Interval = interval; AsyncAction = asyncAction; + Name = name ?? $"TimedUpdate-{Guid.NewGuid().ToString().Substring(0, 8)}"; Feature.Enabled += FeatureOnEnabled; Feature.Disabled += FeatureOnDisabled; @@ -69,7 +72,12 @@ namespace Artemis.Core public Func? AsyncAction { get; } /// - /// Starts calling the or at the configured + /// Gets the name of this timed update + /// + public string Name { get; } + + /// + /// Starts calling the or at the configured /// Note: Called automatically when the plugin enables /// public void Start() @@ -93,7 +101,7 @@ namespace Artemis.Core } /// - /// Stops calling the or at the configured + /// Stops calling the or at the configured /// Note: Called automatically when the plugin disables /// public void Stop() @@ -113,49 +121,6 @@ namespace Artemis.Core } } - private void TimerOnElapsed(object? sender, ElapsedEventArgs e) - { - if (!Feature.IsEnabled) - return; - - lock (_lock) - { - TimeSpan interval = DateTime.Now - _lastEvent; - _lastEvent = DateTime.Now; - - // Modules don't always want to update, honor that - if (Feature is Module module && !module.IsUpdateAllowed) - return; - - try - { - if (Action != null) - Action(interval.TotalSeconds); - else if (AsyncAction != null) - { - Task task = AsyncAction(interval.TotalSeconds); - task.Wait(); - } - } - catch (Exception exception) - { - _logger.Error(exception, "Timed update uncaught exception in plugin {plugin}", Feature.Plugin); - } - } - } - - private void FeatureOnEnabled(object? sender, EventArgs e) - { - Start(); - } - - private void FeatureOnDisabled(object? sender, EventArgs e) - { - Stop(); - } - - #region IDisposable - /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// @@ -176,13 +141,70 @@ namespace Artemis.Core } } + private void TimerOnElapsed(object? sender, ElapsedEventArgs e) + { + if (!Feature.IsEnabled) + return; + + lock (_lock) + { + Feature.Profiler.StartMeasurement(ToString()); + + TimeSpan interval = DateTime.Now - _lastEvent; + _lastEvent = DateTime.Now; + + // Modules don't always want to update, honor that + if (Feature is Module module && !module.IsUpdateAllowed) + return; + + try + { + if (Action != null) + { + Action(interval.TotalSeconds); + } + else if (AsyncAction != null) + { + Task task = AsyncAction(interval.TotalSeconds); + task.Wait(); + } + } + catch (Exception exception) + { + _logger.Error(exception, "{timedUpdate} uncaught exception in plugin {plugin}", this, Feature.Plugin); + } + finally + { + Feature.Profiler.StopMeasurement(ToString()); + } + } + } + + private void FeatureOnEnabled(object? sender, EventArgs e) + { + Start(); + } + + private void FeatureOnDisabled(object? sender, EventArgs e) + { + Stop(); + } + /// public void Dispose() { Dispose(true); + Feature.Profiler.ClearMeasurements(ToString()); + GC.SuppressFinalize(this); } - #endregion + /// + public sealed override string ToString() + { + if (Interval.TotalSeconds >= 1) + return $"{Name} ({Interval.TotalSeconds} sec)"; + return $"{Name} ({Interval.TotalMilliseconds} ms)"; + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 28f590376..f907bd0e8 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -119,11 +119,24 @@ namespace Artemis.Core.Services try { _frameStopWatch.Restart(); + + // Render all active modules + SKTexture texture = _rgbService.OpenRender(); + lock (_dataModelExpansions) { // Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled)) - dataModelExpansion.InternalUpdate(args.DeltaTime); + { + try + { + dataModelExpansion.InternalUpdate(args.DeltaTime); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + } } List modules; @@ -137,10 +150,16 @@ namespace Artemis.Core.Services // Update all active modules foreach (Module module in modules) - module.InternalUpdate(args.DeltaTime); - - // Render all active modules - SKTexture texture = _rgbService.OpenRender(); + { + try + { + module.InternalUpdate(args.DeltaTime); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + } SKCanvas canvas = texture.Surface.Canvas; canvas.Save(); @@ -152,7 +171,16 @@ namespace Artemis.Core.Services if (!ModuleRenderingDisabled) { foreach (Module module in modules.Where(m => m.IsActivated)) - module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo); + { + try + { + module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo); + } + catch (Exception e) + { + _updateExceptions.Add(e); + } + } } OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index bed2d47de..abb7ef643 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -76,8 +76,8 @@ namespace Artemis.Core.Services public void IdentifyDevice(ArtemisDevice device) { - if (device.RgbDevice.DeviceInfo.DeviceType != RGBDeviceType.Keyboard && device.RgbDevice.DeviceInfo.DeviceType != RGBDeviceType.Mouse) - throw new ArtemisCoreException($"Cannot initialize input-identification for a device of type {device.RgbDevice.DeviceInfo.DeviceType}. \r\n" + + if (device.DeviceType != RGBDeviceType.Keyboard && device.DeviceType != RGBDeviceType.Mouse) + throw new ArtemisCoreException($"Cannot initialize input-identification for a device of type {device.DeviceType}. \r\n" + "Only keyboard and mouse is supported."); _identifyingDevice = device; @@ -121,7 +121,7 @@ namespace Artemis.Core.Services { if (_cachedFallbackKeyboard != null) return _cachedFallbackKeyboard; - _cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard); + _cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard); return _cachedFallbackKeyboard; } @@ -129,7 +129,7 @@ namespace Artemis.Core.Services { if (_cachedFallbackMouse != null) return _cachedFallbackMouse; - _cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse); + _cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse); return _cachedFallbackMouse; } @@ -165,8 +165,8 @@ namespace Artemis.Core.Services { // Don't match if there is no device or if the device type differs from the event device type if (_identifyingDevice == null || - _identifyingDevice.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard && e.DeviceType == InputDeviceType.Mouse || - _identifyingDevice.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse && e.DeviceType == InputDeviceType.Keyboard) + _identifyingDevice.DeviceType == RGBDeviceType.Keyboard && e.DeviceType == InputDeviceType.Mouse || + _identifyingDevice.DeviceType == RGBDeviceType.Mouse && e.DeviceType == InputDeviceType.Keyboard) return; if (!(sender is InputProvider inputProvider)) return; diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index cfcb5ffb0..10ec9fa1b 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -83,7 +83,7 @@ namespace Artemis.Core.Services /// /// The device to apply the best available layout to /// The layout that was applied to the device - ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device); + ArtemisLayout? ApplyBestDeviceLayout(ArtemisDevice device); /// /// Apples the provided to the provided diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index a6f85bcfd..22bc8299c 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; +using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; using Serilog; using Timer = System.Timers.Timer; @@ -17,13 +18,15 @@ namespace Artemis.Core.Services private static readonly SemaphoreSlim ActiveModuleSemaphore = new(1, 1); private readonly ILogger _logger; private readonly IModuleRepository _moduleRepository; + private readonly IProfileRepository _profileRepository; private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; - public ModuleService(ILogger logger, IModuleRepository moduleRepository, IPluginManagementService pluginManagementService, IProfileService profileService) + public ModuleService(ILogger logger, IModuleRepository moduleRepository, IProfileRepository profileRepository, IPluginManagementService pluginManagementService, IProfileService profileService) { _logger = logger; _moduleRepository = moduleRepository; + _profileRepository = profileRepository; _pluginManagementService = pluginManagementService; _profileService = profileService; _pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled; @@ -45,12 +48,24 @@ namespace Artemis.Core.Services { try { + ProfileModule? profileModule = module as ProfileModule; + + if (profileModule != null && profileModule.DefaultProfiles.Any()) + { + List descriptors = _profileService.GetProfileDescriptors(profileModule); + foreach (ProfileEntity defaultProfile in profileModule.DefaultProfiles) + { + if (descriptors.All(d => d.Id != defaultProfile.Id)) + _profileRepository.Add(defaultProfile); + } + } + module.Activate(false); try { // If this is a profile module, activate the last active profile after module activation - if (module is ProfileModule profileModule) + if (profileModule != null) await _profileService.ActivateLastProfileAnimated(profileModule); } catch (Exception e) diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 4f833282d..5655b8112 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -39,9 +39,9 @@ namespace Artemis.Core.Services ProcessQueuedActions(); } - private void CopyBuiltInPlugin(FileInfo zipFileInfo, ZipArchive zipArchive) + private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) { - DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", Path.GetFileNameWithoutExtension(zipFileInfo.Name))); + DirectoryInfo pluginDirectory = new(Path.Combine(Constants.DataFolder, "plugins", targetDirectory)); bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); // Remove the old directory if it exists @@ -81,12 +81,18 @@ namespace Artemis.Core.Services using StreamReader reader = new(metaDataFileEntry.Open()); PluginInfo builtInPluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; + string preferred = builtInPluginInfo.PreferredPluginDirectory; + string oldPreferred = Path.GetFileNameWithoutExtension(zipFile.Name); + // Rename folders to the new format + // TODO: Get rid of this eventually, it's nice to keep around but it's extra IO that's best avoided + if (pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == oldPreferred) != null) + Directory.Move(Path.Combine(pluginDirectory.FullName, oldPreferred), Path.Combine(pluginDirectory.FullName, preferred)); // Find the matching plugin in the plugin folder - DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == Path.GetFileNameWithoutExtension(zipFile.Name)); + DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred); if (match == null) { - CopyBuiltInPlugin(zipFile, archive); + CopyBuiltInPlugin(archive, preferred); } else { @@ -94,7 +100,7 @@ namespace Artemis.Core.Services if (!File.Exists(metadataFile)) { _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(zipFile, archive); + CopyBuiltInPlugin(archive, preferred); } else { @@ -114,7 +120,7 @@ namespace Artemis.Core.Services if (builtInPluginInfo.Version > pluginInfo.Version) { _logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo); - CopyBuiltInPlugin(zipFile, archive); + CopyBuiltInPlugin(archive, preferred); } } catch (Exception e) @@ -241,7 +247,16 @@ namespace Artemis.Core.Services } foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) - EnablePlugin(plugin, false, ignorePluginLock); + { + try + { + EnablePlugin(plugin, false, ignorePluginLock); + } + catch (ArtemisPluginPrerequisiteException) + { + _logger.Warning("Skipped enabling plugin {plugin} because not all prerequisites are met", plugin); + } + } _logger.Debug("Enabled {count} plugin(s)", _plugins.Count(p => p.IsEnabled)); // ReSharper restore InconsistentlySynchronizedField @@ -331,16 +346,24 @@ namespace Artemis.Core.Services } foreach (Type featureType in featureTypes) - plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); + { + // Load the enabled state and if not found, default to true + PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? + new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName!}; + plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); + } if (!featureTypes.Any()) _logger.Warning("Plugin {plugin} contains no features", plugin); - List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList(); + List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList(); if (bootstrappers.Count > 1) _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); if (bootstrappers.Any()) - plugin.Bootstrapper = (IPluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); + { + plugin.Bootstrapper = (PluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); + plugin.Bootstrapper?.InternalOnPluginLoaded(plugin); + } lock (_plugins) { @@ -369,6 +392,9 @@ namespace Artemis.Core.Services return; } + if (!plugin.Info.ArePrerequisitesMet()) + throw new ArtemisPluginPrerequisiteException(plugin.Info, "Cannot enable a plugin whose prerequisites aren't all met"); + // Create the Ninject child kernel and load the module plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin)); OnPluginEnabling(new PluginEventArgs(plugin)); @@ -391,10 +417,8 @@ namespace Artemis.Core.Services featureInfo.Instance = instance; instance.Info = featureInfo; instance.Plugin = plugin; - - // Load the enabled state and if not found, default to true - instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ?? - new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!}; + instance.Profiler = plugin.GetProfiler("Feature - " + featureInfo.Name); + instance.Entity = featureInfo.Entity; } catch (Exception e) { @@ -403,17 +427,8 @@ namespace Artemis.Core.Services } // Activate features after they are all loaded - foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.Instance.Entity.IsEnabled || f.AlwaysEnabled))) - { - try - { - EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); - } - catch (Exception) - { - // ignored, logged in EnablePluginFeature - } - } + foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.EnabledInStorage || f.AlwaysEnabled))) + EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); if (saveState) { @@ -493,6 +508,7 @@ namespace Artemis.Core.Services Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (existing != null) + { try { RemovePlugin(existing, false); @@ -501,20 +517,14 @@ namespace Artemis.Core.Services { throw new ArtemisPluginException("A plugin with the same GUID is already loaded, failed to remove old version", e); } - - string targetDirectory = pluginInfo.Main.Split(".dll")[0].Replace("/", "").Replace("\\", ""); - string uniqueTargetDirectory = targetDirectory; - int attempt = 2; - - // Find a unique folder - while (pluginDirectory.EnumerateDirectories().Any(d => d.Name == uniqueTargetDirectory)) - { - uniqueTargetDirectory = targetDirectory + "-" + attempt; - attempt++; } + string targetDirectory = pluginInfo.PreferredPluginDirectory; + if (Directory.Exists(Path.Combine(pluginDirectory.FullName, targetDirectory))) + throw new ArtemisPluginException($"A directory for this plugin already exists {Path.Combine(pluginDirectory.FullName, targetDirectory)}"); + // Extract everything in the same archive directory to the unique plugin directory - DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, uniqueTargetDirectory)); + DirectoryInfo directoryInfo = new(Path.Combine(pluginDirectory.FullName, targetDirectory)); Utilities.CreateAccessibleDirectory(directoryInfo.FullName); string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); foreach (ZipArchiveEntry zipArchiveEntry in archive.Entries) @@ -570,7 +580,10 @@ namespace Artemis.Core.Services if (pluginFeature.Plugin.Info.RequiresAdmin && !_isElevated) { if (!saveState) + { + OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature)); throw new ArtemisCoreException("Cannot enable a feature that requires elevation without saving it's state."); + } pluginFeature.Entity.IsEnabled = true; pluginFeature.Plugin.Entity.IsEnabled = true; @@ -581,6 +594,12 @@ namespace Artemis.Core.Services return; } + if (!pluginFeature.Info.ArePrerequisitesMet()) + { + OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature)); + throw new ArtemisPluginPrerequisiteException(pluginFeature.Info, "Cannot enable a plugin feature whose prerequisites aren't all met"); + } + try { pluginFeature.SetEnabled(true, isAutoEnable); @@ -593,7 +612,6 @@ namespace Artemis.Core.Services new ArtemisPluginException(pluginFeature.Plugin, $"Exception during SetEnabled(true) on {pluginFeature}", e), "Failed to enable plugin" ); - throw; } finally { diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index b794930af..ae1ea1fc9 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using Artemis.Core.DeviceProviders; using Artemis.Core.Services.Models; @@ -149,6 +150,7 @@ namespace Artemis.Core.Services RemoveDevice(device); List providerExceptions = new(); + void DeviceProviderOnException(object? sender, ExceptionEventArgs e) { if (e.IsCritical) @@ -314,12 +316,15 @@ namespace Artemis.Core.Services { SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(); surfaceArrangement.Arrange(_devices); + foreach (ArtemisDevice artemisDevice in _devices) + artemisDevice.ApplyDefaultCategories(); + SaveDevices(); } - public ArtemisLayout ApplyBestDeviceLayout(ArtemisDevice device) + public ArtemisLayout? ApplyBestDeviceLayout(ArtemisDevice device) { - ArtemisLayout layout; + ArtemisLayout? layout; // Configured layout path takes precedence over all other options if (device.CustomLayoutPath != null) @@ -349,19 +354,19 @@ namespace Artemis.Core.Services } // Finally fall back to a default layout - layout = LoadDefaultLayout(device); - ApplyDeviceLayout(device, layout); + layout = ArtemisLayout.GetDefaultLayout(device); + if (layout != null) + ApplyDeviceLayout(device, layout); return layout; } - private ArtemisLayout LoadDefaultLayout(ArtemisDevice device) - { - return new("NYI", LayoutSource.Default); - } - public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout) { - device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); + if (layout.Source == LayoutSource.Default) + device.ApplyLayout(layout, false, false); + else + device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); + UpdateLedGroup(); } diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index bd34ed32b..9a11fbf1c 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -121,5 +121,11 @@ namespace Artemis.Core.Services /// Text to add after the name of the profile (separated by a dash) /// ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix = "imported"); + + /// + /// Adapts a given profile to the currently active devices + /// + /// The profile to adapt + void AdaptProfile(Profile profile); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs index 5f25fe5f2..146f89bc1 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs @@ -23,12 +23,12 @@ namespace Artemis.Core.Services.Models public bool HasDevices(List devices) { - return devices.Any(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType); + return devices.Any(d => d.DeviceType == DeviceType); } public void Arrange(List devices) { - devices = devices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType).ToList(); + devices = devices.Where(d => d.DeviceType == DeviceType).ToList(); if (!devices.Any()) return; @@ -58,7 +58,7 @@ namespace Artemis.Core.Services.Models public Point GetEdge(HorizontalArrangementPosition horizontalPosition, VerticalArrangementPosition verticalPosition) { - List devices = SurfaceArrangement.ArrangedDevices.Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType || DeviceType == RGBDeviceType.All).ToList(); + List devices = SurfaceArrangement.ArrangedDevices.Where(d => d.DeviceType == DeviceType || DeviceType == RGBDeviceType.All).ToList(); if (!devices.Any()) return new Point(); diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index bdae901c8..e75eec263 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -32,8 +32,8 @@ namespace Artemis.Core.Services _rgbService.LedsChanged += RgbServiceOnLedsChanged; } - public JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All}; - public JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; + public static JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All}; + public static JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; public ProfileDescriptor? GetLastActiveProfile(ProfileModule module) { @@ -64,8 +64,19 @@ namespace Artemis.Core.Services private void ActiveProfilesPopulateLeds() { List profileModules = _pluginManagementService.GetFeaturesOfType(); - foreach (ProfileModule profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) - profileModule.ActiveProfile?.PopulateLeds(_rgbService.EnabledDevices); // Avoid race condition + foreach (ProfileModule profileModule in profileModules) + { + // Avoid race condition, make the check here + if (profileModule.ActiveProfile == null) + continue; + + profileModule.ActiveProfile.PopulateLeds(_rgbService.EnabledDevices); + if (profileModule.ActiveProfile.IsFreshImport) + { + _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileModule.ActiveProfile); + AdaptProfile(profileModule.ActiveProfile); + } + } } public List GetProfileDescriptors(ProfileModule module) @@ -109,6 +120,12 @@ namespace Artemis.Core.Services InstantiateProfile(profile); profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices); + if (profile.IsFreshImport) + { + _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile); + AdaptProfile(profile); + } + SaveActiveProfile(profileDescriptor.ProfileModule); return profile; @@ -122,7 +139,7 @@ namespace Artemis.Core.Services ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId); Profile profile = new(module, entity); InstantiateProfile(profile); - + module.ChangeActiveProfile(null, _rgbService.EnabledDevices); module.ChangeActiveProfile(profile, _rgbService.EnabledDevices); } @@ -156,6 +173,12 @@ namespace Artemis.Core.Services _rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged; await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices); + if (profile.IsFreshImport) + { + _logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile); + AdaptProfile(profile); + } + SaveActiveProfile(profileDescriptor.ProfileModule); _pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle; @@ -166,7 +189,6 @@ namespace Artemis.Core.Services } - public void ClearActiveProfile(ProfileModule module) { module.ChangeActiveProfile(null, _rgbService.EnabledDevices); @@ -194,15 +216,14 @@ namespace Artemis.Core.Services public void DeleteProfile(ProfileDescriptor profileDescriptor) { ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); + if (profileEntity == null) + return; _profileRepository.Remove(profileEntity); } public void UpdateProfile(Profile profile, bool includeChildren) { - _logger.Debug("Updating profile " + profile); string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); - profile.RedoStack.Clear(); - profile.UndoStack.Push(memento); profile.Save(); if (includeChildren) @@ -213,6 +234,22 @@ namespace Artemis.Core.Services layer.Save(); } + // If there are no changes, don't bother saving + string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + if (memento.Equals(updatedMemento)) + { + _logger.Debug("Updating profile - Skipping save, no changes"); + return; + } + + _logger.Debug("Updating profile - Saving " + profile); + profile.RedoStack.Clear(); + profile.UndoStack.Push(memento); + + // At this point the user made actual changes, save that + profile.IsFreshImport = false; + profile.ProfileEntity.IsFreshImport = false; + _profileRepository.Save(profile.ProfileEntity); } @@ -289,18 +326,43 @@ namespace Artemis.Core.Services // Assign a new GUID to make sure it is unique in case of a previous import of the same content profileEntity.UpdateGuid(Guid.NewGuid()); profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; + profileEntity.IsFreshImport = true; + profileEntity.IsActive = false; _profileRepository.Add(profileEntity); return new ProfileDescriptor(profileModule, profileEntity); } + /// + public void AdaptProfile(Profile profile) + { + string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); + + List devices = _rgbService.EnabledDevices.ToList(); + foreach (Layer layer in profile.GetAllLayers()) + layer.Adapter.Adapt(devices); + + profile.Save(); + + foreach (Folder folder in profile.GetAllFolders()) + folder.Save(); + foreach (Layer layer in profile.GetAllLayers()) + layer.Save(); + + _logger.Debug("Adapt profile - Saving " + profile); + profile.RedoStack.Clear(); + profile.UndoStack.Push(memento); + + _profileRepository.Save(profile.ProfileEntity); + } + #region Event handlers private void RgbServiceOnLedsChanged(object? sender, EventArgs e) { ActiveProfilesPopulateLeds(); } - + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs new file mode 100644 index 000000000..bd5fb6f08 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/CategoryAdaptionHintEntity.cs @@ -0,0 +1,13 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public class CategoryAdaptionHintEntity : IAdaptionHintEntity + { + public int Category { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } + } + + +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs new file mode 100644 index 000000000..ec6912505 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/DeviceAdaptionHintEntity.cs @@ -0,0 +1,11 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public class DeviceAdaptionHintEntity : IAdaptionHintEntity + { + public int DeviceType { get; set; } + + public bool LimitAmount { get; set; } + public int Skip { get; set; } + public int Amount { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs new file mode 100644 index 000000000..9b81e630d --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/IAdaptionHintEntity.cs @@ -0,0 +1,6 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public interface IAdaptionHintEntity + { + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs b/src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs new file mode 100644 index 000000000..e5a2ddb64 --- /dev/null +++ b/src/Artemis.Storage/Entities/Profile/AdaptionHints/KeyboardSectionAdaptionHintEntity.cs @@ -0,0 +1,7 @@ +namespace Artemis.Storage.Entities.Profile.AdaptionHints +{ + public class KeyboardSectionAdaptionHintEntity : IAdaptionHintEntity + { + public int Section { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 035e160ec..4be0f1d02 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Artemis.Storage.Entities.Profile.Abstract; +using Artemis.Storage.Entities.Profile.AdaptionHints; using LiteDB; namespace Artemis.Storage.Entities.Profile @@ -10,6 +11,7 @@ namespace Artemis.Storage.Entities.Profile public LayerEntity() { Leds = new List(); + AdaptionHints = new List(); PropertyEntities = new List(); LayerEffects = new List(); ExpandedPropertyGroups = new List(); @@ -20,6 +22,7 @@ namespace Artemis.Storage.Entities.Profile public bool Suspended { get; set; } public List Leds { get; set; } + public List AdaptionHints { get; set; } [BsonRef("ProfileEntity")] public ProfileEntity Profile { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/LedEntity.cs b/src/Artemis.Storage/Entities/Profile/LedEntity.cs index 1b09e9111..248fbef45 100644 --- a/src/Artemis.Storage/Entities/Profile/LedEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LedEntity.cs @@ -4,5 +4,7 @@ { public string LedName { get; set; } public string DeviceIdentifier { get; set; } + + public int? PhysicalLayout { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs index 177d8c00b..8fac49757 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileEntity.cs @@ -17,6 +17,7 @@ namespace Artemis.Storage.Entities.Profile public string Name { get; set; } public bool IsActive { get; set; } + public bool IsFreshImport { get; set; } public List Folders { get; set; } public List Layers { get; set; } diff --git a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs index bc1fb4e82..ecacd69dd 100644 --- a/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs +++ b/src/Artemis.Storage/Entities/Surface/DeviceEntity.cs @@ -8,8 +8,9 @@ namespace Artemis.Storage.Entities.Surface { InputIdentifiers = new List(); InputMappings = new List(); + Categories = new List(); } - + public string Id { get; set; } public float X { get; set; } public float Y { get; set; } @@ -27,7 +28,7 @@ namespace Artemis.Storage.Entities.Surface public List InputIdentifiers { get; set; } public List InputMappings { get; set; } - + public List Categories { get; set; } } public class InputMappingEntity diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 893ff97d3..52563d5ef 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -50,7 +50,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml index 740f659f0..c28b32576 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml @@ -73,7 +73,9 @@ MinWidth="95" MaxLength="9" Margin="0" - HorizontalAlignment="Stretch"> + HorizontalAlignment="Stretch" + FontFamily="Consolas" + CharacterCasing="Upper"> + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Resources/Images/Logo/bow.ico b/src/Artemis.UI/Resources/Images/Logo/bow.ico new file mode 100644 index 000000000..027af5dd6 Binary files /dev/null and b/src/Artemis.UI/Resources/Images/Logo/bow.ico differ diff --git a/src/Artemis.UI/Resources/Images/Logo/bow.svg b/src/Artemis.UI/Resources/Images/Logo/bow.svg index 66817473d..0c7dd46db 100644 --- a/src/Artemis.UI/Resources/Images/Logo/bow.svg +++ b/src/Artemis.UI/Resources/Images/Logo/bow.svg @@ -1,44 +1,43 @@ - - - - -Created by potrace 1.15, written by Peter Selinger 2001-2017 - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Resources/Images/Logo/logo-512.ico b/src/Artemis.UI/Resources/Images/Logo/logo-512.ico deleted file mode 100644 index 015d1ffa8..000000000 Binary files a/src/Artemis.UI/Resources/Images/Logo/logo-512.ico and /dev/null differ diff --git a/src/Artemis.UI/Resources/Images/Logo/logo-512.png b/src/Artemis.UI/Resources/Images/Logo/logo-512.png deleted file mode 100644 index d1cd8ebe7..000000000 Binary files a/src/Artemis.UI/Resources/Images/Logo/logo-512.png and /dev/null differ diff --git a/src/Artemis.UI/Screens/Home/HomeView.xaml b/src/Artemis.UI/Screens/Home/HomeView.xaml index 78db3be5c..ce8cd134b 100644 --- a/src/Artemis.UI/Screens/Home/HomeView.xaml +++ b/src/Artemis.UI/Screens/Home/HomeView.xaml @@ -7,10 +7,11 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:home="clr-namespace:Artemis.UI.Screens.Home" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" mc:Ignorable="d" d:DesignHeight="574.026" d:DesignWidth="1029.87" - d:DataContext="{d:DesignInstance home:HomeViewModel, IsDesignTimeCreatable=True}"> + d:DataContext="{d:DesignInstance home:HomeViewModel}"> @@ -36,11 +37,28 @@ - - + + - Welcome to Artemis, RGB on steroids. + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - Open Source - - This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, - I like numbers. You could even make plugins, there's a full documentation on the website - - - - + + + + + + + + + + + + + + + Have a chat + + If you need help, have some feedback or have any other questions feel free to contact us through any of the + following channels. + + + + + + + + + - - Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. - - - - - - - + + + + + + + + + + + + + + + + + + + + + Open Source + + This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, + I like numbers. You could even make plugins, there's a full documentation on the website + + + + + + + Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml index 3a47534f6..f75173816 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorView.xaml @@ -35,13 +35,13 @@ @@ -169,6 +169,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs new file mode 100644 index 000000000..b6ff0db12 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintViewModel.cs @@ -0,0 +1,46 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints +{ + public class CategoryAdaptionHintViewModel : AdaptionHintViewModel + { + private bool _takeAllDevices; + + /// + public CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint) : base(adaptionHint) + { + CategoryAdaptionHint = adaptionHint; + Categories = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(DeviceCategory))); + } + + public CategoryAdaptionHint CategoryAdaptionHint { get; } + public BindableCollection Categories { get; } + + public bool TakeAllDevices + { + get => _takeAllDevices; + set => SetAndNotify(ref _takeAllDevices, value); + } + + #region Overrides of Screen + + /// + protected override void OnInitialActivate() + { + TakeAllDevices = !CategoryAdaptionHint.LimitAmount; + base.OnInitialActivate(); + } + + + /// + protected override void OnClose() + { + CategoryAdaptionHint.LimitAmount = !TakeAllDevices; + base.OnClose(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml new file mode 100644 index 000000000..601bd6854 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintView.xaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs new file mode 100644 index 000000000..bf307ac5c --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/DeviceAdaptionHintViewModel.cs @@ -0,0 +1,47 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using RGB.NET.Core; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints +{ + public class DeviceAdaptionHintViewModel : AdaptionHintViewModel + { + private bool _takeAllDevices; + + /// + public DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint) : base(adaptionHint) + { + DeviceAdaptionHint = adaptionHint; + DeviceTypes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(RGBDeviceType))); + } + + public DeviceAdaptionHint DeviceAdaptionHint { get; } + public BindableCollection DeviceTypes { get; } + + public bool TakeAllDevices + { + get => _takeAllDevices; + set => SetAndNotify(ref _takeAllDevices, value); + } + + #region Overrides of Screen + + /// + protected override void OnInitialActivate() + { + TakeAllDevices = !DeviceAdaptionHint.LimitAmount; + base.OnInitialActivate(); + } + + + /// + protected override void OnClose() + { + DeviceAdaptionHint.LimitAmount = !TakeAllDevices; + base.OnClose(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml new file mode 100644 index 000000000..2d5a3ecb9 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintView.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs new file mode 100644 index 000000000..1796757d9 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/KeyboardSectionAdaptionHintViewModel.cs @@ -0,0 +1,19 @@ +using Artemis.Core; +using Artemis.UI.Shared; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints +{ + public class KeyboardSectionAdaptionHintViewModel : AdaptionHintViewModel + { + /// + public KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint) : base(adaptionHint) + { + KeyboardSectionAdaptionHint = adaptionHint; + Sections = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(KeyboardSection))); + } + + public KeyboardSectionAdaptionHint KeyboardSectionAdaptionHint { get; } + public BindableCollection Sections { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml new file mode 100644 index 000000000..2e82fc085 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogView.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Introduction + + + In this window you can tell Artemis how this layer should be adapted when the profile is applied to a different set of devices by providing so-called adaption hints. + This is useful when sharing your profile with others, avoiding the need for manual adjustments. + + To learn more about profile adaption, check out + + this wiki article + + . + + + + Adaption hints + + + + You haven't set up any adaption hints + Artemis will attempt to directly map the LEDs of this layer to different surfaces but results may vary. + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + PolyForm Noncommercial License 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + Robert 'Spoinky' Beekman + + + Project owner, main contributor + + + + + + + + + + + + + + + + + + + + + + + + + Darth Affe + + + + RGB.NET + developer, main contributor + + + + + + + + + + + + + + + + + + + + + + + + + Diogo 'DrMeteor' Trindade + + + Main contributor + + + + + + + + + + + + + + + + + + + + + + + + + Kai Werling + + + Graphics design + + + + + + + + + + + + Special thanks + + + + - The various people creating PRs to Artemis.Plugins and the main repository + - All the people on Discord providing feedback and testing + + + + + + External libraries + + + + - Ben.Demystifier + + https://github.com/benaadams/Ben.Demystifier + + - EmbedIO + + https://unosquare.github.io/embedio/ + + - FluentValidation + + https://fluentvalidation.net/ + + - Furl.Http + + https://flurl.dev/ + + - gong-wpf-dragdrop + + https://github.com/punker76/gong-wpf-dragdrop + + - Hardcodet.NotifyIcon.Wpf.NetCore + + https://github.com/HavenDV/H.NotifyIcon.WPF + + - Humanizer + + https://github.com/Humanizr/Humanizer + + - LiteDB + + https://www.litedb.org/ + + - MaterialDesignThemes + + https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit + + - MaterialDesignExtensions + + https://spiegelp.github.io/MaterialDesignExtensions/ + + - McMaster.NETCore.Plugins + + https://github.com/natemcmaster/DotNetCorePlugins + + - Newtonsoft.Json + + https://www.newtonsoft.com/json + + - Ninject + + http://www.ninject.org/ + + - Ookii.Dialogs.Wpf + + https://github.com/ookii-dialogs/ookii-dialogs-wpf + + - RawInput.Sharp + + https://github.com/mfakane/rawinput-sharp + + - RGB.NET + + https://github.com/DarthAffe/RGB.NET + + - Serilog + + https://serilog.net/ + + - SkiaSharp + + https://github.com/mono/SkiaSharp + + - Stylet + + https://github.com/canton7/Stylet + + - Unclassified.NetRevisionTask + + https://unclassified.software/en/apps/netrevisiontask + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabViewModel.cs index cbf2c00ae..0f03ebc7d 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/About/AboutTabViewModel.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Windows.Navigation; using Artemis.Core; using Stylet; @@ -19,6 +20,16 @@ namespace Artemis.UI.Screens.Settings.Tabs.About set => SetAndNotify(ref _version, value); } + public void OpenHyperlink(object sender, RequestNavigateEventArgs e) + { + Core.Utilities.OpenUrl(e.Uri.AbsoluteUri); + } + + public void OpenUrl(string url) + { + Core.Utilities.OpenUrl(url); + } + #region Overrides of Screen /// diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml index a6564ec50..018ed9f28 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsView.xaml @@ -30,7 +30,12 @@ - + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs index a4e8587dc..97ca37389 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Screens.Plugins; using Artemis.UI.Shared.Services; using Stylet; @@ -13,11 +16,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins { private readonly ICoreService _coreService; private readonly IDialogService _dialogService; + private readonly IMessageService _messageService; private readonly IPluginManagementService _pluginManagementService; private bool _enabling; - private readonly IMessageService _messageService; - public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, + public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield, ICoreService coreService, IDialogService dialogService, @@ -51,6 +54,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } public bool CanToggleEnabled => FeatureInfo.Plugin.IsEnabled && !FeatureInfo.AlwaysEnabled; + public bool CanInstallPrerequisites => FeatureInfo.Prerequisites.Any(); + public bool CanRemovePrerequisites => FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any()); + public bool IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites; public void ShowLogsFolder() { @@ -72,6 +78,21 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins _dialogService.ShowExceptionDialog("Feature failed to enable", LoadException); } + public async Task InstallPrerequisites() + { + if (FeatureInfo.Prerequisites.Any()) + await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List { FeatureInfo }); + } + + public async Task RemovePrerequisites() + { + if (FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any())) + { + await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, new List {FeatureInfo}); + NotifyOfPropertyChange(nameof(IsEnabled)); + } + } + protected override void OnInitialActivate() { _pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling; @@ -80,7 +101,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins FeatureInfo.Plugin.Enabled += PluginOnToggled; FeatureInfo.Plugin.Disabled += PluginOnToggled; - + base.OnInitialActivate(); } @@ -120,7 +141,18 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } - await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance, true)); + // Check if all prerequisites are met async + if (!FeatureInfo.ArePrerequisitesMet()) + { + await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List { FeatureInfo }); + if (!FeatureInfo.ArePrerequisitesMet()) + { + NotifyOfPropertyChange(nameof(IsEnabled)); + return; + } + } + + await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance!, true)); } catch (Exception e) { @@ -138,8 +170,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } - #region Event handlers - private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e) { if (e.PluginFeature != FeatureInfo.Instance) return; @@ -159,7 +189,5 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins { NotifyOfPropertyChange(nameof(CanToggleEnabled)); } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabView.xaml index 5faa21825..e0f0eba86 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsTabView.xaml @@ -49,7 +49,6 @@ + + + + + + + + - - - + + - + + @@ -39,7 +47,7 @@ Height="48" Margin="0 5 0 0" Grid.Row="0" - Grid.RowSpan="2" + Grid.RowSpan="3" HorizontalAlignment="Center" VerticalAlignment="Top" /> @@ -51,6 +59,12 @@ + + + + + + + + + - - - - - + + + + + + + + + + + + + - - - Plugin enabled - + - - + + + Plugin enabled + + + - + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs index fca3bb96e..f25606309 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -6,24 +7,26 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Plugins; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; -using MaterialDesignThemes.Wpf; using Ninject; using Stylet; -using Constants = Artemis.Core.Constants; namespace Artemis.UI.Screens.Settings.Tabs.Plugins { public class PluginSettingsViewModel : Conductor.Collection.AllActive { + private readonly ICoreService _coreService; private readonly IDialogService _dialogService; + private readonly IMessageService _messageService; private readonly IPluginManagementService _pluginManagementService; private readonly ISettingsVmFactory _settingsVmFactory; - private readonly ICoreService _coreService; - private readonly IMessageService _messageService; private readonly IWindowManager _windowManager; + private bool _canInstallPrerequisites; + private bool _canRemovePrerequisites; private bool _enabling; + private bool _isSettingsPopupOpen; private Plugin _plugin; public PluginSettingsViewModel(Plugin plugin, @@ -68,6 +71,28 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins set => Task.Run(() => UpdateEnabled(value)); } + public bool IsSettingsPopupOpen + { + get => _isSettingsPopupOpen; + set + { + if (!SetAndNotify(ref _isSettingsPopupOpen, value)) return; + CheckPrerequisites(); + } + } + + public bool CanInstallPrerequisites + { + get => _canInstallPrerequisites; + set => SetAndNotify(ref _canInstallPrerequisites, value); + } + + public bool CanRemovePrerequisites + { + get => _canRemovePrerequisites; + set => SetAndNotify(ref _canRemovePrerequisites, value); + } + public void OpenSettings() { PluginConfigurationDialog configurationViewModel = (PluginConfigurationDialog) Plugin.ConfigurationDialog; @@ -86,6 +111,57 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } + public void OpenPluginDirectory() + { + try + { + Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Plugin.Directory.FullName); + } + catch (Exception e) + { + _dialogService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e); + } + } + + public async Task Reload() + { + bool wasEnabled = IsEnabled; + + _pluginManagementService.UnloadPlugin(Plugin); + Items.Clear(); + + Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory); + foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) + Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); + + if (wasEnabled) + await UpdateEnabled(true); + + _messageService.ShowMessage("Reloaded plugin."); + } + + public async Task InstallPrerequisites() + { + List subjects = new() {Plugin.Info}; + subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); + + if (subjects.Any(s => s.Prerequisites.Any())) + await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, subjects); + } + + public async Task RemovePrerequisites(bool forPluginRemoval = false) + { + List subjects = new() {Plugin.Info}; + subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); + + if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) + { + await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, subjects, forPluginRemoval ? "SKIP, REMOVE PLUGIN" : "CANCEL"); + NotifyOfPropertyChange(nameof(IsEnabled)); + NotifyOfPropertyChange(nameof(CanOpenSettings)); + } + } + public async Task RemoveSettings() { bool confirmed = await _dialogService.ShowConfirmDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); @@ -107,10 +183,16 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins public async Task Remove() { - bool confirmed = await _dialogService.ShowConfirmDialog("Delete plugin", "Are you sure you want to delete this plugin?"); + bool confirmed = await _dialogService.ShowConfirmDialog("Remove plugin", "Are you sure you want to remove this plugin?"); if (!confirmed) return; + // If the plugin or any of its features has uninstall actions, offer to run these + List subjects = new() {Plugin.Info}; + subjects.AddRange(Plugin.Features); + if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) + await RemovePrerequisites(true); + try { _pluginManagementService.RemovePlugin(Plugin, false); @@ -137,12 +219,15 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } - protected override void OnInitialActivate() + public void OpenUri(Uri uri) { - foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) - Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); + Core.Utilities.OpenUrl(uri.ToString()); + } - base.OnInitialActivate(); + private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e) + { + NotifyOfPropertyChange(nameof(IsEnabled)); + NotifyOfPropertyChange(nameof(CanOpenSettings)); } private async Task UpdateEnabled(bool enable) @@ -162,33 +247,82 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins bool confirmed = await _dialogService.ShowConfirmDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?"); if (!confirmed) { - Enabling = false; - NotifyOfPropertyChange(nameof(IsEnabled)); - NotifyOfPropertyChange(nameof(CanOpenSettings)); + CancelEnable(); return; } } - try + // Check if all prerequisites are met async + List subjects = new() {Plugin.Info}; + subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); + + if (subjects.Any(s => !s.ArePrerequisitesMet())) { - await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true)); + await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, subjects); + if (!subjects.All(s => s.ArePrerequisitesMet())) + { + CancelEnable(); + return; + } } - catch (Exception e) + + await Task.Run(() => { - _messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); - } - finally - { - Enabling = false; - } + try + { + _pluginManagementService.EnablePlugin(Plugin, true, true); + } + catch (Exception e) + { + _messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); + } + finally + { + Enabling = false; + } + }); } else - { _pluginManagementService.DisablePlugin(Plugin, true); - } NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(CanOpenSettings)); } + + private void CancelEnable() + { + Enabling = false; + NotifyOfPropertyChange(nameof(IsEnabled)); + NotifyOfPropertyChange(nameof(CanOpenSettings)); + } + + private void CheckPrerequisites() + { + CanInstallPrerequisites = Plugin.Info.Prerequisites.Any() || + Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.Prerequisites.Any()); + CanRemovePrerequisites = Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()) || + Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.Prerequisites.Any(p => p.UninstallActions.Any())); + } + + #region Overrides of Screen + + protected override void OnInitialActivate() + { + foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) + Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); + + _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled; + _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled; + base.OnInitialActivate(); + } + + protected override void OnClose() + { + _pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled; + _pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled; + base.OnClose(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml index 0d1655136..ad7623595 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml @@ -18,7 +18,7 @@ d:DesignHeight="800" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:PluginSettingsWindowViewModel}" - Icon="/Resources/Images/Logo/logo-512.png"> + Icon="/Resources/Images/Logo/bow.ico"> diff --git a/src/Artemis.UI/Screens/Splash/SplashView.xaml b/src/Artemis.UI/Screens/Splash/SplashView.xaml index 0840bde6f..6031dab27 100644 --- a/src/Artemis.UI/Screens/Splash/SplashView.xaml +++ b/src/Artemis.UI/Screens/Splash/SplashView.xaml @@ -6,28 +6,29 @@ xmlns:splash="clr-namespace:Artemis.UI.Screens.Splash" xmlns:controls="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions" xmlns:s="https://github.com/canton7/Stylet" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" mc:Ignorable="d" Title=" " ResizeMode="NoResize" - BorderBackgroundBrush="{DynamicResource PrimaryHueMidBrush}" + BorderBackgroundBrush="{DynamicResource MaterialDesignPaper}" Height="450" Width="400" WindowStartupLocation="CenterScreen" FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto" MouseDown="{s:Action MouseDown}" d:DataContext="{d:DesignInstance splash:SplashViewModel}"> - + - - + + Artemis is initializing... - - + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.xaml b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.xaml index b81bbab63..8ef0f1ff3 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.xaml +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.xaml @@ -6,13 +6,14 @@ xmlns:mde="https://spiegelp.github.io/MaterialDesignExtensions/winfx/xaml" xmlns:s="https://github.com/canton7/Stylet" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" mc:Ignorable="d" Width="800" Height="600" ResizeMode="NoResize" - Icon="/Resources/Images/Logo/logo-512.png" + Icon="/Resources/Images/Logo/bow.ico" Title="Artemis startup wizard" - TitleBarIcon="{StaticResource BowIcon}" + TitleBarIcon="{svgc:SvgImage Source=/Resources/Images/Logo/bow-white.svg}" Foreground="{DynamicResource MaterialDesignBody}" Background="{DynamicResource MaterialDesignPaper}" FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto" diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs index cd35e79f1..13e02d9b9 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs @@ -19,7 +19,7 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs { Device = device; Title = $"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input"; - IsMouse = Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse; + IsMouse = Device.DeviceType == RGBDeviceType.Mouse; _inputService = inputService; _messageService = messageService; diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml index 0cbe91d56..049674b42 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml +++ b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/ListDeviceView.xaml @@ -23,10 +23,11 @@ - diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs index a1c31e961..50b3bcec1 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/Visualization/SurfaceDeviceViewModel.cs @@ -33,8 +33,8 @@ namespace Artemis.UI.Screens.SurfaceEditor.Visualization set => SetAndNotify(ref _selectionStatus, value); } - public bool CanDetectInput => Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard || - Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse; + public bool CanDetectInput => Device.DeviceType == RGBDeviceType.Keyboard || + Device.DeviceType == RGBDeviceType.Mouse; public Cursor Cursor { diff --git a/src/Artemis.UI/Screens/TrayView.xaml b/src/Artemis.UI/Screens/TrayView.xaml index 6e43c046d..746897449 100644 --- a/src/Artemis.UI/Screens/TrayView.xaml +++ b/src/Artemis.UI/Screens/TrayView.xaml @@ -7,11 +7,10 @@ xmlns:s="https://github.com/canton7/Stylet" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" mc:Ignorable="d"> - + DoubleClickCommand="{s:Action TrayBringToForeground}"> diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs index 9b94f0594..b8d96909d 100644 --- a/src/Artemis.UI/Screens/TrayViewModel.cs +++ b/src/Artemis.UI/Screens/TrayViewModel.cs @@ -1,29 +1,33 @@ using System; -using System.Drawing; -using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; +using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Events; +using Artemis.UI.Screens.Settings.Tabs.General; using Artemis.UI.Screens.Splash; using Artemis.UI.Services; using Artemis.UI.Shared.Services; +using Artemis.UI.Utilities; using Hardcodet.Wpf.TaskbarNotification; using MaterialDesignThemes.Wpf; using Ninject; using Stylet; -using Icon = System.Drawing.Icon; namespace Artemis.UI.Screens { - public class TrayViewModel : Screen, IMainWindowProvider, INotificationProvider + public class TrayViewModel : Screen, IMainWindowProvider { + private readonly PluginSetting _colorScheme; private readonly IDebugService _debugService; private readonly IEventAggregator _eventAggregator; private readonly IKernel _kernel; + private readonly ThemeWatcher _themeWatcher; private readonly IWindowManager _windowManager; + private ImageSource _icon; + private bool _openingMainWindow; private RootViewModel _rootViewModel; private SplashViewModel _splashViewModel; private TaskbarIcon _taskBarIcon; @@ -31,7 +35,6 @@ namespace Artemis.UI.Screens public TrayViewModel(IKernel kernel, IWindowManager windowManager, IWindowService windowService, - IMessageService messageService, IUpdateService updateService, IEventAggregator eventAggregator, ICoreService coreService, @@ -46,14 +49,24 @@ namespace Artemis.UI.Screens Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnShutdownRequested; + _themeWatcher = new ThemeWatcher(); + _colorScheme = settingsService.GetSetting("UI.ColorScheme", ApplicationColorScheme.Automatic); + _colorScheme.SettingChanged += ColorSchemeOnSettingChanged; + _themeWatcher.SystemThemeChanged += _themeWatcher_SystemThemeChanged; + _themeWatcher.AppsThemeChanged += _themeWatcher_AppsThemeChanged; + + ApplyColorSchemeSetting(); + ApplyTrayIconTheme(_themeWatcher.GetSystemTheme()); + windowService.ConfigureMainWindowProvider(this); - messageService.ConfigureNotificationProvider(this); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); bool minimized = Bootstrapper.StartupArguments.Contains("--minimized"); bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value; if (autoRunning && !showOnAutoRun || minimized) + { coreService.Initialized += (_, _) => updateService.AutoUpdate(); + } else { ShowSplashScreen(); @@ -61,28 +74,53 @@ namespace Artemis.UI.Screens } } + public ImageSource Icon + { + get => _icon; + set => SetAndNotify(ref _icon, value); + } + public void TrayBringToForeground() { - if (IsMainWindowOpen) - { - Execute.PostToUIThread(FocusMainWindow); + if (_openingMainWindow) return; - } - // Initialize the shared UI when first showing the window - if (!UI.Shared.Bootstrapper.Initialized) - UI.Shared.Bootstrapper.Initialize(_kernel); - - Execute.OnUIThreadSync(() => + try { - _splashViewModel?.RequestClose(); - _splashViewModel = null; - _rootViewModel = _kernel.Get(); - _rootViewModel.Closed += RootViewModelOnClosed; - _windowManager.ShowWindow(_rootViewModel); - }); + _openingMainWindow = true; - OnMainWindowOpened(); + if (IsMainWindowOpen) + { + Execute.OnUIThreadSync(() => + { + FocusMainWindow(); + _openingMainWindow = false; + }); + return; + } + + // Initialize the shared UI when first showing the window + if (!UI.Shared.Bootstrapper.Initialized) + UI.Shared.Bootstrapper.Initialize(_kernel); + + Execute.OnUIThreadSync(() => + { + _splashViewModel?.RequestClose(); + _splashViewModel = null; + _rootViewModel = _kernel.Get(); + _rootViewModel.Closed += RootViewModelOnClosed; + _windowManager.ShowWindow(_rootViewModel); + + IsMainWindowOpen = true; + _openingMainWindow = false; + }); + + OnMainWindowOpened(); + } + finally + { + _openingMainWindow = false; + } } public void TrayActivateSidebarItem(string sidebarItem) @@ -106,14 +144,6 @@ namespace Artemis.UI.Screens _taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content; } - public void OnTrayBalloonTipClicked(object sender, EventArgs e) - { - if (!IsMainWindowOpen) - TrayBringToForeground(); - else - FocusMainWindow(); - } - private void FocusMainWindow() { // Wrestle the main window to the front @@ -142,50 +172,76 @@ namespace Artemis.UI.Screens private void RootViewModelOnClosed(object sender, CloseEventArgs e) { - if (_rootViewModel != null) + lock (this) { - _rootViewModel.Closed -= RootViewModelOnClosed; - _rootViewModel = null; + if (_rootViewModel != null) + { + _rootViewModel.Closed -= RootViewModelOnClosed; + _rootViewModel = null; + } + + IsMainWindowOpen = false; } OnMainWindowClosed(); } - #region Implementation of INotificationProvider + #region Theme - /// - public void ShowNotification(string title, string message, PackIconKind icon) + private void ApplyColorSchemeSetting() { - Execute.OnUIThread(() => + if (_colorScheme.Value == ApplicationColorScheme.Automatic) + ApplyUITheme(_themeWatcher.GetAppsTheme()); + else + ChangeMaterialColors(_colorScheme.Value); + } + + private void ApplyUITheme(ThemeWatcher.WindowsTheme theme) + { + if (_colorScheme.Value != ApplicationColorScheme.Automatic) + return; + if (theme == ThemeWatcher.WindowsTheme.Dark) + ChangeMaterialColors(ApplicationColorScheme.Dark); + else + ChangeMaterialColors(ApplicationColorScheme.Light); + } + + private void ApplyTrayIconTheme(ThemeWatcher.WindowsTheme theme) + { + Execute.PostToUIThread(() => { - // Convert the PackIcon to an icon by drawing it on a visual - DrawingVisual drawingVisual = new(); - DrawingContext drawingContext = drawingVisual.RenderOpen(); - - PackIcon packIcon = new() {Kind = icon}; - Geometry geometry = Geometry.Parse(packIcon.Data); - - // Scale the icon up to fit a 256x256 image and draw it - geometry = Geometry.Combine(geometry, Geometry.Empty, GeometryCombineMode.Union, new ScaleTransform(256 / geometry.Bounds.Right, 256 / geometry.Bounds.Bottom)); - drawingContext.DrawGeometry(new SolidColorBrush(Colors.White), null, geometry); - drawingContext.Close(); - - // Render the visual and add it to a PNG encoder (we want opacity in our icon) - RenderTargetBitmap renderTargetBitmap = new(256, 256, 96, 96, PixelFormats.Pbgra32); - renderTargetBitmap.Render(drawingVisual); - PngBitmapEncoder encoder = new(); - encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap)); - - // Save the PNG and get an icon handle - using MemoryStream stream = new(); - encoder.Save(stream); - Icon convertedIcon = Icon.FromHandle(new Bitmap(stream).GetHicon()); - - // Show the 'balloon' - _taskBarIcon.ShowBalloonTip(title, message, convertedIcon, true); + Icon = theme == ThemeWatcher.WindowsTheme.Dark + ? new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-white.ico")) + : new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-black.ico")); }); } + private void ChangeMaterialColors(ApplicationColorScheme colorScheme) + { + PaletteHelper paletteHelper = new(); + ITheme theme = paletteHelper.GetTheme(); + theme.SetBaseTheme(colorScheme == ApplicationColorScheme.Dark ? Theme.Dark : Theme.Light); + paletteHelper.SetTheme(theme); + + MaterialDesignExtensions.Themes.PaletteHelper extensionsPaletteHelper = new(); + extensionsPaletteHelper.SetLightDark(colorScheme == ApplicationColorScheme.Dark); + } + + private void _themeWatcher_AppsThemeChanged(object sender, WindowsThemeEventArgs e) + { + ApplyUITheme(e.Theme); + } + + private void _themeWatcher_SystemThemeChanged(object sender, WindowsThemeEventArgs e) + { + ApplyTrayIconTheme(e.Theme); + } + + private void ColorSchemeOnSettingChanged(object sender, EventArgs e) + { + ApplyColorSchemeSetting(); + } + #endregion #region Implementation of IMainWindowProvider @@ -194,10 +250,7 @@ namespace Artemis.UI.Screens public bool OpenMainWindow() { - if (IsMainWindowOpen) - Execute.OnUIThread(FocusMainWindow); - else - TrayBringToForeground(); + TrayBringToForeground(); return _rootViewModel.ScreenState == ScreenState.Active; } @@ -213,13 +266,11 @@ namespace Artemis.UI.Screens protected virtual void OnMainWindowOpened() { - IsMainWindowOpen = true; MainWindowOpened?.Invoke(this, EventArgs.Empty); } protected virtual void OnMainWindowClosed() { - IsMainWindowOpen = false; MainWindowClosed?.Invoke(this, EventArgs.Empty); } diff --git a/src/Artemis.UI/Services/DeviceLayoutService.cs b/src/Artemis.UI/Services/DeviceLayoutService.cs index d224ae36b..536763729 100644 --- a/src/Artemis.UI/Services/DeviceLayoutService.cs +++ b/src/Artemis.UI/Services/DeviceLayoutService.cs @@ -52,7 +52,7 @@ namespace Artemis.UI.Services private bool DeviceNeedsLayout(ArtemisDevice d) { - return d.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Keyboard && + return d.DeviceType == RGBDeviceType.Keyboard && (d.LogicalLayout == null || d.PhysicalLayout == KeyboardLayoutType.Unknown) && (!d.DeviceProvider.CanDetectLogicalLayout || !d.DeviceProvider.CanDetectPhysicalLayout); } diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index f8c4f8733..563908898 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -6,8 +6,8 @@ using Artemis.UI.Controllers; using Artemis.UI.DefaultTypes.DataModel.Display; using Artemis.UI.DefaultTypes.DataModel.Input; using Artemis.UI.DefaultTypes.PropertyInput; -using Artemis.UI.InputProviders; using Artemis.UI.Ninject; +using Artemis.UI.Providers; using Artemis.UI.Shared.Services; using Artemis.UI.SkiaSharp; using Serilog; @@ -22,6 +22,7 @@ namespace Artemis.UI.Services private readonly IProfileEditorService _profileEditorService; private readonly IPluginManagementService _pluginManagementService; private readonly IInputService _inputService; + private readonly IMessageService _messageService; private readonly IWebServerService _webServerService; private readonly IRgbService _rgbService; private readonly ISettingsService _settingsService; @@ -36,6 +37,7 @@ namespace Artemis.UI.Services IProfileEditorService profileEditorService, IPluginManagementService pluginManagementService, IInputService inputService, + IMessageService messageService, IWebServerService webServerService, IRgbService rgbService, ISettingsService settingsService) @@ -46,6 +48,7 @@ namespace Artemis.UI.Services _profileEditorService = profileEditorService; _pluginManagementService = pluginManagementService; _inputService = inputService; + _messageService = messageService; _webServerService = webServerService; _rgbService = rgbService; _settingsService = settingsService; @@ -99,9 +102,10 @@ namespace Artemis.UI.Services _registeredBuiltInPropertyEditors = true; } - public void RegisterInputProvider() + public void RegisterProviders() { _inputService.AddInputProvider(new NativeWindowInputProvider(_logger, _inputService)); + _messageService.SetNotificationProvider(new ToastNotificationProvider()); } public void RegisterControllers() @@ -160,7 +164,7 @@ namespace Artemis.UI.Services void RegisterBuiltInDataModelDisplays(); void RegisterBuiltInDataModelInputs(); void RegisterBuiltInPropertyEditors(); - void RegisterInputProvider(); + void RegisterProviders(); void RegisterControllers(); void ApplyPreferredGraphicsContext(); } diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs index cdffa7b59..35dada594 100644 --- a/src/Artemis.UI/Services/UpdateService.cs +++ b/src/Artemis.UI/Services/UpdateService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Timers; +using Windows.UI.Notifications; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Exceptions; @@ -16,9 +17,8 @@ using Artemis.UI.Services.Models.UpdateService; using Artemis.UI.Shared.Services; using Flurl; using Flurl.Http; -using MaterialDesignThemes.Wpf; +using Microsoft.Toolkit.Uwp.Notifications; using Serilog; -using Constants = Artemis.Core.Constants; using File = System.IO.File; namespace Artemis.UI.Services @@ -32,14 +32,12 @@ namespace Artemis.UI.Services private readonly PluginSetting _checkForUpdates; private readonly IDialogService _dialogService; private readonly ILogger _logger; - private readonly IMessageService _messageService; private readonly IWindowService _windowService; - public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IMessageService messageService, IWindowService windowService) + public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IWindowService windowService) { _logger = logger; _dialogService = dialogService; - _messageService = messageService; _windowService = windowService; _windowService.MainWindowOpened += WindowServiceOnMainWindowOpened; @@ -52,8 +50,6 @@ namespace Artemis.UI.Services timer.Start(); } - public bool SuspendAutoUpdate { get; set; } - private async Task OfferUpdate(DevOpsBuild buildInfo) { object result = await _dialogService.ShowDialog(new Dictionary {{"buildInfo", buildInfo}}); @@ -82,6 +78,31 @@ namespace Artemis.UI.Services await httpResponseMessage.Content.CopyToAsync(fs); } + private async void TOnActivated(ToastNotification sender, object args) + { + if (args is not ToastActivatedEventArgs toastEventArgs) + return; + + if (toastEventArgs.Arguments == "update") + await ApplyUpdate(); + else if (toastEventArgs.Arguments == "later") + SuspendAutoUpdate = true; + } + + private async void CheckForUpdatesOnSettingChanged(object sender, EventArgs e) + { + // Run an auto-update as soon as the setting gets changed to enabled + if (_checkForUpdates.Value) + await AutoUpdate(); + } + + private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e) + { + await AutoUpdate(); + } + + public bool SuspendAutoUpdate { get; set; } + public async Task AutoUpdate() { if (Constants.BuildInfo.IsLocalBuild) @@ -114,27 +135,26 @@ namespace Artemis.UI.Services return false; if (_windowService.IsMainWindowOpen) - { await OfferUpdate(buildInfo); - } else if (_autoInstallUpdates.Value) { - // Lets go - _messageService.ShowNotification( - "Installing new version", - $"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.", - PackIconKind.Update - ); + new ToastContentBuilder() + .AddText("Installing new version", AdaptiveTextStyle.Header) + .AddText($"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.") + .AddProgressBar(null, null, true) + .Show(); + await ApplyUpdate(); } else { // If auto-install is disabled and the window is closed, best we can do is notify the user and stop. - _messageService.ShowNotification( - "New version available", - $"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.", - PackIconKind.Update - ); + new ToastContentBuilder() + .AddText("New version available", AdaptiveTextStyle.Header) + .AddText($"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.") + .AddButton("Update", ToastActivationType.Background, "update") + .AddButton("Later", ToastActivationType.Background, "later") + .Show(t => t.Activated += TOnActivated); } return true; @@ -157,9 +177,7 @@ namespace Artemis.UI.Services // Always update installer if it is missing ^^ if (!File.Exists(installerPath)) - { await UpdateInstaller(); - } // Compare the creation date of the installer with the build date and update if needed else { @@ -226,22 +244,6 @@ namespace Artemis.UI.Services .WithHeader("Accept", "application/vnd.github.v3+json") .GetJsonAsync(); } - - #region Event handlers - - private async void CheckForUpdatesOnSettingChanged(object sender, EventArgs e) - { - // Run an auto-update as soon as the setting gets changed to enabled - if (_checkForUpdates.Value) - await AutoUpdate(); - } - - private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e) - { - await AutoUpdate(); - } - - #endregion } public interface IUpdateService : IArtemisUIService diff --git a/src/Artemis.UI/Utilities/ProcessUtilities.cs b/src/Artemis.UI/Utilities/ProcessUtilities.cs index dc418ace1..58043c5d9 100644 --- a/src/Artemis.UI/Utilities/ProcessUtilities.cs +++ b/src/Artemis.UI/Utilities/ProcessUtilities.cs @@ -2,33 +2,11 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; -using System.Threading.Tasks; namespace Artemis.UI.Utilities { public static class ProcessUtilities { - public static Task RunProcessAsync(string fileName, string arguments) - { - TaskCompletionSource tcs = new(); - - Process process = new() - { - StartInfo = {FileName = fileName, Arguments = arguments}, - EnableRaisingEvents = true - }; - - process.Exited += (sender, args) => - { - tcs.SetResult(process.ExitCode); - process.Dispose(); - }; - - process.Start(); - - return tcs.Task; - } - public static Process RunAsDesktopUser(string fileName, string arguments, bool hideWindow) { if (string.IsNullOrWhiteSpace(fileName)) @@ -198,24 +176,24 @@ namespace Artemis.UI.Utilities [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct STARTUPINFO { - public int cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public int dwX; - public int dwY; - public int dwXSize; - public int dwYSize; - public int dwXCountChars; - public int dwYCountChars; - public int dwFillAttribute; + public readonly int cb; + public readonly string lpReserved; + public readonly string lpDesktop; + public readonly string lpTitle; + public readonly int dwX; + public readonly int dwY; + public readonly int dwXSize; + public readonly int dwYSize; + public readonly int dwXCountChars; + public readonly int dwYCountChars; + public readonly int dwFillAttribute; public int dwFlags; public short wShowWindow; - public short cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; + public readonly short cbReserved2; + public readonly IntPtr lpReserved2; + public readonly IntPtr hStdInput; + public readonly IntPtr hStdOutput; + public readonly IntPtr hStdError; } [DllImport("kernel32.dll", ExactSpelling = true)] diff --git a/src/Artemis.UI/Utilities/ThemeWatcher.cs b/src/Artemis.UI/Utilities/ThemeWatcher.cs index 29e577079..cc0989ad5 100644 --- a/src/Artemis.UI/Utilities/ThemeWatcher.cs +++ b/src/Artemis.UI/Utilities/ThemeWatcher.cs @@ -11,7 +11,8 @@ namespace Artemis.UI.Utilities { private const string RegistryKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; - private const string RegistryValueName = "AppsUseLightTheme"; + private const string appsThemeRegistryValueName = "AppsUseLightTheme"; + private const string systemThemeRegistryValueName = "SystemUsesLightTheme"; public ThemeWatcher() { @@ -21,24 +22,44 @@ namespace Artemis.UI.Utilities public void WatchTheme() { WindowsIdentity currentUser = WindowsIdentity.GetCurrent(); - string query = string.Format( + string appsThemequery = string.Format( CultureInfo.InvariantCulture, @"SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '{0}\\{1}' AND ValueName = '{2}'", currentUser.User.Value, RegistryKeyPath.Replace(@"\", @"\\"), - RegistryValueName); + appsThemeRegistryValueName); + + string systemThemequery = string.Format( + CultureInfo.InvariantCulture, + @"SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '{0}\\{1}' AND ValueName = '{2}'", + currentUser.User.Value, + RegistryKeyPath.Replace(@"\", @"\\"), + systemThemeRegistryValueName); try { - ManagementEventWatcher watcher = new(query); - watcher.EventArrived += (sender, args) => + // For Apps theme + ManagementEventWatcher appsThemWatcher = new(appsThemequery); + appsThemWatcher.EventArrived += (_, _) => { - WindowsTheme newWindowsTheme = GetWindowsTheme(); - OnThemeChanged(new WindowsThemeEventArgs(newWindowsTheme)); + WindowsTheme newWindowsTheme = GetAppsTheme(); + OnAppsThemeChanged(new WindowsThemeEventArgs(newWindowsTheme)); }; - // Start listening for events - watcher.Start(); + // Start listening for apps theme events + appsThemWatcher.Start(); + + + // For System theme + ManagementEventWatcher systemThemWatcher = new(systemThemequery); + systemThemWatcher.EventArrived += (_, _) => + { + WindowsTheme newWindowsTheme = GetSystemTheme(); + OnSystemThemeChanged(new WindowsThemeEventArgs(newWindowsTheme)); + }; + + // Start listening for system theme events + systemThemWatcher.Start(); } catch (Exception) { @@ -46,25 +67,40 @@ namespace Artemis.UI.Utilities } } - public WindowsTheme GetWindowsTheme() + private WindowsTheme GetTheme(string themeKeyName) { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(RegistryKeyPath)) { - object registryValueObject = key?.GetValue(RegistryValueName); + object registryValueObject = key?.GetValue(themeKeyName); if (registryValueObject == null) return WindowsTheme.Light; - int registryValue = (int) registryValueObject; + int registryValue = (int)registryValueObject; return registryValue > 0 ? WindowsTheme.Light : WindowsTheme.Dark; } } - public event EventHandler ThemeChanged; - - - protected virtual void OnThemeChanged(WindowsThemeEventArgs e) + public WindowsTheme GetAppsTheme() { - ThemeChanged?.Invoke(this, e); + return GetTheme(appsThemeRegistryValueName); + } + + public WindowsTheme GetSystemTheme() + { + return GetTheme(systemThemeRegistryValueName); + } + + public event EventHandler AppsThemeChanged; + public event EventHandler SystemThemeChanged; + + protected virtual void OnAppsThemeChanged(WindowsThemeEventArgs e) + { + AppsThemeChanged?.Invoke(this, e); + } + + protected virtual void OnSystemThemeChanged(WindowsThemeEventArgs e) + { + SystemThemeChanged?.Invoke(this, e); } public enum WindowsTheme diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 86089ffc7..6df570a98 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -4,9 +4,9 @@ ".NETCoreApp,Version=v5.0": { "FluentValidation": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "jNFPbLjBy/bfIWx4BV/WVEsS+1OxBVf22mmSdvVa9RCHJDkNhAjbKZkxgA0s1rYNFxVn+a1fQbos95t4j/z3Zg==" + "requested": "[10.1.0, )", + "resolved": "10.1.0", + "contentHash": "RxhhfY9IcEY2qUMYjoUxegInbuE5Bwll7dVLsXpiJf25g0ztmzUK+HHqtPcub1caPemhMJsC+NwjHei+NgAkvA==" }, "Flurl.Http": { "type": "Direct", @@ -60,6 +60,18 @@ "MaterialDesignColors": "2.0.0" } }, + "Microsoft.Toolkit.Uwp.Notifications": { + "type": "Direct", + "requested": "[7.0.2, )", + "resolved": "7.0.2", + "contentHash": "UWwo9Jdkk52E3zmUMoO+JC2Aix1gizCPIHtVBUON/uyzjKlnjgqoBd7zeS8HJ94Vsm2mW4OjVtPVhz3sEwEDQA==", + "dependencies": { + "Microsoft.Win32.Registry": "4.7.0", + "System.Drawing.Common": "4.7.0", + "System.Reflection.Emit": "4.7.0", + "System.ValueTuple": "4.5.0" + } + }, "Microsoft.Win32.Registry": { "type": "Direct", "requested": "[5.0.0, )", @@ -990,15 +1002,8 @@ }, "System.Reflection.Emit": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", - "dependencies": { - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Reflection.Emit.ILGeneration": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" }, "System.Reflection.Emit.ILGeneration": { "type": "Transitive", @@ -1443,11 +1448,6 @@ "resolved": "3.0.0", "contentHash": "noPwJJl1Q9uparXy1ogtkmyAPGNfSGb0BLT1292nFH1jdMKje6o2kvvrQUvF9Xklj+IoiAI0UzF6Aqxlvo10lw==" }, - "WriteableBitmapEx": { - "type": "Transitive", - "resolved": "1.6.7", - "contentHash": "GoPhIdYzG/DWJq/MADb8gFzRN5PsxM+1DSJFS8Jzupg4DXqFSnlejYEjJw4ffPEUyELL07O4XIGUZydE4jQK0Q==" - }, "artemis.core": { "type": "Project", "dependencies": { @@ -1498,8 +1498,7 @@ "SkiaSharp.Views.WPF": "2.80.2", "Stylet": "1.3.6", "System.Buffers": "4.5.1", - "System.Numerics.Vectors": "4.5.0", - "WriteableBitmapEx": "1.6.7" + "System.Numerics.Vectors": "4.5.0" } } }