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
+
+
+ M0.8314498662948608,0.14738866686820984 L0.9718837738037109,0.14738866686820984 C0.9818984270095825,0.14737924933433533 0.9911608695983887,0.15217334032058716 0.9961943626403809,0.1599714159965515 1.0012279748916626,0.1677694320678711 1.0012708902359009,0.17739155888557434 0.9963071346282959,0.18522578477859497 L0.9271892309188843,0.2952173352241516 C0.9220999479293823,0.303014874458313,0.9128080606460571,0.30780303478240967,0.9027659893035889,0.30780303478240967 C0.892723798751831,0.30780303478240967,0.8834319114685059,0.303014874458313,0.8783426284790039,0.2952173352241516 L0.8644212484359741,0.2732190191745758 0.8512327671051025,0.2802584767341614 C0.8458596467971802,0.2705792188644409 0.8399980068206787,0.26089999079704285 0.8338921070098877,0.25144070386886597 0.8277863264083862,0.2419814169406891 0.8177727460861206,0.22724255919456482 0.8094688653945923,0.2155834138393402 L0.8224133253097534,0.20898395776748657 0.8070266246795654,0.18566575646400452 C0.8019757270812988,0.17778605222702026,0.8019649982452393,0.16808003187179565,0.8069984912872314,0.16019132733345032 C0.8120319843292236,0.15230253338813782,0.8213481903076172,0.1474246382713318,0.8314498662948608,0.14738866686820984z M0.10265883803367615,0.2802584767341614 C0.11096274852752686,0.2850980758666992 0.11951091885566711,0.28993773460388184 0.12708213925361633,0.2943373918533325 0.14149188995361328,0.3020368218421936 0.15663433074951172,0.3090762495994568 0.1715325117111206,0.31633567810058594 0.21944981813430786,0.33654189109802246 0.2699943780899048,0.35124915838241577 0.32197999954223633,0.3601123094558716 L0.20377129316329956,0.4168679714202881 0.1908269226551056,0.42302751541137695 C0.14440736174583435,0.4102410674095154 0.09916770458221436,0.3942067623138428 0.05552190542221069,0.3750711679458618 -0.05462715029716492,0.3422936797142029 0.01937541365623474,0.22108301520347595 0.10265883803367615,0.2802584767341614z M0.4714505672454834,0.5930745005607605 C0.46778708696365356,0.602533757686615 0.4646120071411133,0.6119930148124695 0.4614369869232178,0.6214522123336792 0.4521523118019104,0.6509119868278503 0.4455362558364868,0.6810019016265869 0.44165414571762085,0.7114253044128418 0.4388039708137512,0.7340653538703918 0.4374176263809204,0.7568352818489075 0.4375022053718567,0.7796201109886169 0.4375022053718567,0.7877594828605652 0.4375022053718567,0.7961188554763794 0.4375022053718567,0.8042582273483276 C0.444272518157959,0.8297898769378662,0.42963647842407227,0.8560013771057129,0.40284329652786255,0.8663284778594971 C0.37605005502700806,0.8766556978225708 0.3448934555053711,0.8680944442749023 0.3290627598762512,0.8460550308227539 0.31538575887680054,0.8240567445755005 0.32368963956832886,0.8020584583282471 0.3246665596961975,0.7783002257347107 0.32564353942871094,0.7545420527458191 0.3297954797744751,0.7343035936355591 0.33419162034988403,0.7123052477836609 0.33712244033813477,0.6960265636444092 0.3407859206199646,0.6799677610397339 0.344693660736084,0.6639089584350586 L0.35763800144195557,0.6566495895385742 0.44629454612731934,0.6069333553314209 0.4714505672454834,0.5930745005607605z M0.5317760705947876,0.3625321388244629 C0.5405619740486145,0.3735124468803406 0.5487139821052551,0.384893536567688 0.5561993718147278,0.39662957191467285 0.5627936720848083,0.40674877166748047 0.5688995122909546,0.41730791330337524 0.5732957124710083,0.4278671145439148 L0.551070511341095,0.4404062032699585 0.4829294681549072,0.47824329137802124 0.30463945865631104,0.5792155265808105 0.1959558129310608,0.640370786190033 0.14173611998558044,0.6705085635185242 C0.13235116004943848,0.675317645072937,0.12175974249839783,0.6778919100761414,0.11096274852752686,0.6779879927635193 C0.08286315202713013,0.6767981648445129,0.05947789549827576,0.6581392884254456,0.054777029901742935,0.6331580281257629 C0.05007615685462952,0.6081767678260803,0.06532919406890869,0.583619236946106,0.0914241373538971,0.5741559267044067 L0.10558965802192688,0.56733638048172 0.13733991980552673,0.5521575808525085 0.25139671564102173,0.4971618056297302 0.43701374530792236,0.4080686569213867 0.5088182091712952,0.3735312819480896 0.5317760705947876,0.3625321388244629z M0.1580997109413147,0 L0.17324218153953552,0 C0.1837441623210907,0,0.19424617290496826,0,0.20450395345687866,0 L0.22892725467681885,0.001539871096611023 0.25506019592285156,0.003959685564041138 C0.2677602767944336,0.005499586462974548 0.279483437538147,0.007039457559585571 0.29242783784866333,0.009239301085472107 0.29804515838623047,0.009239301085472107 0.3036625385284424,0.010999158024787903 0.30952417850494385,0.012099072337150574 L0.31294339895248413,0.012099072337150574 C0.321491539478302,0.013858944177627563 0.3297954797744751,0.015398815274238586 0.3373667001724243,0.017598658800125122 0.35522812604904175,0.02149401605129242 0.37283915281295776,0.02626737952232361 0.3901209831237793,0.031897544860839844 0.4018441438674927,0.03541727364063263 0.4130789041519165,0.03915698826313019 0.42431360483169556,0.0433366596698761 0.4323732852935791,0.04641643166542053 0.4406772255897522,0.049496203660964966 0.44873690605163574,0.05301591753959656 0.4567965865135193,0.05653566122055054 0.46290236711502075,0.05895546078681946 0.46974092721939087,0.06225523352622986 L0.47633522748947144,0.06511500477790833 C0.4831737279891968,0.06819474697113037 0.4900122880935669,0.07149451971054077 0.4963623285293579,0.07501423358917236 0.5027123689651489,0.07853397727012634 0.5119932889938354,0.08293363451957703 0.5207855701446533,0.08711332082748413 0.532753050327301,0.09393277764320374 0.5452088713645935,0.10075226426124573 0.5561993718147278,0.10911163687705994 L0.5598629117012024,0.11153143644332886 C0.5674340724945068,0.11637106537818909,0.575005292892456,0.12165066599845886,0.58257657289505,0.12693023681640625 L0.5852631330490112,0.12869009375572205 C0.5940554738044739,0.1348496377468109 0.6026036739349365,0.14144912362098694 0.6111517548561096,0.14826861023902893 0.6192114949226379,0.1544281542301178 0.6267826557159424,0.16080763936042786 0.6355750560760498,0.16762709617614746 0.6443674564361572,0.17444661259651184 0.6512059569358826,0.18126606941223145 0.6585330367088318,0.18852552771568298 0.6609752774238586,0.19072535634040833 0.6631733775138855,0.19314518570899963 0.6656156778335571,0.19556495547294617 0.6719657778739929,0.201944500207901 0.6783158183097839,0.20832398533821106 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21602341532707214 0.6858870983123779,0.21690335869789124 0.6927255392074585,0.22438275814056396 0.6993198394775391,0.23186218738555908 0.7056698799133301,0.23890167474746704 0.7120199799537659,0.2459411323070526 0.7191027402877808,0.2554003894329071 0.7256970405578613,0.26419970393180847 L0.7325356006622314,0.27387893199920654 C0.7376644611358643,0.28113842010498047 0.7427933216094971,0.2886178195476532 0.7476779222488403,0.29587724804878235 0.887993574142456,0.5147373676300049 0.8601140975952148,0.7894638180732727 0.6780716180801392,0.9817845821380615 0.6709265112876892,0.989209771156311 0.6617487668991089,0.9948219060897827 0.6514502167701721,0.998063325881958 L0.6431463360786438,0.9998232126235962 C0.6424370408058167,1.000058889389038 0.6416575908660889,1.000058889389038 0.6409482359886169,0.9998232126235962 0.6382670402526855,1.0000406503677368 0.6355696320533752,1.0000406503677368 0.6328884959220886,0.9998232126235962 C0.6096473932266235,1.0000758171081543,0.5884177088737488,0.9879860877990723,0.5787044167518616,0.9689667224884033 C0.5689911842346191,0.9499474763870239 0.5726134181022644,0.9275609254837036 0.5879496335983276,0.911829948425293 0.6916045546531677,0.8017895221710205 0.7362658977508545,0.6562486886978149 0.7100660800933838,0.5138804912567139 0.7100660800933838,0.5086009502410889 0.7081122398376465,0.5033213496208191 0.7068911194801331,0.4980417490005493 0.7056698799133301,0.49276214838027954 0.7044487595558167,0.48748254776000977 0.7029833793640137,0.4819830060005188 0.7015179395675659,0.47648340463638306 0.7002968192100525,0.47142380475997925 0.69858717918396,0.4661442041397095 0.6968775987625122,0.4608646035194397 0.6956563591957092,0.45624494552612305 0.6937025189399719,0.45118534564971924 0.6888028383255005,0.4367102384567261 0.6830968260765076,0.42246657609939575 0.6766061782836914,0.4085085988044739 0.6751407384872437,0.4045489430427551 0.6731869578361511,0.40102916955947876 0.6714773178100586,0.3972894549369812 0.6635856032371521,0.381019651889801 0.6546980738639832,0.3651564121246338 0.6448559165000916,0.349773108959198 0.6414366960525513,0.34493350982666016 0.638261616230011,0.3398738503456116 0.6350865960121155,0.3352542519569397 0.6289807558059692,0.32645493745803833 0.622630774974823,0.3180955648422241 0.6162806153297424,0.3097361922264099 0.6099306344985962,0.3013768196105957 0.600894033908844,0.2910376489162445 0.591857373714447,0.2820183336734772 L0.5801341533660889,0.2696993052959442 C0.5715860724449158,0.26089999079704285 0.5625494718551636,0.25254058837890625 0.5535128116607666,0.2446211874485016 0.5493572354316711,0.24065852165222168 0.5449538826942444,0.23691263794898987 0.5403242707252502,0.2334021031856537 L0.529333770275116,0.22460272908210754 C0.5064655542373657,0.20664280652999878 0.48195523023605347,0.19045335054397583 0.4560639262199402,0.17620646953582764 0.45191192626953125,0.17378664016723633 0.44727152585983276,0.171586811542511 0.44287532567977905,0.16938698291778564 0.43847912549972534,0.1671871840953827 0.43261754512786865,0.16410738229751587 0.42748862504959106,0.16168758273124695 L0.416253924369812,0.15662795305252075 C0.39798784255981445,0.14821934700012207,0.37914353609085083,0.14087164402008057,0.3598361015319824,0.13462966680526733 L0.3490898609161377,0.13132989406585693 0.3466475009918213,0.13132989406585693 C0.3337032198905945,0.1273702085018158,0.32051461935043335,0.1238504946231842,0.3068375587463379,0.12077072262763977 L0.3007317781448364,0.11945080757141113 C0.284625768661499,0.11583057045936584 0.26832127571105957,0.1129668653011322 0.25188517570495605,0.11087149381637573 0.2433369755744934,0.1095515787601471 0.23478883504867554,0.108671635389328 0.22746187448501587,0.10801169276237488 0.2201349139213562,0.10735175013542175 0.2079232633113861,0.10647183656692505 0.19815391302108765,0.10625183582305908 0.18838459253311157,0.1060318648815155 0.18227878212928772,0.10625183582305908 0.17373061180114746,0.10625183582305908 0.14096742868423462,0.10622519254684448 0.1082688570022583,0.10887587070465088 0.0760374665260315,0.11417123675346375 0.07254058122634888,0.11437812447547913 0.06903234124183655,0.11437812447547913 0.06553545594215393,0.11417123675346375 C0.03505754470825195,0.11396905779838562,0.00974804162979126,0.09292516112327576,0.007091343402862549,0.06557714939117432 C0.0044345855712890625,0.03822912275791168 0.02530190348625183,0.013545975089073181 0.05527767539024353,0.008579328656196594 0.08923646807670593,0.0032099932432174683 0.12362384796142578,0.00034074485301971436 0.1580997109413147,0z
+ 214
+ 5
+ 0.65
+ 0.7
+
+
+ 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
+
+
+ M0.8314498662948608,0.14738866686820984 L0.9718837738037109,0.14738866686820984 C0.9818984270095825,0.14737924933433533 0.9911608695983887,0.15217334032058716 0.9961943626403809,0.1599714159965515 1.0012279748916626,0.1677694320678711 1.0012708902359009,0.17739155888557434 0.9963071346282959,0.18522578477859497 L0.9271892309188843,0.2952173352241516 C0.9220999479293823,0.303014874458313,0.9128080606460571,0.30780303478240967,0.9027659893035889,0.30780303478240967 C0.892723798751831,0.30780303478240967,0.8834319114685059,0.303014874458313,0.8783426284790039,0.2952173352241516 L0.8644212484359741,0.2732190191745758 0.8512327671051025,0.2802584767341614 C0.8458596467971802,0.2705792188644409 0.8399980068206787,0.26089999079704285 0.8338921070098877,0.25144070386886597 0.8277863264083862,0.2419814169406891 0.8177727460861206,0.22724255919456482 0.8094688653945923,0.2155834138393402 L0.8224133253097534,0.20898395776748657 0.8070266246795654,0.18566575646400452 C0.8019757270812988,0.17778605222702026,0.8019649982452393,0.16808003187179565,0.8069984912872314,0.16019132733345032 C0.8120319843292236,0.15230253338813782,0.8213481903076172,0.1474246382713318,0.8314498662948608,0.14738866686820984z M0.10265883803367615,0.2802584767341614 C0.11096274852752686,0.2850980758666992 0.11951091885566711,0.28993773460388184 0.12708213925361633,0.2943373918533325 0.14149188995361328,0.3020368218421936 0.15663433074951172,0.3090762495994568 0.1715325117111206,0.31633567810058594 0.21944981813430786,0.33654189109802246 0.2699943780899048,0.35124915838241577 0.32197999954223633,0.3601123094558716 L0.20377129316329956,0.4168679714202881 0.1908269226551056,0.42302751541137695 C0.14440736174583435,0.4102410674095154 0.09916770458221436,0.3942067623138428 0.05552190542221069,0.3750711679458618 -0.05462715029716492,0.3422936797142029 0.01937541365623474,0.22108301520347595 0.10265883803367615,0.2802584767341614z M0.4714505672454834,0.5930745005607605 C0.46778708696365356,0.602533757686615 0.4646120071411133,0.6119930148124695 0.4614369869232178,0.6214522123336792 0.4521523118019104,0.6509119868278503 0.4455362558364868,0.6810019016265869 0.44165414571762085,0.7114253044128418 0.4388039708137512,0.7340653538703918 0.4374176263809204,0.7568352818489075 0.4375022053718567,0.7796201109886169 0.4375022053718567,0.7877594828605652 0.4375022053718567,0.7961188554763794 0.4375022053718567,0.8042582273483276 C0.444272518157959,0.8297898769378662,0.42963647842407227,0.8560013771057129,0.40284329652786255,0.8663284778594971 C0.37605005502700806,0.8766556978225708 0.3448934555053711,0.8680944442749023 0.3290627598762512,0.8460550308227539 0.31538575887680054,0.8240567445755005 0.32368963956832886,0.8020584583282471 0.3246665596961975,0.7783002257347107 0.32564353942871094,0.7545420527458191 0.3297954797744751,0.7343035936355591 0.33419162034988403,0.7123052477836609 0.33712244033813477,0.6960265636444092 0.3407859206199646,0.6799677610397339 0.344693660736084,0.6639089584350586 L0.35763800144195557,0.6566495895385742 0.44629454612731934,0.6069333553314209 0.4714505672454834,0.5930745005607605z M0.5317760705947876,0.3625321388244629 C0.5405619740486145,0.3735124468803406 0.5487139821052551,0.384893536567688 0.5561993718147278,0.39662957191467285 0.5627936720848083,0.40674877166748047 0.5688995122909546,0.41730791330337524 0.5732957124710083,0.4278671145439148 L0.551070511341095,0.4404062032699585 0.4829294681549072,0.47824329137802124 0.30463945865631104,0.5792155265808105 0.1959558129310608,0.640370786190033 0.14173611998558044,0.6705085635185242 C0.13235116004943848,0.675317645072937,0.12175974249839783,0.6778919100761414,0.11096274852752686,0.6779879927635193 C0.08286315202713013,0.6767981648445129,0.05947789549827576,0.6581392884254456,0.054777029901742935,0.6331580281257629 C0.05007615685462952,0.6081767678260803,0.06532919406890869,0.583619236946106,0.0914241373538971,0.5741559267044067 L0.10558965802192688,0.56733638048172 0.13733991980552673,0.5521575808525085 0.25139671564102173,0.4971618056297302 0.43701374530792236,0.4080686569213867 0.5088182091712952,0.3735312819480896 0.5317760705947876,0.3625321388244629z M0.1580997109413147,0 L0.17324218153953552,0 C0.1837441623210907,0,0.19424617290496826,0,0.20450395345687866,0 L0.22892725467681885,0.001539871096611023 0.25506019592285156,0.003959685564041138 C0.2677602767944336,0.005499586462974548 0.279483437538147,0.007039457559585571 0.29242783784866333,0.009239301085472107 0.29804515838623047,0.009239301085472107 0.3036625385284424,0.010999158024787903 0.30952417850494385,0.012099072337150574 L0.31294339895248413,0.012099072337150574 C0.321491539478302,0.013858944177627563 0.3297954797744751,0.015398815274238586 0.3373667001724243,0.017598658800125122 0.35522812604904175,0.02149401605129242 0.37283915281295776,0.02626737952232361 0.3901209831237793,0.031897544860839844 0.4018441438674927,0.03541727364063263 0.4130789041519165,0.03915698826313019 0.42431360483169556,0.0433366596698761 0.4323732852935791,0.04641643166542053 0.4406772255897522,0.049496203660964966 0.44873690605163574,0.05301591753959656 0.4567965865135193,0.05653566122055054 0.46290236711502075,0.05895546078681946 0.46974092721939087,0.06225523352622986 L0.47633522748947144,0.06511500477790833 C0.4831737279891968,0.06819474697113037 0.4900122880935669,0.07149451971054077 0.4963623285293579,0.07501423358917236 0.5027123689651489,0.07853397727012634 0.5119932889938354,0.08293363451957703 0.5207855701446533,0.08711332082748413 0.532753050327301,0.09393277764320374 0.5452088713645935,0.10075226426124573 0.5561993718147278,0.10911163687705994 L0.5598629117012024,0.11153143644332886 C0.5674340724945068,0.11637106537818909,0.575005292892456,0.12165066599845886,0.58257657289505,0.12693023681640625 L0.5852631330490112,0.12869009375572205 C0.5940554738044739,0.1348496377468109 0.6026036739349365,0.14144912362098694 0.6111517548561096,0.14826861023902893 0.6192114949226379,0.1544281542301178 0.6267826557159424,0.16080763936042786 0.6355750560760498,0.16762709617614746 0.6443674564361572,0.17444661259651184 0.6512059569358826,0.18126606941223145 0.6585330367088318,0.18852552771568298 0.6609752774238586,0.19072535634040833 0.6631733775138855,0.19314518570899963 0.6656156778335571,0.19556495547294617 0.6719657778739929,0.201944500207901 0.6783158183097839,0.20832398533821106 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21602341532707214 0.6858870983123779,0.21690335869789124 0.6927255392074585,0.22438275814056396 0.6993198394775391,0.23186218738555908 0.7056698799133301,0.23890167474746704 0.7120199799537659,0.2459411323070526 0.7191027402877808,0.2554003894329071 0.7256970405578613,0.26419970393180847 L0.7325356006622314,0.27387893199920654 C0.7376644611358643,0.28113842010498047 0.7427933216094971,0.2886178195476532 0.7476779222488403,0.29587724804878235 0.887993574142456,0.5147373676300049 0.8601140975952148,0.7894638180732727 0.6780716180801392,0.9817845821380615 0.6709265112876892,0.989209771156311 0.6617487668991089,0.9948219060897827 0.6514502167701721,0.998063325881958 L0.6431463360786438,0.9998232126235962 C0.6424370408058167,1.000058889389038 0.6416575908660889,1.000058889389038 0.6409482359886169,0.9998232126235962 0.6382670402526855,1.0000406503677368 0.6355696320533752,1.0000406503677368 0.6328884959220886,0.9998232126235962 C0.6096473932266235,1.0000758171081543,0.5884177088737488,0.9879860877990723,0.5787044167518616,0.9689667224884033 C0.5689911842346191,0.9499474763870239 0.5726134181022644,0.9275609254837036 0.5879496335983276,0.911829948425293 0.6916045546531677,0.8017895221710205 0.7362658977508545,0.6562486886978149 0.7100660800933838,0.5138804912567139 0.7100660800933838,0.5086009502410889 0.7081122398376465,0.5033213496208191 0.7068911194801331,0.4980417490005493 0.7056698799133301,0.49276214838027954 0.7044487595558167,0.48748254776000977 0.7029833793640137,0.4819830060005188 0.7015179395675659,0.47648340463638306 0.7002968192100525,0.47142380475997925 0.69858717918396,0.4661442041397095 0.6968775987625122,0.4608646035194397 0.6956563591957092,0.45624494552612305 0.6937025189399719,0.45118534564971924 0.6888028383255005,0.4367102384567261 0.6830968260765076,0.42246657609939575 0.6766061782836914,0.4085085988044739 0.6751407384872437,0.4045489430427551 0.6731869578361511,0.40102916955947876 0.6714773178100586,0.3972894549369812 0.6635856032371521,0.381019651889801 0.6546980738639832,0.3651564121246338 0.6448559165000916,0.349773108959198 0.6414366960525513,0.34493350982666016 0.638261616230011,0.3398738503456116 0.6350865960121155,0.3352542519569397 0.6289807558059692,0.32645493745803833 0.622630774974823,0.3180955648422241 0.6162806153297424,0.3097361922264099 0.6099306344985962,0.3013768196105957 0.600894033908844,0.2910376489162445 0.591857373714447,0.2820183336734772 L0.5801341533660889,0.2696993052959442 C0.5715860724449158,0.26089999079704285 0.5625494718551636,0.25254058837890625 0.5535128116607666,0.2446211874485016 0.5493572354316711,0.24065852165222168 0.5449538826942444,0.23691263794898987 0.5403242707252502,0.2334021031856537 L0.529333770275116,0.22460272908210754 C0.5064655542373657,0.20664280652999878 0.48195523023605347,0.19045335054397583 0.4560639262199402,0.17620646953582764 0.45191192626953125,0.17378664016723633 0.44727152585983276,0.171586811542511 0.44287532567977905,0.16938698291778564 0.43847912549972534,0.1671871840953827 0.43261754512786865,0.16410738229751587 0.42748862504959106,0.16168758273124695 L0.416253924369812,0.15662795305252075 C0.39798784255981445,0.14821934700012207,0.37914353609085083,0.14087164402008057,0.3598361015319824,0.13462966680526733 L0.3490898609161377,0.13132989406585693 0.3466475009918213,0.13132989406585693 C0.3337032198905945,0.1273702085018158,0.32051461935043335,0.1238504946231842,0.3068375587463379,0.12077072262763977 L0.3007317781448364,0.11945080757141113 C0.284625768661499,0.11583057045936584 0.26832127571105957,0.1129668653011322 0.25188517570495605,0.11087149381637573 0.2433369755744934,0.1095515787601471 0.23478883504867554,0.108671635389328 0.22746187448501587,0.10801169276237488 0.2201349139213562,0.10735175013542175 0.2079232633113861,0.10647183656692505 0.19815391302108765,0.10625183582305908 0.18838459253311157,0.1060318648815155 0.18227878212928772,0.10625183582305908 0.17373061180114746,0.10625183582305908 0.14096742868423462,0.10622519254684448 0.1082688570022583,0.10887587070465088 0.0760374665260315,0.11417123675346375 0.07254058122634888,0.11437812447547913 0.06903234124183655,0.11437812447547913 0.06553545594215393,0.11417123675346375 C0.03505754470825195,0.11396905779838562,0.00974804162979126,0.09292516112327576,0.007091343402862549,0.06557714939117432 C0.0044345855712890625,0.03822912275791168 0.02530190348625183,0.013545975089073181 0.05527767539024353,0.008579328656196594 0.08923646807670593,0.0032099932432174683 0.12362384796142578,0.00034074485301971436 0.1580997109413147,0z
+ 214
+ 5
+ 0.65
+ 0.7
+
+
+ 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
+
+
+ M0.8314498662948608,0.14738866686820984 L0.9718837738037109,0.14738866686820984 C0.9818984270095825,0.14737924933433533 0.9911608695983887,0.15217334032058716 0.9961943626403809,0.1599714159965515 1.0012279748916626,0.1677694320678711 1.0012708902359009,0.17739155888557434 0.9963071346282959,0.18522578477859497 L0.9271892309188843,0.2952173352241516 C0.9220999479293823,0.303014874458313,0.9128080606460571,0.30780303478240967,0.9027659893035889,0.30780303478240967 C0.892723798751831,0.30780303478240967,0.8834319114685059,0.303014874458313,0.8783426284790039,0.2952173352241516 L0.8644212484359741,0.2732190191745758 0.8512327671051025,0.2802584767341614 C0.8458596467971802,0.2705792188644409 0.8399980068206787,0.26089999079704285 0.8338921070098877,0.25144070386886597 0.8277863264083862,0.2419814169406891 0.8177727460861206,0.22724255919456482 0.8094688653945923,0.2155834138393402 L0.8224133253097534,0.20898395776748657 0.8070266246795654,0.18566575646400452 C0.8019757270812988,0.17778605222702026,0.8019649982452393,0.16808003187179565,0.8069984912872314,0.16019132733345032 C0.8120319843292236,0.15230253338813782,0.8213481903076172,0.1474246382713318,0.8314498662948608,0.14738866686820984z M0.10265883803367615,0.2802584767341614 C0.11096274852752686,0.2850980758666992 0.11951091885566711,0.28993773460388184 0.12708213925361633,0.2943373918533325 0.14149188995361328,0.3020368218421936 0.15663433074951172,0.3090762495994568 0.1715325117111206,0.31633567810058594 0.21944981813430786,0.33654189109802246 0.2699943780899048,0.35124915838241577 0.32197999954223633,0.3601123094558716 L0.20377129316329956,0.4168679714202881 0.1908269226551056,0.42302751541137695 C0.14440736174583435,0.4102410674095154 0.09916770458221436,0.3942067623138428 0.05552190542221069,0.3750711679458618 -0.05462715029716492,0.3422936797142029 0.01937541365623474,0.22108301520347595 0.10265883803367615,0.2802584767341614z M0.4714505672454834,0.5930745005607605 C0.46778708696365356,0.602533757686615 0.4646120071411133,0.6119930148124695 0.4614369869232178,0.6214522123336792 0.4521523118019104,0.6509119868278503 0.4455362558364868,0.6810019016265869 0.44165414571762085,0.7114253044128418 0.4388039708137512,0.7340653538703918 0.4374176263809204,0.7568352818489075 0.4375022053718567,0.7796201109886169 0.4375022053718567,0.7877594828605652 0.4375022053718567,0.7961188554763794 0.4375022053718567,0.8042582273483276 C0.444272518157959,0.8297898769378662,0.42963647842407227,0.8560013771057129,0.40284329652786255,0.8663284778594971 C0.37605005502700806,0.8766556978225708 0.3448934555053711,0.8680944442749023 0.3290627598762512,0.8460550308227539 0.31538575887680054,0.8240567445755005 0.32368963956832886,0.8020584583282471 0.3246665596961975,0.7783002257347107 0.32564353942871094,0.7545420527458191 0.3297954797744751,0.7343035936355591 0.33419162034988403,0.7123052477836609 0.33712244033813477,0.6960265636444092 0.3407859206199646,0.6799677610397339 0.344693660736084,0.6639089584350586 L0.35763800144195557,0.6566495895385742 0.44629454612731934,0.6069333553314209 0.4714505672454834,0.5930745005607605z M0.5317760705947876,0.3625321388244629 C0.5405619740486145,0.3735124468803406 0.5487139821052551,0.384893536567688 0.5561993718147278,0.39662957191467285 0.5627936720848083,0.40674877166748047 0.5688995122909546,0.41730791330337524 0.5732957124710083,0.4278671145439148 L0.551070511341095,0.4404062032699585 0.4829294681549072,0.47824329137802124 0.30463945865631104,0.5792155265808105 0.1959558129310608,0.640370786190033 0.14173611998558044,0.6705085635185242 C0.13235116004943848,0.675317645072937,0.12175974249839783,0.6778919100761414,0.11096274852752686,0.6779879927635193 C0.08286315202713013,0.6767981648445129,0.05947789549827576,0.6581392884254456,0.054777029901742935,0.6331580281257629 C0.05007615685462952,0.6081767678260803,0.06532919406890869,0.583619236946106,0.0914241373538971,0.5741559267044067 L0.10558965802192688,0.56733638048172 0.13733991980552673,0.5521575808525085 0.25139671564102173,0.4971618056297302 0.43701374530792236,0.4080686569213867 0.5088182091712952,0.3735312819480896 0.5317760705947876,0.3625321388244629z M0.1580997109413147,0 L0.17324218153953552,0 C0.1837441623210907,0,0.19424617290496826,0,0.20450395345687866,0 L0.22892725467681885,0.001539871096611023 0.25506019592285156,0.003959685564041138 C0.2677602767944336,0.005499586462974548 0.279483437538147,0.007039457559585571 0.29242783784866333,0.009239301085472107 0.29804515838623047,0.009239301085472107 0.3036625385284424,0.010999158024787903 0.30952417850494385,0.012099072337150574 L0.31294339895248413,0.012099072337150574 C0.321491539478302,0.013858944177627563 0.3297954797744751,0.015398815274238586 0.3373667001724243,0.017598658800125122 0.35522812604904175,0.02149401605129242 0.37283915281295776,0.02626737952232361 0.3901209831237793,0.031897544860839844 0.4018441438674927,0.03541727364063263 0.4130789041519165,0.03915698826313019 0.42431360483169556,0.0433366596698761 0.4323732852935791,0.04641643166542053 0.4406772255897522,0.049496203660964966 0.44873690605163574,0.05301591753959656 0.4567965865135193,0.05653566122055054 0.46290236711502075,0.05895546078681946 0.46974092721939087,0.06225523352622986 L0.47633522748947144,0.06511500477790833 C0.4831737279891968,0.06819474697113037 0.4900122880935669,0.07149451971054077 0.4963623285293579,0.07501423358917236 0.5027123689651489,0.07853397727012634 0.5119932889938354,0.08293363451957703 0.5207855701446533,0.08711332082748413 0.532753050327301,0.09393277764320374 0.5452088713645935,0.10075226426124573 0.5561993718147278,0.10911163687705994 L0.5598629117012024,0.11153143644332886 C0.5674340724945068,0.11637106537818909,0.575005292892456,0.12165066599845886,0.58257657289505,0.12693023681640625 L0.5852631330490112,0.12869009375572205 C0.5940554738044739,0.1348496377468109 0.6026036739349365,0.14144912362098694 0.6111517548561096,0.14826861023902893 0.6192114949226379,0.1544281542301178 0.6267826557159424,0.16080763936042786 0.6355750560760498,0.16762709617614746 0.6443674564361572,0.17444661259651184 0.6512059569358826,0.18126606941223145 0.6585330367088318,0.18852552771568298 0.6609752774238586,0.19072535634040833 0.6631733775138855,0.19314518570899963 0.6656156778335571,0.19556495547294617 0.6719657778739929,0.201944500207901 0.6783158183097839,0.20832398533821106 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21602341532707214 0.6858870983123779,0.21690335869789124 0.6927255392074585,0.22438275814056396 0.6993198394775391,0.23186218738555908 0.7056698799133301,0.23890167474746704 0.7120199799537659,0.2459411323070526 0.7191027402877808,0.2554003894329071 0.7256970405578613,0.26419970393180847 L0.7325356006622314,0.27387893199920654 C0.7376644611358643,0.28113842010498047 0.7427933216094971,0.2886178195476532 0.7476779222488403,0.29587724804878235 0.887993574142456,0.5147373676300049 0.8601140975952148,0.7894638180732727 0.6780716180801392,0.9817845821380615 0.6709265112876892,0.989209771156311 0.6617487668991089,0.9948219060897827 0.6514502167701721,0.998063325881958 L0.6431463360786438,0.9998232126235962 C0.6424370408058167,1.000058889389038 0.6416575908660889,1.000058889389038 0.6409482359886169,0.9998232126235962 0.6382670402526855,1.0000406503677368 0.6355696320533752,1.0000406503677368 0.6328884959220886,0.9998232126235962 C0.6096473932266235,1.0000758171081543,0.5884177088737488,0.9879860877990723,0.5787044167518616,0.9689667224884033 C0.5689911842346191,0.9499474763870239 0.5726134181022644,0.9275609254837036 0.5879496335983276,0.911829948425293 0.6916045546531677,0.8017895221710205 0.7362658977508545,0.6562486886978149 0.7100660800933838,0.5138804912567139 0.7100660800933838,0.5086009502410889 0.7081122398376465,0.5033213496208191 0.7068911194801331,0.4980417490005493 0.7056698799133301,0.49276214838027954 0.7044487595558167,0.48748254776000977 0.7029833793640137,0.4819830060005188 0.7015179395675659,0.47648340463638306 0.7002968192100525,0.47142380475997925 0.69858717918396,0.4661442041397095 0.6968775987625122,0.4608646035194397 0.6956563591957092,0.45624494552612305 0.6937025189399719,0.45118534564971924 0.6888028383255005,0.4367102384567261 0.6830968260765076,0.42246657609939575 0.6766061782836914,0.4085085988044739 0.6751407384872437,0.4045489430427551 0.6731869578361511,0.40102916955947876 0.6714773178100586,0.3972894549369812 0.6635856032371521,0.381019651889801 0.6546980738639832,0.3651564121246338 0.6448559165000916,0.349773108959198 0.6414366960525513,0.34493350982666016 0.638261616230011,0.3398738503456116 0.6350865960121155,0.3352542519569397 0.6289807558059692,0.32645493745803833 0.622630774974823,0.3180955648422241 0.6162806153297424,0.3097361922264099 0.6099306344985962,0.3013768196105957 0.600894033908844,0.2910376489162445 0.591857373714447,0.2820183336734772 L0.5801341533660889,0.2696993052959442 C0.5715860724449158,0.26089999079704285 0.5625494718551636,0.25254058837890625 0.5535128116607666,0.2446211874485016 0.5493572354316711,0.24065852165222168 0.5449538826942444,0.23691263794898987 0.5403242707252502,0.2334021031856537 L0.529333770275116,0.22460272908210754 C0.5064655542373657,0.20664280652999878 0.48195523023605347,0.19045335054397583 0.4560639262199402,0.17620646953582764 0.45191192626953125,0.17378664016723633 0.44727152585983276,0.171586811542511 0.44287532567977905,0.16938698291778564 0.43847912549972534,0.1671871840953827 0.43261754512786865,0.16410738229751587 0.42748862504959106,0.16168758273124695 L0.416253924369812,0.15662795305252075 C0.39798784255981445,0.14821934700012207,0.37914353609085083,0.14087164402008057,0.3598361015319824,0.13462966680526733 L0.3490898609161377,0.13132989406585693 0.3466475009918213,0.13132989406585693 C0.3337032198905945,0.1273702085018158,0.32051461935043335,0.1238504946231842,0.3068375587463379,0.12077072262763977 L0.3007317781448364,0.11945080757141113 C0.284625768661499,0.11583057045936584 0.26832127571105957,0.1129668653011322 0.25188517570495605,0.11087149381637573 0.2433369755744934,0.1095515787601471 0.23478883504867554,0.108671635389328 0.22746187448501587,0.10801169276237488 0.2201349139213562,0.10735175013542175 0.2079232633113861,0.10647183656692505 0.19815391302108765,0.10625183582305908 0.18838459253311157,0.1060318648815155 0.18227878212928772,0.10625183582305908 0.17373061180114746,0.10625183582305908 0.14096742868423462,0.10622519254684448 0.1082688570022583,0.10887587070465088 0.0760374665260315,0.11417123675346375 0.07254058122634888,0.11437812447547913 0.06903234124183655,0.11437812447547913 0.06553545594215393,0.11417123675346375 C0.03505754470825195,0.11396905779838562,0.00974804162979126,0.09292516112327576,0.007091343402862549,0.06557714939117432 C0.0044345855712890625,0.03822912275791168 0.02530190348625183,0.013545975089073181 0.05527767539024353,0.008579328656196594 0.08923646807670593,0.0032099932432174683 0.12362384796142578,0.00034074485301971436 0.1580997109413147,0z
+ 174
+ 5
+ 0.65
+ 0.7
+
+
+ 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
+
+
+ M0.8314498662948608,0.14738866686820984 L0.9718837738037109,0.14738866686820984 C0.9818984270095825,0.14737924933433533 0.9911608695983887,0.15217334032058716 0.9961943626403809,0.1599714159965515 1.0012279748916626,0.1677694320678711 1.0012708902359009,0.17739155888557434 0.9963071346282959,0.18522578477859497 L0.9271892309188843,0.2952173352241516 C0.9220999479293823,0.303014874458313,0.9128080606460571,0.30780303478240967,0.9027659893035889,0.30780303478240967 C0.892723798751831,0.30780303478240967,0.8834319114685059,0.303014874458313,0.8783426284790039,0.2952173352241516 L0.8644212484359741,0.2732190191745758 0.8512327671051025,0.2802584767341614 C0.8458596467971802,0.2705792188644409 0.8399980068206787,0.26089999079704285 0.8338921070098877,0.25144070386886597 0.8277863264083862,0.2419814169406891 0.8177727460861206,0.22724255919456482 0.8094688653945923,0.2155834138393402 L0.8224133253097534,0.20898395776748657 0.8070266246795654,0.18566575646400452 C0.8019757270812988,0.17778605222702026,0.8019649982452393,0.16808003187179565,0.8069984912872314,0.16019132733345032 C0.8120319843292236,0.15230253338813782,0.8213481903076172,0.1474246382713318,0.8314498662948608,0.14738866686820984z M0.10265883803367615,0.2802584767341614 C0.11096274852752686,0.2850980758666992 0.11951091885566711,0.28993773460388184 0.12708213925361633,0.2943373918533325 0.14149188995361328,0.3020368218421936 0.15663433074951172,0.3090762495994568 0.1715325117111206,0.31633567810058594 0.21944981813430786,0.33654189109802246 0.2699943780899048,0.35124915838241577 0.32197999954223633,0.3601123094558716 L0.20377129316329956,0.4168679714202881 0.1908269226551056,0.42302751541137695 C0.14440736174583435,0.4102410674095154 0.09916770458221436,0.3942067623138428 0.05552190542221069,0.3750711679458618 -0.05462715029716492,0.3422936797142029 0.01937541365623474,0.22108301520347595 0.10265883803367615,0.2802584767341614z M0.4714505672454834,0.5930745005607605 C0.46778708696365356,0.602533757686615 0.4646120071411133,0.6119930148124695 0.4614369869232178,0.6214522123336792 0.4521523118019104,0.6509119868278503 0.4455362558364868,0.6810019016265869 0.44165414571762085,0.7114253044128418 0.4388039708137512,0.7340653538703918 0.4374176263809204,0.7568352818489075 0.4375022053718567,0.7796201109886169 0.4375022053718567,0.7877594828605652 0.4375022053718567,0.7961188554763794 0.4375022053718567,0.8042582273483276 C0.444272518157959,0.8297898769378662,0.42963647842407227,0.8560013771057129,0.40284329652786255,0.8663284778594971 C0.37605005502700806,0.8766556978225708 0.3448934555053711,0.8680944442749023 0.3290627598762512,0.8460550308227539 0.31538575887680054,0.8240567445755005 0.32368963956832886,0.8020584583282471 0.3246665596961975,0.7783002257347107 0.32564353942871094,0.7545420527458191 0.3297954797744751,0.7343035936355591 0.33419162034988403,0.7123052477836609 0.33712244033813477,0.6960265636444092 0.3407859206199646,0.6799677610397339 0.344693660736084,0.6639089584350586 L0.35763800144195557,0.6566495895385742 0.44629454612731934,0.6069333553314209 0.4714505672454834,0.5930745005607605z M0.5317760705947876,0.3625321388244629 C0.5405619740486145,0.3735124468803406 0.5487139821052551,0.384893536567688 0.5561993718147278,0.39662957191467285 0.5627936720848083,0.40674877166748047 0.5688995122909546,0.41730791330337524 0.5732957124710083,0.4278671145439148 L0.551070511341095,0.4404062032699585 0.4829294681549072,0.47824329137802124 0.30463945865631104,0.5792155265808105 0.1959558129310608,0.640370786190033 0.14173611998558044,0.6705085635185242 C0.13235116004943848,0.675317645072937,0.12175974249839783,0.6778919100761414,0.11096274852752686,0.6779879927635193 C0.08286315202713013,0.6767981648445129,0.05947789549827576,0.6581392884254456,0.054777029901742935,0.6331580281257629 C0.05007615685462952,0.6081767678260803,0.06532919406890869,0.583619236946106,0.0914241373538971,0.5741559267044067 L0.10558965802192688,0.56733638048172 0.13733991980552673,0.5521575808525085 0.25139671564102173,0.4971618056297302 0.43701374530792236,0.4080686569213867 0.5088182091712952,0.3735312819480896 0.5317760705947876,0.3625321388244629z M0.1580997109413147,0 L0.17324218153953552,0 C0.1837441623210907,0,0.19424617290496826,0,0.20450395345687866,0 L0.22892725467681885,0.001539871096611023 0.25506019592285156,0.003959685564041138 C0.2677602767944336,0.005499586462974548 0.279483437538147,0.007039457559585571 0.29242783784866333,0.009239301085472107 0.29804515838623047,0.009239301085472107 0.3036625385284424,0.010999158024787903 0.30952417850494385,0.012099072337150574 L0.31294339895248413,0.012099072337150574 C0.321491539478302,0.013858944177627563 0.3297954797744751,0.015398815274238586 0.3373667001724243,0.017598658800125122 0.35522812604904175,0.02149401605129242 0.37283915281295776,0.02626737952232361 0.3901209831237793,0.031897544860839844 0.4018441438674927,0.03541727364063263 0.4130789041519165,0.03915698826313019 0.42431360483169556,0.0433366596698761 0.4323732852935791,0.04641643166542053 0.4406772255897522,0.049496203660964966 0.44873690605163574,0.05301591753959656 0.4567965865135193,0.05653566122055054 0.46290236711502075,0.05895546078681946 0.46974092721939087,0.06225523352622986 L0.47633522748947144,0.06511500477790833 C0.4831737279891968,0.06819474697113037 0.4900122880935669,0.07149451971054077 0.4963623285293579,0.07501423358917236 0.5027123689651489,0.07853397727012634 0.5119932889938354,0.08293363451957703 0.5207855701446533,0.08711332082748413 0.532753050327301,0.09393277764320374 0.5452088713645935,0.10075226426124573 0.5561993718147278,0.10911163687705994 L0.5598629117012024,0.11153143644332886 C0.5674340724945068,0.11637106537818909,0.575005292892456,0.12165066599845886,0.58257657289505,0.12693023681640625 L0.5852631330490112,0.12869009375572205 C0.5940554738044739,0.1348496377468109 0.6026036739349365,0.14144912362098694 0.6111517548561096,0.14826861023902893 0.6192114949226379,0.1544281542301178 0.6267826557159424,0.16080763936042786 0.6355750560760498,0.16762709617614746 0.6443674564361572,0.17444661259651184 0.6512059569358826,0.18126606941223145 0.6585330367088318,0.18852552771568298 0.6609752774238586,0.19072535634040833 0.6631733775138855,0.19314518570899963 0.6656156778335571,0.19556495547294617 0.6719657778739929,0.201944500207901 0.6783158183097839,0.20832398533821106 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21602341532707214 0.6858870983123779,0.21690335869789124 0.6927255392074585,0.22438275814056396 0.6993198394775391,0.23186218738555908 0.7056698799133301,0.23890167474746704 0.7120199799537659,0.2459411323070526 0.7191027402877808,0.2554003894329071 0.7256970405578613,0.26419970393180847 L0.7325356006622314,0.27387893199920654 C0.7376644611358643,0.28113842010498047 0.7427933216094971,0.2886178195476532 0.7476779222488403,0.29587724804878235 0.887993574142456,0.5147373676300049 0.8601140975952148,0.7894638180732727 0.6780716180801392,0.9817845821380615 0.6709265112876892,0.989209771156311 0.6617487668991089,0.9948219060897827 0.6514502167701721,0.998063325881958 L0.6431463360786438,0.9998232126235962 C0.6424370408058167,1.000058889389038 0.6416575908660889,1.000058889389038 0.6409482359886169,0.9998232126235962 0.6382670402526855,1.0000406503677368 0.6355696320533752,1.0000406503677368 0.6328884959220886,0.9998232126235962 C0.6096473932266235,1.0000758171081543,0.5884177088737488,0.9879860877990723,0.5787044167518616,0.9689667224884033 C0.5689911842346191,0.9499474763870239 0.5726134181022644,0.9275609254837036 0.5879496335983276,0.911829948425293 0.6916045546531677,0.8017895221710205 0.7362658977508545,0.6562486886978149 0.7100660800933838,0.5138804912567139 0.7100660800933838,0.5086009502410889 0.7081122398376465,0.5033213496208191 0.7068911194801331,0.4980417490005493 0.7056698799133301,0.49276214838027954 0.7044487595558167,0.48748254776000977 0.7029833793640137,0.4819830060005188 0.7015179395675659,0.47648340463638306 0.7002968192100525,0.47142380475997925 0.69858717918396,0.4661442041397095 0.6968775987625122,0.4608646035194397 0.6956563591957092,0.45624494552612305 0.6937025189399719,0.45118534564971924 0.6888028383255005,0.4367102384567261 0.6830968260765076,0.42246657609939575 0.6766061782836914,0.4085085988044739 0.6751407384872437,0.4045489430427551 0.6731869578361511,0.40102916955947876 0.6714773178100586,0.3972894549369812 0.6635856032371521,0.381019651889801 0.6546980738639832,0.3651564121246338 0.6448559165000916,0.349773108959198 0.6414366960525513,0.34493350982666016 0.638261616230011,0.3398738503456116 0.6350865960121155,0.3352542519569397 0.6289807558059692,0.32645493745803833 0.622630774974823,0.3180955648422241 0.6162806153297424,0.3097361922264099 0.6099306344985962,0.3013768196105957 0.600894033908844,0.2910376489162445 0.591857373714447,0.2820183336734772 L0.5801341533660889,0.2696993052959442 C0.5715860724449158,0.26089999079704285 0.5625494718551636,0.25254058837890625 0.5535128116607666,0.2446211874485016 0.5493572354316711,0.24065852165222168 0.5449538826942444,0.23691263794898987 0.5403242707252502,0.2334021031856537 L0.529333770275116,0.22460272908210754 C0.5064655542373657,0.20664280652999878 0.48195523023605347,0.19045335054397583 0.4560639262199402,0.17620646953582764 0.45191192626953125,0.17378664016723633 0.44727152585983276,0.171586811542511 0.44287532567977905,0.16938698291778564 0.43847912549972534,0.1671871840953827 0.43261754512786865,0.16410738229751587 0.42748862504959106,0.16168758273124695 L0.416253924369812,0.15662795305252075 C0.39798784255981445,0.14821934700012207,0.37914353609085083,0.14087164402008057,0.3598361015319824,0.13462966680526733 L0.3490898609161377,0.13132989406585693 0.3466475009918213,0.13132989406585693 C0.3337032198905945,0.1273702085018158,0.32051461935043335,0.1238504946231842,0.3068375587463379,0.12077072262763977 L0.3007317781448364,0.11945080757141113 C0.284625768661499,0.11583057045936584 0.26832127571105957,0.1129668653011322 0.25188517570495605,0.11087149381637573 0.2433369755744934,0.1095515787601471 0.23478883504867554,0.108671635389328 0.22746187448501587,0.10801169276237488 0.2201349139213562,0.10735175013542175 0.2079232633113861,0.10647183656692505 0.19815391302108765,0.10625183582305908 0.18838459253311157,0.1060318648815155 0.18227878212928772,0.10625183582305908 0.17373061180114746,0.10625183582305908 0.14096742868423462,0.10622519254684448 0.1082688570022583,0.10887587070465088 0.0760374665260315,0.11417123675346375 0.07254058122634888,0.11437812447547913 0.06903234124183655,0.11437812447547913 0.06553545594215393,0.11417123675346375 C0.03505754470825195,0.11396905779838562,0.00974804162979126,0.09292516112327576,0.007091343402862549,0.06557714939117432 C0.0044345855712890625,0.03822912275791168 0.02530190348625183,0.013545975089073181 0.05527767539024353,0.008579328656196594 0.08923646807670593,0.0032099932432174683 0.12362384796142578,0.00034074485301971436 0.1580997109413147,0z
+ 174
+ 5
+ 0.65
+ 0.7
+
+
+ 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
+
+
+ M0.8314498662948608,0.14738866686820984 L0.9718837738037109,0.14738866686820984 C0.9818984270095825,0.14737924933433533 0.9911608695983887,0.15217334032058716 0.9961943626403809,0.1599714159965515 1.0012279748916626,0.1677694320678711 1.0012708902359009,0.17739155888557434 0.9963071346282959,0.18522578477859497 L0.9271892309188843,0.2952173352241516 C0.9220999479293823,0.303014874458313,0.9128080606460571,0.30780303478240967,0.9027659893035889,0.30780303478240967 C0.892723798751831,0.30780303478240967,0.8834319114685059,0.303014874458313,0.8783426284790039,0.2952173352241516 L0.8644212484359741,0.2732190191745758 0.8512327671051025,0.2802584767341614 C0.8458596467971802,0.2705792188644409 0.8399980068206787,0.26089999079704285 0.8338921070098877,0.25144070386886597 0.8277863264083862,0.2419814169406891 0.8177727460861206,0.22724255919456482 0.8094688653945923,0.2155834138393402 L0.8224133253097534,0.20898395776748657 0.8070266246795654,0.18566575646400452 C0.8019757270812988,0.17778605222702026,0.8019649982452393,0.16808003187179565,0.8069984912872314,0.16019132733345032 C0.8120319843292236,0.15230253338813782,0.8213481903076172,0.1474246382713318,0.8314498662948608,0.14738866686820984z M0.10265883803367615,0.2802584767341614 C0.11096274852752686,0.2850980758666992 0.11951091885566711,0.28993773460388184 0.12708213925361633,0.2943373918533325 0.14149188995361328,0.3020368218421936 0.15663433074951172,0.3090762495994568 0.1715325117111206,0.31633567810058594 0.21944981813430786,0.33654189109802246 0.2699943780899048,0.35124915838241577 0.32197999954223633,0.3601123094558716 L0.20377129316329956,0.4168679714202881 0.1908269226551056,0.42302751541137695 C0.14440736174583435,0.4102410674095154 0.09916770458221436,0.3942067623138428 0.05552190542221069,0.3750711679458618 -0.05462715029716492,0.3422936797142029 0.01937541365623474,0.22108301520347595 0.10265883803367615,0.2802584767341614z M0.4714505672454834,0.5930745005607605 C0.46778708696365356,0.602533757686615 0.4646120071411133,0.6119930148124695 0.4614369869232178,0.6214522123336792 0.4521523118019104,0.6509119868278503 0.4455362558364868,0.6810019016265869 0.44165414571762085,0.7114253044128418 0.4388039708137512,0.7340653538703918 0.4374176263809204,0.7568352818489075 0.4375022053718567,0.7796201109886169 0.4375022053718567,0.7877594828605652 0.4375022053718567,0.7961188554763794 0.4375022053718567,0.8042582273483276 C0.444272518157959,0.8297898769378662,0.42963647842407227,0.8560013771057129,0.40284329652786255,0.8663284778594971 C0.37605005502700806,0.8766556978225708 0.3448934555053711,0.8680944442749023 0.3290627598762512,0.8460550308227539 0.31538575887680054,0.8240567445755005 0.32368963956832886,0.8020584583282471 0.3246665596961975,0.7783002257347107 0.32564353942871094,0.7545420527458191 0.3297954797744751,0.7343035936355591 0.33419162034988403,0.7123052477836609 0.33712244033813477,0.6960265636444092 0.3407859206199646,0.6799677610397339 0.344693660736084,0.6639089584350586 L0.35763800144195557,0.6566495895385742 0.44629454612731934,0.6069333553314209 0.4714505672454834,0.5930745005607605z M0.5317760705947876,0.3625321388244629 C0.5405619740486145,0.3735124468803406 0.5487139821052551,0.384893536567688 0.5561993718147278,0.39662957191467285 0.5627936720848083,0.40674877166748047 0.5688995122909546,0.41730791330337524 0.5732957124710083,0.4278671145439148 L0.551070511341095,0.4404062032699585 0.4829294681549072,0.47824329137802124 0.30463945865631104,0.5792155265808105 0.1959558129310608,0.640370786190033 0.14173611998558044,0.6705085635185242 C0.13235116004943848,0.675317645072937,0.12175974249839783,0.6778919100761414,0.11096274852752686,0.6779879927635193 C0.08286315202713013,0.6767981648445129,0.05947789549827576,0.6581392884254456,0.054777029901742935,0.6331580281257629 C0.05007615685462952,0.6081767678260803,0.06532919406890869,0.583619236946106,0.0914241373538971,0.5741559267044067 L0.10558965802192688,0.56733638048172 0.13733991980552673,0.5521575808525085 0.25139671564102173,0.4971618056297302 0.43701374530792236,0.4080686569213867 0.5088182091712952,0.3735312819480896 0.5317760705947876,0.3625321388244629z M0.1580997109413147,0 L0.17324218153953552,0 C0.1837441623210907,0,0.19424617290496826,0,0.20450395345687866,0 L0.22892725467681885,0.001539871096611023 0.25506019592285156,0.003959685564041138 C0.2677602767944336,0.005499586462974548 0.279483437538147,0.007039457559585571 0.29242783784866333,0.009239301085472107 0.29804515838623047,0.009239301085472107 0.3036625385284424,0.010999158024787903 0.30952417850494385,0.012099072337150574 L0.31294339895248413,0.012099072337150574 C0.321491539478302,0.013858944177627563 0.3297954797744751,0.015398815274238586 0.3373667001724243,0.017598658800125122 0.35522812604904175,0.02149401605129242 0.37283915281295776,0.02626737952232361 0.3901209831237793,0.031897544860839844 0.4018441438674927,0.03541727364063263 0.4130789041519165,0.03915698826313019 0.42431360483169556,0.0433366596698761 0.4323732852935791,0.04641643166542053 0.4406772255897522,0.049496203660964966 0.44873690605163574,0.05301591753959656 0.4567965865135193,0.05653566122055054 0.46290236711502075,0.05895546078681946 0.46974092721939087,0.06225523352622986 L0.47633522748947144,0.06511500477790833 C0.4831737279891968,0.06819474697113037 0.4900122880935669,0.07149451971054077 0.4963623285293579,0.07501423358917236 0.5027123689651489,0.07853397727012634 0.5119932889938354,0.08293363451957703 0.5207855701446533,0.08711332082748413 0.532753050327301,0.09393277764320374 0.5452088713645935,0.10075226426124573 0.5561993718147278,0.10911163687705994 L0.5598629117012024,0.11153143644332886 C0.5674340724945068,0.11637106537818909,0.575005292892456,0.12165066599845886,0.58257657289505,0.12693023681640625 L0.5852631330490112,0.12869009375572205 C0.5940554738044739,0.1348496377468109 0.6026036739349365,0.14144912362098694 0.6111517548561096,0.14826861023902893 0.6192114949226379,0.1544281542301178 0.6267826557159424,0.16080763936042786 0.6355750560760498,0.16762709617614746 0.6443674564361572,0.17444661259651184 0.6512059569358826,0.18126606941223145 0.6585330367088318,0.18852552771568298 0.6609752774238586,0.19072535634040833 0.6631733775138855,0.19314518570899963 0.6656156778335571,0.19556495547294617 0.6719657778739929,0.201944500207901 0.6783158183097839,0.20832398533821106 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21602341532707214 0.6858870983123779,0.21690335869789124 0.6927255392074585,0.22438275814056396 0.6993198394775391,0.23186218738555908 0.7056698799133301,0.23890167474746704 0.7120199799537659,0.2459411323070526 0.7191027402877808,0.2554003894329071 0.7256970405578613,0.26419970393180847 L0.7325356006622314,0.27387893199920654 C0.7376644611358643,0.28113842010498047 0.7427933216094971,0.2886178195476532 0.7476779222488403,0.29587724804878235 0.887993574142456,0.5147373676300049 0.8601140975952148,0.7894638180732727 0.6780716180801392,0.9817845821380615 0.6709265112876892,0.989209771156311 0.6617487668991089,0.9948219060897827 0.6514502167701721,0.998063325881958 L0.6431463360786438,0.9998232126235962 C0.6424370408058167,1.000058889389038 0.6416575908660889,1.000058889389038 0.6409482359886169,0.9998232126235962 0.6382670402526855,1.0000406503677368 0.6355696320533752,1.0000406503677368 0.6328884959220886,0.9998232126235962 C0.6096473932266235,1.0000758171081543,0.5884177088737488,0.9879860877990723,0.5787044167518616,0.9689667224884033 C0.5689911842346191,0.9499474763870239 0.5726134181022644,0.9275609254837036 0.5879496335983276,0.911829948425293 0.6916045546531677,0.8017895221710205 0.7362658977508545,0.6562486886978149 0.7100660800933838,0.5138804912567139 0.7100660800933838,0.5086009502410889 0.7081122398376465,0.5033213496208191 0.7068911194801331,0.4980417490005493 0.7056698799133301,0.49276214838027954 0.7044487595558167,0.48748254776000977 0.7029833793640137,0.4819830060005188 0.7015179395675659,0.47648340463638306 0.7002968192100525,0.47142380475997925 0.69858717918396,0.4661442041397095 0.6968775987625122,0.4608646035194397 0.6956563591957092,0.45624494552612305 0.6937025189399719,0.45118534564971924 0.6888028383255005,0.4367102384567261 0.6830968260765076,0.42246657609939575 0.6766061782836914,0.4085085988044739 0.6751407384872437,0.4045489430427551 0.6731869578361511,0.40102916955947876 0.6714773178100586,0.3972894549369812 0.6635856032371521,0.381019651889801 0.6546980738639832,0.3651564121246338 0.6448559165000916,0.349773108959198 0.6414366960525513,0.34493350982666016 0.638261616230011,0.3398738503456116 0.6350865960121155,0.3352542519569397 0.6289807558059692,0.32645493745803833 0.622630774974823,0.3180955648422241 0.6162806153297424,0.3097361922264099 0.6099306344985962,0.3013768196105957 0.600894033908844,0.2910376489162445 0.591857373714447,0.2820183336734772 L0.5801341533660889,0.2696993052959442 C0.5715860724449158,0.26089999079704285 0.5625494718551636,0.25254058837890625 0.5535128116607666,0.2446211874485016 0.5493572354316711,0.24065852165222168 0.5449538826942444,0.23691263794898987 0.5403242707252502,0.2334021031856537 L0.529333770275116,0.22460272908210754 C0.5064655542373657,0.20664280652999878 0.48195523023605347,0.19045335054397583 0.4560639262199402,0.17620646953582764 0.45191192626953125,0.17378664016723633 0.44727152585983276,0.171586811542511 0.44287532567977905,0.16938698291778564 0.43847912549972534,0.1671871840953827 0.43261754512786865,0.16410738229751587 0.42748862504959106,0.16168758273124695 L0.416253924369812,0.15662795305252075 C0.39798784255981445,0.14821934700012207,0.37914353609085083,0.14087164402008057,0.3598361015319824,0.13462966680526733 L0.3490898609161377,0.13132989406585693 0.3466475009918213,0.13132989406585693 C0.3337032198905945,0.1273702085018158,0.32051461935043335,0.1238504946231842,0.3068375587463379,0.12077072262763977 L0.3007317781448364,0.11945080757141113 C0.284625768661499,0.11583057045936584 0.26832127571105957,0.1129668653011322 0.25188517570495605,0.11087149381637573 0.2433369755744934,0.1095515787601471 0.23478883504867554,0.108671635389328 0.22746187448501587,0.10801169276237488 0.2201349139213562,0.10735175013542175 0.2079232633113861,0.10647183656692505 0.19815391302108765,0.10625183582305908 0.18838459253311157,0.1060318648815155 0.18227878212928772,0.10625183582305908 0.17373061180114746,0.10625183582305908 0.14096742868423462,0.10622519254684448 0.1082688570022583,0.10887587070465088 0.0760374665260315,0.11417123675346375 0.07254058122634888,0.11437812447547913 0.06903234124183655,0.11437812447547913 0.06553545594215393,0.11417123675346375 C0.03505754470825195,0.11396905779838562,0.00974804162979126,0.09292516112327576,0.007091343402862549,0.06557714939117432 C0.0044345855712890625,0.03822912275791168 0.02530190348625183,0.013545975089073181 0.05527767539024353,0.008579328656196594 0.08923646807670593,0.0032099932432174683 0.12362384796142578,0.00034074485301971436 0.1580997109413147,0z
+ 226
+ 5
+ 0.65
+ 0.7
+
+
+ 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
+
+
+ M0.8314498662948608,0.14738866686820984 L0.9718837738037109,0.14738866686820984 C0.9818984270095825,0.14737924933433533 0.9911608695983887,0.15217334032058716 0.9961943626403809,0.1599714159965515 1.0012279748916626,0.1677694320678711 1.0012708902359009,0.17739155888557434 0.9963071346282959,0.18522578477859497 L0.9271892309188843,0.2952173352241516 C0.9220999479293823,0.303014874458313,0.9128080606460571,0.30780303478240967,0.9027659893035889,0.30780303478240967 C0.892723798751831,0.30780303478240967,0.8834319114685059,0.303014874458313,0.8783426284790039,0.2952173352241516 L0.8644212484359741,0.2732190191745758 0.8512327671051025,0.2802584767341614 C0.8458596467971802,0.2705792188644409 0.8399980068206787,0.26089999079704285 0.8338921070098877,0.25144070386886597 0.8277863264083862,0.2419814169406891 0.8177727460861206,0.22724255919456482 0.8094688653945923,0.2155834138393402 L0.8224133253097534,0.20898395776748657 0.8070266246795654,0.18566575646400452 C0.8019757270812988,0.17778605222702026,0.8019649982452393,0.16808003187179565,0.8069984912872314,0.16019132733345032 C0.8120319843292236,0.15230253338813782,0.8213481903076172,0.1474246382713318,0.8314498662948608,0.14738866686820984z M0.10265883803367615,0.2802584767341614 C0.11096274852752686,0.2850980758666992 0.11951091885566711,0.28993773460388184 0.12708213925361633,0.2943373918533325 0.14149188995361328,0.3020368218421936 0.15663433074951172,0.3090762495994568 0.1715325117111206,0.31633567810058594 0.21944981813430786,0.33654189109802246 0.2699943780899048,0.35124915838241577 0.32197999954223633,0.3601123094558716 L0.20377129316329956,0.4168679714202881 0.1908269226551056,0.42302751541137695 C0.14440736174583435,0.4102410674095154 0.09916770458221436,0.3942067623138428 0.05552190542221069,0.3750711679458618 -0.05462715029716492,0.3422936797142029 0.01937541365623474,0.22108301520347595 0.10265883803367615,0.2802584767341614z M0.4714505672454834,0.5930745005607605 C0.46778708696365356,0.602533757686615 0.4646120071411133,0.6119930148124695 0.4614369869232178,0.6214522123336792 0.4521523118019104,0.6509119868278503 0.4455362558364868,0.6810019016265869 0.44165414571762085,0.7114253044128418 0.4388039708137512,0.7340653538703918 0.4374176263809204,0.7568352818489075 0.4375022053718567,0.7796201109886169 0.4375022053718567,0.7877594828605652 0.4375022053718567,0.7961188554763794 0.4375022053718567,0.8042582273483276 C0.444272518157959,0.8297898769378662,0.42963647842407227,0.8560013771057129,0.40284329652786255,0.8663284778594971 C0.37605005502700806,0.8766556978225708 0.3448934555053711,0.8680944442749023 0.3290627598762512,0.8460550308227539 0.31538575887680054,0.8240567445755005 0.32368963956832886,0.8020584583282471 0.3246665596961975,0.7783002257347107 0.32564353942871094,0.7545420527458191 0.3297954797744751,0.7343035936355591 0.33419162034988403,0.7123052477836609 0.33712244033813477,0.6960265636444092 0.3407859206199646,0.6799677610397339 0.344693660736084,0.6639089584350586 L0.35763800144195557,0.6566495895385742 0.44629454612731934,0.6069333553314209 0.4714505672454834,0.5930745005607605z M0.5317760705947876,0.3625321388244629 C0.5405619740486145,0.3735124468803406 0.5487139821052551,0.384893536567688 0.5561993718147278,0.39662957191467285 0.5627936720848083,0.40674877166748047 0.5688995122909546,0.41730791330337524 0.5732957124710083,0.4278671145439148 L0.551070511341095,0.4404062032699585 0.4829294681549072,0.47824329137802124 0.30463945865631104,0.5792155265808105 0.1959558129310608,0.640370786190033 0.14173611998558044,0.6705085635185242 C0.13235116004943848,0.675317645072937,0.12175974249839783,0.6778919100761414,0.11096274852752686,0.6779879927635193 C0.08286315202713013,0.6767981648445129,0.05947789549827576,0.6581392884254456,0.054777029901742935,0.6331580281257629 C0.05007615685462952,0.6081767678260803,0.06532919406890869,0.583619236946106,0.0914241373538971,0.5741559267044067 L0.10558965802192688,0.56733638048172 0.13733991980552673,0.5521575808525085 0.25139671564102173,0.4971618056297302 0.43701374530792236,0.4080686569213867 0.5088182091712952,0.3735312819480896 0.5317760705947876,0.3625321388244629z M0.1580997109413147,0 L0.17324218153953552,0 C0.1837441623210907,0,0.19424617290496826,0,0.20450395345687866,0 L0.22892725467681885,0.001539871096611023 0.25506019592285156,0.003959685564041138 C0.2677602767944336,0.005499586462974548 0.279483437538147,0.007039457559585571 0.29242783784866333,0.009239301085472107 0.29804515838623047,0.009239301085472107 0.3036625385284424,0.010999158024787903 0.30952417850494385,0.012099072337150574 L0.31294339895248413,0.012099072337150574 C0.321491539478302,0.013858944177627563 0.3297954797744751,0.015398815274238586 0.3373667001724243,0.017598658800125122 0.35522812604904175,0.02149401605129242 0.37283915281295776,0.02626737952232361 0.3901209831237793,0.031897544860839844 0.4018441438674927,0.03541727364063263 0.4130789041519165,0.03915698826313019 0.42431360483169556,0.0433366596698761 0.4323732852935791,0.04641643166542053 0.4406772255897522,0.049496203660964966 0.44873690605163574,0.05301591753959656 0.4567965865135193,0.05653566122055054 0.46290236711502075,0.05895546078681946 0.46974092721939087,0.06225523352622986 L0.47633522748947144,0.06511500477790833 C0.4831737279891968,0.06819474697113037 0.4900122880935669,0.07149451971054077 0.4963623285293579,0.07501423358917236 0.5027123689651489,0.07853397727012634 0.5119932889938354,0.08293363451957703 0.5207855701446533,0.08711332082748413 0.532753050327301,0.09393277764320374 0.5452088713645935,0.10075226426124573 0.5561993718147278,0.10911163687705994 L0.5598629117012024,0.11153143644332886 C0.5674340724945068,0.11637106537818909,0.575005292892456,0.12165066599845886,0.58257657289505,0.12693023681640625 L0.5852631330490112,0.12869009375572205 C0.5940554738044739,0.1348496377468109 0.6026036739349365,0.14144912362098694 0.6111517548561096,0.14826861023902893 0.6192114949226379,0.1544281542301178 0.6267826557159424,0.16080763936042786 0.6355750560760498,0.16762709617614746 0.6443674564361572,0.17444661259651184 0.6512059569358826,0.18126606941223145 0.6585330367088318,0.18852552771568298 0.6609752774238586,0.19072535634040833 0.6631733775138855,0.19314518570899963 0.6656156778335571,0.19556495547294617 0.6719657778739929,0.201944500207901 0.6783158183097839,0.20832398533821106 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21492350101470947 0.6844216585159302,0.21602341532707214 0.6858870983123779,0.21690335869789124 0.6927255392074585,0.22438275814056396 0.6993198394775391,0.23186218738555908 0.7056698799133301,0.23890167474746704 0.7120199799537659,0.2459411323070526 0.7191027402877808,0.2554003894329071 0.7256970405578613,0.26419970393180847 L0.7325356006622314,0.27387893199920654 C0.7376644611358643,0.28113842010498047 0.7427933216094971,0.2886178195476532 0.7476779222488403,0.29587724804878235 0.887993574142456,0.5147373676300049 0.8601140975952148,0.7894638180732727 0.6780716180801392,0.9817845821380615 0.6709265112876892,0.989209771156311 0.6617487668991089,0.9948219060897827 0.6514502167701721,0.998063325881958 L0.6431463360786438,0.9998232126235962 C0.6424370408058167,1.000058889389038 0.6416575908660889,1.000058889389038 0.6409482359886169,0.9998232126235962 0.6382670402526855,1.0000406503677368 0.6355696320533752,1.0000406503677368 0.6328884959220886,0.9998232126235962 C0.6096473932266235,1.0000758171081543,0.5884177088737488,0.9879860877990723,0.5787044167518616,0.9689667224884033 C0.5689911842346191,0.9499474763870239 0.5726134181022644,0.9275609254837036 0.5879496335983276,0.911829948425293 0.6916045546531677,0.8017895221710205 0.7362658977508545,0.6562486886978149 0.7100660800933838,0.5138804912567139 0.7100660800933838,0.5086009502410889 0.7081122398376465,0.5033213496208191 0.7068911194801331,0.4980417490005493 0.7056698799133301,0.49276214838027954 0.7044487595558167,0.48748254776000977 0.7029833793640137,0.4819830060005188 0.7015179395675659,0.47648340463638306 0.7002968192100525,0.47142380475997925 0.69858717918396,0.4661442041397095 0.6968775987625122,0.4608646035194397 0.6956563591957092,0.45624494552612305 0.6937025189399719,0.45118534564971924 0.6888028383255005,0.4367102384567261 0.6830968260765076,0.42246657609939575 0.6766061782836914,0.4085085988044739 0.6751407384872437,0.4045489430427551 0.6731869578361511,0.40102916955947876 0.6714773178100586,0.3972894549369812 0.6635856032371521,0.381019651889801 0.6546980738639832,0.3651564121246338 0.6448559165000916,0.349773108959198 0.6414366960525513,0.34493350982666016 0.638261616230011,0.3398738503456116 0.6350865960121155,0.3352542519569397 0.6289807558059692,0.32645493745803833 0.622630774974823,0.3180955648422241 0.6162806153297424,0.3097361922264099 0.6099306344985962,0.3013768196105957 0.600894033908844,0.2910376489162445 0.591857373714447,0.2820183336734772 L0.5801341533660889,0.2696993052959442 C0.5715860724449158,0.26089999079704285 0.5625494718551636,0.25254058837890625 0.5535128116607666,0.2446211874485016 0.5493572354316711,0.24065852165222168 0.5449538826942444,0.23691263794898987 0.5403242707252502,0.2334021031856537 L0.529333770275116,0.22460272908210754 C0.5064655542373657,0.20664280652999878 0.48195523023605347,0.19045335054397583 0.4560639262199402,0.17620646953582764 0.45191192626953125,0.17378664016723633 0.44727152585983276,0.171586811542511 0.44287532567977905,0.16938698291778564 0.43847912549972534,0.1671871840953827 0.43261754512786865,0.16410738229751587 0.42748862504959106,0.16168758273124695 L0.416253924369812,0.15662795305252075 C0.39798784255981445,0.14821934700012207,0.37914353609085083,0.14087164402008057,0.3598361015319824,0.13462966680526733 L0.3490898609161377,0.13132989406585693 0.3466475009918213,0.13132989406585693 C0.3337032198905945,0.1273702085018158,0.32051461935043335,0.1238504946231842,0.3068375587463379,0.12077072262763977 L0.3007317781448364,0.11945080757141113 C0.284625768661499,0.11583057045936584 0.26832127571105957,0.1129668653011322 0.25188517570495605,0.11087149381637573 0.2433369755744934,0.1095515787601471 0.23478883504867554,0.108671635389328 0.22746187448501587,0.10801169276237488 0.2201349139213562,0.10735175013542175 0.2079232633113861,0.10647183656692505 0.19815391302108765,0.10625183582305908 0.18838459253311157,0.1060318648815155 0.18227878212928772,0.10625183582305908 0.17373061180114746,0.10625183582305908 0.14096742868423462,0.10622519254684448 0.1082688570022583,0.10887587070465088 0.0760374665260315,0.11417123675346375 0.07254058122634888,0.11437812447547913 0.06903234124183655,0.11437812447547913 0.06553545594215393,0.11417123675346375 C0.03505754470825195,0.11396905779838562,0.00974804162979126,0.09292516112327576,0.007091343402862549,0.06557714939117432 C0.0044345855712890625,0.03822912275791168 0.02530190348625183,0.013545975089073181 0.05527767539024353,0.008579328656196594 0.08923646807670593,0.0032099932432174683 0.12362384796142578,0.00034074485301971436 0.1580997109413147,0z
+ 226
+ 5
+ 0.65
+ 0.7
+
+
+ 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">
@@ -82,7 +84,7 @@
diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs
index e3fd07f40..130cbe0c4 100644
--- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs
+++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs
@@ -100,15 +100,11 @@ namespace Artemis.UI.Shared
// Determine the scale required to fit the desired size of the control
Size measureSize = MeasureDevice();
- double scale = Math.Min(DesiredSize.Width / measureSize.Width, DesiredSize.Height / measureSize.Height);
- Rect scaledRect = new(0, 0, measureSize.Width * scale, measureSize.Height * scale);
+ double scale = Math.Min(RenderSize.Width / measureSize.Width, RenderSize.Height / measureSize.Height);
- // Center and scale the visualization in the desired bounding box
- if (DesiredSize.Width > 0 && DesiredSize.Height > 0)
- {
- drawingContext.PushTransform(new TranslateTransform(DesiredSize.Width / 2 - scaledRect.Width / 2, DesiredSize.Height / 2 - scaledRect.Height / 2));
+ // Scale the visualization in the desired bounding box
+ if (RenderSize.Width > 0 && RenderSize.Height > 0)
drawingContext.PushTransform(new ScaleTransform(scale, scale));
- }
// Determine the offset required to rotate within bounds
Rect rotationRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
diff --git a/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml b/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml
index 9fced07e6..a65e58422 100644
--- a/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml
+++ b/src/Artemis.UI.Shared/Screens/Dialogs/ConfirmDialogView.xaml
@@ -1,14 +1,18 @@
-
+ d:DataContext="{d:DesignInstance {x:Type dialogs:ConfirmDialogViewModel}}">
+
+
+
+ Content="{Binding CancelText}"
+ Visibility="{Binding CancelText, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}" />
- Gradient
-
+
+ Gradient
+
+
+
+
+
+
+
+
+
+ Clear Gradient?
+
+
+
+
+
+
+
+
+
@@ -90,8 +165,7 @@
-
- Selected stop
+ Selected stop:
@@ -100,7 +174,7 @@
-
+
-
@@ -122,8 +196,15 @@
Margin="8,0,0,0"
IsEnabled="{Binding HasSelectedColorStopViewModel}"
Command="{s:Action RemoveColorStop}"
- CommandParameter="{Binding SelectedColorStopViewModel}">
- DELETE
+ CommandParameter="{Binding SelectedColorStopViewModel}"
+ ToolTip="Delete Selected Stop">
+
+
+
+
+
+ Delete
+
diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs
index 497d81963..cf94d437f 100644
--- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs
+++ b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
@@ -27,19 +28,9 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
PropertyChanged += UpdateColorStopViewModels;
ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
+ ColorStopViewModels.CollectionChanged += ColorStopViewModelsOnCollectionChanged;
}
- #region Overrides of DialogViewModelBase
-
- ///
- public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
- {
- ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
- base.OnDialogClosed(sender, e);
- }
-
- #endregion
-
public BindableCollection ColorStopViewModels { get; }
public ColorStopViewModel? SelectedColorStopViewModel
@@ -53,6 +44,9 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
}
public bool HasSelectedColorStopViewModel => SelectedColorStopViewModel != null;
+ public bool HasMoreThanOneStop => ColorStopViewModels.Count > 1;
+ private bool popupOpen = false;
+ public bool ClearGradientPopupOpen => popupOpen;
public ColorGradient ColorGradient { get; }
@@ -62,11 +56,20 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
set => SetAndNotify(ref _previewWidth, value);
}
- public ColorGradient Stops
+ public ColorGradient Stops => ColorGradient;
+
+ #region Overrides of DialogViewModelBase
+
+ ///
+ public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
{
- get => ColorGradient;
+ ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
+ ColorStopViewModels.CollectionChanged -= ColorStopViewModelsOnCollectionChanged;
+ base.OnDialogClosed(sender, e);
}
+ #endregion
+
public void AddColorStop(object sender, MouseEventArgs e)
{
Canvas? child = VisualTreeUtilities.FindChild
public virtual void OnDialogClosed(object sender, DialogClosingEventArgs e)
{
+ ScreenExtensions.TryClose(this);
}
///
@@ -61,6 +62,7 @@ namespace Artemis.UI.Shared.Services
internal void OnDialogOpened(object sender, DialogOpenedEventArgs e)
{
Session = e.Session;
+ ScreenExtensions.TryActivate(this);
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs
index a5bbc26f8..6fcee45e3 100644
--- a/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs
+++ b/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs
@@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.Services
{
internal interface IColorPickerService : IArtemisSharedUIService
{
- Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
+ Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
PluginSetting PreviewSetting { get; }
LinkedList RecentColors { get; }
diff --git a/src/Artemis.UI.Shared/Services/Message/IMessageService.cs b/src/Artemis.UI.Shared/Services/Message/IMessageService.cs
index 13d0f89f9..7522c7db0 100644
--- a/src/Artemis.UI.Shared/Services/Message/IMessageService.cs
+++ b/src/Artemis.UI.Shared/Services/Message/IMessageService.cs
@@ -17,7 +17,7 @@ namespace Artemis.UI.Shared.Services
/// Sets up the notification provider that shows desktop notifications
///
/// The notification provider that shows desktop notifications
- void ConfigureNotificationProvider(INotificationProvider notificationProvider);
+ void SetNotificationProvider(INotificationProvider notificationProvider);
///
/// Queues a notification message for display in a snackbar.
@@ -123,7 +123,9 @@ namespace Artemis.UI.Shared.Services
///
/// The title of the notification
/// The message of the notification
- void ShowNotification(string title, string message);
+ /// An optional callback that is invoked when the notification is clicked
+ /// An optional callback that is invoked when the notification is dismissed
+ void ShowNotification(string title, string message, Action? activatedCallback = null, Action? dismissedCallback = null);
///
/// Shows a desktop notification with a Material Design icon
@@ -131,7 +133,9 @@ namespace Artemis.UI.Shared.Services
/// The title of the notification
/// The message of the notification
/// The name of the icon
- void ShowNotification(string title, string message, PackIconKind icon);
+ /// An optional callback that is invoked when the notification is clicked
+ /// An optional callback that is invoked when the notification is dismissed
+ void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback = null, Action? dismissedCallback = null);
///
/// Shows a desktop notification with a Material Design icon
@@ -139,6 +143,8 @@ namespace Artemis.UI.Shared.Services
/// The title of the notification
/// The message of the notification
/// The name of the icon as a string
- void ShowNotification(string title, string message, string icon);
+ /// An optional callback that is invoked when the notification is clicked
+ /// An optional callback that is invoked when the notification is dismissed
+ void ShowNotification(string title, string message, string icon, Action? activatedCallback = null, Action? dismissedCallback = null);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs b/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs
index 23757ef9f..6bf4fa766 100644
--- a/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs
+++ b/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs
@@ -1,4 +1,5 @@
-using MaterialDesignThemes.Wpf;
+using System;
+using MaterialDesignThemes.Wpf;
namespace Artemis.UI.Shared.Services
{
@@ -6,7 +7,7 @@ namespace Artemis.UI.Shared.Services
/// Represents a class provides desktop notifications so that can us it to show desktop
/// notifications
///
- public interface INotificationProvider
+ public interface INotificationProvider : IDisposable
{
///
/// Shows a notification
@@ -14,6 +15,8 @@ namespace Artemis.UI.Shared.Services
/// The title of the notification
/// The message of the notification
/// The Material Design icon to show in the notification
- void ShowNotification(string title, string message, PackIconKind icon);
+ /// A callback that is invoked when the notification is clicked
+ /// A callback that is invoked when the notification is dismissed
+ void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback, Action? dismissedCallback);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Services/Message/MessageService.cs b/src/Artemis.UI.Shared/Services/Message/MessageService.cs
index b0b63d13d..d8e4e44d5 100644
--- a/src/Artemis.UI.Shared/Services/Message/MessageService.cs
+++ b/src/Artemis.UI.Shared/Services/Message/MessageService.cs
@@ -3,7 +3,7 @@ using MaterialDesignThemes.Wpf;
namespace Artemis.UI.Shared.Services
{
- internal class MessageService : IMessageService
+ internal class MessageService : IMessageService, IDisposable
{
private INotificationProvider? _notificationProvider;
public ISnackbarMessageQueue MainMessageQueue { get; }
@@ -14,8 +14,12 @@ namespace Artemis.UI.Shared.Services
}
///
- public void ConfigureNotificationProvider(INotificationProvider notificationProvider)
+ public void SetNotificationProvider(INotificationProvider notificationProvider)
{
+ if (ReferenceEquals(_notificationProvider, notificationProvider))
+ return;
+
+ _notificationProvider?.Dispose();
_notificationProvider = notificationProvider;
}
@@ -72,22 +76,32 @@ namespace Artemis.UI.Shared.Services
}
///
- public void ShowNotification(string title, string message)
+ public void ShowNotification(string title, string message, Action? activatedCallback = null, Action? dismissedCallback = null)
{
- _notificationProvider?.ShowNotification(title, message, PackIconKind.None);
+ _notificationProvider?.ShowNotification(title, message, PackIconKind.None, activatedCallback, dismissedCallback);
}
///
- public void ShowNotification(string title, string message, PackIconKind icon)
+ public void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback = null, Action? dismissedCallback = null)
{
- _notificationProvider?.ShowNotification(title, message, icon);
+ _notificationProvider?.ShowNotification(title, message, icon, activatedCallback, dismissedCallback);
}
///
- public void ShowNotification(string title, string message, string icon)
+ public void ShowNotification(string title, string message, string icon, Action? activatedCallback = null, Action? dismissedCallback = null)
{
Enum.TryParse(typeof(PackIconKind), icon, true, out object? iconKind);
- _notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None));
+ _notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None), activatedCallback, dismissedCallback);
}
+
+ #region IDisposable
+
+ ///
+ public void Dispose()
+ {
+ _notificationProvider?.Dispose();
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json
index 27b93fa8d..88e8b3b52 100644
--- a/src/Artemis.UI.Shared/packages.lock.json
+++ b/src/Artemis.UI.Shared/packages.lock.json
@@ -113,12 +113,6 @@
"resolved": "0.4.0",
"contentHash": "2Tzz6qoQNciyO4uT6wxByMiyvbve+u4eFIiDewDUbeHuxr/rKuba3/EhhXDsEH9WoKzYlpzqxDfQGPE/bIe5Rw=="
},
- "WriteableBitmapEx": {
- "type": "Direct",
- "requested": "[1.6.7, )",
- "resolved": "1.6.7",
- "contentHash": "GoPhIdYzG/DWJq/MADb8gFzRN5PsxM+1DSJFS8Jzupg4DXqFSnlejYEjJw4ffPEUyELL07O4XIGUZydE4jQK0Q=="
- },
"Castle.Core": {
"type": "Transitive",
"resolved": "4.2.0",
diff --git a/src/Artemis.UI/App.xaml b/src/Artemis.UI/App.xaml
index ea1d2f328..1e96c39a4 100644
--- a/src/Artemis.UI/App.xaml
+++ b/src/Artemis.UI/App.xaml
@@ -17,7 +17,17 @@
-
+
+
+
+
+
+
+
+
+
+
+
@@ -33,41 +43,7 @@
-
-
-
-
-
-
-
-
-
-
+
pack://application:,,,/Resources/Fonts/#Roboto Mono
diff --git a/src/Artemis.UI/ApplicationStateManager.cs b/src/Artemis.UI/ApplicationStateManager.cs
index 3802d5494..2a08b4a3a 100644
--- a/src/Artemis.UI/ApplicationStateManager.cs
+++ b/src/Artemis.UI/ApplicationStateManager.cs
@@ -118,15 +118,18 @@ namespace Artemis.UI
private void UtilitiesOnShutdownRequested(object sender, EventArgs e)
{
- // Use PowerShell to kill the process after 2 sec just in case
- ProcessStartInfo info = new()
+ // Use PowerShell to kill the process after 8 sec just in case
+ if (!StartupArguments.Contains("--disable-forced-shutdown"))
{
- Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}",
- WindowStyle = ProcessWindowStyle.Hidden,
- CreateNoWindow = true,
- FileName = "PowerShell.exe"
- };
- Process.Start(info);
+ ProcessStartInfo info = new()
+ {
+ Arguments = "-Command \"& {Start-Sleep -s 8; (Get-Process -Id " + Process.GetCurrentProcess().Id + ").kill()}",
+ WindowStyle = ProcessWindowStyle.Hidden,
+ CreateNoWindow = true,
+ FileName = "PowerShell.exe"
+ };
+ Process.Start(info);
+ }
Execute.OnUIThread(() => Application.Current.Shutdown());
}
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index 48e202680..5ae4e77bb 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -1,366 +1,379 @@
-
- WinExe
- net5.0-windows
- {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- true
- Artemis
- Artemis
- en-US
- Provides advanced unified lighting across many different brands RGB peripherals
- Copyright © Robert Beekman - 2021
- 2.0.0.0
- bin\
- true
- x64
- windows
-
-
- x64
-
+
+ WinExe
+ net5.0-windows10.0.17763.0
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ true
+ Artemis
+ Artemis
+ en-US
+ Provides advanced unified lighting across many different brands RGB peripherals
+ Copyright © Robert Beekman - 2021
+ 2.0.0.0
+ bin\net5.0-windows\
+ False
+ true
+ x64
+ windows
+
+
+ x64
+
-
- Resources\Images\Logo\logo-512.ico
-
-
-
- 2.0.0.0
- 2.0.0
-
-
- 2.0-{chash:6}
- true
- true
- true
- v[0-9]*
- true
- git
- true
-
-
-
-
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll
-
-
- ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll
-
-
-
-
-
- ResXFileCodeGenerator
- Designer
- Resources.Designer.cs
-
-
-
-
- true
-
-
- true
-
-
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- True
- Resources.resx
-
-
- True
- True
- Settings.settings
-
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- Designer
-
-
- $(DefaultXamlRuntime)
-
-
+
+ Resources\Images\Logo\bow.ico
+
+
+
+ 2.0.0.0
+ 2.0.0
+
+
+ 2.0-{chash:6}
+ true
+ true
+ true
+ v[0-9]*
+ true
+ git
+ true
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll
+
+
+ ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll
+
+
+
+
+
+ ResXFileCodeGenerator
+ Designer
+ Resources.Designer.cs
+
+
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ Designer
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
+ $(DefaultXamlRuntime)
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Behaviors/HighlightTermBehavior.cs b/src/Artemis.UI/Behaviors/HighlightTermBehavior.cs
index 2d30ae887..53277b426 100644
--- a/src/Artemis.UI/Behaviors/HighlightTermBehavior.cs
+++ b/src/Artemis.UI/Behaviors/HighlightTermBehavior.cs
@@ -115,7 +115,7 @@ namespace Artemis.UI.Behaviors
private static bool TextIsEmpty(string text)
{
- return text.Length == 0;
+ return string.IsNullOrEmpty(text);
}
private static bool TextIsNotContainingTermToBeHighlighted(string text, string termToBeHighlighted)
diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs
index f1ba4a212..6b27e2fd1 100644
--- a/src/Artemis.UI/Bootstrapper.cs
+++ b/src/Artemis.UI/Bootstrapper.cs
@@ -98,7 +98,7 @@ namespace Artemis.UI
});
IRegistrationService registrationService = Kernel.Get();
- registrationService.RegisterInputProvider();
+ registrationService.RegisterProviders();
registrationService.RegisterControllers();
Execute.OnUIThreadSync(() => { registrationService.ApplyPreferredGraphicsContext(); });
diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs
index e24134dce..2f8a72426 100644
--- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs
+++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs
@@ -2,6 +2,7 @@
using Artemis.Core.Modules;
using Artemis.UI.Screens.Modules;
using Artemis.UI.Screens.Modules.Tabs;
+using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.Conditions;
using Artemis.UI.Screens.ProfileEditor.LayerProperties;
@@ -11,6 +12,8 @@ using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDataBi
using Artemis.UI.Screens.ProfileEditor.LayerProperties.LayerEffects;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree;
+using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs;
+using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem;
using Artemis.UI.Screens.ProfileEditor.Visualization;
using Artemis.UI.Screens.ProfileEditor.Visualization.Tools;
@@ -58,6 +61,14 @@ namespace Artemis.UI.Ninject.Factories
LayerViewModel LayerViewModel(ProfileElement layer);
}
+ public interface ILayerHintVmFactory : IVmFactory
+ {
+ LayerHintsDialogViewModel LayerHintsDialogViewModel(Layer layer);
+ CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(CategoryAdaptionHint adaptionHint);
+ DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(DeviceAdaptionHint adaptionHint);
+ KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(KeyboardSectionAdaptionHint adaptionHint);
+ }
+
public interface IProfileLayerVmFactory : IVmFactory
{
ProfileLayerViewModel Create(Layer layer, PanZoomViewModel panZoomViewModel);
@@ -95,7 +106,13 @@ namespace Artemis.UI.Ninject.Factories
TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection layerPropertyGroups);
}
- public interface IDataBindingsVmFactory
+ public interface IPrerequisitesVmFactory : IVmFactory
+ {
+ PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
+ }
+
+ // TODO: Move these two
+ public interface IDataBindingsVmFactory
{
IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration);
DirectDataBindingModeViewModel DirectDataBindingModeViewModel(DirectDataBinding directDataBinding);
@@ -104,7 +121,7 @@ namespace Artemis.UI.Ninject.Factories
DataBindingConditionViewModel DataBindingConditionViewModel(DataBindingCondition dataBindingCondition);
}
- public interface IPropertyVmFactory
+ public interface IPropertyVmFactory
{
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
diff --git a/src/Artemis.UI/Properties/launchSettings.json b/src/Artemis.UI/Properties/launchSettings.json
index 1268340dc..ff7efa8ab 100644
--- a/src/Artemis.UI/Properties/launchSettings.json
+++ b/src/Artemis.UI/Properties/launchSettings.json
@@ -2,7 +2,7 @@
"profiles": {
"Artemis.UI": {
"commandName": "Project",
- "commandLineArgs": "--force-elevation --pcmr"
+ "commandLineArgs": "--force-elevation --disable-forced-shutdown --pcmr"
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/InputProviders/NativeWindowInputProvider.cs b/src/Artemis.UI/Providers/NativeWindowInputProvider.cs
similarity index 99%
rename from src/Artemis.UI/InputProviders/NativeWindowInputProvider.cs
rename to src/Artemis.UI/Providers/NativeWindowInputProvider.cs
index c2dfd8c6c..0e513555e 100644
--- a/src/Artemis.UI/InputProviders/NativeWindowInputProvider.cs
+++ b/src/Artemis.UI/Providers/NativeWindowInputProvider.cs
@@ -1,11 +1,9 @@
using System;
using System.Diagnostics;
-using System.Linq;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows.Forms;
using System.Windows.Input;
-using System.Windows.Interop;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Utilities;
@@ -14,7 +12,7 @@ using Linearstar.Windows.RawInput.Native;
using Serilog;
using MouseButton = Artemis.Core.Services.MouseButton;
-namespace Artemis.UI.InputProviders
+namespace Artemis.UI.Providers
{
public class NativeWindowInputProvider : InputProvider
{
diff --git a/src/Artemis.UI/InputProviders/SpongeWindow.cs b/src/Artemis.UI/Providers/SpongeWindow.cs
similarity index 93%
rename from src/Artemis.UI/InputProviders/SpongeWindow.cs
rename to src/Artemis.UI/Providers/SpongeWindow.cs
index 34898ffcd..7940d55bd 100644
--- a/src/Artemis.UI/InputProviders/SpongeWindow.cs
+++ b/src/Artemis.UI/Providers/SpongeWindow.cs
@@ -1,7 +1,7 @@
using System;
using System.Windows.Forms;
-namespace Artemis.UI.InputProviders
+namespace Artemis.UI.Providers
{
public sealed class SpongeWindow : NativeWindow
{
diff --git a/src/Artemis.UI/Providers/ToastNotificationProvider.cs b/src/Artemis.UI/Providers/ToastNotificationProvider.cs
new file mode 100644
index 000000000..53e8d5d25
--- /dev/null
+++ b/src/Artemis.UI/Providers/ToastNotificationProvider.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using Windows.UI.Notifications;
+using Artemis.UI.Shared.Services;
+using Artemis.UI.Utilities;
+using MaterialDesignThemes.Wpf;
+using Microsoft.Toolkit.Uwp.Notifications;
+using Stylet;
+
+namespace Artemis.UI.Providers
+{
+ public class ToastNotificationProvider : INotificationProvider
+ {
+ private ThemeWatcher _themeWatcher;
+
+ public ToastNotificationProvider()
+ {
+ _themeWatcher = new ThemeWatcher();
+ }
+
+ public static PngBitmapEncoder GetEncoderForIcon(PackIconKind icon, Color color)
+ {
+ // 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(color), 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));
+
+ return encoder;
+ }
+
+ private void ToastDismissed(string imagePath, Action dismissedCallback)
+ {
+ if (File.Exists(imagePath))
+ File.Delete(imagePath);
+
+ dismissedCallback?.Invoke();
+ }
+
+ private void ToastActivated(string imagePath, Action activatedCallback)
+ {
+ if (File.Exists(imagePath))
+ File.Delete(imagePath);
+
+ activatedCallback?.Invoke();
+ }
+
+ #region Implementation of INotificationProvider
+
+ ///
+ public void ShowNotification(string title, string message, PackIconKind icon, Action activatedCallback, Action dismissedCallback)
+ {
+ string imagePath = Path.GetTempFileName().Replace(".tmp", "png");
+
+ Execute.OnUIThreadSync(() =>
+ {
+ using FileStream stream = File.OpenWrite(imagePath);
+ GetEncoderForIcon(icon, _themeWatcher.GetSystemTheme() == ThemeWatcher.WindowsTheme.Dark ? Colors.White : Colors.Black).Save(stream);
+ });
+
+ new ToastContentBuilder()
+ .AddAppLogoOverride(new Uri(imagePath))
+ .AddText(title, AdaptiveTextStyle.Header)
+ .AddText(message)
+ .Show(t =>
+ {
+ t.Dismissed += (_, _) => ToastDismissed(imagePath, dismissedCallback);
+ t.Activated += (_, _) => ToastActivated(imagePath, activatedCallback);
+ t.Data = new NotificationData(new List> {new("image", imagePath)});
+ });
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ public void Dispose()
+ {
+ ToastNotificationManagerCompat.Uninstall();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Resources/Images/Logo/bow-black.ico b/src/Artemis.UI/Resources/Images/Logo/bow-black.ico
new file mode 100644
index 000000000..9649063ce
Binary files /dev/null and b/src/Artemis.UI/Resources/Images/Logo/bow-black.ico differ
diff --git a/src/Artemis.UI/Resources/Images/Logo/bow-white.ico b/src/Artemis.UI/Resources/Images/Logo/bow-white.ico
new file mode 100644
index 000000000..ab10952b9
Binary files /dev/null and b/src/Artemis.UI/Resources/Images/Logo/bow-white.ico differ
diff --git a/src/Artemis.UI/Resources/Images/Logo/bow-white.svg b/src/Artemis.UI/Resources/Images/Logo/bow-white.svg
new file mode 100644
index 000000000..8d2160536
--- /dev/null
+++ b/src/Artemis.UI/Resources/Images/Logo/bow-white.svg
@@ -0,0 +1,12 @@
+
\ 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 @@
-
-
-
+
\ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Adapt profile
+
+
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs
index 23a9176b4..af4afaba1 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs
@@ -14,7 +14,6 @@ using Artemis.UI.Screens.ProfileEditor.LayerProperties;
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Visualization;
using Artemis.UI.Shared.Services;
-using MaterialDesignThemes.Wpf;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor
@@ -209,6 +208,20 @@ namespace Artemis.UI.Screens.ProfileEditor
SelectedProfile = copy;
}
+ public async Task AdaptActiveProfile()
+ {
+ if (_profileEditorService.SelectedProfile == null)
+ return;
+
+ if (!await DialogService.ShowConfirmDialog(
+ "Adapt profile",
+ "Are you sure you want to adapt the profile to your current surface? Layer assignments may change."
+ ))
+ return;
+
+ _profileService.AdaptProfile(_profileEditorService.SelectedProfile);
+ }
+
public async Task ExportActiveProfile()
{
await DialogService.ShowDialog(new Dictionary
@@ -374,7 +387,7 @@ namespace Artemis.UI.Screens.ProfileEditor
{
// Get all profiles from the database
Profiles.Clear();
- Profiles.AddRange(_profileService.GetProfileDescriptors(Module).OrderBy(d => d.Name));
+ Profiles.AddRange(_profileService.GetProfileDescriptors(Module).OrderBy(p => p.Name));
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/AdaptionHintViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/AdaptionHintViewModel.cs
new file mode 100644
index 000000000..eadcb181d
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/AdaptionHintViewModel.cs
@@ -0,0 +1,20 @@
+using Artemis.Core;
+using Stylet;
+
+namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints
+{
+ public abstract class AdaptionHintViewModel : Screen
+ {
+ protected AdaptionHintViewModel(IAdaptionHint adaptionHint)
+ {
+ AdaptionHint = adaptionHint;
+ }
+
+ public IAdaptionHint AdaptionHint { get; }
+
+ public void Remove()
+ {
+ ((LayerHintsDialogViewModel) Parent).RemoveAdaptionHint(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintView.xaml
new file mode 100644
index 000000000..0d18317f6
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/AdaptionHints/CategoryAdaptionHintView.xaml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AUTO-DETERMINE HINTS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs
new file mode 100644
index 000000000..0682e404f
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/Dialogs/LayerHintsDialogViewModel.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows.Navigation;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Ninject.Factories;
+using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
+using Artemis.UI.Shared.Services;
+using MaterialDesignThemes.Wpf;
+using Stylet;
+
+namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs
+{
+ public class LayerHintsDialogViewModel : Conductor.Collection.AllActive
+ {
+ private readonly IRgbService _rgbService;
+ private readonly ILayerHintVmFactory _vmFactory;
+ private readonly IProfileEditorService _profileEditorService;
+ private SnackbarMessageQueue _layerHintsMessageQueue;
+
+ public LayerHintsDialogViewModel(Layer layer, IRgbService rgbService, ILayerHintVmFactory vmFactory, IProfileEditorService profileEditorService)
+ {
+ _rgbService = rgbService;
+ _vmFactory = vmFactory;
+ _profileEditorService = profileEditorService;
+
+ Layer = layer;
+ DisplayName = "Layer hints | Artemis";
+ }
+
+ public Layer Layer { get; }
+ public bool HasAdaptionHints => Items.Any();
+
+ public SnackbarMessageQueue LayerHintsMessageQueue
+ {
+ get => _layerHintsMessageQueue;
+ set => SetAndNotify(ref _layerHintsMessageQueue, value);
+ }
+
+ public void AutoDetermineHints()
+ {
+ List newHints = Layer.Adapter.DetermineHints(_rgbService.EnabledDevices);
+ CreateHintViewModels(newHints);
+ }
+
+ public void AddCategoryHint()
+ {
+ CategoryAdaptionHint hint = new();
+ Layer.Adapter.AdaptionHints.Add(hint);
+ Items.Add(_vmFactory.CategoryAdaptionHintViewModel(hint));
+ NotifyOfPropertyChange(nameof(HasAdaptionHints));
+ }
+
+ public void AddDeviceHint()
+ {
+ DeviceAdaptionHint hint = new();
+ Layer.Adapter.AdaptionHints.Add(hint);
+ Items.Add(_vmFactory.DeviceAdaptionHintViewModel(hint));
+ NotifyOfPropertyChange(nameof(HasAdaptionHints));
+ }
+
+ public void AddKeyboardSectionHint()
+ {
+ KeyboardSectionAdaptionHint hint = new();
+ Layer.Adapter.AdaptionHints.Add(hint);
+ Items.Add(_vmFactory.KeyboardSectionAdaptionHintViewModel(hint));
+ NotifyOfPropertyChange(nameof(HasAdaptionHints));
+ }
+
+ public void RemoveAdaptionHint(AdaptionHintViewModel adaptionHintViewModel)
+ {
+ Layer.Adapter.AdaptionHints.Remove(adaptionHintViewModel.AdaptionHint);
+ Items.Remove(adaptionHintViewModel);
+ NotifyOfPropertyChange(nameof(HasAdaptionHints));
+ }
+
+ public void OpenHyperlink(object sender, RequestNavigateEventArgs e)
+ {
+ Core.Utilities.OpenUrl(e.Uri.AbsoluteUri);
+ }
+
+ #region Overrides of Screen
+
+ ///
+ protected override void OnInitialActivate()
+ {
+ LayerHintsMessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
+ CreateHintViewModels(Layer.Adapter.AdaptionHints);
+
+ base.OnInitialActivate();
+ }
+
+ #region Overrides of AllActive
+
+ ///
+ protected override void OnClose()
+ {
+ _profileEditorService.UpdateSelectedProfileElement();
+ base.OnClose();
+ }
+
+ #endregion
+
+ #endregion
+
+ private void CreateHintViewModels(List hints)
+ {
+ foreach (IAdaptionHint adapterAdaptionHint in hints)
+ {
+ switch (adapterAdaptionHint)
+ {
+ case CategoryAdaptionHint categoryAdaptionHint:
+ Items.Add(_vmFactory.CategoryAdaptionHintViewModel(categoryAdaptionHint));
+ break;
+ case DeviceAdaptionHint deviceAdaptionHint:
+ Items.Add(_vmFactory.DeviceAdaptionHintViewModel(deviceAdaptionHint));
+ break;
+ case KeyboardSectionAdaptionHint keyboardSectionAdaptionHint:
+ Items.Add(_vmFactory.KeyboardSectionAdaptionHintViewModel(keyboardSectionAdaptionHint));
+ break;
+ }
+ }
+ NotifyOfPropertyChange(nameof(HasAdaptionHints));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml
index eca32dc70..67cf2e553 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml
+++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml
@@ -35,6 +35,12 @@
+
+
-
+
Device enabled
-
+
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs
index 2005b6753..3b6616a35 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/Devices/DeviceSettingsViewModel.cs
@@ -35,7 +35,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices
_rgbService = rgbService;
Device = device;
- Type = Device.RgbDevice.DeviceInfo.DeviceType.ToString().Humanize();
+ Type = Device.DeviceType.ToString().Humanize();
Name = Device.RgbDevice.DeviceInfo.Model;
Manufacturer = Device.RgbDevice.DeviceInfo.Manufacturer;
}
@@ -46,8 +46,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.Devices
public string Name { get; }
public string Manufacturer { get; }
- 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 bool IsDeviceEnabled
{
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml
index 5055b02ac..92b177023 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml
+++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml
@@ -12,7 +12,13 @@
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginFeatureViewModel}">
-
+
+
+
+
+
+
@@ -23,11 +29,11 @@
+ Icon="{Binding FeatureInfo.Icon}"
+ Width="20"
+ VerticalAlignment="Center"
+ HorizontalAlignment="Center"
+ Visibility="{Binding LoadException, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, FallbackValue=Collapsed}" />
-
-
Feature enabled
+
+
+
+
+
+
+ Install prerequisites
+
+
+
+
+
+ Remove prerequisites
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+ SETTINGS
+
-
-
- SETTINGS
-
-
-
-
-
-
-
-
+
+
+
+
+
+ Open plugin directory
+
+
+
+
+
+ Reload plugin
+
+
+
+
+
+
+ Install prerequisites
+
+
+
+
+
+ Remove prerequisites
+
+
+
+
+
+
+ Clear plugin settings
+
+
+
+
+
+ Remove plugin
+
+
+
+
+
+
+
-
-
- 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"
}
}
}