Browse Source

Merge remote-tracking branch 'origin/master' into cropped-layer-previews

Equbuxu 2 years ago
parent
commit
0274a78ad7
100 changed files with 2837 additions and 503 deletions
  1. 3 6
      README.md
  2. 15 4
      src/ChunkyImageLib/ChunkyImage.cs
  3. 2 1
      src/ChunkyImageLib/ChunkyImageLib.csproj
  4. 2 2
      src/ChunkyImageLib/DataHolders/ShapeCorners.cs
  5. 2 2
      src/ChunkyImageLib/DataHolders/ShapeData.cs
  6. 5 5
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  7. 3 3
      src/ChunkyImageLib/Operations/ChunkyImageOperation.cs
  8. 4 4
      src/ChunkyImageLib/Operations/ClearPathOperation.cs
  9. 3 3
      src/ChunkyImageLib/Operations/ClearRegionOperation.cs
  10. 5 5
      src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs
  11. 3 3
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  12. 1 1
      src/ChunkyImageLib/Operations/IMirroredDrawOperation.cs
  13. 4 4
      src/ChunkyImageLib/Operations/ImageOperation.cs
  14. 4 4
      src/ChunkyImageLib/Operations/PathOperation.cs
  15. 31 4
      src/ChunkyImageLib/Operations/PixelOperation.cs
  16. 4 3
      src/ChunkyImageLib/Operations/PixelsOperation.cs
  17. 4 4
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  18. 1 3
      src/ChunkyImageLib/Surface.cs
  19. 1 1
      src/ChunkyImageLibTest/ChunkyImageLibTest.csproj
  20. 1 1
      src/ChunkyImageLibVis/ChunkyImageLibVis.csproj
  21. 2 0
      src/PixiEditor.Builder/build.ps1
  22. 1 0
      src/PixiEditor.Builder/build.sh
  23. 14 0
      src/PixiEditor.Builder/build/PixiEditor.Builder.csproj
  24. 137 0
      src/PixiEditor.Builder/build/Program.cs
  25. 20 0
      src/PixiEditor.Builder/stylecop.json
  26. 1 1
      src/PixiEditor.ChangeableDocument.Gen/PixiEditor.ChangeableDocument.Gen.csproj
  27. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/Size_ChangeInfo.cs
  28. 1 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryAxisPosition_ChangeInfo.cs
  29. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  30. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  31. 8 3
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs
  32. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  33. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  34. 6 6
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  35. 5 5
      src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs
  36. 10 13
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs
  37. 11 2
      src/PixiEditor.ChangeableDocument/Enums/SelectionMode.cs
  38. 2 1
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  39. 9 0
      src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs
  40. 11 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs
  41. 10 0
      src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs
  42. 14 0
      src/PixiEditor.DrawingApi.Core/Numerics/VecI.cs
  43. 2 1
      src/PixiEditor.DrawingApi.Core/PixiEditor.DrawingApi.Core.csproj
  44. 2 1
      src/PixiEditor.DrawingApi.Skia/PixiEditor.DrawingApi.Skia.csproj
  45. 1 1
      src/PixiEditor.MSIX/Package.appxmanifest
  46. 12 1
      src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj
  47. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  48. 3 1
      src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj
  49. 19 6
      src/PixiEditor.UpdateModule/UpdateChecker.cs
  50. 2 1
      src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj
  51. 125 90
      src/PixiEditor.sln
  52. 12 3
      src/PixiEditor/App.xaml.cs
  53. 6 0
      src/PixiEditor/BuildConstants.cs
  54. 2 0
      src/PixiEditor/Data/Localization/Languages/ar.json
  55. 2 0
      src/PixiEditor/Data/Localization/Languages/cs.json
  56. 2 0
      src/PixiEditor/Data/Localization/Languages/de.json
  57. 528 0
      src/PixiEditor/Data/Localization/Languages/en.json
  58. 2 0
      src/PixiEditor/Data/Localization/Languages/es.json
  59. 504 0
      src/PixiEditor/Data/Localization/Languages/pl.json
  60. 2 0
      src/PixiEditor/Data/Localization/Languages/ru.json
  61. 2 0
      src/PixiEditor/Data/Localization/Languages/uk.json
  62. 54 0
      src/PixiEditor/Data/Localization/LocalizationData.json
  63. 54 0
      src/PixiEditor/Data/Localization/LocalizationDataSchema.json
  64. 2 1
      src/PixiEditor/Helpers/Converters/BlendModeToStringConverter.cs
  65. 14 3
      src/PixiEditor/Helpers/Converters/BoolToValueConverter.cs
  66. 3 2
      src/PixiEditor/Helpers/Converters/EnumToStringConverter.cs
  67. 13 14
      src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  68. 17 0
      src/PixiEditor/Helpers/Converters/LangConverter.cs
  69. 24 0
      src/PixiEditor/Helpers/Converters/SubtractConverter.cs
  70. 8 2
      src/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs
  71. 34 0
      src/PixiEditor/Helpers/CrashHelper.cs
  72. 10 2
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  73. 36 0
      src/PixiEditor/Helpers/EnumExtension.cs
  74. 20 19
      src/PixiEditor/Helpers/Extensions/BlendModeEx.cs
  75. 6 1
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  76. 5 0
      src/PixiEditor/Helpers/Extensions/SerializableDocumentEx.cs
  77. 10 3
      src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs
  78. 64 103
      src/PixiEditor/Helpers/GlobalMouseHook.cs
  79. 39 48
      src/PixiEditor/Helpers/InputKeyHelpers.cs
  80. 45 0
      src/PixiEditor/Helpers/LocalizationExtension.cs
  81. 100 0
      src/PixiEditor/Helpers/LowLevelWindow.cs
  82. 39 0
      src/PixiEditor/Helpers/RegistryHelpers.cs
  83. 21 0
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  84. 19 25
      src/PixiEditor/Helpers/VersionHelpers.cs
  85. 281 0
      src/PixiEditor/Helpers/Win32.cs
  86. 9 70
      src/PixiEditor/Helpers/WindowSizeHelper.cs
  87. BIN
      src/PixiEditor/Images/LanguageFlags/ar.png
  88. BIN
      src/PixiEditor/Images/LanguageFlags/cs.png
  89. BIN
      src/PixiEditor/Images/LanguageFlags/de.png
  90. BIN
      src/PixiEditor/Images/LanguageFlags/en.png
  91. BIN
      src/PixiEditor/Images/LanguageFlags/es.png
  92. BIN
      src/PixiEditor/Images/LanguageFlags/pl.png
  93. BIN
      src/PixiEditor/Images/LanguageFlags/ru.png
  94. BIN
      src/PixiEditor/Images/LanguageFlags/uk.png
  95. 22 0
      src/PixiEditor/Localization/ILocalizationProvider.cs
  96. 37 0
      src/PixiEditor/Localization/Language.cs
  97. 18 0
      src/PixiEditor/Localization/LanguageData.cs
  98. 9 0
      src/PixiEditor/Localization/LocalizationData.cs
  99. 114 0
      src/PixiEditor/Localization/LocalizationProvider.cs
  100. 112 0
      src/PixiEditor/Localization/LocalizedString.cs

+ 3 - 6
README.md

@@ -22,8 +22,7 @@ Want to create beautiful pixel art for your games? PixiEditor can help you! Our
 
 Have you ever used Photoshop or Gimp? Reinventing the wheel is unnecessary, we wanted users to get familiar with the tool quickly and with ease. 
 
-![](https://user-images.githubusercontent.com/45312141/146670495-ae521a18-a89e-4e94-9317-6838b51407fa.png)
-
+![](https://user-images.githubusercontent.com/45312141/235351211-e00bcaea-9c63-4ecd-a2ee-e4fb2b2c9651.png)
 
 ### Fast
 
@@ -38,9 +37,9 @@ PixiEditor started in 2018 and it's been actively developed since. We continuous
 
 <a href='//www.microsoft.com/store/apps/9NDDRHS8PBRN?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/store/badges/images/English_get-it-from-MS.png' alt='Microsoft Store badge' width="184"/></a>
 
-Wishlist on Steam now!
+Get it on Steam now!
 
-[![wishlist_steam](https://user-images.githubusercontent.com/25402427/214952291-d81a4d79-bb75-44f2-bd24-10d7c3404997.png)](https://store.steampowered.com/app/2218560/PixiEditor__Pixel_Art_Editor?utm_source=GitHub)
+[![Get PixiEditor on Steam](https://user-images.githubusercontent.com/121322/228988640-32fe5bd3-9dd0-4f3b-a8f2-f744bd9b50b5.png)](https://store.steampowered.com/app/2218560/PixiEditor__Pixel_Art_Editor?utm_source=GitHub)
 
 **Or**
 
@@ -85,8 +84,6 @@ Struggling with something? You can find support in a few places:
 
 * .NET 7
 
-* latest Visual Studio 2022 (in order to code generators to work)
-
 ### Instructions
 
 1. Clone Repository

+ 15 - 4
src/ChunkyImageLib/ChunkyImage.cs

@@ -82,8 +82,8 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     private BlendMode blendMode = BlendMode.Src;
     private bool lockTransparency = false;
     private VectorPath? clippingPath;
-    private int? horizontalSymmetryAxis = null;
-    private int? verticalSymmetryAxis = null;
+    private double? horizontalSymmetryAxis = null;
+    private double? verticalSymmetryAxis = null;
 
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> committedChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
@@ -451,7 +451,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void SetHorizontalAxisOfSymmetry(int position)
+    public void SetHorizontalAxisOfSymmetry(double position)
     {
         lock (lockObject)
         {
@@ -463,7 +463,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
     }
 
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public void SetVerticalAxisOfSymmetry(int position)
+    public void SetVerticalAxisOfSymmetry(double position)
     {
         lock (lockObject)
         {
@@ -628,6 +628,17 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
         }
     }
 
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public void EnqueueDrawPixel(VecI pos, PixelProcessor pixelProcessor, BlendMode blendMode)
+    {
+        lock (lockObject)
+        {
+            ThrowIfDisposed();
+            PixelOperation operation = new(pos, pixelProcessor, blendMode);
+            EnqueueOperation(operation);
+        }
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public void EnqueueDrawChunkyImage(VecI pos, ChunkyImage image, bool flipHor = false, bool flipVer = false)
     {

+ 2 - 1
src/ChunkyImageLib/ChunkyImageLib.csproj

@@ -6,8 +6,9 @@
     <Nullable>enable</Nullable>
     <WarningsAsErrors>Nullable</WarningsAsErrors>
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 2 - 2
src/ChunkyImageLib/DataHolders/ShapeCorners.cs

@@ -127,7 +127,7 @@ public struct ShapeCorners
         return crossTop == crossRight && crossTop == crossLeft && crossTop == crossBottom;
     }
 
-    public ShapeCorners AsMirroredAcrossHorAxis(int horAxisY) => new ShapeCorners
+    public ShapeCorners AsMirroredAcrossHorAxis(double horAxisY) => new ShapeCorners
     {
         BottomLeft = BottomLeft.ReflectY(horAxisY),
         BottomRight = BottomRight.ReflectY(horAxisY),
@@ -135,7 +135,7 @@ public struct ShapeCorners
         TopRight = TopRight.ReflectY(horAxisY)
     };
 
-    public ShapeCorners AsMirroredAcrossVerAxis(int verAxisX) => new ShapeCorners
+    public ShapeCorners AsMirroredAcrossVerAxis(double verAxisX) => new ShapeCorners
     {
         BottomLeft = BottomLeft.ReflectX(verAxisX),
         BottomRight = BottomRight.ReflectX(verAxisX),

+ 2 - 2
src/ChunkyImageLib/DataHolders/ShapeData.cs

@@ -26,9 +26,9 @@ public record struct ShapeData
     public double Angle { get; }
     public int StrokeWidth { get; }
 
-    public ShapeData AsMirroredAcrossHorAxis(int horAxisY)
+    public ShapeData AsMirroredAcrossHorAxis(double horAxisY)
         => new ShapeData(Center.ReflectY(horAxisY), new(Size.X, -Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
-    public ShapeData AsMirroredAcrossVerAxis(int verAxisX)
+    public ShapeData AsMirroredAcrossVerAxis(double verAxisX)
         => new ShapeData(Center.ReflectX(verAxisX), new(-Size.X, Size.Y), -Angle, StrokeWidth, StrokeColor, FillColor, BlendMode);
 
 }

+ 5 - 5
src/ChunkyImageLib/Operations/BresenhamLineOperation.cs

@@ -44,19 +44,19 @@ internal class BresenhamLineOperation : IMirroredDrawOperation
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         RectI newFrom = new RectI(from, new VecI(1));
         RectI newTo = new RectI(to, new VecI(1));
         if (verAxisX is not null)
         {
-            newFrom = newFrom.ReflectX((int)verAxisX);
-            newTo = newTo.ReflectX((int)verAxisX);
+            newFrom = (RectI)newFrom.ReflectX((double)verAxisX).Round();
+            newTo = (RectI)newTo.ReflectX((double)verAxisX).Round();
         }
         if (horAxisY is not null)
         {
-            newFrom = newFrom.ReflectY((int)horAxisY);
-            newTo = newTo.ReflectY((int)horAxisY);
+            newFrom = (RectI)newFrom.ReflectY((double)horAxisY).Round();
+            newTo = (RectI)newTo.ReflectY((double)horAxisY).Round();
         }
         return new BresenhamLineOperation(newFrom.Pos, newTo.Pos, color, blendMode);
     }

+ 3 - 3
src/ChunkyImageLib/Operations/ChunkyImageOperation.cs

@@ -108,13 +108,13 @@ internal class ChunkyImageOperation : IMirroredDrawOperation
         return topLeft;
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         var newPos = targetPos;
         if (verAxisX is not null)
-            newPos = newPos.ReflectX((int)verAxisX);
+            newPos = (VecI)newPos.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            newPos = newPos.ReflectY((int)horAxisY);
+            newPos = (VecI)newPos.ReflectY((double)horAxisY).Round();
         return new ChunkyImageOperation(imageToDraw, newPos, mirrorHorizontal ^ (verAxisX is not null), mirrorVertical ^ (horAxisY is not null));
     }
 

+ 4 - 4
src/ChunkyImageLib/Operations/ClearPathOperation.cs

@@ -38,17 +38,17 @@ internal class ClearPathOperation : IMirroredDrawOperation
         path.Dispose();
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
-        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, verAxisX ?? 0, horAxisY ?? 0);
+        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, (float?)verAxisX ?? 0, (float?)horAxisY ?? 0);
         using var copy = new VectorPath(path);
         copy.Transform(matrix);
 
         var newRect = pathTightBounds;
         if (verAxisX is not null)
-            newRect = newRect.ReflectX((int)verAxisX);
+            newRect = (RectI)newRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            newRect = newRect.ReflectY((int)horAxisY);
+            newRect = (RectI)newRect.ReflectY((double)horAxisY).Round();
         return new ClearPathOperation(copy, newRect);
     }
 }

+ 3 - 3
src/ChunkyImageLib/Operations/ClearRegionOperation.cs

@@ -31,13 +31,13 @@ internal class ClearRegionOperation : IMirroredDrawOperation
     }
     public void Dispose() { }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         var newRect = rect;
         if (verAxisX is not null)
-            newRect = newRect.ReflectX((int)verAxisX);
+            newRect = (RectI)newRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            newRect = newRect.ReflectY((int)horAxisY);
+            newRect = (RectI)newRect.ReflectY((double)horAxisY).Round();
         return new ClearRegionOperation(newRect);
     }
 }

+ 5 - 5
src/ChunkyImageLib/Operations/DrawingSurfaceLineOperation.cs

@@ -44,19 +44,19 @@ internal class DrawingSurfaceLineOperation : IMirroredDrawOperation
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         VecI newFrom = from;
         VecI newTo = to;
         if (verAxisX is not null)
         {
-            newFrom = newFrom.ReflectX((int)verAxisX);
-            newTo = newTo.ReflectX((int)verAxisX);
+            newFrom = (VecI)newFrom.ReflectX((double)verAxisX).Round();
+            newTo = (VecI)newTo.ReflectX((double)verAxisX).Round();
         }
         if (horAxisY is not null)
         {
-            newFrom = newFrom.ReflectY((int)horAxisY);
-            newTo = newTo.ReflectY((int)horAxisY);
+            newFrom = (VecI)newFrom.ReflectY((double)horAxisY).Round();
+            newTo = (VecI)newTo.ReflectY((double)horAxisY).Round();
         }
         return new DrawingSurfaceLineOperation(newFrom, newTo, paint.StrokeCap, paint.StrokeWidth, paint.Color, paint.BlendMode);
     }

+ 3 - 3
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -105,13 +105,13 @@ internal class EllipseOperation : IMirroredDrawOperation
         return new AffectedArea(chunks, location);
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         RectI newLocation = location;
         if (verAxisX is not null)
-            newLocation = newLocation.ReflectX((int)verAxisX);
+            newLocation = (RectI)newLocation.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            newLocation = newLocation.ReflectY((int)horAxisY);
+            newLocation = (RectI)newLocation.ReflectY((double)horAxisY).Round();
         return new EllipseOperation(newLocation, strokeColor, fillColor, strokeWidth, paint);
     }
 

+ 1 - 1
src/ChunkyImageLib/Operations/IMirroredDrawOperation.cs

@@ -2,5 +2,5 @@
 
 internal interface IMirroredDrawOperation : IDrawOperation
 {
-    IDrawOperation AsMirrored(int? verAxisX, int? horAxisY);
+    IDrawOperation AsMirrored(double? verAxisX, double? horAxisY);
 }

+ 4 - 4
src/ChunkyImageLib/Operations/ImageOperation.cs

@@ -106,22 +106,22 @@ internal class ImageOperation : IMirroredDrawOperation
         customPaint?.Dispose();
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         if (verAxisX is not null && horAxisY is not null)
         {
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((int)verAxisX).AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+                (corners.AsMirroredAcrossVerAxis((double)verAxisX).AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
         }
         if (verAxisX is not null)
         {
             return new ImageOperation
-                (corners.AsMirroredAcrossVerAxis((int)verAxisX), toPaint, customPaint, imageWasCopied);
+                (corners.AsMirroredAcrossVerAxis((double)verAxisX), toPaint, customPaint, imageWasCopied);
         }
         if (horAxisY is not null)
         {
             return new ImageOperation
-                (corners.AsMirroredAcrossHorAxis((int)horAxisY), toPaint, customPaint, imageWasCopied);
+                (corners.AsMirroredAcrossHorAxis((double)horAxisY), toPaint, customPaint, imageWasCopied);
         }
         return new ImageOperation(corners, toPaint, customPaint, imageWasCopied);
     }

+ 4 - 4
src/ChunkyImageLib/Operations/PathOperation.cs

@@ -40,17 +40,17 @@ internal class PathOperation : IMirroredDrawOperation
         return new AffectedArea(OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize), bounds);
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
-        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, verAxisX ?? 0, horAxisY ?? 0);
+        var matrix = Matrix3X3.CreateScale(verAxisX is not null ? -1 : 1, horAxisY is not null ? -1 : 1, (float?)verAxisX ?? 0, (float?)horAxisY ?? 0);
         using var copy = new VectorPath(path);
         copy.Transform(matrix);
 
         RectI newBounds = bounds;
         if (verAxisX is not null)
-            newBounds = newBounds.ReflectX((int)verAxisX);
+            newBounds = (RectI)newBounds.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            newBounds = newBounds.ReflectY((int)horAxisY);
+            newBounds = (RectI)newBounds.ReflectY((double)horAxisY).Round();
         return new PathOperation(copy, paint.Color, paint.StrokeWidth, paint.StrokeCap, paint.BlendMode, newBounds);
     }
 

+ 31 - 4
src/ChunkyImageLib/Operations/PixelOperation.cs

@@ -6,6 +6,7 @@ using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 
 namespace ChunkyImageLib.Operations;
 
+public delegate Color PixelProcessor(Color input);
 internal class PixelOperation : IMirroredDrawOperation
 {
     public bool IgnoreEmptyChunks => false;
@@ -14,6 +15,8 @@ internal class PixelOperation : IMirroredDrawOperation
     private readonly BlendMode blendMode;
     private readonly Paint paint;
 
+    private readonly PixelProcessor? _colorProcessor = null;
+
     public PixelOperation(VecI pixel, Color color, BlendMode blendMode)
     {
         this.pixel = pixel;
@@ -22,10 +25,18 @@ internal class PixelOperation : IMirroredDrawOperation
         paint = new Paint() { BlendMode = blendMode };
     }
 
+    public PixelOperation(VecI pixel, PixelProcessor colorProcessor, BlendMode blendMode)
+    {
+        this.pixel = pixel;
+        this._colorProcessor = colorProcessor;
+        this.blendMode = blendMode;
+        paint = new Paint() { BlendMode = blendMode };
+    }
+
     public void DrawOnChunk(Chunk chunk, VecI chunkPos)
     {
         // a hacky way to make the lines look slightly better on non full res chunks
-        paint.Color = new Color(color.R, color.G, color.B, (byte)(color.A * chunk.Resolution.Multiplier()));
+        paint.Color = GetColor(chunk, chunkPos);
 
         DrawingSurface surf = chunk.Surface.DrawingSurface;
         surf.Canvas.Save();
@@ -35,18 +46,34 @@ internal class PixelOperation : IMirroredDrawOperation
         surf.Canvas.Restore();
     }
 
+    private Color GetColor(Chunk chunk, VecI chunkPos)
+    {
+        Color pixelColor = color;
+        if (_colorProcessor != null)
+        {
+            pixelColor = _colorProcessor(chunk.Surface.GetSRGBPixel(pixel - chunkPos * ChunkyImage.FullChunkSize));
+        }
+
+        return new Color(pixelColor.R, pixelColor.G, pixelColor.B, (byte)(pixelColor.A * chunk.Resolution.Multiplier()));
+    }
+
     public AffectedArea FindAffectedArea(VecI imageSize)
     {
         return new AffectedArea(new HashSet<VecI>() { OperationHelper.GetChunkPos(pixel, ChunkyImage.FullChunkSize) }, new RectI(pixel, VecI.One));
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         RectI pixelRect = new RectI(pixel, new VecI(1, 1));
         if (verAxisX is not null)
-            pixelRect = pixelRect.ReflectX((int)verAxisX);
+            pixelRect = (RectI)pixelRect.ReflectX((double)verAxisX).Round();
         if (horAxisY is not null)
-            pixelRect = pixelRect.ReflectY((int)horAxisY);
+            pixelRect = (RectI)pixelRect.ReflectY((double)horAxisY);
+        if (_colorProcessor != null)
+        {
+            return new PixelOperation(pixelRect.Pos, _colorProcessor, blendMode);
+        }
+
         return new PixelOperation(pixelRect.Pos, color, blendMode);
     }
 

+ 4 - 3
src/ChunkyImageLib/Operations/PixelsOperation.cs

@@ -1,4 +1,5 @@
 using System.ComponentModel.DataAnnotations.Schema;
+using System.Linq;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
@@ -52,11 +53,11 @@ internal class PixelsOperation : IMirroredDrawOperation
         return new AffectedArea(affectedChunks, affectedArea);
     }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         var arr = pixels.Select(pixel => new VecI(
-            verAxisX is not null ? 2 * (int)verAxisX - (int)pixel.X - 1 : (int)pixel.X,
-            horAxisY is not null ? 2 * (int)horAxisY - (int)pixel.Y - 1 : (int)pixel.Y
+            verAxisX is not null ? (int)Math.Round(2 * (double)verAxisX - (int)pixel.X - 1) : (int)pixel.X,
+            horAxisY is not null ? (int)Math.Round(2 * (double)horAxisY - (int)pixel.Y - 1) : (int)pixel.Y
         ));
         return new PixelsOperation(arr, color, blendMode);
     }

+ 4 - 4
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -72,14 +72,14 @@ internal class RectangleOperation : IMirroredDrawOperation
 
     public void Dispose() { }
 
-    public IDrawOperation AsMirrored(int? verAxisX, int? horAxisY)
+    public IDrawOperation AsMirrored(double? verAxisX, double? horAxisY)
     {
         if (verAxisX is not null && horAxisY is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((int)horAxisY).AsMirroredAcrossVerAxis((int)verAxisX));
+            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((double)horAxisY).AsMirroredAcrossVerAxis((double)verAxisX));
         else if (verAxisX is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossVerAxis((int)verAxisX));
+            return new RectangleOperation(Data.AsMirroredAcrossVerAxis((double)verAxisX));
         else if (horAxisY is not null)
-            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((int)horAxisY));
+            return new RectangleOperation(Data.AsMirroredAcrossHorAxis((double)horAxisY));
         return new RectangleOperation(Data);
     }
 }

+ 1 - 3
src/ChunkyImageLib/Surface.cs

@@ -22,9 +22,7 @@ public class Surface : IDisposable
     public Surface(VecI size)
     {
         if (size.X < 1 || size.Y < 1)
-            throw new ArgumentException("Width and height must be >1");
-        if (size.X > 10000 || size.Y > 10000)
-            throw new ArgumentException("Width and height must be <=10000");
+            throw new ArgumentException("Width and height must be >=1");
 
         Size = size;
 

+ 1 - 1
src/ChunkyImageLibTest/ChunkyImageLibTest.csproj

@@ -6,7 +6,7 @@
 
     <IsPackable>false</IsPackable>
 
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
 
     <Platforms>AnyCPU;x64;x86</Platforms>
   </PropertyGroup>

+ 1 - 1
src/ChunkyImageLibVis/ChunkyImageLibVis.csproj

@@ -5,7 +5,7 @@
     <TargetFramework>net7.0-windows</TargetFramework>
     <Nullable>enable</Nullable>
     <UseWPF>true</UseWPF>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
   </PropertyGroup>
 

+ 2 - 0
src/PixiEditor.Builder/build.ps1

@@ -0,0 +1,2 @@
+dotnet run --project build/PixiEditor.Builder.csproj -- $args
+exit $LASTEXITCODE;

+ 1 - 0
src/PixiEditor.Builder/build.sh

@@ -0,0 +1 @@
+dotnet run --project ./build/PixiEditor.Builder.csproj -- "$@"

+ 14 - 0
src/PixiEditor.Builder/build/PixiEditor.Builder.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+    <PropertyGroup>
+        <OutputType>Exe</OutputType>
+        <TargetFramework>net7.0</TargetFramework>
+        <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
+        <AssemblyName>PixiEditor.Builder</AssemblyName>
+        <RootNamespace>PixiEditor.Builder</RootNamespace>
+        <Configurations>Debug;Release;DevRelease</Configurations>
+        <Platforms>AnyCPU;x86;x64</Platforms>
+    </PropertyGroup>
+    <ItemGroup>
+        <PackageReference Include="Cake.Frosting" Version="3.0.0" />
+    </ItemGroup>
+</Project>

+ 137 - 0
src/PixiEditor.Builder/build/Program.cs

@@ -0,0 +1,137 @@
+using System.IO;
+using Cake.Common.Build;
+using Cake.Common.Tools.DotNet;
+using Cake.Common.Tools.DotNet.Publish;
+using Cake.Core;
+using Cake.Core.Diagnostics;
+using Cake.Frosting;
+using Path = System.IO.Path;
+
+namespace PixiEditor.Cake.Builder;
+
+public static class Program
+{
+    public static int Main(string[] args)
+    {
+        return new CakeHost()
+            .UseContext<BuildContext>()
+            .Run(args);
+    }
+}
+
+public class BuildContext : FrostingContext
+{
+    public string PathToProject { get; set; } = "../PixiEditor/PixiEditor.csproj";
+
+    public string CrashReportWebhookUrl { get; set; }
+
+    public string BackedUpConstants { get; set; }
+
+    public string BuildConfiguration { get; set; } = "Release";
+
+    public string OutputDirectory { get; set; } = "Builds";
+
+    public bool SelfContained { get; set; } = false;
+    
+    public string Runtime { get; set; }
+
+    public BuildContext(ICakeContext context)
+        : base(context)
+    {
+        bool hasWebhook = context.Arguments.HasArgument("crash-report-webhook-url");
+        CrashReportWebhookUrl = hasWebhook
+            ? context.Arguments.GetArgument("crash-report-webhook-url")
+            : string.Empty;
+
+        bool hasCustomProjectPath = context.Arguments.HasArgument("project-path");
+        if (hasCustomProjectPath)
+        {
+            PathToProject = context.Arguments.GetArgument("project-path");
+        }
+
+        bool hasCustomConfiguration = context.Arguments.HasArgument("build-configuration");
+        if (hasCustomConfiguration)
+        {
+            BuildConfiguration = context.Arguments.GetArgument("build-configuration");
+        }
+
+        bool hasCustomOutputDirectory = context.Arguments.HasArgument("o");
+        if (hasCustomOutputDirectory)
+        {
+            OutputDirectory = context.Arguments.GetArgument("o");
+        }
+        
+        bool hasSelfContained = context.Arguments.HasArgument("self-contained");
+        if (hasSelfContained)
+        {
+            SelfContained = true;
+        }
+
+        Runtime = context.Arguments.GetArgument("runtime");
+    }
+}
+
+[TaskName("Default")]
+[IsDependentOn(typeof(BuildProjectTask))]
+public sealed class DefaultTask : FrostingTask<BuildContext>
+{
+    public override void Run(BuildContext context)
+    {
+        context.Log.Information("Built project successfully!");
+    }
+}
+
+[TaskName("ReplaceSpecialStrings")]
+public sealed class ReplaceSpecialStringsTask : FrostingTask<BuildContext>
+{
+    public override void Run(BuildContext context)
+    {
+        context.Log.Information("Replacing special strings...");
+        string projectPath = context.PathToProject;
+        string filePath = Path.Combine(projectPath, "BuildConstants.cs");
+
+        string result;
+        var fileContent = File.ReadAllText(filePath);
+        context.BackedUpConstants = fileContent;
+        result = ReplaceSpecialStrings(context, fileContent);
+
+        File.WriteAllText(filePath, result);
+    }
+
+    private string ReplaceSpecialStrings(BuildContext context, string fileContent)
+    {
+        string result = fileContent
+            .Replace("${crash-report-webhook-url}", context.CrashReportWebhookUrl);
+
+        return result;
+    }
+}
+
+[TaskName("BuildProject")]
+[IsDependentOn(typeof(ReplaceSpecialStringsTask))]
+public sealed class BuildProjectTask : FrostingTask<BuildContext>
+{
+    public override void Run(BuildContext context)
+    {
+        context.Log.Information("Building project...");
+        string projectPath = context.PathToProject;
+
+        var settings = new DotNetPublishSettings()
+        {
+            Configuration = context.BuildConfiguration,
+            SelfContained = context.SelfContained,
+            Runtime = context.Runtime,
+            OutputDirectory = context.OutputDirectory,
+        };
+
+        context.DotNetPublish(projectPath, settings);
+    }
+
+    public override void Finally(BuildContext context)
+    {
+        context.Log.Information("Cleaning up...");
+        string constantsPath = Path.Combine(context.PathToProject, "BuildConstants.cs");
+
+        File.WriteAllText(constantsPath, context.BackedUpConstants);
+    }
+}

+ 20 - 0
src/PixiEditor.Builder/stylecop.json

@@ -0,0 +1,20 @@
+{
+  "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+  "settings": {
+    "indentation": {
+      "indentationSize": 4
+    },
+    "maintainabilityRules": {
+      "topLevelTypes": [ "class", "interface", "enum", "struct" ]
+    },
+    "readabilityRules": {
+      "allowBuiltInTypeAliases": false
+    },
+    "documentationRules": {
+      "xmlHeader": false,
+      "documentInterfaces": false,
+      "documentExposedElements": false,
+      "documentInternalElements": false
+    }
+  }
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument.Gen/PixiEditor.ChangeableDocument.Gen.csproj

@@ -6,7 +6,7 @@
     <Nullable>enable</Nullable>
     <ImplicitUsings>true</ImplicitUsings>
     <LangVersion>Latest</LangVersion>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
   </PropertyGroup>
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/Size_ChangeInfo.cs

@@ -2,4 +2,4 @@
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
 
-public record class Size_ChangeInfo(VecI Size, int VerticalSymmetryAxisX, int HorizontalSymmetryAxisY) : IChangeInfo;
+public record class Size_ChangeInfo(VecI Size, double VerticalSymmetryAxisX, double HorizontalSymmetryAxisY) : IChangeInfo;

+ 1 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/SymmetryAxisPosition_ChangeInfo.cs

@@ -1,4 +1,4 @@
 using PixiEditor.ChangeableDocument.Enums;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Root;
-public record class SymmetryAxisPosition_ChangeInfo(SymmetryAxisDirection Direction, int NewPosition) : IChangeInfo;
+public record class SymmetryAxisPosition_ChangeInfo(SymmetryAxisDirection Direction, double NewPosition) : IChangeInfo;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -26,8 +26,8 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
     public VecI Size { get; set; } = DefaultSize;
     public bool HorizontalSymmetryAxisEnabled { get; set; }
     public bool VerticalSymmetryAxisEnabled { get; set; }
-    public int HorizontalSymmetryAxisY { get; set; }
-    public int VerticalSymmetryAxisX { get; set; }
+    public double HorizontalSymmetryAxisY { get; set; }
+    public double VerticalSymmetryAxisX { get; set; }
 
     public void Dispose()
     {

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs

@@ -33,12 +33,12 @@ public interface IReadOnlyDocument
     /// <summary>
     /// The position of the horizontal symmetry axis (Mirrors top and bottom)
     /// </summary>
-    int HorizontalSymmetryAxisY { get; }
+    double HorizontalSymmetryAxisY { get; }
 
     /// <summary>
     /// The position of the vertical symmetry axis (Mirrors left and right)
     /// </summary>
-    int VerticalSymmetryAxisX { get; }
+    double VerticalSymmetryAxisX { get; }
 
     /// <summary>
     /// Performs the specified action on each readonly member of the document

+ 8 - 3
src/PixiEditor.ChangeableDocument/Changes/Drawing/ChangeBrightness_UpdateableChange.cs

@@ -88,9 +88,14 @@ internal class ChangeBrightness_UpdateableChange : UpdateableChange
             
             for (VecI pos = new VecI(left.X, y); pos.X <= right.X; pos.X++)
             {
-                Color pixel = tempSurface.GetSRGBPixel(pos);
-                Color newColor = ColorHelper.ChangeColorBrightness(pixel, correctionFactor);
-                layerImage.EnqueueDrawPixel(pos + offset, newColor, BlendMode.Src);
+                layerImage.EnqueueDrawPixel(
+                    pos + offset,
+                    (pixel) =>
+                    {
+                        Color newColor = ColorHelper.ChangeColorBrightness(pixel, correctionFactor);
+                        return ColorHelper.ChangeColorBrightness(newColor, correctionFactor);
+                    },
+                    BlendMode.Src);
             }
         }
     }

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -6,8 +6,8 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 internal abstract class ResizeBasedChangeBase : Change
 {
     protected VecI _originalSize;
-    protected int _originalHorAxisY;
-    protected int _originalVerAxisX;
+    protected double _originalHorAxisY;
+    protected double _originalVerAxisX;
     protected Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     protected Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
     

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -12,8 +12,8 @@ internal class ResizeImage_Change : Change
     private readonly VecI newSize;
     private readonly ResamplingMethod method;
     private VecI originalSize;
-    private int originalHorAxisY;
-    private int originalVerAxisX;
+    private double originalHorAxisY;
+    private double originalVerAxisX;
     
     private Dictionary<Guid, CommittedChunkStorage> savedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> savedMaskChunks = new();

+ 6 - 6
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -14,8 +14,8 @@ internal sealed class RotateImage_Change : Change
     private List<Guid> membersToRotate;
     
     private VecI originalSize;
-    private int originalHorAxisY;
-    private int originalVerAxisX;
+    private double originalHorAxisY;
+    private double originalVerAxisX;
     private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
     private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
 
@@ -161,12 +161,12 @@ internal sealed class RotateImage_Change : Change
 
         VecI newSize = new VecI(newWidth, newHeight);
 
-        float normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
-        float normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
+        double normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
+        double normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
 
         target.Size = newSize;
-        target.VerticalSymmetryAxisX = (int)(newSize.X * normalizedSymmX);
-        target.HorizontalSymmetryAxisY = (int)(newSize.Y * normalizedSymmY);
+        target.VerticalSymmetryAxisX = Math.Round(newSize.X * normalizedSymmX * 2) / 2;
+        target.HorizontalSymmetryAxisY = Math.Round(newSize.Y * normalizedSymmY * 2) / 2;
 
         target.ForEveryMember((member) =>
         {

+ 5 - 5
src/PixiEditor.ChangeableDocument/Changes/Root/SymmetryAxisPosition_UpdateableChange.cs

@@ -5,18 +5,18 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
 {
     private readonly SymmetryAxisDirection direction;
-    private int newPos;
-    private int originalPos;
+    private double newPos;
+    private double originalPos;
 
     [GenerateUpdateableChangeActions]
-    public SymmetryAxisPosition_UpdateableChange(SymmetryAxisDirection direction, int pos)
+    public SymmetryAxisPosition_UpdateableChange(SymmetryAxisDirection direction, double pos)
     {
         this.direction = direction;
         newPos = pos;
     }
 
     [UpdateChangeMethod]
-    public void Update(int pos)
+    public void Update(double pos)
     {
         newPos = pos;
     }
@@ -32,7 +32,7 @@ internal class SymmetryAxisPosition_UpdateableChange : UpdateableChange
         return true;
     }
 
-    private void SetPosition(Document target, int position)
+    private void SetPosition(Document target, double position)
     {
         if (direction == SymmetryAxisDirection.Horizontal)
             target.HorizontalSymmetryAxisY = position;

+ 10 - 13
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWand_Change.cs

@@ -10,19 +10,15 @@ internal class MagicWand_Change : Change
     private VectorPath? originalPath;
     private VectorPath path = new() { FillType = PathFillType.EvenOdd };
     private VecI point;
-    private readonly Guid memberGuid;
-    private readonly bool referenceAll;
-    private readonly bool drawOnMask;
+    private readonly List<Guid> memberGuids;
     private readonly SelectionMode mode;
 
     [GenerateMakeChangeAction]
-    public MagicWand_Change(Guid memberGuid, VecI point, SelectionMode mode, bool referenceAll, bool drawOnMask)
+    public MagicWand_Change(List<Guid> memberGuids, VecI point, SelectionMode mode)
     {
         path.MoveTo(point);
         this.mode = mode;
-        this.memberGuid = memberGuid;
-        this.referenceAll = referenceAll;
-        this.drawOnMask = drawOnMask;
+        this.memberGuids = memberGuids;
         this.point = point;
     }
 
@@ -34,13 +30,14 @@ internal class MagicWand_Change : Change
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
-        var image = DrawingChangeHelper.GetTargetImageOrThrow(target, memberGuid, drawOnMask);
-
         HashSet<Guid> membersToReference = new();
-        if (referenceAll)
-            target.ForEveryReadonlyMember(member => membersToReference.Add(member.GuidValue));
-        else
-            membersToReference.Add(memberGuid);
+
+        target.ForEveryReadonlyMember(member =>
+        {
+            if (memberGuids.Contains(member.GuidValue))
+                membersToReference.Add(member.GuidValue);
+        });
+
         path = MagicWandHelper.DoMagicWandFloodFill(point, membersToReference, target);
 
         ignoreInUndo = false;

+ 11 - 2
src/PixiEditor.ChangeableDocument/Enums/SelectionMode.cs

@@ -1,5 +1,14 @@
-namespace PixiEditor.ChangeableDocument.Enums;
+using System.ComponentModel;
+
+namespace PixiEditor.ChangeableDocument.Enums;
 public enum SelectionMode
 {
-    New, Add, Subtract, Intersect
+    [Description("NEW")]
+    New,
+    [Description("ADD")]
+    Add,
+    [Description("SUBTRACT")]
+    Subtract,
+    [Description("INTERSECT")]
+    Intersect
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -6,8 +6,9 @@
     <Nullable>enable</Nullable>
     <WarningsAsErrors>Nullable</WarningsAsErrors>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 9 - 0
src/PixiEditor.DrawingApi.Core/ColorsImpl/Color.cs

@@ -208,5 +208,14 @@ namespace PixiEditor.DrawingApi.Core.ColorsImpl
           return false;
       }
     }
+
+    /// <summary>
+    ///     Returns hex string representation of the color.
+    /// </summary>
+    /// <returns>Color string in format: AARRGGBB</returns>
+    public string? ToHex()
+    {
+        return this == Empty ? null : $"{this._colorValue:X8}";
+    }
   }
 }

+ 11 - 0
src/PixiEditor.DrawingApi.Core/Numerics/RectD.cs

@@ -260,6 +260,17 @@ public struct RectD : IEquatable<RectD>
         }
     }
 
+    public readonly RectD Round()
+    {
+        return new RectD()
+        {
+            Left = Math.Round(left),
+            Right = Math.Round(right),
+            Top = Math.Round(top),
+            Bottom = Math.Round(bottom)
+        };
+    }
+
     public readonly RectD RoundOutwards()
     {
         return new RectD()

+ 10 - 0
src/PixiEditor.DrawingApi.Core/Numerics/RectI.cs

@@ -149,6 +149,16 @@ public struct RectI : IEquatable<RectI>
         };
     }
 
+    public readonly RectD ReflectX(double verLineX)
+    {
+        return RectD.FromTwoPoints(Pos.ReflectX(verLineX), (Pos + Size).ReflectX(verLineX));
+    }
+
+    public readonly RectD ReflectY(double horLineY)
+    {
+        return RectD.FromTwoPoints(Pos.ReflectY(horLineY), (Pos + Size).ReflectY(horLineY));
+    }
+
     public readonly RectI ReflectX(int verLineX)
     {
         return RectI.FromTwoPoints(Pos.ReflectX(verLineX), (Pos + Size).ReflectX(verLineX));

+ 14 - 0
src/PixiEditor.DrawingApi.Core/Numerics/VecI.cs

@@ -53,6 +53,20 @@ public struct VecI : IEquatable<VecI>
     {
         return new(X, 2 * lineY - Y);
     }
+    /// <summary>
+    /// Reflects the vector across a vertical line with the specified x position
+    /// </summary>
+    public VecD ReflectX(double lineX)
+    {
+        return new(2 * lineX - X, Y);
+    }
+    /// <summary>
+    /// Reflects the vector across a horizontal line with the specified y position
+    /// </summary>
+    public VecD ReflectY(double lineY)
+    {
+        return new(X, 2 * lineY - Y);
+    }
     public static VecI operator +(VecI a, VecI b)
     {
         return new VecI(a.X + b.X, a.Y + b.Y);

+ 2 - 1
src/PixiEditor.DrawingApi.Core/PixiEditor.DrawingApi.Core.csproj

@@ -5,8 +5,9 @@
         <Nullable>enable</Nullable>
         <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
         <LangVersion>10</LangVersion>
-        <Configurations>Debug;Release;Steam</Configurations>
+        <Configurations>Debug;Release;Steam;DevRelease</Configurations>
         <Platforms>AnyCPU;x64;x86</Platforms>
+      <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
     </PropertyGroup>
 
     <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 2 - 1
src/PixiEditor.DrawingApi.Skia/PixiEditor.DrawingApi.Skia.csproj

@@ -4,8 +4,9 @@
         <TargetFramework>netstandard2.1</TargetFramework>
         <Nullable>enable</Nullable>
         <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-        <Configurations>Debug;Release;Steam</Configurations>
+        <Configurations>Debug;Release;Steam;DevRelease</Configurations>
         <Platforms>AnyCPU;x64;x86</Platforms>
+      <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
     </PropertyGroup>
 
     <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 1 - 1
src/PixiEditor.MSIX/Package.appxmanifest

@@ -9,7 +9,7 @@
   <Identity
     Name="56069PixiEditorOrganizati.PixiEditor"
     Publisher="CN=0AFA75AD-56A3-481D-B5E4-D3C6274DD38A"
-    Version="1.0.0.0" />
+    Version="1.0.3.0" />
 
   <Properties>
     <DisplayName>PixiEditor</DisplayName>

+ 12 - 1
src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj

@@ -59,7 +59,7 @@
     <PackageCertificateKeyFile>PixiEditor.MSIX_TemporaryKey.pfx</PackageCertificateKeyFile>
     <GenerateAppInstallerFile>False</GenerateAppInstallerFile>
     <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
-    <AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
+    <AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
     <GenerateTestArtifacts>True</GenerateTestArtifacts>
     <AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
     <GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
@@ -116,6 +116,17 @@
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">
     <AppxBundle>Always</AppxBundle>
   </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x86' ">
+    <OutputPath>bin\x86\DevRelease\</OutputPath>
+    <PlatformTarget>x86</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x64' ">
+    <OutputPath>bin\x64\DevRelease\</OutputPath>
+    <PlatformTarget>x64</PlatformTarget>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|AnyCPU' ">
+    <OutputPath>bin\DevRelease\</OutputPath>
+  </PropertyGroup>
   <ItemGroup>
     <AppxManifest Include="Package.appxmanifest">
       <SubType>Designer</SubType>

+ 1 - 1
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -6,7 +6,7 @@
     <UseWPF>true</UseWPF>
     <ApplicationManifest>app.manifest</ApplicationManifest>
     <Platforms>AnyCPU;x64;x86</Platforms>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
   </PropertyGroup>
 
   <ItemGroup>

+ 3 - 1
src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj

@@ -3,7 +3,9 @@
   <PropertyGroup>
     <TargetFramework>net7.0</TargetFramework>
     <Platforms>AnyCPU;x64;x86</Platforms>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 19 - 6
src/PixiEditor.UpdateModule/UpdateChecker.cs

@@ -43,7 +43,7 @@ public class UpdateChecker
     /// <returns>True if semantic version is higher.</returns>
     public static bool VersionDifferent(string originalVer, string newVer)
     {
-        return NormalizeVersionString(originalVer) != NormalizeVersionString(newVer);
+        return ExtractVersionString(originalVer) != ExtractVersionString(newVer);
     }
     
     /// <summary>
@@ -54,8 +54,8 @@ public class UpdateChecker
     /// <returns>True if originalVer is smaller than newVer.</returns>
     public static bool VersionSmaller(string originalVer, string newVer)
     {
-        string normalizedOriginal = NormalizeVersionString(originalVer);
-        string normalizedNew = NormalizeVersionString(newVer);
+        string normalizedOriginal = ExtractVersionString(originalVer);
+        string normalizedNew = ExtractVersionString(newVer);
 
         if (normalizedOriginal == normalizedNew) return false;
 
@@ -81,12 +81,15 @@ public class UpdateChecker
 
     public bool CheckUpdateAvailable(ReleaseInfo latestRelease)
     {
+        if (latestRelease == null || string.IsNullOrEmpty(latestRelease.TagName)) return false;
+        if (CurrentVersionTag == null) return false;
+        
         return latestRelease.WasDataFetchSuccessful && VersionDifferent(CurrentVersionTag, latestRelease.TagName);
     }
 
     public bool IsUpdateCompatible(string[] incompatibleVersions)
     {
-        return !incompatibleVersions.Select(x => x.Trim()).Contains(CurrentVersionTag[..7].Trim());
+        return !incompatibleVersions.Select(x => x.Trim()).Contains(ExtractVersionString(CurrentVersionTag));
     }
 
     public async Task<bool> IsUpdateCompatible()
@@ -128,8 +131,18 @@ public class UpdateChecker
         return new ReleaseInfo(false);
     }
 
-    private static string NormalizeVersionString(string versionString)
+    private static string ExtractVersionString(string versionString)
     {
-        return versionString[..7];
+        if (string.IsNullOrEmpty(versionString)) return string.Empty;
+        
+        for (int i = 0; i < versionString.Length; i++)
+        {
+            if (!char.IsDigit(versionString[i]) && versionString[i] != '.')
+            {
+                return versionString[..i];
+            }
+        }
+        
+        return versionString;
     }
 }

+ 2 - 1
src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj

@@ -5,8 +5,9 @@
     <Nullable>enable</Nullable>
     <UseWPF>true</UseWPF>
     <WarningsAsErrors>Nullable</WarningsAsErrors>
-    <Configurations>Debug;Release;Steam</Configurations>
+    <Configurations>Debug;Release;Steam;DevRelease</Configurations>
     <Platforms>AnyCPU;x64;x86</Platforms>
+    <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Steam|AnyCPU'">

+ 125 - 90
src/PixiEditor.sln

@@ -38,14 +38,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.DrawingApi.Skia"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditorGen", "PixiEditorGen\PixiEditorGen.csproj", "{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Builder", "PixiEditor.Builder\build\PixiEditor.Builder.csproj", "{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|x64 = Debug|x64
 		Debug|x86 = Debug|x86
-		Dev Release|Any CPU = Dev Release|Any CPU
-		Dev Release|x64 = Dev Release|x64
-		Dev Release|x86 = Dev Release|x86
 		MSIX Debug|Any CPU = MSIX Debug|Any CPU
 		MSIX Debug|x64 = MSIX Debug|x64
 		MSIX Debug|x86 = MSIX Debug|x86
@@ -58,6 +57,9 @@ Global
 		Steam|Any CPU = Steam|Any CPU
 		Steam|x64 = Steam|x64
 		Steam|x86 = Steam|x86
+		DevRelease|Any CPU = DevRelease|Any CPU
+		DevRelease|x64 = DevRelease|x64
+		DevRelease|x86 = DevRelease|x86
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|Any CPU.ActiveCfg = Debug|x86
@@ -66,12 +68,6 @@ Global
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x64.Build.0 = Debug|x64
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x86.ActiveCfg = Debug|x86
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Debug|x86.Build.0 = Debug|x86
-		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|Any CPU.ActiveCfg = Dev Release|Any CPU
-		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|Any CPU.Build.0 = Dev Release|Any CPU
-		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x64.ActiveCfg = Dev Release|x64
-		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x64.Build.0 = Dev Release|x64
-		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x86.ActiveCfg = Dev Release|x86
-		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Dev Release|x86.Build.0 = Dev Release|x86
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.MSIX Debug|Any CPU.ActiveCfg = MSIX Debug|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.MSIX Debug|Any CPU.Build.0 = MSIX Debug|Any CPU
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.MSIX Debug|x64.ActiveCfg = MSIX Debug|x64
@@ -96,18 +92,18 @@ Global
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Steam|x64.Build.0 = Steam|x64
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Steam|x86.ActiveCfg = Steam|x86
 		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.Steam|x86.Build.0 = Steam|x86
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x64.Build.0 = DevRelease|x64
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{2CCDDE79-06CB-4771-AF85-7B25313EBA30}.DevRelease|x86.Build.0 = DevRelease|x86
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.ActiveCfg = Debug|x64
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x64.Build.0 = Debug|x64
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.ActiveCfg = Debug|x86
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Debug|x86.Build.0 = Debug|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x64.ActiveCfg = Release|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x64.Build.0 = Release|x64
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x86.ActiveCfg = Release|x86
-		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Dev Release|x86.Build.0 = Release|x86
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.MSIX Debug|x64.ActiveCfg = Debug|x64
@@ -132,18 +128,18 @@ Global
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x64.Build.0 = Steam|x64
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x86.ActiveCfg = Steam|x86
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Steam|x86.Build.0 = Steam|x86
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.Build.0 = DevRelease|x64
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x86.Build.0 = DevRelease|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x64.ActiveCfg = Debug|x64
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x64.Build.0 = Debug|x64
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x86.ActiveCfg = Debug|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Debug|x86.Build.0 = Debug|x86
-		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x64.ActiveCfg = Release|x64
-		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x64.Build.0 = Release|x64
-		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x86.ActiveCfg = Release|x86
-		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Dev Release|x86.Build.0 = Release|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.MSIX Debug|x64.ActiveCfg = Debug|x64
@@ -168,18 +164,18 @@ Global
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Steam|x64.Build.0 = Steam|x64
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Steam|x86.ActiveCfg = Steam|x86
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Steam|x86.Build.0 = Steam|x86
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|x64.Build.0 = DevRelease|x64
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|x86.Build.0 = DevRelease|x86
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x64.ActiveCfg = Debug|x64
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x64.Build.0 = Debug|x64
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x86.ActiveCfg = Debug|x86
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Debug|x86.Build.0 = Debug|x86
-		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x64.ActiveCfg = Release|x64
-		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x64.Build.0 = Release|x64
-		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x86.ActiveCfg = Release|x86
-		{5193C1C1-8362-40FD-802B-E097E8C88082}.Dev Release|x86.Build.0 = Release|x86
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.MSIX Debug|x64.ActiveCfg = Debug|x64
@@ -204,6 +200,12 @@ Global
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Steam|x64.Build.0 = Steam|x64
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Steam|x86.ActiveCfg = Steam|x86
 		{5193C1C1-8362-40FD-802B-E097E8C88082}.Steam|x86.Build.0 = Steam|x86
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.DevRelease|x64.Build.0 = DevRelease|x64
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{5193C1C1-8362-40FD-802B-E097E8C88082}.DevRelease|x86.Build.0 = DevRelease|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
@@ -213,15 +215,6 @@ Global
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x86.ActiveCfg = Debug|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x86.Build.0 = Debug|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x86.Deploy.0 = Debug|x86
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|Any CPU.Deploy.0 = Release|Any CPU
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x64.ActiveCfg = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x64.Build.0 = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x64.Deploy.0 = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x86.ActiveCfg = Release|x86
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x86.Build.0 = Release|x86
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Dev Release|x86.Deploy.0 = Release|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|Any CPU.Deploy.0 = Debug|Any CPU
@@ -251,18 +244,18 @@ Global
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Steam|x64.Build.0 = Steam|x64
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Steam|x86.ActiveCfg = Steam|x86
 		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Steam|x86.Build.0 = Steam|x86
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x64.Build.0 = DevRelease|x64
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x86.Build.0 = DevRelease|x86
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|x64.Build.0 = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|x86.Build.0 = Debug|Any CPU
-		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Dev Release|x64.ActiveCfg = Release|Any CPU
-		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Dev Release|x64.Build.0 = Release|Any CPU
-		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Dev Release|x86.ActiveCfg = Release|Any CPU
-		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Dev Release|x86.Build.0 = Release|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -287,18 +280,18 @@ Global
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Steam|x64.Build.0 = Steam|x64
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Steam|x86.ActiveCfg = Steam|x86
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Steam|x86.Build.0 = Steam|x86
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x64.Build.0 = DevRelease|x64
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x86.Build.0 = DevRelease|x86
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Debug|x64.Build.0 = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Debug|x86.Build.0 = Debug|Any CPU
-		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Dev Release|x64.ActiveCfg = Release|Any CPU
-		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Dev Release|x64.Build.0 = Release|Any CPU
-		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Dev Release|x86.ActiveCfg = Release|Any CPU
-		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Dev Release|x86.Build.0 = Release|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -323,18 +316,18 @@ Global
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Steam|x64.Build.0 = Steam|x64
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Steam|x86.ActiveCfg = Steam|x86
 		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.Steam|x86.Build.0 = Steam|x86
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.DevRelease|x64.Build.0 = DevRelease|x64
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{E31A8266-5BCA-4877-B9E5-9C5BB42829D6}.DevRelease|x86.Build.0 = DevRelease|x86
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x64.Build.0 = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x86.Build.0 = Debug|Any CPU
-		{510ED47C-2455-4DCE-A561-1074725E1236}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{510ED47C-2455-4DCE-A561-1074725E1236}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{510ED47C-2455-4DCE-A561-1074725E1236}.Dev Release|x64.ActiveCfg = Release|Any CPU
-		{510ED47C-2455-4DCE-A561-1074725E1236}.Dev Release|x64.Build.0 = Release|Any CPU
-		{510ED47C-2455-4DCE-A561-1074725E1236}.Dev Release|x86.ActiveCfg = Release|Any CPU
-		{510ED47C-2455-4DCE-A561-1074725E1236}.Dev Release|x86.Build.0 = Release|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -359,18 +352,18 @@ Global
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Steam|x64.Build.0 = Steam|x64
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Steam|x86.ActiveCfg = Steam|x86
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Steam|x86.Build.0 = Steam|x86
+		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|x64.Build.0 = DevRelease|x64
+		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|x86.Build.0 = DevRelease|x86
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|x64.Build.0 = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|x86.Build.0 = Debug|Any CPU
-		{294FD171-9536-474C-A679-83F0266275FB}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{294FD171-9536-474C-A679-83F0266275FB}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{294FD171-9536-474C-A679-83F0266275FB}.Dev Release|x64.ActiveCfg = Release|Any CPU
-		{294FD171-9536-474C-A679-83F0266275FB}.Dev Release|x64.Build.0 = Release|Any CPU
-		{294FD171-9536-474C-A679-83F0266275FB}.Dev Release|x86.ActiveCfg = Release|Any CPU
-		{294FD171-9536-474C-A679-83F0266275FB}.Dev Release|x86.Build.0 = Release|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -395,18 +388,18 @@ Global
 		{294FD171-9536-474C-A679-83F0266275FB}.Steam|x64.Build.0 = Steam|x64
 		{294FD171-9536-474C-A679-83F0266275FB}.Steam|x86.ActiveCfg = Steam|x86
 		{294FD171-9536-474C-A679-83F0266275FB}.Steam|x86.Build.0 = Steam|x86
+		{294FD171-9536-474C-A679-83F0266275FB}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{294FD171-9536-474C-A679-83F0266275FB}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{294FD171-9536-474C-A679-83F0266275FB}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{294FD171-9536-474C-A679-83F0266275FB}.DevRelease|x64.Build.0 = DevRelease|x64
+		{294FD171-9536-474C-A679-83F0266275FB}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{294FD171-9536-474C-A679-83F0266275FB}.DevRelease|x86.Build.0 = DevRelease|x86
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|x64.Build.0 = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|x86.Build.0 = Debug|Any CPU
-		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Dev Release|x64.ActiveCfg = Release|Any CPU
-		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Dev Release|x64.Build.0 = Release|Any CPU
-		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Dev Release|x86.ActiveCfg = Release|Any CPU
-		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Dev Release|x86.Build.0 = Release|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -431,18 +424,18 @@ Global
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Steam|x64.Build.0 = Steam|x64
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Steam|x86.ActiveCfg = Steam|x86
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Steam|x86.Build.0 = Steam|x86
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|x64.Build.0 = DevRelease|x64
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|x86.Build.0 = DevRelease|x86
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x64.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x86.Build.0 = Debug|Any CPU
-		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Dev Release|Any CPU.ActiveCfg = Release|Any CPU
-		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Dev Release|Any CPU.Build.0 = Release|Any CPU
-		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Dev Release|x64.ActiveCfg = Release|Any CPU
-		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Dev Release|x64.Build.0 = Release|Any CPU
-		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Dev Release|x86.ActiveCfg = Release|Any CPU
-		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Dev Release|x86.Build.0 = Release|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -467,18 +460,18 @@ Global
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Steam|x64.Build.0 = Steam|x64
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Steam|x86.ActiveCfg = Steam|x86
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Steam|x86.Build.0 = Steam|x86
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x64.Build.0 = DevRelease|x64
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x86.Build.0 = DevRelease|x86
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Debug|x64.Build.0 = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Debug|x86.Build.0 = Debug|Any CPU
-		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Dev Release|Any CPU.ActiveCfg = Debug|Any CPU
-		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Dev Release|Any CPU.Build.0 = Debug|Any CPU
-		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Dev Release|x64.ActiveCfg = Debug|Any CPU
-		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Dev Release|x64.Build.0 = Debug|Any CPU
-		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Dev Release|x86.ActiveCfg = Debug|Any CPU
-		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Dev Release|x86.Build.0 = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -503,18 +496,18 @@ Global
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Steam|x64.Build.0 = Steam|x64
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Steam|x86.ActiveCfg = Steam|x86
 		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.Steam|x86.Build.0 = Steam|x86
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.DevRelease|x64.Build.0 = DevRelease|x64
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{5FC5E9C5-F439-43AA-92AF-9B7554D6FA13}.DevRelease|x86.Build.0 = DevRelease|x86
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Debug|x64.Build.0 = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Debug|x86.Build.0 = Debug|Any CPU
-		{98040E8A-F08E-45F8-956F-6480C8272049}.Dev Release|Any CPU.ActiveCfg = Debug|Any CPU
-		{98040E8A-F08E-45F8-956F-6480C8272049}.Dev Release|Any CPU.Build.0 = Debug|Any CPU
-		{98040E8A-F08E-45F8-956F-6480C8272049}.Dev Release|x64.ActiveCfg = Debug|Any CPU
-		{98040E8A-F08E-45F8-956F-6480C8272049}.Dev Release|x64.Build.0 = Debug|Any CPU
-		{98040E8A-F08E-45F8-956F-6480C8272049}.Dev Release|x86.ActiveCfg = Debug|Any CPU
-		{98040E8A-F08E-45F8-956F-6480C8272049}.Dev Release|x86.Build.0 = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{98040E8A-F08E-45F8-956F-6480C8272049}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -539,18 +532,18 @@ Global
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Steam|x64.Build.0 = Steam|x64
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Steam|x86.ActiveCfg = Steam|x86
 		{98040E8A-F08E-45F8-956F-6480C8272049}.Steam|x86.Build.0 = Steam|x86
+		{98040E8A-F08E-45F8-956F-6480C8272049}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{98040E8A-F08E-45F8-956F-6480C8272049}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{98040E8A-F08E-45F8-956F-6480C8272049}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{98040E8A-F08E-45F8-956F-6480C8272049}.DevRelease|x64.Build.0 = DevRelease|x64
+		{98040E8A-F08E-45F8-956F-6480C8272049}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{98040E8A-F08E-45F8-956F-6480C8272049}.DevRelease|x86.Build.0 = DevRelease|x86
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x64.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x86.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x86.Build.0 = Debug|Any CPU
-		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Dev Release|Any CPU.ActiveCfg = Debug|Any CPU
-		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Dev Release|Any CPU.Build.0 = Debug|Any CPU
-		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Dev Release|x64.ActiveCfg = Debug|Any CPU
-		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Dev Release|x64.Build.0 = Debug|Any CPU
-		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Dev Release|x86.ActiveCfg = Debug|Any CPU
-		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Dev Release|x86.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -575,6 +568,48 @@ Global
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Steam|x64.Build.0 = Steam|x64
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Steam|x86.ActiveCfg = Steam|x86
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Steam|x86.Build.0 = Steam|x86
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x64.Build.0 = DevRelease|x64
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x86.Build.0 = DevRelease|x86
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|x64.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|x86.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX|x64.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.MSIX|x86.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Release|Any CPU.Build.0 = Release|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Release|x64.ActiveCfg = Release|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Release|x64.Build.0 = Release|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Release|x86.ActiveCfg = Release|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Release|x86.Build.0 = Release|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x64.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x86.Build.0 = Debug|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|Any CPU.ActiveCfg = DevRelease|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|Any CPU.Build.0 = DevRelease|Any CPU
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x64.ActiveCfg = DevRelease|x64
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x64.Build.0 = DevRelease|x64
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x86.ActiveCfg = DevRelease|x86
+		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.DevRelease|x86.Build.0 = DevRelease|x86
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 12 - 3
src/PixiEditor/App.xaml.cs

@@ -1,6 +1,7 @@
 using System.IO;
 using System.Text.RegularExpressions;
 using System.Windows;
+using PixiEditor.Localization;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
@@ -68,8 +69,14 @@ internal partial class App : Application
                                 if (mainWindow != null)
                                 {
                                     mainWindow.BringToForeground();
-                                    StartupArgs.Args = File.ReadAllText(passedArgsFile).Split(' ').ToList();
-                                    File.Delete(passedArgsFile);
+                                    List<string> args = new List<string>();
+                                    if (File.Exists(passedArgsFile))
+                                    {
+                                        args = File.ReadAllText(passedArgsFile).Split(' ').ToList();
+                                        File.Delete(passedArgsFile);
+                                    }
+                                    
+                                    StartupArgs.Args = args;
                                     StartupArgs.Args.Add("--openedInExisting");
                                     mainWindow.DataContext.OnStartupCommand.Execute(null);
                                 }
@@ -104,7 +111,9 @@ internal partial class App : Application
 
         if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
         {
-            ConfirmationType confirmation = ConfirmationDialog.Show($"{e.ReasonSessionEnding} with unsaved data. Are you sure?", $"{e.ReasonSessionEnding}");
+            ConfirmationType confirmation = ConfirmationDialog.Show(
+                new LocalizedString("SESSION_UNSAVED_DATA", e.ReasonSessionEnding),
+                $"{e.ReasonSessionEnding}");
             e.Cancel = confirmation != ConfirmationType.Yes;
         }
     }

+ 6 - 0
src/PixiEditor/BuildConstants.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor;
+
+public static class BuildConstants
+{
+    public const string CrashReportWebhookUrl = "${crash-report-webhook-url}";
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/ar.json

@@ -0,0 +1,2 @@
+{
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/cs.json

@@ -0,0 +1,2 @@
+{
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/de.json

@@ -0,0 +1,2 @@
+{
+}

+ 528 - 0
src/PixiEditor/Data/Localization/Languages/en.json

@@ -0,0 +1,528 @@
+{
+    "RECENT_FILES": "Recent Files",
+    "OPEN_FILE": "Open file",
+    "NEW_FILE": "New",
+    "RECENT_EMPTY_TEXT": "So much empty space",
+    "LANGUAGE": "Language",
+    "GENERAL": "General",
+    "DISCORD": "Discord",
+    "KEY_BINDINGS": "Key Bindings",
+    "MISC": "Misc",
+    "SHOW_STARTUP_WINDOW": "Show Startup Window",
+    "SHOW_IMAGE_PREVIEW_TASKBAR": "Show image preview in taskbar",
+    "RECENT_FILE_LENGTH": "Recent file list length",
+    "RECENT_FILE_LENGTH_TOOLTIP": "How many documents are shown under File > Recent. Default: 8",
+    "DEFAULT_NEW_SIZE": "Default new file size",
+    "WIDTH": "Width",
+    "HEIGHT": "Height",
+    "TOOLS": "Tools",
+    "ENABLE_SHARED_TOOLBAR": "Enable shared toolbar",
+    "AUTOMATIC_UPDATES": "Automatic Updates",
+    "CHECK_FOR_UPDATES": "Check updates on startup",
+    "UPDATE_STREAM": "Update stream",
+    "UPDATE_CHANNEL_HELP_TOOLTIP": "Update channels can only be changed in standalone version (downloaded from https://pixieditor.net).\nSteam and Microsoft Store versions handle updates separately.",
+    "DEBUG": "Debug",
+    "ENABLE_DEBUG_MODE": "Enable Debug mode",
+    "OPEN_CRASH_REPORTS_DIR": "Open crash reports directory",
+    "DISCORD_RICH_PRESENCE": "Rich Presence",
+    "ENABLED": "Enabled",
+    "SHOW_IMAGE_NAME": "Show image name",
+    "SHOW_IMAGE_SIZE": "Show image size",
+    "SHOW_LAYER_COUNT": "Show layer count",
+    "FILE": "File",
+    "RECENT": "Recent",
+    "OPEN": "Open",
+    "SAVE_PIXI": "Save (.pixi)",
+    "SAVE_AS_PIXI": "Save as... (.pixi)",
+    "EXPORT_IMG": "Export (.png, .jpg, etc.)",
+    "EDIT": "Edit",
+    "EXIT": "Exit",
+    "PERCENTAGE": "Percentage",
+    "ABSOLUTE": "Absolute",
+    "PRESERVE_ASPECT_RATIO": "Preserve aspect ratio",
+    "ANCHOR_POINT": "Anchor point",
+    "RESIZE_IMAGE": "Resize image",
+    "RESIZE": "Resize",
+    "DOCUMENTATION": "Documentation",
+    "WEBSITE": "Website",
+    "OPEN_WEBSITE": "Open website",
+    "REPOSITORY": "Repository",
+    "OPEN_REPOSITORY": "Open repository",
+    "LICENSE": "License",
+    "OPEN_LICENSE": "Open license",
+    "THIRD_PARTY_LICENSES": "Third party licenses",
+    "OPEN_THIRD_PARTY_LICENSES": "Open third party licenses",
+    "APPLY_TRANSFORM": "Apply transform",
+    "INCREASE_TOOL_SIZE": "Increase tool size",
+    "DECREASE_TOOL_SIZE": "Decrease tool size",
+    "TO_INSTALL_UPDATE": "to install update {0}",
+    "DOWNLOADING_UPDATE": "Downloading update...",
+    "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
+    "NEW_UPDATE": "New update",
+    "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
+    "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
+    "UPDATE_CHECK_FAILED": "Update check failed",
+    "COULD_NOT_CHECK_FOR_UPDATES": "Could not check if there is an update available.",
+    "VERSION": "Version {0}",
+    "OPEN_TEMP_DIR": "Open temp directory",
+    "OPEN_LOCAL_APPDATA_DIR": "Open Local AppData directory",
+    "OPEN_ROAMING_APPDATA_DIR": "Open Roaming AppData directory",
+    "OPEN_INSTALLATION_DIR": "Open installation directory",
+    "DUMP_ALL_COMMANDS": "Dump all commands",
+    "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Dump all commands to a text file",
+    "CRASH": "Crash",
+    "CRASH_APP": "Crash application",
+    "DELETE_USR_PREFS": "Delete user preferences (Roaming AppData)",
+    "DELETE_SHORTCUT_FILE": "Delete shortcut file (Roaming AppData)",
+    "DELETE_EDITOR_DATA": "Delete editor data (Local AppData)",
+    "GENERATE_KEY_BINDINGS_TEMPLATE": "Generate key bindings template",
+    "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE": "Generate key bindings json template",
+    "VALIDATE_SHORTCUT_MAP": "Validate shortcut map",
+    "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE": "Validates shortcut map",
+    "VALIDATION_KEYS_NOTICE_DIALOG": "Empty keys: {0}\nUnknown Commands: {1}",
+    "RESULT": "Result",
+    "CLEAR_RECENT_DOCUMENTS": "Clear recent documents",
+    "CLEAR_RECENTLY_OPENED_DOCUMENTS": "Clear recently opened documents",
+    "OPEN_CMD_DEBUG_WINDOW": "Open command debug window",
+    "PATH_DOES_NOT_EXIST": "{0} does not exist.",
+    "LOCATION_DOES_NOT_EXIST": "Location does not exist.",
+    "FILE_NOT_FOUND": "File not found.",
+    "ARE_YOU_SURE": "Are you sure?",
+    "ARE_YOU_SURE_PATH_FULL_PATH": "Are you sure you want to delete {0}?\nThis data will be lost for all installations.\n(Full Path: {1})",
+    "FAILED_TO_OPEN_FILE": "Failed to open the file",
+    "OLD_FILE_FORMAT": "Old file format",
+    "OLD_FILE_FORMAT_DESCRIPTION": "This .pixi file uses the old format,\n which is no longer supported and can't be opened.",
+    "NOTHING_FOUND": "Nothing found",
+    "EXPORT": "Export",
+    "EXPORT_IMAGE": "Export image",
+    "IMPORT": "Import",
+    "SHORTCUT_TEMPLATES": "Shortcut templates",
+    "RESET_ALL": "Reset all",
+    "LAYER": "Layer",
+    "LAYER_DELETE_SELECTED": "Delete active layer/folder",
+    "LAYER_DELETE_SELECTED_DESCRIPTIVE": "Delete active layer or folder",
+    "LAYER_DELETE_ALL_SELECTED": "Delete all selected layers/folders",
+    "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Delete all selected layers and/or folders",
+    "DELETE_SELECTED_PIXELS": "Delete selected pixels",
+    "NEW_FOLDER": "New folder",
+    "CREATE_NEW_FOLDER": "Create new folder",
+    "NEW_LAYER": "New layer",
+    "CREATE_NEW_LAYER": "Create new layer",
+    "NEW_IMAGE": "New image",
+    "CREATE_NEW_IMAGE": "Create new image",
+    "SAVE": "Save",
+    "SAVE_AS": "Save as...",
+    "IMAGE": "Image",
+    "SAVE_IMAGE": "Save image",
+    "SAVE_IMAGE_AS": "Save image as new",
+    "DUPLICATE": "Duplicate",
+    "DUPLICATE_SELECTED_LAYER": "Duplicate selected layer",
+    "CREATE_MASK": "Create mask",
+    "DELETE_MASK": "Delete mask",
+    "TOGGLE_MASK": "Toggle mask",
+    "APPLY_MASK": "Apply mask",
+    "TOGGLE_VISIBILITY": "Toggle visibility",
+    "MOVE_MEMBER_UP": "Move member upwards",
+    "MOVE_MEMBER_UP_DESCRIPTIVE": "Move selected layer or folder upwards",
+    "MOVE_MEMBER_DOWN": "Move member downwards",
+    "MOVE_MEMBER_DOWN_DESCRIPTIVE": "Move selected layer or folder downwards",
+    "MERGE_ALL_SELECTED_LAYERS": "Merge all selected layers",
+    "MERGE_WITH_ABOVE": "Merge selected layer with above",
+    "MERGE_WITH_ABOVE_DESCRIPTIVE": "Merge selected layer with the one above it",
+    "MERGE_WITH_BELOW": "Merge selected layer with below",
+    "MERGE_WITH_BELOW_DESCRIPTIVE": "Merge selected layer with the one below it",
+    "ADD_REFERENCE_LAYER": "Add Reference Layer",
+    "DELETE_REFERENCE_LAYER": "Delete reference layer",
+    "TRANSFORM_REFERENCE_LAYER": "Transform reference layer",
+    "TOGGLE_REFERENCE_LAYER_POS": "Toggle reference layer position",
+    "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "Toggle reference layer between topmost or most below",
+    "RESET_REFERENCE_LAYER_POS": "Reset reference layer position",
+    "CLIP_CANVAS": "Clip Canvas",
+    "FLIP_IMG_VERTICALLY": "Flip Image Vertically",
+    "FLIP_IMG_HORIZONTALLY": "Flip Image Horizontally",
+    "FLIP_LAYERS_VERTICALLY": "Flip Selected Layers Vertically",
+    "FLIP_LAYERS_HORIZONTALLY": "Flip Selected Layers Horizontally",
+    "ROT_IMG_90": "Rotate Image 90 degrees",
+    "ROT_IMG_180": "Rotate Image 180 degrees",
+    "ROT_IMG_-90": "Rotate Image -90 degrees",
+    "ROT_LAYERS_90": "Rotate Selected Layers 90 degrees",
+    "ROT_LAYERS_180": "Rotate Selected Layers 180 degrees",
+    "ROT_LAYERS_-90": "Rotate Selected Layers -90 degrees",
+    "TOGGLE_VERT_SYMMETRY_AXIS": "Toggle vertical symmetry axis",
+    "TOGGLE_HOR_SYMMETRY_AXIS": "Toggle horizontal symmetry axis",
+    "DELETE_PIXELS": "Delete pixels",
+    "DELETE_PIXELS_DESCRIPTIVE": "Delete selected pixels",
+    "RESIZE_DOCUMENT": "Resize document",
+    "RESIZE_CANVAS": "Resize canvas",
+    "CENTER_CONTENT": "Center content",
+    "CUT": "Cut",
+    "CUT_DESCRIPTIVE": "Cut selected area/layers",
+    "PASTE": "Paste",
+    "PASTE_DESCRIPTIVE": "Paste clipboard contents",
+    "PASTE_AS_NEW_LAYER": "Paste as new layer",
+    "PASTE_AS_NEW_LAYER_DESCRIPTIVE": "Paste from clipboard as new layer",
+    "PASTE_REFERENCE_LAYER": "Paste reference layer",
+    "PASTE_REFERENCE_LAYER_DESCRIPTIVE": "Paste clipboard contents as reference layer",
+    "PASTE_COLOR": "Paste color",
+    "PASTE_COLOR_DESCRIPTIVE": "Paste color from clipboard",
+    "PASTE_COLOR_SECONDARY": "Paste color as secondary",
+    "PASTE_COLOR_SECONDARY_DESCRIPTIVE": "Paste color from clipboard as secondary color",
+    "CLIPBOARD": "Clipboard",
+    "COPY": "Copy",
+    "COPY_DESCRIPTIVE": "Copy to clipboard",
+    "COPY_COLOR_HEX": "Copy primary color (HEX)",
+    "COPY_COLOR_HEX_DESCRIPTIVE": "Copy primary color as HEX code",
+    "COPY_COLOR_RGB": "Copy primary color (RGB)",
+    "COPY_COLOR_RGB_DESCRIPTIVE": "Copy primary color as RGB code",
+    "COPY_COLOR_SECONDARY_HEX": "Copy secondary color (HEX)",
+    "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "Copy secondary color as HEX code",
+    "COPY_COLOR_SECONDARY_RGB": "Copy secondary color (RGB)",
+    "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "Copy secondary color as RGB code",
+    "PALETTE_COLORS": "Palette Colors",
+    "REPLACE_SECONDARY_BY_PRIMARY": "Replace secondary color by primary",
+    "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "Replace the secondary color by the primary color",
+    "REPLACE_PRIMARY_BY_SECONDARY": "Replace primary color by secondary",
+    "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "Replace the primary color by the secondary color",
+    "OPEN_PALETTE_BROWSER": "Open palette browser",
+    "OVERWRITE_PALETTE_CONSENT": "Palette '{0}' already exists, do you want to overwrite it?",
+    "PALETTE_EXISTS": "Palette already exists",
+    "REPLACE_PALETTE_CONSENT": "Replace current palette with selected one?",
+    "REPLACE_PALETTE": "Replace current palette",
+    "SELECT_COLOR_1": "Select color 1",
+    "SELECT_COLOR_2": "Select color 2",
+    "SELECT_COLOR_3": "Select color 3",
+    "SELECT_COLOR_4": "Select color 4",
+    "SELECT_COLOR_5": "Select color 5",
+    "SELECT_COLOR_6": "Select color 6",
+    "SELECT_COLOR_7": "Select color 7",
+    "SELECT_COLOR_8": "Select color 8",
+    "SELECT_COLOR_9": "Select color 9",
+    "SELECT_COLOR_10": "Select color 10",
+    "SELECT_TOOL": "Select {0} Tool",
+    "SELECT_COLOR_1_DESCRIPTIVE": "Select the first color in the palette",
+    "SELECT_COLOR_2_DESCRIPTIVE": "Select the second color in the palette",
+    "SELECT_COLOR_3_DESCRIPTIVE": "Select the third color in the palette",
+    "SELECT_COLOR_4_DESCRIPTIVE": "Select the fourth color in the palette",
+    "SELECT_COLOR_5_DESCRIPTIVE": "Select the fifth color in the palette",
+    "SELECT_COLOR_6_DESCRIPTIVE": "Select the sixth color in the palette",
+    "SELECT_COLOR_7_DESCRIPTIVE": "Select the seventh color in the palette",
+    "SELECT_COLOR_8_DESCRIPTIVE": "Select the eighth color in the palette",
+    "SELECT_COLOR_9_DESCRIPTIVE": "Select the ninth color in the palette",
+    "SELECT_COLOR_10_DESCRIPTIVE": "Select the tenth color in the palette",
+    "SWAP_COLORS": "Swap colors",
+    "SWAP_COLORS_DESCRIPTIVE": "Swap primary and secondary colors",
+    "SEARCH": "Search",
+    "COMMAND_SEARCH": "Command search",
+    "OPEN_COMMAND_SEARCH": "Open command search window",
+    "SELECT": "Select",
+    "DESELECT": "Deselect",
+    "INVERT": "Invert",
+    "SELECTION": "Selection",
+    "SELECT_ALL": "Select all",
+    "SELECT_ALL_DESCRIPTIVE": "Select everything",
+    "CLEAR_SELECTION": "Clear selection",
+    "INVERT_SELECTION": "Invert selection",
+    "INVERT_SELECTION_DESCRIPTIVE": "Invert the selection",
+    "TRANSFORM_SELECTED_AREA": "Transform selected area",
+    "NUDGE_SELECTED_LEFT": "Nudge selected object left",
+    "NUDGE_SELECTED_RIGHT": "Nudge selected object right",
+    "NUDGE_SELECTED_UP": "Nudge selected object up",
+    "NUDGE_SELECTED_DOWN": "Nudge selected object down",
+    "MASK_FROM_SELECTION": "New mask from selection",
+    "MASK_FROM_SELECTION_DESCRIPTIVE": "Selection to new mask",
+    "ADD_SELECTION_TO_MASK": "Add selection to mask",
+    "SUBTRACT_SELECTION_FROM_MASK": "Subtract selection from mask",
+    "INTERSECT_SELECTION_MASK": "Intersect selection with mask",
+    "SELECTION_TO_MASK": "Selection to mask",
+    "TO_NEW_MASK": "to new mask",
+    "ADD_TO_MASK": "add to mask",
+    "SUBTRACT_FROM_MASK": "subtract from mask",
+    "INTERSECT_WITH_MASK": "intersect with mask",
+    "STYLUS": "Stylus",
+    "TOGGLE_PEN_MODE": "Toggle pen mode",
+    "UNDO": "Undo",
+    "UNDO_DESCRIPTIVE": "Undo last action",
+    "REDO": "Redo",
+    "REDO_DESCRIPTIVE": "Redo last action",
+    "WINDOWS": "Windows",
+    "TOGGLE_GRIDLINES": "Toggle gridlines",
+    "ZOOM_IN": "Zoom in",
+    "ZOOM_OUT": "Zoom out",
+    "NEW_WINDOW_FOR_IMG": "New window for current image",
+    "CENTER_ACTIVE_VIEWPORT": "Center active viewport",
+    "FLIP_VIEWPORT_HORIZONTALLY": "Flip viewport horizontally",
+    "FLIP_VIEWPORT_VERTICALLY": "Flip viewport vertically",
+    "SETTINGS": "Settings",
+    "OPEN_SETTINGS": "Open settings",
+    "OPEN_SETTINGS_DESCRIPTIVE": "Open settings window",
+    "OPEN_STARTUP_WINDOW": "Open startup window",
+    "OPEN_SHORTCUT_WINDOW": "Open shortcuts window",
+    "OPEN_ABOUT_WINDOW": "Open about window",
+    "OPEN_NAVIGATION_WINDOW": "Open navigation window",
+    "ERROR": "Error",
+    "INTERNAL_ERROR": "Internal error",
+    "ERROR_SAVE_LOCATION": "Couldn't save the file to the specified location",
+    "ERROR_WHILE_SAVING": "An internal error occured while saving. Please try again.",
+    "UNKNOWN_ERROR_SAVING": "An error occured while saving.",
+    "FAILED_ASSOCIATE_LOSPEC": "Failed to associate Lospec Palette protocol.",
+    "REDDIT": "Reddit",
+    "GITHUB": "GitHub",
+    "YOUTUBE": "YouTube",
+    "DONATE": "Donate",
+    "YES": "Yes",
+    "NO": "No",
+    "CANCEL": "Cancel",
+    "UNNAMED": "Unnamed",
+    "OPEN_COMMAND_DEBUG_WINDOW": "Open command debug window",
+    "DELETE": "Delete",
+    "USER_PREFS": "User preferences (Roaming)",
+    "SHORTCUT_FILE": "Shortcut file (Roaming)",
+    "EDITOR_DATA": "Editor data (Local)",
+    "MOVE_VIEWPORT_TOOLTIP": "Moves viewport. ({0})",
+    "MOVE_VIEWPORT_ACTION_DISPLAY": "Click and move to pan the viewport",
+    "MOVE_TOOL_TOOLTIP": "Moves selected pixels ({0}). Hold Ctrl to move all layers.",
+    "MOVE_TOOL_ACTION_DISPLAY": "Hold mouse to move selected pixels. Hold Ctrl to move all layers.",
+    "PEN_TOOL_TOOLTIP": "Pen. ({0})",
+    "PEN_TOOL_ACTION_DISPLAY": "Click and move to draw.",
+    "PIXEL_PERFECT_SETTING": "Pixel perfect",
+    "RECTANGLE_TOOL_TOOLTIP": "Draws rectangle on canvas ({0}). Hold Shift to draw a square.",
+    "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to draw a rectangle. Hold Shift to draw a square.",
+    "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT": "Click and move to draw a square.",
+    "KEEP_ORIGINAL_IMAGE_SETTING": "Keep original image",
+    "ROTATE_VIEWPORT_TOOLTIP": "Rotates viewport. ({0})",
+    "ROTATE_VIEWPORT_ACTION_DISPLAY": "Click and move to rotate the viewport",
+    "SELECT_TOOL_TOOLTIP": "Selects area. ({0})",
+    "SELECT_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to select an area. Hold Shift to add to existing selection. Hold Ctrl to subtract from it.",
+    "SELECT_TOOL_ACTION_DISPLAY_SHIFT": "Click and move to add to the current selection.",
+    "SELECT_TOOL_ACTION_DISPLAY_CTRL": "Click and move to subtract from the current selection.",
+    "ZOOM_TOOL_TOOLTIP": "Zooms viewport ({0}). Click to zoom in, hold alt and click to zoom out.",
+    "ZOOM_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to zoom. Click to zoom in, hold ctrl and click to zoom out.",
+    "ZOOM_TOOL_ACTION_DISPLAY_CTRL": "Click and move to zoom. Click to zoom out, release ctrl and click to zoom in.",
+    "BRIGHTNESS_TOOL_TOOLTIP": "Makes pixels brighter or darker ({0}). Hold Ctrl to make pixels darker.",
+    "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT": "Draw on pixels to make them brighter. Hold Ctrl to darken.",
+    "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL": "Draw on pixels to make them darker. Release Ctrl to brighten.",
+    "COLOR_PICKER_TOOLTIP": "Picks the primary color from the canvas. ({0})",
+    "COLOR_PICKER_ACTION_DISPLAY_DEFAULT": "Click to pick colors. Hold Ctrl to hide the canvas. Hold Shift to hide the reference layer",
+    "COLOR_PICKER_ACTION_DISPLAY_CTRL": "Click to pick colors from the reference layer.",
+    "COLOR_PICKER_ACTION_DISPLAY_SHIFT": "Click to pick colors from the canvas.",
+    "ELLIPSE_TOOL_TOOLTIP": "Draws an ellipse on canvas ({0}). Hold Shift to draw a circle.",
+    "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move mouse to draw an ellipse. Hold Shift to draw a circle.",
+    "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT": "Click and move mouse to draw a circle.",
+    "ERASER_TOOL_TOOLTIP": "Erases color from pixel. ({0})",
+    "ERASER_TOOL_ACTION_DISPLAY": "Click and move to erase.",
+    "FLOOD_FILL_TOOL_TOOLTIP": "Fills area with color. ({0})",
+    "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT": "Press on an area to fill it. Hold down Ctrl to consider all layers.",
+    "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL": "Press on an area to fill it. Release Ctrl to only consider the current layers.",
+    "LASSO_TOOL_TOOLTIP": "Lasso. ({0})",
+    "LASSO_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to select pixels inside of the lasso. Hold Shift to add to existing selection. Hold Ctrl to subtract from it.",
+    "LASSO_TOOL_ACTION_DISPLAY_SHIFT": "Click and move to add pixels inside of the lasso to the selection.",
+    "LASSO_TOOL_ACTION_DISPLAY_CTRL": "Click and move to subtract pixels inside of the lasso from the selection.",
+    "LINE_TOOL_TOOLTIP": "Draws line on canvas ({0}). Hold Shift to enable snapping.",
+    "LINE_TOOL_ACTION_DISPLAY_DEFAULT": "Click and move to draw a line. Hold Shift to enable snapping.",
+    "LINE_TOOL_ACTION_DISPLAY_SHIFT": "Click and move mouse to draw a line with snapping enabled.",
+    "MAGIC_WAND_TOOL_TOOLTIP": "Magic Wand ({0}). Flood's the selection",
+    "MAGIC_WAND_ACTION_DISPLAY": "Click to flood the selection.",
+    "PEN_TOOL": "Pen",
+    "BRIGHTNESS_TOOL": "Brightness",
+    "COLOR_PICKER_TOOL": "Color Picker",
+    "ELLIPSE_TOOL": "Ellipse",
+    "ERASER_TOOL": "Eraser",
+    "FLOOD_FILL_TOOL": "Flood Fill",
+    "LASSO_TOOL": "Lasso",
+    "LINE_TOOL": "Line",
+    "MAGIC_WAND_TOOL": "Magic Wand",
+    "MOVE_TOOL": "Move",
+    "MOVE_VIEWPORT_TOOL": "Move Viewport",
+    "RECTANGLE_TOOL": "Rectangle",
+    "ROTATE_VIEWPORT_TOOL": "Rotate Viewport",
+    "SELECT_TOOL_NAME": "Select",
+    "ZOOM_TOOL": "Zoom",
+    "SHAPE_LABEL": "Shape",
+    "MODE_LABEL": "Mode",
+    "SCOPE_LABEL": "Scope",
+    "FILL_SHAPE_LABEL": "Fill shape",
+    "FILL_COLOR_LABEL": "Fill color",
+    "TOOL_SIZE_LABEL": "Tool size",
+    "STRENGTH_LABEL": "Strength",
+    "NEW": "New",
+    "ADD": "Add",
+    "SUBTRACT": "Subtract",
+    "INTERSECT": "Intersect",
+    "RECTANGLE": "Rectangle",
+    "CIRCLE": "Circle",
+    "ABOUT": "About",
+    "MINIMIZE": "Minimize",
+    "RESTORE": "Restore",
+    "MAXIMIZE": "Maximize",
+    "CLOSE": "Close",
+    "EXPORT_SIZE_HINT": "If you want to share the image, try {0}% for the best clarity",
+    "CREATE": "Create",
+    "BASE_LAYER_NAME": "Base layer",
+    "ENABLE_MASK": "Enable mask",
+    "SELECTED_AREA_EMPTY": "Selected area is empty",
+    "NOTHING_TO_COPY": "Nothing to copy",
+    "REFERENCE_LAYER_PATH": "Reference layer path",
+    "FLIP": "Flip",
+    "ROTATION": "Rotation",
+    "ROT_IMG_90_D": "Rotate Image 90°",
+    "ROT_IMG_180_D": "Rotate Image 180°",
+    "ROT_IMG_-90_D": "Rotate Image -90°",
+    "ROT_LAYERS_90_D": "Rotate Selected Layers 90°",
+    "ROT_LAYERS_180_D": "Rotate Selected Layers 180°",
+    "ROT_LAYERS_-90_D": "Rotate Selected Layers -90°",
+    "UNNAMED_PALETTE": "Unnamed Palette",
+    "CLICK_SELECT_PRIMARY": "Click to select as main color.",
+    "PEN_MODE": "Pen mode",
+    "VIEW": "View",
+    "HORIZONTAL_LINE_SYMMETRY": "Horizontal line symmetry",
+    "VERTICAL_LINE_SYMMETRY": "Vertical line symmetry",
+    "COLOR_PICKER_TITLE": "Color Picker",
+    "COLOR_SLIDERS_TITLE": "Color Sliders",
+    "PALETTE_TITLE": "Palette",
+    "SWATCHES_TITLE": "Swatches",
+    "LAYERS_TITLE": "Layers",
+    "NAVIGATION_TITLE": "Navigation",
+    "NORMAL_BLEND_MODE": "Normal",
+    "DARKEN_BLEND_MODE": "Darken",
+    "MULTIPLY_BLEND_MODE": "Multiply",
+    "COLOR_BURN_BLEND_MODE": "Color burn",
+    "LIGHTEN_BLEND_MODE": "Lighten",
+    "SCREEN_BLEND_MODE": "Screen",
+    "COLOR_DODGE_BLEND_MODE": "Color dodge",
+    "OVERLAY_BLEND_MODE": "Overlay",
+    "SOFT_LIGHT_BLEND_MODE": "Soft light",
+    "HARD_LIGHT_BLEND_MODE": "Hard light",
+    "DIFFERENCE_BLEND_MODE": "Difference",
+    "EXCLUSION_BLEND_MODE": "Exclusion",
+    "HUE_BLEND_MODE": "Hue",
+    "SATURATION_BLEND_MODE": "Saturation",
+    "LUMINOSITY_BLEND_MODE": "Luminosity",
+    "COLOR_BLEND_MODE": "Color",
+    "NOT_SUPPORTED_BLEND_MODE": "Not supported",
+    "RESTART": "Restart",
+    "SORT_BY": "Sort by",
+    "NAME": "Name",
+    "COLORS": "Colors",
+    "DEFAULT": "Default",
+    "ALPHABETICAL": "Alphabetical",
+    "COLOR_COUNT": "Color count",
+    "ANY": "Any",
+    "MAX": "Max",
+    "MIN": "Min",
+    "EXACT": "Exact",
+    "ASCENDING": "Ascending",
+    "DESCENDING": "Descending",
+    "NAME_IS_TOO_LONG": "The name is too long",
+    "STOP_IT_TEXT1": "That's enough. Tidy up your file names.",
+    "STOP_IT_TEXT2": "Can you stop copying these names please?",
+    "REPLACER_TOOLTIP": "Right click on palette color and choose 'Replace' or drop it here.",
+    "CLICK_TO_CHOOSE_COLOR": "Click to choose the color",
+    "REPLACE_COLOR": "Replace color",
+    "PALETTE_COLOR_TOOLTIP": "Click to select as main color. Drag and drop onto another palette color to swap them.",
+    "ADD_FROM_SWATCHES": "Add from swatches",
+    "ADD_COLOR_TO_PALETTE": "Add color to palette",
+    "USE_IN_CURRENT_IMAGE": "Use in current image",
+    "ADD_TO_FAVORITES": "Add to favorites",
+    "BROWSE_PALETTES": "Browse palettes",
+    "LOAD_PALETTE": "Load palette",
+    "SAVE_PALETTE": "Save palette",
+    "DISCARD_PALETTE": "Discard palette",
+    "DISCARD_PALETTE_CONFIRMATION": "Are you sure you want to discard current palette? This cannot be undone.",
+    "FAVORITES": "Favorites",
+    "ADD_FROM_CURRENT_PALETTE": "Add from current palette",
+    "OPEN_PALETTES_DIR_TOOLTIP": "Open palettes directory in explorer",
+    "BROWSE_ON_LOSPEC_TOOLTIP": "Browse palettes on Lospec",
+    "IMPORT_FROM_FILE_TOOLTIP": "Import from file",
+    "TOP_LEFT": "Top left",
+    "TOP_CENTER": "Top center",
+    "TOP_RIGHT": "Top right",
+    "MIDDLE_LEFT": "Middle left",
+    "MIDDLE_CENTER": "Middle center",
+    "MIDDLE_RIGHT": "Middle right",
+    "BOTTOM_LEFT": "Bottom left",
+    "BOTTOM_CENTER": "Bottom center",
+    "BOTTOM_RIGHT": "Bottom right",
+    "CLIP_TO_BELOW": "Clip to layer below",
+    "MOVE_UPWARDS": "Move upwards",
+    "MOVE_DOWNWARDS": "Move downwards",
+    "MERGE_SELECTED": "Merge selected",
+    "LOCK_TRANSPARENCY": "Lock transparency",
+    "COULD_NOT_LOAD_PALETTE": "Couldn't fetch palettes",
+    "NO_PALETTES_FOUND": "No palettes found.",
+    "LOSPEC_LINK_TEXT": "I heard you can find some here: lospec.com/palette-list",
+    "PALETTE_BROWSER": "Palette Browser",
+    "DELETE_PALETTE_CONFIRMATION": "Are you sure you want to delete this palette? This cannot be undone.",
+    "SHORTCUTS_IMPORTED": "Shortcuts from {0} were imported successfully.",
+    "SHORTCUT_PROVIDER_DETECTED": "We've detected, that you have {0} installed. Do you want to import shortcuts from it?",
+    "IMPORT_FROM_INSTALLATION": "Import from installation",
+    "IMPORT_INSTALLATION_OPTION1": "Import from installation",
+    "IMPORT_INSTALLATION_OPTION2": "Use defaults",
+    "IMPORT_FROM_TEMPLATE": "Import from template",
+    "SHORTCUTS_IMPORTED_SUCCESS": "Shortcuts were imported successfully.",
+    "WARNING_RESET_SHORTCUTS_DEFAULT": "Are you sure you want to reset all shortcuts to their default value?",
+    "SUCCESS": "Success",
+    "WARNING": "Warning",
+    "ERROR_IMPORTING_IMAGE": "An error occured while importing the image.",
+    "SHORTCUTS_CORRUPTED_TITLE": "Corrupted shortcuts file",
+    "SHORTCUTS_CORRUPTED": "Shortcuts file was corrupted, resetting to default.",
+    "FAILED_DOWNLOAD_PALETTE": "Failed to download palette",
+    "FILE_INCORRECT_FORMAT": "The file was not in a correct format",
+    "INVALID_FILE": "Invalid file",
+    "SHORTCUTS_FILE_INCORRECT_FORMAT": "Shortcuts file was not in a correct format",
+    "UNSUPPORTED_FILE_FORMAT": "This file format is unsupported",
+    "ALREADY_ASSIGNED": "Already assigned",
+    "REPLACE": "Replace",
+    "SWAP": "Swap",
+    "SHORTCUT_ALREADY_ASSIGNED_SWAP": "This shortcut is already assigned to '{0}'\nDo you want to replace the existing shortcut or swap the two?",
+    "SHORTCUT_ALREADY_ASSIGNED_OVERWRITE": "This shortcut is already assigned to '{0}'\nDo you want to replace the existing shortcut?",
+    "UNSAVED_CHANGES": "Unsaved changes",
+    "DOCUMENT_MODIFIED_SAVE": "The document has been modified. Do you want to save changes?",
+    "SESSION_UNSAVED_DATA": "{0} with unsaved data. Are you sure?",
+    "PROJECT_MAINTAINERS": "Project Maintainers",
+    "OTHER_AWESOME_CONTRIBUTORS": "And other awesome contributors",
+    "HELP": "Help",
+    "STOP_IT_TEXT3": "No, really, stop it.",
+    "STOP_IT_TEXT4": "Don't you have anything better to do?",
+    "LINEAR_DODGE_BLEND_MODE": "Linear dodge (Add)",
+    "PRESS_ANY_KEY": "Press any key",
+    "NONE_SHORTCUT": "None",
+    "REFERENCE": "Reference",
+    "PUT_REFERENCE_LAYER_ABOVE": "Put reference layer above",
+    "PUT_REFERENCE_LAYER_BELOW": "Put reference layer below",
+    "TOGGLE_VERTICAL_SYMMETRY": "Toggle vertical symmetry",
+    "TOGGLE_HORIZONTAL_SYMMETRY": "Toggle horizontal symmetry",
+    "RESET_VIEWPORT": "Reset viewport",
+    "VIEWPORT_SETTINGS": "Viewport settings",
+    "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "Click and hold mouse to move pixels in selected layers.",
+    "MOVE_TOOL_ACTION_DISPLAY_CTRL": "Hold mouse to move all layers.",
+    "CTRL_KEY": "Ctrl",
+    "SHIFT_KEY": "Shift",
+    "ALT_KEY": "Alt",
+    "RENAME": "Rename",
+    "PIXEL_UNIT": "px",
+    "OPEN_LOCALIZATION_DEBUG_WINDOW": "Open Localization Debug Window",
+    "FORCE_OTHER_FLOW_DIRECTION": "Force other flow direction",
+    "API_KEY": "API Key",
+    "LOCALIZATION_VIEW_TYPE": "Localization View Type",
+    "LOAD_LANGUAGE_FROM_FILE": "Load language from file",
+    "LOG_IN": "Log in",
+    "SYNC": "Sync",
+    "NOT_LOGGED_IN": "Not logged in",
+    "POE_EDITOR_ERROR": "POEditor Error: {0} {1}",
+    "HTTP_ERROR_MESSAGE": "HTTP Error: {0} {1}",
+    "LOGGED_IN": "Logged in",
+    "SYNCED_SUCCESSFULLY": "Synced successfully",
+    "EXCEPTION_ERROR": "Exception: {0}",
+    "DROP_PALETTE": "Drop palette here",
+    "SECURITY_ERROR": "Security error",
+    "SECURITY_ERROR_MSG": "No rights to write to the specified location.",
+    "IO_ERROR": "IO error",
+    "IO_ERROR_MSG": "Error while writing to disk.",
+    "FAILED_ASSOCIATE_PIXI": "Failed to associate .pixi file with PixiEditor.",
+    "COULD_NOT_SAVE_PALETTE": "There was an error while saving the palette.",
+    "NO_COLORS_TO_SAVE": "There are no colors to save.",
+    "ALL_LAYERS": "All Layers",
+    "SINGLE_LAYER": "Single Layer",
+    "CHOOSE": "Choose",
+    "REMOVE": "Remove"
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/es.json

@@ -0,0 +1,2 @@
+{
+}

+ 504 - 0
src/PixiEditor/Data/Localization/Languages/pl.json

@@ -0,0 +1,504 @@
+{
+  "RECENT_FILES": "Ostatnie pliki",
+  "OPEN_FILE": "Otwórz plik",
+  "NEW_FILE": "Nowy",
+  "RECENT_EMPTY_TEXT": "Ale tu pusto",
+  "LANGUAGE": "Język",
+  "GENERAL": "Ogólne",
+  "DISCORD": "Discord",
+  "KEY_BINDINGS": "Skróty klawiszowe",
+  "MISC": "Inne",
+  "SHOW_STARTUP_WINDOW": "Otwórz okno powitalne na starcie",
+  "SHOW_IMAGE_PREVIEW_TASKBAR": "Pokaż podgląd obrazu w pasku zadań",
+  "RECENT_FILE_LENGTH": "Ilość ostatnich plików",
+  "RECENT_FILE_LENGTH_TOOLTIP": "Ile plików jest wyświetlanych w menu Plik -> Ostatnie. Domyślnie 8",
+  "DEFAULT_NEW_SIZE": "Domyslny rozmiar nowego obrazu",
+  "WIDTH": "Szerokość",
+  "HEIGHT": "Wysokość",
+  "TOOLS": "Narzędzia",
+  "ENABLE_SHARED_TOOLBAR": "Włącz wspólny pasek narzędzi",
+  "AUTOMATIC_UPDATES": "Automatyczne aktualizacje",
+  "CHECK_FOR_UPDATES": "Sprawdzaj aktualizacje na starcie",
+  "UPDATE_STREAM": "Kanał aktualizacji",
+  "UPDATE_CHANNEL_HELP_TOOLTIP": "Kanały aktualizacji mogą być tylko ustawione w wersji samodzielnej (pobranej z https://pixieditor.net).\nSteam i Microsoft Store zajmuje się aktualizacjami osobno.",
+  "DEBUG": "Debugowanie",
+  "ENABLE_DEBUG_MODE": "Włącz tryb debugowania",
+  "OPEN_CRASH_REPORTS_DIR": "Otwórz folder z raportami o awariach",
+  "DISCORD_RICH_PRESENCE": "Rich Presence",
+  "ENABLED": "Włączony",
+  "SHOW_IMAGE_NAME": "Pokaż nazwę obrazu",
+  "SHOW_IMAGE_SIZE": "Pokaż wielkość obrazu",
+  "SHOW_LAYER_COUNT": "Pokaż ilość warstw",
+  "FILE": "Plik",
+  "RECENT": "Ostatnie",
+  "OPEN": "Otwórz",
+  "SAVE_PIXI": "Zapisz (.pixi)",
+  "SAVE_AS_PIXI": "Zapisz jako... (.pixi)",
+  "EXPORT_IMG": "Eksportuj (.png, .jpg, itp.)",
+  "EDIT": "Edytuj",
+  "EXIT": "Wyjdź",
+  "PERCENTAGE": "Procentowo",
+  "ABSOLUTE": "Absolutnie",
+  "PRESERVE_ASPECT_RATIO": "Zachowaj proporcje",
+  "ANCHOR_POINT": "Punkt zaczepienia",
+  "RESIZE_IMAGE": "Zmień rozmiar obrazu",
+  "RESIZE": "Zmień rozmiar",
+  "EDITING_IMG": "Edytuje obraz",
+  "EDITING_IMG_DETAIL": "Edytuje {0}",
+  "ONE_LAYER": "1 warstwa",
+  "LAYERS": "{0} warstw",
+  "DOCUMENTATION": "Dokumentacja",
+  "WEBSITE": "Strona internetowa",
+  "OPEN_WEBSITE": "Otwórz stronę internetową",
+  "REPOSITORY": "Repozytorium",
+  "OPEN_REPOSITORY": "Otwórz repozytorium",
+  "LICENSE": "Licencja",
+  "OPEN_LICENSE": "Otwórz licencję",
+  "THIRD_PARTY_LICENSES": "Licencje stron trzecich",
+  "OPEN_THIRD_PARTY_LICENSES": "Otwórz licencje stron trzecich",
+  "APPLY_TRANSFORM": "Zastosuj transformację",
+  "INCREASE_TOOL_SIZE": "Zwiększ rozmiar narzędzia",
+  "DECREASE_TOOL_SIZE": "Zmniejsz rozmiar narzędzia",
+  "TO_INSTALL_UPDATE": "aby zainstalować aktualizacje {0}",
+  "DOWNLOADING_UPDATE": "Pobieranie aktualizacji...",
+  "UPDATE_READY": "Aktualizacja jest gotowa do instalacji. Czy chcesz ją zainstalować teraz?",
+  "NEW_UPDATE": "Nowa aktualizacja",
+  "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Nie można zaaktualizować bez uprawnien administratora. Włącz PixiEditor jako administrator.",
+  "INSUFFICIENT_PERMISSIONS": "Niewystarczające uprawnienia",
+  "UPDATE_CHECK_FAILED": "Sprawdzanie aktualizacji nie powiodło się",
+  "COULD_NOT_CHECK_FOR_UPDATES": "Nie udało się sprawdzić aktualizacji",
+  "VERSION": "Wersja {0}",
+  "OPEN_TEMP_DIR": "Otwórz folder temp",
+  "OPEN_LOCAL_APPDATA_DIR": "Otwórz folder Local AppData",
+  "OPEN_ROAMING_APPDATA_DIR": "Otwórz folder Roaming AppData",
+  "OPEN_INSTALLATION_DIR": "Otwórz folder instalacji",
+  "DUMP_ALL_COMMANDS": "Wypisz wszystkie komendy",
+  "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Wypisz wszystkie komendy do pliku",
+  "CRASH": "Crash",
+  "CRASH_APP": "Zcrashuj aplikację",
+  "DELETE_USR_PREFS": "Usuń preferencje użytkownika (Roaming AppData)",
+  "DELETE_SHORTCUT_FILE": "Usuń plik skrótów klawiszowych (Roaming AppData)",
+  "DELETE_EDITOR_DATA": "Usuń dane edytora (Local AppData)",
+  "GENERATE_KEY_BINDINGS_TEMPLATE": "Stwórz szablon skrótów klawiszowych",
+  "GENERATE_KEY_BINDINGS_TEMPLATE_DESCRIPTIVE": "Stwórz szablon JSON skrótów klawiszowych",
+  "VALIDATE_SHORTCUT_MAP": "Sprawdź poprawność mapy skrótów",
+  "VALIDATE_SHORTCUT_MAP_DESCRIPTIVE": "Sprawdza poprawność mapy skrótów",
+  "VALIDATION_KEYS_NOTICE_DIALOG": "Puste klucze: {0}\nNieznane komendy: {1}",
+  "RESULT": "Wynik",
+  "CLEAR_RECENT_DOCUMENTS": "Wyczyść ostatnie dokumenty",
+  "CLEAR_RECENTLY_OPENED_DOCUMENTS": "Wyczyść ostatnio otwarte dokumenty",
+  "OPEN_CMD_DEBUG_WINDOW": "Otwórz okno do debugowania komend",
+  "PATH_DOES_NOT_EXIST": "{0} nie istnieje.",
+  "LOCATION_DOES_NOT_EXIST": "Lokalizacja nie istnieje.",
+  "FILE_NOT_FOUND": "Plik nie znaleziony.",
+  "FILE_NOT_FOUND_PATH_FULL_PATH": "Plik {0} nie istnieje\n(Pełna ścieżka: {1})",
+  "ARE_YOU_SURE": "Jesteś pewien?",
+  "ARE_YOU_SURE_PATH_FULL_PATH": "Czy na pewno chcesz usunąć {0}?\nDane zostaną utracone dla wszystkich instalacji.\n(Pełna ścieżka: {1})",
+  "FAILED_TO_OPEN_FILE": "Nie udało się otworzyć pliku",
+  "OLD_FILE_FORMAT": "Stary format pliku",
+  "OLD_FILE_FORMAT_DESCRIPTION": "Ten plik .pixi używa starego formatu, który nie jest już wspierany i nie można go otworzyć.",
+  "NOTHING_FOUND": "Nic nie znaleziono",
+  "EXPORT": "Eksportuj",
+  "EXPORT_IMAGE": "Eksportuj obraz",
+  "IMPORT": "Importuj",
+  "SHORTCUT_TEMPLATES": "Szablony",
+  "RESET_ALL": "Resetuj wszystko",
+  "LAYER": "Warstwa",
+  "LAYER_DELETE_SELECTED": "Usuń aktywną warstwę/folder",
+  "LAYER_DELETE_SELECTED_DESCRIPTIVE": "Usuń aktywną warstwę lub folder",
+  "LAYER_DELETE_ALL_SELECTED": "Usuń wszystkie zaznaczone warstwy/foldery",
+  "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Usuń wszystkie zaznaczone warstwy i/lub foldery",
+  "DELETE_SELECTED_PIXELS": "Usuń zaznaczone piksele",
+  "NEW_FOLDER": "Nowy folder",
+  "CREATE_NEW_FOLDER": "Stwórz nowy folder",
+  "NEW_LAYER": "Nowa warstwa",
+  "CREATE_NEW_LAYER": "Stwórz nową warstwę",
+  "NEW_IMAGE": "Nowy obraz",
+  "CREATE_NEW_IMAGE": "Stwórz nowy obraz",
+  "SAVE": "Zapisz",
+  "SAVE_AS": "Zapisz jako...",
+  "IMAGE": "Obraz",
+  "SAVE_IMAGE": "Zapisz obraz",
+  "SAVE_IMAGE_AS": "Zapisz obraz jako",
+  "DUPLICATE": "Duplikuj",
+  "DUPLICATE_SELECTED_LAYER": "Duplikuj zaznaczoną warstwę",
+  "CREATE_MASK": "Stwórz maskę",
+  "DELETE_MASK": "Usuń maskę",
+  "TOGGLE_MASK": "Przełącz maskę",
+  "APPLY_MASK": "Zastosuj maskę",
+  "TOGGLE_VISIBILITY": "Przełącz widoczność",
+  "MOVE_MEMBER_UP": "Przesuń obiekt wyżej",
+  "MOVE_MEMBER_UP_DESCRIPTIVE": "Przesuń zaznaczoną warstwę lub folder wyżej",
+  "MOVE_MEMBER_DOWN": "Przesuń obiekt niżej",
+  "MOVE_MEMBER_DOWN_DESCRIPTIVE": "Przesuń zaznaczoną warstwę lub folder niżej",
+  "MERGE_ALL_SELECTED_LAYERS": "Scal wszystkie warstwy",
+  "MERGE_WITH_ABOVE": "Scal zaznaczoną warstwę z obiektem wyżej",
+  "MERGE_WITH_ABOVE_DESCRIPTIVE": "Scal zaznaczoną warstwę z warstwą wyżej",
+  "MERGE_WITH_BELOW": "Scal zaznaczoną warstwę z obiektem niżej",
+  "MERGE_WITH_BELOW_DESCRIPTIVE": "Scal zaznaczoną warstwę z warstwą niżej",
+  "ADD_REFERENCE_LAYER": "Dodaj warstwę referencyjną",
+  "DELETE_REFERENCE_LAYER": "Usuń warstwę referencyjną",
+  "TRANSFORM_REFERENCE_LAYER": "Modyfikuj warstwę referencyjną",
+  "TOGGLE_REFERENCE_LAYER_POS": "Przełącz pozycję warstwy referencyjnej",
+  "TOGGLE_REFERENCE_LAYER_POS_DESCRIPTIVE": "Przełącz warstwę referencyjną pomiędzy górą a dołem",
+  "RESET_REFERENCE_LAYER_POS": "Resetuj pozycję warstwy referencyjnej",
+  "CLIP_CANVAS": "Dopasuj płótno do zawartości",
+  "FLIP_IMG_VERTICALLY": "Przerzuć obraz w pionie",
+  "FLIP_IMG_HORIZONTALLY": "Przerzuć obraz w poziomie",
+  "FLIP_LAYERS_VERTICALLY": "Przerzuć zaznaczone warstwy w pionie",
+  "FLIP_LAYERS_HORIZONTALLY": "Przerzuć zaznaczone warstwy w poziomie",
+  "ROT_IMG_90": "Obróć obraz 90 stopni",
+  "ROT_IMG_180": "Obróć obraz 180 stopni",
+  "ROT_IMG_-90": "Obróć obraz -90 stopni",
+  "ROT_LAYERS_90": "Obróć zaznaczone warstwy 90 stopni",
+  "ROT_LAYERS_180": "Obróć zaznaczone warstwy 180 stopni",
+  "ROT_LAYERS_-90": "Obróć zaznaczone warstwy -90 stopni",
+  "TOGGLE_VERT_SYMMETRY_AXIS": "Przełącz oś symetrii w pionie",
+  "TOGGLE_HOR_SYMMETRY_AXIS": "Przełącz oś symetrii w poziomie",
+  "DELETE_PIXELS": "Usuń piksele",
+  "DELETE_PIXELS_DESCRIPTIVE": "Usuń zaznaczone piksele",
+  "RESIZE_DOCUMENT": "Zmień rozmiar dokumentu",
+  "RESIZE_CANVAS": "Zmien rozmiar płótna",
+  "CENTER_CONTENT": "Wyśrodkuj zawartość",
+  "CUT": "Wytnij",
+  "CUT_DESCRIPTIVE": "Wytnij zaznaczony obszar/warstwę",
+  "PASTE": "Wklej",
+  "PASTE_DESCRIPTIVE": "Wklej zawartość schowka",
+  "PASTE_AS_NEW_LAYER": "Wklej jako nowa warstwa",
+  "PASTE_AS_NEW_LAYER_DESCRIPTIVE": "Wklej ze schowka jako nowa warstwa",
+  "PASTE_REFERENCE_LAYER": "Wklej warstwę referencyjną",
+  "PASTE_REFERENCE_LAYER_DESCRIPTIVE": "Wklej zawartość schowka jako warstwa referencyjna",
+  "PASTE_COLOR": "Wklej kolor",
+  "PASTE_COLOR_DESCRIPTIVE": "Wklej kolor ze schowka",
+  "PASTE_COLOR_SECONDARY": "Wklej kolor jako drugorzędny",
+  "PASTE_COLOR_SECONDARY_DESCRIPTIVE": "Wklej kolor ze schowka jako drugorzędny",
+  "CLIPBOARD": "Schowek",
+  "COPY": "Kopiuj",
+  "COPY_DESCRIPTIVE": "Kopiuj do schowka",
+  "COPY_COLOR_HEX": "Kopiuj pierwszorzędny kolor (HEX)",
+  "COPY_COLOR_HEX_DESCRIPTIVE": "Kopiuj pierwszorzędny kolor jako kod HEX",
+  "COPY_COLOR_RGB": "Kopiuj pierwszorzędny kolor (RGB)",
+  "COPY_COLOR_RGB_DESCRIPTIVE": "Kopiuj pierwszorzędny kolor jako kod RGB",
+  "COPY_COLOR_SECONDARY_HEX": "Kopiuj drugorzędny kolor (HEX)",
+  "COPY_COLOR_SECONDARY_HEX_DESCRIPTIVE": "Kopiuj drugorzędny kolor jako kod HEX",
+  "COPY_COLOR_SECONDARY_RGB": "Kopiuj drugorzędny kolor (RGB)",
+  "COPY_COLOR_SECONDARY_RGB_DESCRIPTIVE": "Kopiuj drugorzędny kolor jako kod RGB",
+  "PALETTE_COLORS": "Paleta Kolorów",
+  "REPLACE_SECONDARY_BY_PRIMARY": "Zastąp drugorzędny kolor pierwszorzędnym",
+  "REPLACE_SECONDARY_BY_PRIMARY_DESCRIPTIVE": "Zastąp drugorzędny kolor pierwszorzędnym",
+  "REPLACE_PRIMARY_BY_SECONDARY": "Zastąp pierwszorzędny kolor drugorzędnym",
+  "REPLACE_PRIMARY_BY_SECONDARY_DESCRIPTIVE": "Zastąp pierwszorzędny kolor drugorzędnym",
+  "OPEN_PALETTE_BROWSER": "Otwórz przeglądarkę palet",
+  "OVERWRITE_PALETTE_CONSENT": "Paleta '{0}' już istnieje, czy chcesz ją nadpisać?",
+  "PALETTE_EXISTS": "Paleta już istnieje",
+  "REPLACE_PALETTE_CONSENT": "Zastąpić aktywną paletę zaznaczoną?",
+  "REPLACE_PALETTE": "Zastąp aktywną paletę",
+  "SELECT_COLOR_1": "Wybierz kolor 1",
+  "SELECT_COLOR_2": "Wybierz kolor 2",
+  "SELECT_COLOR_3": "Wybierz kolor 3",
+  "SELECT_COLOR_4": "Wybierz kolor 4",
+  "SELECT_COLOR_5": "Wybierz kolor 5",
+  "SELECT_COLOR_6": "Wybierz kolor 6",
+  "SELECT_COLOR_7": "Wybierz kolor 7",
+  "SELECT_COLOR_8": "Wybierz kolor 8",
+  "SELECT_COLOR_9": "Wybierz kolor 9",
+  "SELECT_COLOR_10": "Wybierz kolor 10",
+  "SELECT_TOOL": "Zaznacz narzędzie {0}",
+  "SELECT_COLOR_1_DESCRIPTIVE": "Zaznacz pierwszy kolor w palecie",
+  "SELECT_COLOR_2_DESCRIPTIVE": "Zaznacz drugi kolor w palecie",
+  "SELECT_COLOR_3_DESCRIPTIVE": "Zaznacz trzeci kolor w palecie",
+  "SELECT_COLOR_4_DESCRIPTIVE": "Zaznacz czwarty kolor w palecie",
+  "SELECT_COLOR_5_DESCRIPTIVE": "Zaznacz piąty kolor w palecie",
+  "SELECT_COLOR_6_DESCRIPTIVE": "Zaznacz szósty kolor w palecie",
+  "SELECT_COLOR_7_DESCRIPTIVE": "Zaznacz siódmy kolor w palecie",
+  "SELECT_COLOR_8_DESCRIPTIVE": "Zaznacz ósmy kolor w palecie",
+  "SELECT_COLOR_9_DESCRIPTIVE": "Zaznacz dziewiąty kolor w palecie",
+  "SELECT_COLOR_10_DESCRIPTIVE": "Zaznacz dziesiąty kolor w palecie",
+  "SWAP_COLORS": "Zamień kolory",
+  "SWAP_COLORS_DESCRIPTIVE": "Zamień pierwszorzędny kolor z drugorzędnym",
+  "SEARCH": "Szukaj",
+  "COMMAND_SEARCH": "Wyszukiwanie komend",
+  "OPEN_COMMAND_SEARCH": "Otwórz okno wyszukiwania komend",
+  "SELECT": "Zaznacz",
+  "DESELECT": "Odznacz",
+  "INVERT": "Odwróć",
+  "SELECTION": "Zaznaczenie",
+  "SELECT_ALL": "Zaznacz wszystko",
+  "SELECT_ALL_DESCRIPTIVE": "Zaznacz wszystko",
+  "CLEAR_SELECTION": "Wyczyść zaznaczenie",
+  "INVERT_SELECTION": "Odwróć zaznaczenie",
+  "INVERT_SELECTION_DESCRIPTIVE": "Odwróć zaznaczony obszar",
+  "TRANSFORM_SELECTED_AREA": "Transformuj zaznaczony obszar",
+  "NUDGE_SELECTED_LEFT": "Pstryknij zaznaczony obiekt w lewo",
+  "NUDGE_SELECTED_RIGHT": "Pstryknij zaznaczony obiekt w prawo",
+  "NUDGE_SELECTED_UP": "Pstryknij zaznaczony obiekt w górę",
+  "NUDGE_SELECTED_DOWN": "Pstryknij zaznaczony obiekt w dół",
+  "MASK_FROM_SELECTION": "Nowa maska z zaznaczenia",
+  "MASK_FROM_SELECTION_DESCRIPTIVE": "Zrób nową maskę z zaznaczenia",
+  "ADD_SELECTION_TO_MASK": "Dodaj zaznaczenie do maski",
+  "SUBTRACT_SELECTION_FROM_MASK": "Odejmij zaznaczenie z maski",
+  "INTERSECT_SELECTION_MASK": "Wykonaj intersekcję zaznaczenia z maską",
+  "SELECTION_TO_MASK": "Zaznaczenie do maski",
+  "TO_NEW_MASK": "do nowej maski",
+  "ADD_TO_MASK": "dodaj do maski",
+  "SUBTRACT_FROM_MASK": "odejmij z maski",
+  "INTERSECT_WITH_MASK": "wykonaj intersekcję z maską",
+  "STYLUS": "Rysik",
+  "TOGGLE_PEN_MODE": "Przełącz tryb długopisu",
+  "UNDO": "Cofnij",
+  "UNDO_DESCRIPTIVE": "Cofnij ostatnią akcję",
+  "REDO": "Ponów",
+  "REDO_DESCRIPTIVE": "Ponów ostatnią akcję",
+  "WINDOWS": "Okna",
+  "TOGGLE_GRIDLINES": "Przełącz linie siatki",
+  "ZOOM_IN": "Przybliż",
+  "ZOOM_OUT": "Oddal",
+  "NEW_WINDOW_FOR_IMG": "Nowe okno dla aktywnego obrazu",
+  "CENTER_ACTIVE_VIEWPORT": "Wyśrodkuj aktywny widok",
+  "FLIP_VIEWPORT_HORIZONTALLY": "Przerzuć widok w poziomie",
+  "FLIP_VIEWPORT_VERTICALLY": "Przerzuć widok w pionie",
+  "SETTINGS": "Ustawienia",
+  "OPEN_SETTINGS": "Otwórz ustawienia",
+  "OPEN_SETTINGS_DESCRIPTIVE": "Otwórz okno ustawień",
+  "OPEN_STARTUP_WINDOW": "Otwórz okno startowe",
+  "OPEN_SHORTCUT_WINDOW": "Otwórz okno ze skrótami klawiszowymi",
+  "OPEN_ABOUT_WINDOW": "Otwórz okno informacji",
+  "OPEN_NAVIGATION_WINDOW": "Otwórz okno nawigacji",
+  "ERROR": "Błąd",
+  "INTERNAL_ERROR": "Wewnętrzny błąd",
+  "ERROR_SAVE_LOCATION": "Nie udało się zapisać pliku do wskazanej lokalizacji",
+  "ERROR_WHILE_SAVING": "Wystąpił wewnętrzny błąd podczas zapisu. Spróbuj ponownie.",
+  "UNKNOWN_ERROR_SAVING": "Wystąpił błąd poczas zapisu.",
+  "FAILED_ASSOCIATE_LOSPEC": "Nie udało się powiązać protokołu palet serwisu Lospec.",
+  "REDDIT": "Reddit",
+  "GITHUB": "GitHub",
+  "YOUTUBE": "YouTube",
+  "DONATE": "Dotacja",
+  "YES": "Tak",
+  "NO": "Nie",
+  "CANCEL": "Anuluj",
+  "UNNAMED": "Bez nazwy",
+  "OPEN_COMMAND_DEBUG_WINDOW": "Otwórz okno debugowania komend",
+  "DELETE": "Usuń",
+  "USER_PREFS": "Preferencje użytkownika (Roaming)",
+  "SHORTCUT_FILE": "Plik skrótów klawiszowych (Roaming)",
+  "EDITOR_DATA": "Dane edytora (Local)",
+  "MOVE_VIEWPORT_TOOLTIP": "Przesuwa widok. ({0})",
+  "MOVE_VIEWPORT_ACTION_DISPLAY": "Kliknij i porusz myszką, aby przesunąć widok",
+  "MOVE_TOOL_TOOLTIP": "Przesuwa zaznaczone piksele ({0}). Przytrzymaj Ctrl aby ruszyć wszystkimi warstwami.",
+  "MOVE_TOOL_ACTION_DISPLAY": "Przytrzymaj myszkę, aby poruszyć zaznaczone piksele. Przytrzymaj Ctrl aby ruszyć wszystkimi warstwami.",
+  "PEN_TOOL_TOOLTIP": "Pióro. ({0})",
+  "PEN_TOOL_ACTION_DISPLAY": "Kliknij i porusz aby rysować.",
+  "PIXEL_PERFECT_SETTING": "Pixel perfect",
+  "RECTANGLE_TOOL_TOOLTIP": "Tworzy prostokąt na płótnie ({0}). Przytrzymaj Shift, żeby narysować kwadrat.",
+  "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT": "Tworzy prostokąt na płótnie. Przytrzymaj Shift, żeby narysować kwadrat.",
+  "RECTANGLE_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz aby narysować kwadrat.",
+  "KEEP_ORIGINAL_IMAGE_SETTING": "Zachowaj oryginalny obraz",
+  "ROTATE_VIEWPORT_TOOLTIP": "Obraca widok. ({0})",
+  "ROTATE_VIEWPORT_ACTION_DISPLAY": "Kliknij i porusz aby obrócić widok",
+  "SELECT_TOOL_TOOLTIP": "Zaznacza obszar. ({0})",
+  "SELECT_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz aby zaznaczyć obszar. Przytrzymaj Shift aby dodać do istniejącego zaznaczenia. Przytrzymaj Ctrl aby usunąć z niego.",
+  "SELECT_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz aby dodać do aktywnego zaznaczenia.",
+  "SELECT_TOOL_ACTION_DISPLAY_CTRL": "Kliknij i porusz myszką aby usunąć z aktywnego zaznaczenia.",
+  "ZOOM_TOOL_TOOLTIP": "Przybliża lub oddala widok ({0}). Kliknij aby przybliżyć, przytrzymaj ctrl i kliknij aby oddalić.",
+  "ZOOM_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką, aby zmienić oddalenie. Kliknij aby przybliżyć, przytrzymaj ctrl i kliknij aby oddalić.",
+  "ZOOM_TOOL_ACTION_DISPLAY_CTRL": "Kliknij i porusz myszką aby zmienić oddalenie. Kliknij aby oddalić, puść ctrl i kliknij aby przybliżyć.",
+  "BRIGHTNESS_TOOL_TOOLTIP": "Rozjaśnia lub przyciemnia piksele ({0}). Przytrzymaj Ctrl aby przyciemniać.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_DEFAULT": "Rysuj po pikselach aby je rozjaśnić. Przytrzymaj Ctrl aby przyciemnić.",
+  "BRIGHTNESS_TOOL_ACTION_DISPLAY_CTRL": "Rysuj po pikselach aby je przyciemnić. Puść Ctrl aby rozjaśnić.",
+  "COLOR_PICKER_TOOLTIP": "Pobiera kolor z płótna i ustawia go jako pierwszorzędny. ({0})",
+  "COLOR_PICKER_ACTION_DISPLAY_DEFAULT": "Kliknij aby pobrać kolor. Przytrzymaj Ctrl aby ukryć płótno. Przytrzymaj Shift aby ukryć warstwę referencyjną.",
+  "COLOR_PICKER_ACTION_DISPLAY_CTRL": "Kliknij aby pobrać kolory z warstwy referencyjnej.",
+  "COLOR_PICKER_ACTION_DISPLAY_SHIFT": "Kliknij aby pobrać kolory z płótna.",
+  "ELLIPSE_TOOL_TOOLTIP": "Rysuje elipsę na płótnie ({0}). Przytrzymaj Shift aby narysować koło.",
+  "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką aby narysować elipsę. Przytrzymaj Shift aby narysować koło.",
+  "ELLIPSE_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz myszką aby narysować koło.",
+  "ERASER_TOOL_TOOLTIP": "Wymazuje piksele z płótna. ({0})",
+  "ERASER_TOOL_ACTION_DISPLAY": "Kliknij i porusz myszką aby zmazywać.",
+  "FLOOD_FILL_TOOL_TOOLTIP": "Zapełnia obszar kolorem. ({0})",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij na obszar, aby go zapełnić. Przytrzymaj Ctrl aby zapełniać z uwzględnieniem wszystkich warstw.",
+  "FLOOD_FILL_TOOL_ACTION_DISPLAY_CTRL": "Kliknij na obszar aby go zapełnić. Puść Ctrl żeby aby zapełniać tylko z uwzględnieniem aktywnej warstwy.",
+  "LASSO_TOOL_TOOLTIP": "Lasso. ({0})",
+  "LASSO_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką aby zaznaczyć piksele wewnątrz lassa. Przytrzymaj Shift aby dodać do istniejącego zaznaczenia. Przytrzymaj Ctrl aby usunąć z niego.",
+  "LASSO_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz myszką aby dodać piksele wewnątrz lassa do zaznaczenia.",
+  "LASSO_TOOL_ACTION_DISPLAY_CTRL": "Kliknij i porusz myszką, aby usunąć piksele wewnątrz lassa z aktywnego zaznaczenia.",
+  "LINE_TOOL_TOOLTIP": "Rysuje linie na płótnie ({0}). Przytrzymaj Shift aby rysować równe linie.",
+  "LINE_TOOL_ACTION_DISPLAY_DEFAULT": "Kliknij i porusz myszką aby narysować linie. Przytrzymaj Shify aby rysować równe linie.",
+  "LINE_TOOL_ACTION_DISPLAY_SHIFT": "Kliknij i porusz myszką aby rysować równe linie.",
+  "MAGIC_WAND_TOOL_TOOLTIP": "Magiczna Różdżka ({0}). Zapełnia obszar zaznaczeniem",
+  "MAGIC_WAND_ACTION_DISPLAY": "Kliknij aby stworzyć zaznaczenie z obszaru.",
+  "PEN_TOOL": "Pióro",
+  "BRIGHTNESS_TOOL": "Rozświetlenie",
+  "COLOR_PICKER_TOOL": "Próbnik Kolorów",
+  "ELLIPSE_TOOL": "Elipsa",
+  "ERASER_TOOL": "Gumka",
+  "FLOOD_FILL_TOOL": "Wiadro",
+  "LASSO_TOOL": "Lasso",
+  "LINE_TOOL": "Linia",
+  "MAGIC_WAND_TOOL": "Magiczna Różdżka",
+  "MOVE_TOOL": "Przesuwanie",
+  "MOVE_VIEWPORT_TOOL": "Przesuwanie widoku",
+  "RECTANGLE_TOOL": "Prostokąt",
+  "ROTATE_VIEWPORT_TOOL": "Obracanie widoku",
+  "SELECT_TOOL_NAME": "Zaznaczenie",
+  "ZOOM_TOOL": "Powiększenie",
+  "SHAPE_LABEL": "Kształt",
+  "MODE_LABEL": "Tryb",
+  "SCOPE_LABEL": "Zakres",
+  "FILL_SHAPE_LABEL": "Zapełnij kształt",
+  "FILL_COLOR_LABEL": "Kolor zapełnienia",
+  "TOOL_SIZE_LABEL": "Wielkość narzędzia",
+  "STRENGTH_LABEL": "Siła",
+  "NEW": "Nowy",
+  "ADD": "Dodaj",
+  "SUBTRACT": "Odejmij",
+  "INTERSECT": "Intersekcja",
+  "RECTANGLE": "Prostokąt",
+  "CIRCLE": "Koło",
+  "ABOUT": "O nas",
+  "MINIMIZE": "Minimializuj",
+  "RESTORE": "Przywróć",
+  "MAXIMIZE": "Maksymalizuj",
+  "CLOSE": "Zamknij",
+  "EXPORT_SIZE_HINT": "Jeżeli chcesz udostępnić obraz, spróbuj {0}% dla najlepszej przejrzystości",
+  "CREATE": "Stwórz",
+  "BASE_LAYER_NAME": "Podstawowa warstwa",
+  "ENABLE_MASK": "Włącz maskę",
+  "SELECTED_AREA_EMPTY": "Zaznaczony obszar jest pusty",
+  "NOTHING_TO_COPY": "Nie ma nic do skopiowania",
+  "REFERENCE_LAYER_PATH": "Ścieżka warstwy referencyjnej",
+  "CLIP_LAYER_BELOW": "Dopasuj do warstwy niżej",
+  "FLIP": "Przerzuć",
+  "ROTATION": "Obrót",
+  "ROT_IMG_90_D": "Obróć obraz 90°",
+  "ROT_IMG_180_D": "Obróć obraz 180°",
+  "ROT_IMG_-90_D": "Obróć obraz -90°",
+  "ROT_LAYERS_90_D": "Obróć zaznaczone warstwy 90°",
+  "ROT_LAYERS_180_D": "Obróć zaznaczone warstwy 180°",
+  "ROT_LAYERS_-90_D": "Obróć zaznaczone warstwy -90°",
+  "UNNAMED_PALETTE": "Paleta bez nazwy",
+  "CLICK_SELECT_PRIMARY": "Kilknij aby ustawić jako kolor główny.",
+  "PEN_MODE": "Tryb pióra",
+  "VIEW": "Widok",
+  "HORIZONTAL_LINE_SYMMETRY": "Pozioma oś symetrii",
+  "VERTICAL_LINE_SYMMETRY": "Pionowa oś symetrii",
+  "COLOR_PICKER_TITLE": "Próbnik Kolorów",
+  "COLOR_SLIDERS_TITLE": "Suwaki Kolorów",
+  "PALETTE_TITLE": "Paleta Kolorów",
+  "SWATCHES_TITLE": "Swatche",
+  "LAYERS_TITLE": "Warstwy",
+  "NAVIGATION_TITLE": "Nawigacja",
+  "NORMAL_BLEND_MODE": "Normalny",
+  "DARKEN_BLEND_MODE": "Przyciemnianie",
+  "MULTIPLY_BLEND_MODE": "Mnożenie",
+  "COLOR_BURN_BLEND_MODE": "Wypalenie koloru",
+  "LIGHTEN_BLEND_MODE": "Rozjaśnianie",
+  "SCREEN_BLEND_MODE": "Przesiewanie",
+  "COLOR_DODGE_BLEND_MODE": "Unikanie",
+  "OVERLAY_BLEND_MODE": "Narzuta",
+  "SOFT_LIGHT_BLEND_MODE": "Miękkie światło",
+  "HARD_LIGHT_BLEND_MODE": "Twarde światło",
+  "DIFFERENCE_BLEND_MODE": "Różnica",
+  "EXCLUSION_BLEND_MODE": "Wykluczenie",
+  "HUE_BLEND_MODE": "Odcień",
+  "SATURATION_BLEND_MODE": "Nasycenie",
+  "LUMINOSITY_BLEND_MODE": "Jasność",
+  "COLOR_BLEND_MODE": "Kolor",
+  "NOT_SUPPORTED_BLEND_MODE": "Nie wspierany",
+  "RESTART": "Restartuj",
+  "SORT_BY": "Sortuj po",
+  "NAME": "Nazwa",
+  "COLORS": "Kolory",
+  "DEFAULT": "Domyślnie",
+  "ALPHABETICAL": "Alfabetycznie",
+  "COLOR_COUNT": "Ilość kolorów",
+  "ANY": "Obojętnie",
+  "MAX": "Maksymalnie",
+  "MIN": "Minimalnie",
+  "EXACT": "Dokładnie",
+  "ASCENDING": "Rosnąco",
+  "DESCENDING": "Malejąco",
+  "NAME_IS_TOO_LONG": "Nazwa jest za długa",
+  "STOP_IT_TEXT1": "Wystarczy. Posprzątaj nazwy swoich plików.",
+  "STOP_IT_TEXT2": "Czy możesz proszę przestać kopiować te nazwy?",
+  "REPLACER_TOOLTIP": "Kliknij prawym przyciskiem myszy na kolor i wybierz 'Zamień' lub przeciągnij go tutaj.",
+  "CLICK_TO_CHOOSE_COLOR": "Kliknij aby wybrać kolor",
+  "REPLACE_COLOR": "Zastąp kolor",
+  "PALETTE_COLOR_TOOLTIP": "Kliknij aby ustawić jako główny kolor. Przeciągnij i upuść na inny kolor z palety, aby zamienić je miejscami.",
+  "ADD_FROM_SWATCHES": "Dodaj ze swatchy",
+  "ADD_COLOR_TO_PALETTE": "Dodaj kolor do palety",
+  "USE_IN_CURRENT_IMAGE": "Użyj w aktywnym obrazie",
+  "ADD_TO_FAVORITES": "Dodaj do ulubionych",
+  "BROWSE_PALETTES": "Przeglądaj palety",
+  "LOAD_PALETTE": "Załaduj paletę",
+  "SAVE_PALETTE": "Zapisz paletę",
+  "FAVORITES": "Ulubione",
+  "ADD_FROM_CURRENT_PALETTE": "Dodaj z aktywnej palety",
+  "OPEN_PALETTES_DIR_TOOLTIP": "Otwórz folder z paletami w explorerze",
+  "BROWSE_ON_LOSPEC_TOOLTIP": "Przeglądaj palety w serwisie Lospec",
+  "IMPORT_FROM_FILE_TOOLTIP": "Importuj z pliku",
+  "TOP_LEFT": "Lewy górny",
+  "TOP_CENTER": "Środkowy górny",
+  "TOP_RIGHT": "Prawy górny",
+  "MIDDLE_LEFT": "Lewy środkowy",
+  "MIDDLE_CENTER": "Środek",
+  "MIDDLE_RIGHT": "Prawy środkowy",
+  "BOTTOM_LEFT": "Lewy dolny",
+  "BOTTOM_CENTER": "Środkowy dolny",
+  "BOTTOM_RIGHT": "Prawy dolny",
+  "CLIP_TO_BELOW": "Dopasuj do warstwy niżej",
+  "MOVE_UPWARDS": "Przesuń w górę",
+  "MOVE_DOWNWARDS": "Przesuń w dół",
+  "MERGE_SELECTED": "Scal zaznaczone",
+  "LOCK_TRANSPARENCY": "Zablokuj przeźroczystość",
+  "COULD_NOT_LOAD_PALETTE": "Nie udało się pobrać palet kolorów",
+  "NO_PALETTES_FOUND": "Nie znaleziono żadnych palet.",
+  "LOSPEC_LINK_TEXT": "Słyszałem, że trochę możesz znaleźć tutaj: lospec.com/palette-list",
+  "PALETTE_BROWSER": "Przeglądarka Palet",
+  "DELETE_PALETTE_CONFIRMATION": "Czy jesteś pewien, że chcesz usunąć tę paletę? Tej akcji nie można cofnąć.",
+  "SHORTCUTS_IMPORTED": "Skróty klawiszowe z {0} zostały zaimportowane poprawnie.",
+  "SHORTCUT_PROVIDER_DETECTED": "Zauważyliśmy, że masz zainstalowane {0}. Czy chcesz zaimportować skróty z tego programu?",
+  "IMPORT_FROM_INSTALLATION": "Importuj z instalacji",
+  "IMPORT_INSTALLATION_OPTION1": "Importuj z instalacji",
+  "IMPORT_INSTALLATION_OPTION2": "Użyj domyślnych",
+  "IMPORT_FROM_TEMPLATE": "Importuj z szablonu",
+  "SHORTCUTS_IMPORTED_SUCCESS": "Skróty klawiszowe zostały zaimportowane poprawnie.",
+  "WARNING_RESET_SHORTCUTS_DEFAULT": "Czy na pewno chcesz zresetować skróty klawiszowe do ich domyślnych wartości?",
+  "SUCCESS": "Sukces",
+  "WARNING": "Ostrzeżenie",
+  "ERROR_IMPORTING_IMAGE": "Wystąpił błąd podczas importowania obrazu.",
+  "SHORTCUTS_CORRUPTED_TITLE": "Popsuty plik ze skrótami klawiszowymi",
+  "SHORTCUTS_CORRUPTED": "Plik skrótów klawiszowych jest popsuty, zresetowano do domyślnych wartości.",
+  "FAILED_DOWNLOAD_PALETTE": "Nie udało się pobrać palety",
+  "FILE_INCORRECT_FORMAT": "Plik nie jest w poprawnym formacie",
+  "INVALID_FILE": "Nieprawidłowy plik",
+  "SHORTCUTS_FILE_INCORRECT_FORMAT": "Plik ze skrótami klawiszowymi jest w niepoprawnym formacie",
+  "UNSUPPORTED_FILE_FORMAT": "Ten format plików nie jest wspierany",
+  "ALREADY_ASSIGNED": "Już przypisano",
+  "REPLACE": "Zastąp",
+  "SWAP": "Zamień",
+  "SHORTCUT_ALREADY_ASSIGNED_SWAP": "Ten skrót został już przypisany do '{0}'\nChcesz go zastąpić czy je zamienić?",
+  "SHORTCUT_ALREADY_ASSIGNED_OVERWRITE": "Ten skrót jest już przypisany do '{0}' \nCzy chcesz go zastąpić?",
+  "UNSAVED_CHANGES": "Niezapisane zmiany.",
+  "DOCUMENT_MODIFIED_SAVE": "Ten dokument został zmodyfikowany. Czy chcesz go zapisać?",
+  "SESSION_UNSAVED_DATA": "{0} ma niezapisane zmiany. Jesteś pewien?",
+  "PROJECT_MAINTAINERS": "Opiekunowie Projektu",
+  "OTHER_AWESOME_CONTRIBUTORS": "I inni świetni współtwórcy",
+  "HELP": "Pomoc",
+  "ALL LAYERS": "Wszystkie warstwy",
+  "SINGLE LAYER": "Pojedyńcza warstwa",
+  "STOP_IT_TEXT3": "Nie no, serio, przestań.",
+  "STOP_IT_TEXT4": "Nie masz nic lepszego do roboty?",
+  "LINEAR_DODGE_BLEND_MODE": "Liniowe unikanie (Dodatnie)",
+  "PRESS_ANY_KEY": "Wciśnij klawisz",
+  "NONE_SHORTCUT": "Brak",
+  "REFERENCE": "Referencja",
+  "PUT_REFERENCE_LAYER_ABOVE": "Daj warstwę referencyjną do góry",
+  "PUT_REFERENCE_LAYER_BELOW": "Daj warstwę referencyjną w dół",
+  "TOGGLE_VERTICAL_SYMMETRY": "Przełącz pionową oś symetrii",
+  "TOGGLE_HORIZONTAL_SYMMETRY": "Przełącz poziomą oś symetrii",
+  "RESET_VIEWPORT": "Resetuj widok",
+  "VIEWPORT_SETTINGS": "Ustawienia widoku",
+  "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "Kliknij i przytrzymaj myszkę, aby poruszyć piksele w zaznaczonej warstwie.",
+  "MOVE_TOOL_ACTION_DISPLAY_CTRL": "Przytrzymaj myszkę aby poruszyć wszystkie warstwy."
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/ru.json

@@ -0,0 +1,2 @@
+{
+}

+ 2 - 0
src/PixiEditor/Data/Localization/Languages/uk.json

@@ -0,0 +1,2 @@
+{
+}

+ 54 - 0
src/PixiEditor/Data/Localization/LocalizationData.json

@@ -0,0 +1,54 @@
+{
+  "$schema": "./LocalizationDataSchema.json",
+  "Languages": [
+    {
+      "name": "English",
+      "code": "en",
+      "localeFileName": "en.json",
+      "iconFileName": "en.png"
+    },
+    {
+      "name": "Polski",
+      "code": "pl",
+      "localeFileName": "pl.json",
+      "iconFileName": "pl.png"
+    },
+    {
+      "name": "Deutsch",
+      "code": "de",
+      "localeFileName": "de.json",
+      "iconFileName": "de.png"
+    },
+    {
+      "name": "Español",
+      "code": "es",
+      "localeFileName": "es.json",
+      "iconFileName": "es.png"
+    },
+    {
+      "name": "Русский",
+      "code": "ru",
+      "localeFileName": "ru.json",
+      "iconFileName": "ru.png"
+    },
+    {
+      "name": "Українська",
+      "code": "uk",
+      "localeFileName": "uk.json",
+      "iconFileName": "uk.png"
+    },
+    {
+      "name": "عربي",
+      "code": "ar",
+      "localeFileName": "ar.json",
+      "iconFileName": "ar.png",
+      "rightToLeft": true
+    },
+    {
+      "name": "Čeština",
+      "code": "cs",
+      "localeFileName": "cs.json",
+      "iconFileName": "cs.png"
+    }
+  ]
+}

+ 54 - 0
src/PixiEditor/Data/Localization/LocalizationDataSchema.json

@@ -0,0 +1,54 @@
+{
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "Languages": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "The localized name of the language"
+          },
+          "code": {
+            "type": "string",
+            "description": "The code associated with the language",
+            "minLength": 2
+          },
+          "localeFileName": {
+            "type": "string",
+            "description": "The name of the key-value json file found in Data/Localization/Languages",
+            "pattern": ".*\\.json",
+            "format": "uri",
+            "default": ".json"
+          },
+          "iconFileName": {
+            "type": "string",
+            "description": "The name of the png icon for the language found in Images/LanguageFlags",
+            "pattern": ".*\\.png",
+            "format": "uri",
+            "default": ".png"
+          },
+          "rightToLeft": {
+            "type": "boolean",
+            "description": "Does the language use RTL Layout",
+            "default": true
+          }
+        },
+        "required": [
+          "name",
+          "code",
+          "localeFileName",
+          "iconFileName"
+        ]
+      }
+    },
+    "$schema": {
+      "type": "string"
+    }
+  },
+  "required": [
+    "Languages"
+  ]
+}

+ 2 - 1
src/PixiEditor/Helpers/Converters/BlendModeToStringConverter.cs

@@ -1,6 +1,7 @@
 using System.Globalization;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 internal class BlendModeToStringConverter : SingleInstanceConverter<BlendModeToStringConverter>
@@ -9,7 +10,7 @@ internal class BlendModeToStringConverter : SingleInstanceConverter<BlendModeToS
     {
         if (value is not BlendMode mode)
             return "<null>";
-        return mode.EnglishName();
+        return new LocalizedString(mode.LocalizedKeys()).Value;
     }
 
     public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

+ 14 - 3
src/PixiEditor/Helpers/Converters/BoolToValueConverter.cs

@@ -1,4 +1,5 @@
 using System.Globalization;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 
@@ -10,12 +11,22 @@ internal class BoolToValueConverter : MarkupConverter
     
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
-        if (value is bool boolean && boolean)
+        if (value is bool and true)
         {
-            return TrueValue;
+            return GetValue(TrueValue);
         }
 
-        return FalseValue;
+        return GetValue(FalseValue);
+    }
+
+    private object GetValue(object value)
+    {
+        if (value is string s && s.StartsWith("localized:"))
+        {
+            return new LocalizedString(s.Split("localized:")[1]);
+        }
+
+        return value;
     }
 
     public override object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

+ 3 - 2
src/PixiEditor/Helpers/Converters/EnumToStringConverter.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Models.Enums;
 using System;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 
@@ -16,7 +17,7 @@ internal class EnumToStringConverter : SingleInstanceConverter<EnumToStringConve
                 if (valueCasted == SizeUnit.Percentage)
                     return "%";
 
-                return "px";
+                return "PIXEL_UNIT";
             }
             return Enum.GetName((value.GetType()), value);
         }
@@ -25,4 +26,4 @@ internal class EnumToStringConverter : SingleInstanceConverter<EnumToStringConve
             return string.Empty;
         }
     }
-}
+}

+ 13 - 14
src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -1,24 +1,23 @@
 using System.Globalization;
 using System.Windows.Input;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Converters;
 
 internal class KeyToStringConverter
     : SingleInstanceConverter<KeyToStringConverter>
 {
-    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-    {
-        if (value is Key key)
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
+        value switch
         {
-            return InputKeyHelpers.GetKeyboardKey(key);
-        }
-        else if (value is ModifierKeys)
-        {
-            return value.ToString();
-        }
-        else
-        {
-            return string.Empty;
-        }
-    }
+            Key key => (object)InputKeyHelpers.GetKeyboardKey(key),
+            ModifierKeys modifier => modifier switch
+            {
+                ModifierKeys.Control => new LocalizedString("CTRL_KEY"),
+                ModifierKeys.Shift => new LocalizedString("SHIFT_KEY"),
+                ModifierKeys.Alt => new LocalizedString("ALT_KEY"),
+                _ => modifier.ToString()
+            },
+            _ => string.Empty
+        };
 }

+ 17 - 0
src/PixiEditor/Helpers/Converters/LangConverter.cs

@@ -0,0 +1,17 @@
+using System.Globalization;
+using PixiEditor.Localization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class LangConverter : SingleInstanceConverter<LangConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if (value is string key)
+        {
+            return new LocalizedString(key);
+        }
+
+        return value;
+    }
+}

+ 24 - 0
src/PixiEditor/Helpers/Converters/SubtractConverter.cs

@@ -0,0 +1,24 @@
+using System.Globalization;
+
+namespace PixiEditor.Helpers.Converters;
+
+internal class SubtractConverter : SingleInstanceConverter<SubtractConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        object parsedValue = value is string stringValue ? double.Parse(stringValue) : value;
+        object parsedParameter = parameter is string parameterString ? double.Parse(parameterString) : parameter;
+        
+        if (parsedValue is not double doubleValue)
+        {
+            return value;
+        }
+
+        if (parsedParameter is not double doubleParameter)
+        {
+            return value;
+        }
+
+        return doubleValue - doubleParameter;
+    }
+}

+ 8 - 2
src/PixiEditor/Helpers/Converters/ToolSizeToIntConverter.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Globalization;
+using System.Text;
 using System.Text.RegularExpressions;
 using System.Windows.Data;
 
@@ -28,6 +29,11 @@ internal class ToolSizeToIntConverter
             return null;
         }
 
-        return int.Parse(match.Groups[0].ValueSpan);
+        if (int.TryParse(match.Groups[0].ValueSpan.ToString().Normalize(NormalizationForm.FormKC), out int result))
+        {
+            return result;
+        }
+
+        return null;
     }
-}
+}

+ 34 - 0
src/PixiEditor/Helpers/CrashHelper.cs

@@ -1,4 +1,7 @@
 using System.Globalization;
+using System.IO;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
 using System.Text;
 using ByteSizeLib;
 using Hardware.Info;
@@ -97,4 +100,35 @@ internal class CrashHelper
             }
         }
     }
+
+    public static async Task SendExceptionInfoToWebhook(Exception e, [CallerFilePath] string filePath = "<unknown>", [CallerMemberName] string memberName = "<unknown>")
+    {
+        if (ViewModelMain.Current.DebugSubViewModel.IsDebugBuild)
+            return;
+        await SendReportTextToWebhook(CrashReport.Generate(e), $"{filePath}; Method {memberName}");
+    }
+
+    public static async Task SendReportTextToWebhook(CrashReport report, string catchLocation = null)
+    {
+        string reportText = report.ReportText;
+        if (catchLocation is not null)
+        {
+            reportText = $"The report was generated from an exception caught in {catchLocation}.\r\n{reportText}";
+        }
+
+        byte[] bytes = Encoding.UTF8.GetBytes(reportText);
+        string filename = Path.GetFileNameWithoutExtension(report.FilePath) + ".txt";
+
+        MultipartFormDataContent formData = new MultipartFormDataContent
+        {
+            { new ByteArrayContent(bytes, 0, bytes.Length), "crash-report", filename }
+        };
+        try
+        {
+            using HttpClient httpClient = new HttpClient();
+            string url = BuildConstants.CrashReportWebhookUrl;
+            await httpClient.PostAsync(url, formData);
+        }
+        catch { }
+    }
 }

+ 10 - 2
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -5,6 +5,7 @@ using ChunkyImageLib.DataHolders;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.Parser;
 using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 
 namespace PixiEditor.Helpers;
@@ -351,9 +352,16 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
             return this;
         }
 
-        public ReferenceLayerBuilder WithRect(VecD offset, VecD size)
+        public ReferenceLayerBuilder WithShape(Corners rect)
         {
-            Shape = new ShapeCorners(new RectD(offset, size));
+            Shape = new ShapeCorners
+            {
+                TopLeft = rect.TopLeft.ToVecD(), 
+                TopRight = rect.TopRight.ToVecD(), 
+                BottomLeft = rect.BottomLeft.ToVecD(), 
+                BottomRight = rect.BottomRight.ToVecD()
+            };
+            
             return this;
         }
     }

+ 36 - 0
src/PixiEditor/Helpers/EnumExtension.cs

@@ -0,0 +1,36 @@
+using System.ComponentModel;
+using System.Windows.Markup;
+
+namespace PixiEditor.Helpers;
+
+public class EnumExtension : MarkupExtension
+{
+    private Type _enumType;
+
+    public EnumExtension(Type enumType)
+    {
+        EnumType = enumType ?? throw new ArgumentNullException(nameof(enumType));
+    }
+
+    public Type EnumType
+    {
+        get { return _enumType; }
+        private set
+        {
+            if (_enumType == value)
+                return;
+
+            var enumType = Nullable.GetUnderlyingType(value) ?? value;
+
+            if (enumType.IsEnum == false)
+                throw new ArgumentException("Type must be an Enum.");
+
+            _enumType = value;
+        }
+    }
+
+    public override object ProvideValue(IServiceProvider serviceProvider) // or IXamlServiceProvider for UWP and WinUI
+    {
+        return Enum.GetValues(EnumType);
+    }
+}

+ 20 - 19
src/PixiEditor/Helpers/Extensions/BlendModeEx.cs

@@ -1,30 +1,31 @@
 using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Localization;
 
 namespace PixiEditor.Helpers.Extensions;
 internal static class BlendModeEx
 {
-    public static string EnglishName(this BlendMode mode)
+    public static string LocalizedKeys(this BlendMode mode)
     {
         return mode switch
         {
-            BlendMode.Normal => "Normal",
-            BlendMode.Darken => "Darken",
-            BlendMode.Multiply => "Multiply",
-            BlendMode.ColorBurn => "Color Burn",
-            BlendMode.Lighten => "Lighten",
-            BlendMode.Screen => "Screen",
-            BlendMode.ColorDodge => "Color Dodge",
-            BlendMode.LinearDodge => "Linear Dodge (Add)",
-            BlendMode.Overlay => "Overlay",
-            BlendMode.SoftLight => "Soft Light",
-            BlendMode.HardLight => "Hard Light",
-            BlendMode.Difference => "Difference",
-            BlendMode.Exclusion => "Exclusion",
-            BlendMode.Hue => "Hue",
-            BlendMode.Saturation => "Saturation",
-            BlendMode.Luminosity => "Luminosity",
-            BlendMode.Color => "Color",
-            _ => "<no name>",
+            BlendMode.Normal => "NORMAL_BLEND_MODE",
+            BlendMode.Darken => "DARKEN_BLEND_MODE",
+            BlendMode.Multiply => "MULTIPLY_BLEND_MODE",
+            BlendMode.ColorBurn => "COLOR_BURN_BLEND_MODE",
+            BlendMode.Lighten => "LIGHTEN_BLEND_MODE",
+            BlendMode.Screen => "SCREEN_BLEND_MODE",
+            BlendMode.ColorDodge => "COLOR_DODGE_BLEND_MODE",
+            BlendMode.LinearDodge => "LINEAR_DODGE_BLEND_MODE",
+            BlendMode.Overlay => "OVERLAY_BLEND_MODE",
+            BlendMode.SoftLight => "SOFT_LIGHT_BLEND_MODE",
+            BlendMode.HardLight => "HARD_LIGHT_BLEND_MODE",
+            BlendMode.Difference => "DIFFERENCE_BLEND_MODE",
+            BlendMode.Exclusion => "EXCLUSION_BLEND_MODE",
+            BlendMode.Hue => "HUE_BLEND_MODE",
+            BlendMode.Saturation => "SATURATION_BLEND_MODE",
+            BlendMode.Luminosity => "LUMINOSITY_BLEND_MODE",
+            BlendMode.Color => "COLOR_BLEND_MODE",
+            _ => "NOT_SUPPORTED_BLEND_MODE"
         };
     }
 }

+ 6 - 1
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -9,6 +9,11 @@ namespace PixiEditor.Helpers.Extensions;
 
 internal static class PixiParserDocumentEx
 {
+    public static VecD ToVecD(this Vector2 vec)
+    {
+        return new VecD(vec.X, vec.Y);
+    }
+    
     public static DocumentViewModel ToDocument(this Document document)
     {
         return DocumentViewModel.Build(b =>
@@ -18,7 +23,7 @@ internal static class PixiParserDocumentEx
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B, x.A))
                 .WithReferenceLayer(document.ReferenceLayer, (r, builder) => builder
                     .WithIsVisible(r.Enabled)
-                    .WithRect(new VecD(r.OffsetX, r.OffsetY), new VecD(r.Width, r.Height))
+                    .WithShape(r.Corners)
                     .WithSurface(Surface.Load(r.ImageBytes)));
 
             BuildChildren(b, document.RootFolder.Children);

+ 5 - 0
src/PixiEditor/Helpers/Extensions/SerializableDocumentEx.cs

@@ -2,6 +2,7 @@
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.Parser;
 using PixiEditor.Parser.Collections.Deprecated;
 using PixiEditor.Parser.Deprecated;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -10,6 +11,10 @@ namespace PixiEditor.Helpers.Extensions;
 
 internal static class SerializableDocumentEx
 {
+    public static Vector2 ToVector2(this VecD serializableVector2)
+    {
+        return new Vector2 { X = serializableVector2.X, Y = serializableVector2.Y };
+    }
     public static Image ToImage(this SerializableLayer serializableLayer)
     {
         if (serializableLayer.PngBytes == null)

+ 10 - 3
src/PixiEditor/Helpers/Extensions/ServiceCollectionHelpers.cs

@@ -1,10 +1,11 @@
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Localization;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataProviders;
 using PixiEditor.Models.IO;
-using PixiEditor.Models.IO.ClsFile;
-using PixiEditor.Models.IO.JascPalFile;
+using PixiEditor.Models.IO.PaletteParsers;
+using PixiEditor.Models.IO.PaletteParsers.JascPalFile;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels.SubViewModels.Document;
@@ -17,11 +18,12 @@ namespace PixiEditor.Helpers.Extensions;
 internal static class ServiceCollectionHelpers
 {
     /// <summary>
-    /// Add's all the services required to fully run PixiEditor's MainWindow
+    /// Adds all the services required to fully run PixiEditor's MainWindow
     /// </summary>
     public static IServiceCollection AddPixiEditor(this IServiceCollection collection) => collection
         .AddSingleton<ViewModelMain>()
         .AddSingleton<IPreferences, PreferencesSettings>()
+        .AddSingleton<ILocalizationProvider, LocalizationProvider>()
         // View Models
         .AddSingleton<StylusViewModel>()
         .AddSingleton<WindowViewModel>()
@@ -62,6 +64,11 @@ internal static class ServiceCollectionHelpers
         // Palette Parsers
         .AddSingleton<PaletteFileParser, JascFileParser>()
         .AddSingleton<PaletteFileParser, ClsFileParser>()
+        .AddSingleton<PaletteFileParser, PngPaletteParser>()
+        .AddSingleton<PaletteFileParser, PaintNetTxtParser>()
+        .AddSingleton<PaletteFileParser, HexPaletteParser>()
+        .AddSingleton<PaletteFileParser, GimpGplParser>()
+        .AddSingleton<PaletteFileParser, PixiPaletteParser>()
         // Palette data sources
         .AddSingleton<PaletteListDataSource, LocalPalettesFetcher>();
 }

+ 64 - 103
src/PixiEditor/Helpers/GlobalMouseHook.cs

@@ -5,7 +5,9 @@ using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Input;
+using System.Windows.Interop;
 using System.Windows.Threading;
+using PixiEditor.Views;
 
 namespace PixiEditor.Helpers;
 
@@ -13,136 +15,95 @@ public delegate void MouseUpEventHandler(object sender, Point p, MouseButton but
 
 // see https://stackoverflow.com/questions/22659925/how-to-capture-mouseup-event-outside-the-wpf-window
 [ExcludeFromCodeCoverage]
-internal static class GlobalMouseHook
+internal class GlobalMouseHook
 {
-    private const int WH_MOUSE_LL = 14;
-    private const int WM_LBUTTONUP = 0x0202;
-    private const int WM_MBUTTONUP = 0x0208;
-    private const int WM_RBUTTONUP = 0x0205;
+    private static readonly Lazy<GlobalMouseHook> lazy = new Lazy<GlobalMouseHook>(() => new GlobalMouseHook());
+    public static GlobalMouseHook Instance => lazy.Value;
 
-    private static int mouseHookHandle;
-    private static HookProc mouseDelegate;
+    public event MouseUpEventHandler OnMouseUp;
 
-    private delegate int HookProc(int nCode, int wParam, IntPtr lParam);
+    private int mouseHookHandle;
+    private Win32.HookProc mouseDelegate;
 
-    public static event MouseUpEventHandler OnMouseUp
+    private Thread mouseHookWindowThread;
+    private IntPtr mainWindowHandle;
+    private IntPtr childWindowHandle;
+
+    private GlobalMouseHook() { }
+
+    public void Initilize(MainWindow window)
     {
-        add
-        {
-            // disable low-level hook in debug to prevent mouse lag when pausing in debugger
-#if !DEBUG
-                Subscribe();
+        // disable low-level hook in debug to prevent mouse lag when pausing in debugger
+#if DEBUG
+        return;
 #endif
-            MouseUp += value;
-        }
+        mainWindowHandle = new WindowInteropHelper(window).Handle;
+        if (mainWindowHandle == IntPtr.Zero)
+            throw new InvalidOperationException();
 
-        remove
+        window.Closed += (_, _) =>
         {
-            MouseUp -= value;
-#if !DEBUG
-                Unsubscribe();
-#endif
-        }
-    }
-
-    private static event MouseUpEventHandler MouseUp;
-
-    public static void RaiseMouseUp()
-    {
-        MouseUp?.Invoke(default, default, default);
-    }
+            if (childWindowHandle != IntPtr.Zero)
+                Win32.PostMessage(childWindowHandle, Win32.WM_CLOSE, 0, 0);
+        };
 
-    private static void Unsubscribe()
-    {
-        if (mouseHookHandle != 0)
+        mouseHookWindowThread = new Thread(StartMouseHook)
         {
-            int result = UnhookWindowsHookEx(mouseHookHandle);
-            mouseHookHandle = 0;
-            mouseDelegate = null;
-            if (result == 0)
-            {
-                int errorCode = Marshal.GetLastWin32Error();
-                throw new Win32Exception(errorCode);
-            }
-        }
+            Name = $"{nameof(GlobalMouseHook)} Thread"
+        };
+        mouseHookWindowThread.Start();
     }
 
-    private static void Subscribe()
+    private void StartMouseHook()
     {
+        LowLevelWindow window = new LowLevelWindow(nameof(GlobalMouseHook), mainWindowHandle);
+        childWindowHandle = window.WindowHandle;
+
+        mouseDelegate = MouseHookProc;
+        mouseHookHandle = Win32.SetWindowsHookEx(
+            Win32.WH_MOUSE_LL,
+            mouseDelegate,
+            Win32.GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
+            0);
         if (mouseHookHandle == 0)
         {
-            mouseDelegate = MouseHookProc;
-            mouseHookHandle = SetWindowsHookEx(
-                WH_MOUSE_LL,
-                mouseDelegate,
-                GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName),
-                0);
-            if (mouseHookHandle == 0)
-            {
-                int errorCode = Marshal.GetLastWin32Error();
-                throw new Win32Exception(errorCode);
-            }
+            int errorCode = Marshal.GetLastWin32Error();
+            throw new Win32Exception(errorCode);
         }
+
+        window.RunEventLoop();
     }
 
-    private static int MouseHookProc(int nCode, int wParam, IntPtr lParam)
+    //private void Unsubscribe()
+    //{
+    //    int result = Win32.UnhookWindowsHookEx(mouseHookHandle);
+    //    mouseHookHandle = 0;
+    //    mouseDelegate = null;
+    //    if (result == 0)
+    //    {
+    //        int errorCode = Marshal.GetLastWin32Error();
+    //        throw new Win32Exception(errorCode);
+    //    }
+    //}
+
+    private int MouseHookProc(int nCode, int wParam, IntPtr lParam)
     {
         if (nCode >= 0)
         {
-            MSLLHOOKSTRUCT mouseHookStruct = (MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
-            if (wParam == WM_LBUTTONUP || wParam == WM_MBUTTONUP || wParam == WM_RBUTTONUP)
+            Win32.MSLLHOOKSTRUCT mouseHookStruct = (Win32.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Win32.MSLLHOOKSTRUCT));
+            if (wParam == Win32.WM_LBUTTONUP || wParam == Win32.WM_MBUTTONUP || wParam == Win32.WM_RBUTTONUP)
             {
-                if (MouseUp != null)
+                if (OnMouseUp is not null)
                 {
 
-                    MouseButton button = wParam == WM_LBUTTONUP ? MouseButton.Left
-                        : wParam == WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
-                    Dispatcher.CurrentDispatcher.BeginInvoke(() =>
-                        MouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button));
+                    MouseButton button = wParam == Win32.WM_LBUTTONUP ? MouseButton.Left
+                        : wParam == Win32.WM_MBUTTONUP ? MouseButton.Middle : MouseButton.Right;
+                    Application.Current?.Dispatcher.BeginInvoke(() =>
+                        OnMouseUp.Invoke(null, new Point(mouseHookStruct.Pt.X, mouseHookStruct.Pt.Y), button));
                 }
             }
         }
 
-        return CallNextHookEx(mouseHookHandle, nCode, wParam, lParam);
-    }
-
-    [DllImport(
-        "user32.dll",
-        CharSet = CharSet.Auto,
-        CallingConvention = CallingConvention.StdCall,
-        SetLastError = true)]
-    private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
-
-    [DllImport(
-        "user32.dll",
-        CharSet = CharSet.Auto,
-        CallingConvention = CallingConvention.StdCall,
-        SetLastError = true)]
-    private static extern int UnhookWindowsHookEx(int idHook);
-
-    [DllImport(
-        "user32.dll",
-        CharSet = CharSet.Auto,
-        CallingConvention = CallingConvention.StdCall)]
-    private static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);
-
-    [DllImport("kernel32.dll")]
-    private static extern IntPtr GetModuleHandle(string name);
-
-    [StructLayout(LayoutKind.Sequential)]
-    private struct POINT
-    {
-        public int X;
-        public int Y;
-    }
-
-    [StructLayout(LayoutKind.Sequential)]
-    private struct MSLLHOOKSTRUCT
-    {
-        public POINT Pt;
-        public uint MouseData;
-        public uint Flags;
-        public uint Time;
-        public IntPtr DwExtraInfo;
+        return Win32.CallNextHookEx(mouseHookHandle, nCode, wParam, lParam);
     }
 }

+ 39 - 48
src/PixiEditor/Helpers/InputKeyHelpers.cs

@@ -7,35 +7,43 @@ namespace PixiEditor.Helpers;
 
 internal static class InputKeyHelpers
 {
+    const string Russian = "00000419";
+    const string Ukrainian = "00000422";
+    const string UkrainianEnhanced = "00020422";
+    const string Arabic1 = "00000401";
+    const string Arabic2 = "00010401";
+    const string Arabic3 = "00020401";
+    private const string InvariantLayoutCode = "00000409"; // Also known as the US Layout
+
+    private static nint? invariantLayout;
+    
     /// <summary>
     /// Returns the charcter of the <paramref name="key"/> mapped to the users keyboard layout
     /// </summary>
-    public static string GetKeyboardKey(Key key) => GetKeyboardKey(key, CultureInfo.CurrentCulture);
-
-    public static string GetKeyboardKey(Key key, CultureInfo culture) => key switch
+    public static string GetKeyboardKey(Key key, bool forceInvariant = false) => key switch
     {
-        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, culture)}",
+        >= Key.NumPad0 and <= Key.Divide => $"Num {GetMappedKey(key, forceInvariant)}",
         Key.Space => nameof(Key.Space),
         Key.Tab => nameof(Key.Tab),
         Key.Return => "Enter",
         Key.Back => "Backspace",
         Key.Escape => "Esc",
-        _ => GetMappedKey(key, culture),
+        _ => GetMappedKey(key, forceInvariant),
     };
 
-    private static string GetMappedKey(Key key, CultureInfo culture)
+    private static string GetMappedKey(Key key, bool forceInvariant)
     {
         int virtualKey = KeyInterop.VirtualKeyFromKey(key);
         byte[] keyboardState = new byte[256];
 
-        uint scanCode = MapVirtualKeyExW((uint)virtualKey, MapType.MAPVK_VK_TO_VSC, culture.KeyboardLayoutId);
-        StringBuilder stringBuilder = new(3);
-
-        int result = ToUnicode((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0);
-
-        string stringResult;
+        nint targetLayout = GetLayoutHkl(forceInvariant);
+        
+        uint scanCode = Win32.MapVirtualKeyExW((uint)virtualKey, Win32.MapType.MAPVK_VK_TO_VSC, targetLayout);
+        
+        StringBuilder stringBuilder = new(5);
+        int result = Win32.ToUnicodeEx((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0, targetLayout);
 
-        stringResult = result switch
+        string stringResult = result switch
         {
             0 => key.ToString(),
             -1 => stringBuilder.ToString().ToUpper(),
@@ -45,42 +53,25 @@ internal static class InputKeyHelpers
         return stringResult;
     }
 
-    private enum MapType : uint
+    private static nint GetLayoutHkl(bool forceInvariant = false)
     {
-        /// <summary>
-        /// The uCode parameter is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If there is no translation, the function returns 0.
-        /// </summary>
-        MAPVK_VK_TO_VSC = 0x0,
-
-        /// <summary>
-        /// The uCode parameter is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0.
-        /// </summary>
-        MAPVK_VSC_TO_VK = 0x1,
-
-        /// <summary>
-        /// The uCode parameter is a virtual-key code and is translated into an unshifted character value in the low order word of the return value. Dead keys (diacritics) are indicated by setting the top bit of the return value. If there is no translation, the function returns 0.
-        /// </summary>
-        MAPVK_VK_TO_CHAR = 0x2,
+        if (forceInvariant)
+        {
+            invariantLayout ??= Win32.LoadKeyboardLayoutA(InvariantLayoutCode, 1);
+            return invariantLayout.Value;
+        }
+        
+        var builder = new StringBuilder(8);
+        bool success = Win32.GetKeyboardLayoutNameW(builder);
+
+        // Fallback to US layout for certain layouts. Do not prepend a 0x and make sure the string is 8 chars long
+        // Layouts can be found here https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values?view=windows-11
+        if (!success || builder.ToString() is not (Russian or Ukrainian or UkrainianEnhanced or Arabic1 or Arabic2 or Arabic3))
+        {
+            return Win32.GetKeyboardLayout(0);
+        }
 
-        /// <summary>
-        /// The uCode parameter is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0.
-        /// </summary>
-        MAPVK_VSC_TO_VK_EX = 0x3,
+        invariantLayout ??= Win32.LoadKeyboardLayoutA(InvariantLayoutCode, 1);
+        return invariantLayout.Value;
     }
-
-    [DllImport("user32.dll")]
-    private static extern int ToUnicode(
-        uint wVirtKey,
-        uint wScanCode,
-        byte[] lpKeyState,
-        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
-        StringBuilder pwszBuff,
-        int cchBuff,
-        uint wFlags);
-
-    [DllImport("user32.dll")]
-    private static extern bool GetKeyboardState(byte[] lpKeyState);
-
-    [DllImport("user32.dll")]
-    private static extern uint MapVirtualKeyExW(uint uCode, MapType uMapType, int hkl);
 }

+ 45 - 0
src/PixiEditor/Helpers/LocalizationExtension.cs

@@ -0,0 +1,45 @@
+using System.Windows;
+using System.Windows.Data;
+using System.Windows.Markup;
+
+namespace PixiEditor.Helpers;
+
+public class LocalizationExtension : MarkupExtension
+{
+    private LocalizationExtensionToProvide toProvide;
+    private static Binding flowDirectionBinding;
+
+    public LocalizationExtension(LocalizationExtensionToProvide toProvide)
+    {
+        
+    }
+    
+    public override object ProvideValue(IServiceProvider serviceProvider)
+    {
+        switch (toProvide)
+        {
+            case LocalizationExtensionToProvide.FlowDirection:
+                return GetFlowDirectionBinding(serviceProvider);
+        }
+
+        throw new NotImplementedException();
+    }
+
+    private object GetFlowDirectionBinding(IServiceProvider serviceProvider)
+    {
+        flowDirectionBinding = new Binding("CurrentLanguage.FlowDirection");
+        flowDirectionBinding.Source = ViewModelMain.Current.LocalizationProvider;
+        flowDirectionBinding.Mode = BindingMode.OneWay;
+
+        var expression = (BindingExpression)flowDirectionBinding.ProvideValue(serviceProvider);
+
+        ViewModelMain.Current.LocalizationProvider.OnLanguageChanged += _ => expression.UpdateTarget();
+
+        return expression;
+    }
+}
+
+public enum LocalizationExtensionToProvide
+{
+    FlowDirection
+}

+ 100 - 0
src/PixiEditor/Helpers/LowLevelWindow.cs

@@ -0,0 +1,100 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace PixiEditor.Helpers;
+internal class LowLevelWindow
+{
+    private bool disposed;
+    private Win32.WndProc wndProcDelegate;
+
+    public IntPtr WindowHandle { get; private set; }
+
+    public LowLevelWindow(string uniqueWindowName, IntPtr parentWindow)
+    {
+        if (string.IsNullOrEmpty(uniqueWindowName))
+            throw new ArgumentException(nameof(uniqueWindowName));
+
+        wndProcDelegate = CustomWndProc;
+
+        // Create WNDCLASS
+        Win32.WNDCLASS windowParams = new Win32.WNDCLASS();
+        windowParams.lpszClassName = uniqueWindowName;
+        windowParams.lpfnWndProc = Marshal.GetFunctionPointerForDelegate(wndProcDelegate);
+
+        ushort classAtom = Win32.RegisterClassW(ref windowParams);
+
+        int lastError = Marshal.GetLastWin32Error();
+        if (classAtom == 0 && lastError != Win32.ERROR_CLASS_ALREADY_EXISTS)
+            throw new Win32Exception("Could not register window class");
+
+        // Create window
+        WindowHandle = Win32.CreateWindowExW(
+            0,
+            uniqueWindowName,
+            String.Empty,
+            Win32.WS_CHILD, //| Win32.WS_OVERLAPPEDWINDOW
+            0,
+            0,
+            0,
+            0,
+            parentWindow,
+            IntPtr.Zero,
+            IntPtr.Zero,
+            IntPtr.Zero
+        );
+
+        if (WindowHandle == 0)
+            throw new Win32Exception("Could not create window");
+
+        //Win32.ShowWindow(WindowHandle, 1);
+        //Win32.UpdateWindow(WindowHandle);
+    }
+
+    public void RunEventLoop()
+    {
+        while (true)
+        {
+            var bRet = Win32.GetMessage(out Win32.MSG msg, WindowHandle, 0, 0);
+            if (bRet == 0 || msg.message == Win32.WM_CLOSE)
+                return;
+
+            if (bRet == -1)
+            {
+                // handle the error and possibly exit
+            }
+            else
+            {
+                Win32.TranslateMessage(ref msg);
+                Win32.DispatchMessage(ref msg);
+            }
+        }
+    }
+
+    private static IntPtr CustomWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
+    {
+        return Win32.DefWindowProcW(hWnd, msg, wParam, lParam);
+    }
+
+    public void Dispose()
+    {
+        Dispose(true);
+        GC.SuppressFinalize(this);
+    }
+
+    private void Dispose(bool disposing)
+    {
+        if (!disposed)
+        {
+            // Dispose unmanaged resources
+            if (WindowHandle != IntPtr.Zero)
+            {
+                Win32.DestroyWindow(WindowHandle);
+                WindowHandle = IntPtr.Zero;
+            }
+            disposed = true;
+        }
+    }
+
+    ~LowLevelWindow() => Dispose(false);
+}

+ 39 - 0
src/PixiEditor/Helpers/RegistryHelpers.cs

@@ -0,0 +1,39 @@
+using System.Diagnostics;
+using System.Security.AccessControl;
+using System.Windows;
+using Microsoft.Win32;
+using PixiEditor.Localization;
+using PixiEditor.Models.Dialogs;
+
+namespace PixiEditor.Helpers;
+
+public static class RegistryHelpers
+{
+    public static bool IsKeyPresentInRoot(string keyName)
+    {
+        using var key = Registry.ClassesRoot.OpenSubKey(keyName, RegistryRights.ReadKey);
+        return key != null;
+    }
+
+    public static bool TryAssociate(Action associationMethod, LocalizedString errorMessage)
+    {
+        try
+        {
+            if (!ProcessHelper.IsRunningAsAdministrator())
+            {
+                ProcessHelper.RunAsAdmin(Process.GetCurrentProcess().MainModule?.FileName);
+                Application.Current.Shutdown();
+            }
+            else
+            {
+                associationMethod();
+            }
+        }
+        catch
+        {
+            NoticeDialog.Show(errorMessage, "ERROR");
+        }
+
+        return false;
+    }
+}

+ 21 - 0
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -36,6 +36,27 @@ internal class SupportedFilesHelper
         return allFileTypeDialogsData.Where(i => i.FileType == type).Single();
     }
 
+    public static string FixFileExtension(string pathWithOrWithoutExtension, FileType requestedType)
+    {
+        if (requestedType == FileType.Unset)
+            throw new ArgumentException("A valid filetype is required", nameof(requestedType));
+
+        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
+        if (typeFromPath != FileType.Unset && typeFromPath == requestedType)
+            return pathWithOrWithoutExtension;
+        return AppendExtension(pathWithOrWithoutExtension, SupportedFilesHelper.GetFileTypeDialogData(requestedType));
+    }
+
+    public static string AppendExtension(string path, FileTypeDialogData data)
+    {
+        string ext = data.Extensions.First();
+        string filename = Path.GetFileName(path);
+        if (filename.Length + ext.Length > 255)
+            filename = filename.Substring(0, 255 - ext.Length);
+        filename += ext;
+        return Path.Combine(Path.GetDirectoryName(path), filename);
+    }
+
     public static bool IsSupportedFile(string path)
     {
         var ext = Path.GetExtension(path.ToLower());

+ 19 - 25
src/PixiEditor/Helpers/VersionHelpers.cs

@@ -10,37 +10,31 @@ internal static class VersionHelpers
 
     public static string GetCurrentAssemblyVersion(Func<Version, string> toString) => toString(GetCurrentAssemblyVersion());
 
-    public static string GetCurrentAssemblyVersionString()
+    public static string GetCurrentAssemblyVersionString(bool moreSpecific = false)
     {
         StringBuilder builder = new(GetCurrentAssemblyVersion().ToString());
 
-        bool isDone = false;
-
-        AppendDevBuild(builder, ref isDone);
+#if DEVRELEASE
+        builder.Append(" Dev Build");
+        return builder.ToString();
+#elif MSIX_DEBUG
+        builder.Append(" MSIX Debug Build");
+        return builder.ToString();
+#elif DEBUG
+        builder.Append(" Debug Build");
+        return builder.ToString();
+#endif
 
-        if (isDone)
-        {
+        if (!moreSpecific)
             return builder.ToString();
-        }
-
-        AppendDebugBuild(builder, ref isDone);
 
+#if STEAM
+        builder.Append(" Steam Build");
+#elif MSIX
+        builder.Append(" MSIX Build");
+#elif RELEASE
+        builder.Append(" Release Build");
+#endif
         return builder.ToString();
     }
-
-    [Conditional("DEV_RELEASE")]
-    private static void AppendDevBuild(StringBuilder builder, ref bool done)
-    {
-        done = true;
-
-        builder.Append(" Dev Build");
-    }
-
-    [Conditional("DEBUG")]
-    private static void AppendDebugBuild(StringBuilder builder, ref bool done)
-    {
-        done = true;
-
-        builder.Append(" Debug Build");
-    }
 }

+ 281 - 0
src/PixiEditor/Helpers/Win32.cs

@@ -0,0 +1,281 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace PixiEditor.Helpers;
+internal class Win32
+{
+    public const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
+    public const int WH_MOUSE_LL = 14;
+
+    public const int WM_GETMINMAXINFO = 0x0024;
+    public const int WM_LBUTTONUP = 0x0202;
+    public const int WM_MBUTTONUP = 0x0208;
+    public const int WM_RBUTTONUP = 0x0205;
+    public const int WM_CLOSE = 0x0010;
+    public const int WM_DESTROY = 0x0002;
+
+    public const int ERROR_CLASS_ALREADY_EXISTS = 1410;
+    public const int CW_USEDEFAULT = unchecked((int)0x80000000);
+
+    public const uint WS_CHILD = 0x40000000;
+    public const uint WS_CAPTION = 0x00C00000;
+    public const uint WS_OVERLAPPED = 0x00000000;
+    public const uint WS_SYSMENU = 0x00080000;
+    public const uint WS_THICKFRAME = 0x00040000;
+    public const uint WS_MINIMIZEBOX = 0x00020000;
+    public const uint WS_MAXIMIZEBOX = 0x00010000;
+    public const uint WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
+
+    public delegate int HookProc(int nCode, int wParam, IntPtr lParam);
+    public delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    internal struct WNDCLASS
+    {
+        public uint style;
+        public IntPtr lpfnWndProc;
+        public int cbClsExtra;
+        public int cbWndExtra;
+        public IntPtr hInstance;
+        public IntPtr hIcon;
+        public IntPtr hCursor;
+        public IntPtr hbrBackground;
+        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
+        public string lpszMenuName;
+        [System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)]
+        public string lpszClassName;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    internal struct MSG
+    {
+        public IntPtr hwnd;
+        public uint message;
+        public IntPtr wParam;
+        public IntPtr lParam;
+        public uint time;
+        public POINT pt;
+        public uint lPrivate;
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct MSLLHOOKSTRUCT
+    {
+        public POINT Pt;
+        public uint MouseData;
+        public uint Flags;
+        public uint Time;
+        public IntPtr DwExtraInfo;
+    }
+
+    public enum MapType : uint
+    {
+        /// <summary>
+        /// The uCode parameter is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VK_TO_VSC = 0x0,
+
+        /// <summary>
+        /// The uCode parameter is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VSC_TO_VK = 0x1,
+
+        /// <summary>
+        /// The uCode parameter is a virtual-key code and is translated into an unshifted character value in the low order word of the return value. Dead keys (diacritics) are indicated by setting the top bit of the return value. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VK_TO_CHAR = 0x2,
+
+        /// <summary>
+        /// The uCode parameter is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0.
+        /// </summary>
+        MAPVK_VSC_TO_VK_EX = 0x3,
+    }
+
+    [Serializable]
+    [StructLayout(LayoutKind.Sequential)]
+    public struct RECT
+    {
+        public int Left;
+        public int Top;
+        public int Right;
+        public int Bottom;
+
+        public RECT(int left, int top, int right, int bottom)
+        {
+            this.Left = left;
+            this.Top = top;
+            this.Right = right;
+            this.Bottom = bottom;
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct MONITORINFO
+    {
+        public int cbSize;
+        public RECT rcMonitor;
+        public RECT rcWork;
+        public uint dwFlags;
+    }
+
+    [Serializable]
+    [StructLayout(LayoutKind.Sequential)]
+    public struct POINT
+    {
+        public int X;
+        public int Y;
+
+        public POINT(int x, int y)
+        {
+            this.X = x;
+            this.Y = y;
+        }
+    }
+
+    [StructLayout(LayoutKind.Sequential)]
+    public struct MINMAXINFO
+    {
+        public POINT ptReserved;
+        public POINT ptMaxSize;
+        public POINT ptMaxPosition;
+        public POINT ptMinTrackSize;
+        public POINT ptMaxTrackSize;
+    }
+
+
+    [DllImport("user32.dll")]
+    public static extern IntPtr MonitorFromWindow(IntPtr handle, uint flags);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
+
+    [DllImport("user32.dll")]
+    public static extern int ToUnicode(
+        uint wVirtKey,
+        uint wScanCode,
+        byte[] lpKeyState,
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder pwszBuff,
+        int cchBuff,
+        uint wFlags);
+
+    [DllImport("user32.dll")]
+    public static extern nint GetKeyboardLayout(
+        uint idThread);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetKeyboardLayoutNameW(
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder klid);
+    
+    [DllImport("user32.dll")]
+    public static extern int ToUnicodeEx(
+        uint wVirtKey,
+        uint wScanCode,
+        byte[] lpKeyState,
+        [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)]
+        StringBuilder pwszBuff,
+        int cchBuff,
+        uint wFlags,
+        nint dwhkl);
+
+    [DllImport("user32.dll")]
+    public static extern bool GetKeyboardState(byte[] lpKeyState);
+
+    [DllImport("user32.dll")]
+    public static extern uint MapVirtualKeyExW(uint uCode, MapType uMapType, nint hkl);
+    
+    [DllImport("user32.dll")]
+    public static extern IntPtr LoadKeyboardLayoutA(string pwszKLID, uint Flags);
+
+    [DllImport(
+        "user32.dll",
+        CharSet = CharSet.Auto,
+        CallingConvention = CallingConvention.StdCall,
+        SetLastError = true)]
+    public static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, int dwThreadId);
+
+    [DllImport(
+        "user32.dll",
+        CharSet = CharSet.Auto,
+        CallingConvention = CallingConvention.StdCall,
+        SetLastError = true)]
+    public static extern int UnhookWindowsHookEx(int idHook);
+
+    [DllImport(
+        "user32.dll",
+        CharSet = CharSet.Auto,
+        CallingConvention = CallingConvention.StdCall)]
+    public static extern int CallNextHookEx(int idHook, int nCode, int wParam, IntPtr lParam);
+
+    [DllImport("kernel32.dll")]
+    public static extern IntPtr GetModuleHandle(string name);
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern IntPtr CreateWindowExW(
+       UInt32 dwExStyle,
+       [MarshalAs(UnmanagedType.LPWStr)]
+       string lpClassName,
+       [MarshalAs(UnmanagedType.LPWStr)]
+       string lpWindowName,
+       UInt32 dwStyle,
+       Int32 x,
+       Int32 y,
+       Int32 nWidth,
+       Int32 nHeight,
+       IntPtr hWndParent,
+       IntPtr hMenu,
+       IntPtr hInstance,
+       IntPtr lpParam
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern IntPtr DefWindowProcW(
+        IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool DestroyWindow(
+        IntPtr hWnd
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool ShowWindow(
+        IntPtr hWnd,
+        int nCmdShow
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool UpdateWindow(
+        IntPtr hWnd
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern int GetMessage(
+        out MSG lpMsg,
+        IntPtr hWnd,
+        uint wMsgFilterMin,
+        uint wMsgFilterMax
+    );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool DispatchMessage(
+            [In] ref MSG lpMsg
+        );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern bool TranslateMessage(
+            [In] ref MSG lpMsg
+        );
+
+    [DllImport("user32.dll", SetLastError = true)]
+    public static extern System.UInt16 RegisterClassW(
+        [In] ref WNDCLASS lpWndClass
+    );
+
+    [DllImport("Kernel32.dll", SetLastError = true)]
+    public static extern int GetCurrentThreadId();
+
+    [DllImport("user32.dll")]
+    public static extern bool PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
+}

+ 9 - 70
src/PixiEditor/Helpers/WindowSizeHelper.cs

@@ -9,22 +9,22 @@ static class WindowSizeHelper
     {
         // All windows messages (msg) can be found here
         // https://docs.microsoft.com/de-de/windows/win32/winmsg/window-notifications
-        if (msg == WM_GETMINMAXINFO)
+        if (msg == Win32.WM_GETMINMAXINFO)
         {
             // We need to tell the system what our size should be when maximized. Otherwise it will
             // cover the whole screen, including the task bar.
-            MINMAXINFO mmi = (MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(MINMAXINFO));
+            Win32.MINMAXINFO mmi = (Win32.MINMAXINFO)Marshal.PtrToStructure(lParam, typeof(Win32.MINMAXINFO));
 
             // Adjust the maximized size and position to fit the work area of the correct monitor
-            IntPtr monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
+            IntPtr monitor = Win32.MonitorFromWindow(hwnd, Win32.MONITOR_DEFAULTTONEAREST);
 
             if (monitor != IntPtr.Zero)
             {
-                MONITORINFO monitorInfo = default;
-                monitorInfo.cbSize = Marshal.SizeOf(typeof(MONITORINFO));
-                GetMonitorInfo(monitor, ref monitorInfo);
-                RECT rcWorkArea = monitorInfo.rcWork;
-                RECT rcMonitorArea = monitorInfo.rcMonitor;
+                Win32.MONITORINFO monitorInfo = default;
+                monitorInfo.cbSize = Marshal.SizeOf(typeof(Win32.MONITORINFO));
+                Win32.GetMonitorInfo(monitor, ref monitorInfo);
+                Win32.RECT rcWorkArea = monitorInfo.rcWork;
+                Win32.RECT rcMonitorArea = monitorInfo.rcMonitor;
                 mmi.ptMaxPosition.X = Math.Abs(rcWorkArea.Left - rcMonitorArea.Left);
                 mmi.ptMaxPosition.Y = Math.Abs(rcWorkArea.Top - rcMonitorArea.Top);
                 mmi.ptMaxSize.X = Math.Abs(rcWorkArea.Right - rcWorkArea.Left);
@@ -36,65 +36,4 @@ static class WindowSizeHelper
 
         return IntPtr.Zero;
     }
-
-    private const int WM_GETMINMAXINFO = 0x0024;
-
-    private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
-
-    [DllImport("user32.dll")]
-    private static extern IntPtr MonitorFromWindow(IntPtr handle, uint flags);
-
-    [DllImport("user32.dll")]
-    private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
-
-    [Serializable]
-    [StructLayout(LayoutKind.Sequential)]
-    private struct RECT
-    {
-        public int Left;
-        public int Top;
-        public int Right;
-        public int Bottom;
-
-        public RECT(int left, int top, int right, int bottom)
-        {
-            this.Left = left;
-            this.Top = top;
-            this.Right = right;
-            this.Bottom = bottom;
-        }
-    }
-
-    [StructLayout(LayoutKind.Sequential)]
-    private struct MONITORINFO
-    {
-        public int cbSize;
-        public RECT rcMonitor;
-        public RECT rcWork;
-        public uint dwFlags;
-    }
-
-    [Serializable]
-    [StructLayout(LayoutKind.Sequential)]
-    private struct POINT
-    {
-        public int X;
-        public int Y;
-
-        public POINT(int x, int y)
-        {
-            this.X = x;
-            this.Y = y;
-        }
-    }
-
-    [StructLayout(LayoutKind.Sequential)]
-    private struct MINMAXINFO
-    {
-        public POINT ptReserved;
-        public POINT ptMaxSize;
-        public POINT ptMaxPosition;
-        public POINT ptMinTrackSize;
-        public POINT ptMaxTrackSize;
-    }
-}
+}

BIN
src/PixiEditor/Images/LanguageFlags/ar.png


BIN
src/PixiEditor/Images/LanguageFlags/cs.png


BIN
src/PixiEditor/Images/LanguageFlags/de.png


BIN
src/PixiEditor/Images/LanguageFlags/en.png


BIN
src/PixiEditor/Images/LanguageFlags/es.png


BIN
src/PixiEditor/Images/LanguageFlags/pl.png


BIN
src/PixiEditor/Images/LanguageFlags/ru.png


BIN
src/PixiEditor/Images/LanguageFlags/uk.png


+ 22 - 0
src/PixiEditor/Localization/ILocalizationProvider.cs

@@ -0,0 +1,22 @@
+using System.IO;
+
+namespace PixiEditor.Localization;
+
+public interface ILocalizationProvider
+{
+    public static ILocalizationProvider Current => ViewModelMain.Current.LocalizationProvider;
+    
+    public string LocalizationDataPath { get; }
+    public LocalizationData LocalizationData { get; }
+    public Language CurrentLanguage { get; set; }
+    public event Action<Language> OnLanguageChanged;
+
+    /// <summary>
+    ///     Loads the localization data from the specified file.
+    /// </summary>
+    public void LoadData();
+    public void LoadLanguage(LanguageData languageData);
+    public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft);
+    public void ReloadLanguage();
+    public Language DefaultLanguage { get; }
+}

+ 37 - 0
src/PixiEditor/Localization/Language.cs

@@ -0,0 +1,37 @@
+using System.Diagnostics;
+using System.Windows;
+
+namespace PixiEditor.Localization;
+
+[DebuggerDisplay("{LanguageData.Name}, strings: {Locale.Count}")]
+public class Language
+{
+    private FlowDirection flowDirection;
+    
+    public LanguageData LanguageData { get; }
+    public IReadOnlyDictionary<string, string> Locale { get; }
+
+    public FlowDirection FlowDirection
+    {
+        get
+        {
+            if (ViewModelMain.Current.DebugSubViewModel.ForceOtherFlowDirection)
+            {
+                return flowDirection switch
+                {
+                    FlowDirection.RightToLeft => FlowDirection.LeftToRight,
+                    FlowDirection.LeftToRight => FlowDirection.RightToLeft
+                };
+            }
+
+            return flowDirection;
+        }
+    }
+    
+    public Language(LanguageData languageData, Dictionary<string, string> locale, bool isRightToLeft)
+    {
+        LanguageData = languageData;
+        Locale = locale;
+        flowDirection = isRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
+    }
+}

+ 18 - 0
src/PixiEditor/Localization/LanguageData.cs

@@ -0,0 +1,18 @@
+namespace PixiEditor.Localization;
+
+public class LanguageData
+{
+    public string Name { get; set; }
+    public string Code { get; set; }
+    public string LocaleFileName { get; set; }
+    
+    // https://icons8.com/icon/set/flags/color
+    public string IconFileName { get; set; }
+    public string IconPath => $"pack://application:,,,/PixiEditor;component/Images/LanguageFlags/{IconFileName}";
+    public bool RightToLeft { get; set; }
+    
+    public override string ToString()
+    {
+        return Name;
+    }
+}

+ 9 - 0
src/PixiEditor/Localization/LocalizationData.cs

@@ -0,0 +1,9 @@
+using System.Diagnostics;
+
+namespace PixiEditor.Localization;
+
+[DebuggerDisplay("{Languages.Length} Language(s)")]
+public class LocalizationData
+{
+    public LanguageData[] Languages { get; set; }
+}

+ 114 - 0
src/PixiEditor/Localization/LocalizationProvider.cs

@@ -0,0 +1,114 @@
+using System.IO;
+using PixiEditor.Models.UserPreferences;
+
+namespace PixiEditor.Localization;
+
+internal class LocalizationProvider : ILocalizationProvider
+{
+    private Language debugLanguage;
+    public string LocalizationDataPath { get; } = Path.Combine("Data", "Localization", "LocalizationData.json");
+    public LocalizationData LocalizationData { get; private set; }
+    public Language CurrentLanguage { get; set; }
+    public event Action<Language> OnLanguageChanged;
+    public void ReloadLanguage() => OnLanguageChanged?.Invoke(CurrentLanguage);
+
+    public Language DefaultLanguage { get; private set; }
+
+    public void LoadData()
+    {
+        Newtonsoft.Json.JsonSerializer serializer = new();
+        
+        if (!File.Exists(LocalizationDataPath))
+        {
+            throw new FileNotFoundException("Localization data file not found.", LocalizationDataPath);
+        }
+        
+        using StreamReader reader = new(LocalizationDataPath);
+        LocalizationData = serializer.Deserialize<LocalizationData>(new Newtonsoft.Json.JsonTextReader(reader));
+            
+        if (LocalizationData is null)
+        {
+            throw new InvalidDataException("Localization data is null.");
+        }
+        
+        if (LocalizationData.Languages is null || LocalizationData.Languages.Length == 0)
+        {
+            throw new InvalidDataException("Localization data does not contain any languages.");
+        }
+
+        DefaultLanguage = LoadLanguageInternal(LocalizationData.Languages[0]);
+        
+        string currentLanguageCode = IPreferences.Current.GetPreference<string>("LanguageCode");
+
+        int languageIndex = 0;
+        
+        for (int i = 0; i < LocalizationData.Languages.Length; i++)
+        {
+            if (LocalizationData.Languages[i].Code == currentLanguageCode)
+            {
+                languageIndex = i;
+                break;
+            }
+        }
+        
+        LoadLanguage(LocalizationData.Languages[languageIndex]);
+    }
+
+    public void LoadLanguage(LanguageData languageData)
+    {
+        if (languageData is null)
+        {
+            throw new ArgumentNullException(nameof(languageData));
+        }
+        
+        if(languageData.Code == CurrentLanguage?.LanguageData.Code)
+        {
+            return;
+        }
+        
+        bool firstLoad = CurrentLanguage is null;
+
+        CurrentLanguage = LoadLanguageInternal(languageData);
+
+        if (!firstLoad)
+        {
+            OnLanguageChanged?.Invoke(CurrentLanguage);
+        }
+    }
+
+    public void LoadDebugKeys(Dictionary<string, string> languageKeys, bool rightToLeft)
+    {
+        debugLanguage = new Language(
+            new LanguageData
+        {
+            Code = "debug",
+            Name = "Debug"
+        }, languageKeys, rightToLeft);
+
+        CurrentLanguage = debugLanguage;
+        
+        OnLanguageChanged?.Invoke(debugLanguage);
+    }
+
+    private Language LoadLanguageInternal(LanguageData languageData)
+    {
+        string localePath = Path.Combine("Data", "Localization", "Languages", languageData.LocaleFileName);
+
+        if (!File.Exists(localePath))
+        {
+            throw new FileNotFoundException("Locale file not found.", localePath);
+        }
+
+        Newtonsoft.Json.JsonSerializer serializer = new();
+        using StreamReader reader = new(localePath);
+        Dictionary<string, string> locale =
+            serializer.Deserialize<Dictionary<string, string>>(new Newtonsoft.Json.JsonTextReader(reader));
+
+        if (locale is null)
+        {
+            throw new InvalidDataException("Locale is null.");
+        }
+
+        return new(languageData, locale, languageData.RightToLeft);
+    }
+}

+ 112 - 0
src/PixiEditor/Localization/LocalizedString.cs

@@ -0,0 +1,112 @@
+using System.Text;
+using PixiEditor.Models.Enums;
+
+namespace PixiEditor.Localization;
+
+public struct LocalizedString
+{
+    private string key;
+
+    public string Key
+    {
+        get => key;
+        set
+        {
+            key = value;
+            #if DEBUG_LOCALIZATION
+            Value = key;
+            #else
+            Value = ViewModelMain.Current.DebugSubViewModel?.LocalizationKeyShowMode switch
+            {
+                LocalizationKeyShowMode.Key => Key,
+                LocalizationKeyShowMode.ValueKey => $"{GetValue(value)} ({Key})",
+                LocalizationKeyShowMode.LALALA => $"#~{GetLongString(GetValue(value).Count(x => x == ' ') + 1)}{Math.Abs(Key.GetHashCode()).ToString()[..2]}~#",
+                _ => GetValue(value)
+            };
+            #endif
+        }
+    }
+    public string Value { get; private set; }
+
+    public object[] Parameters { get; set; }
+
+    public LocalizedString(string key)
+    {
+        Key = key;
+    }
+
+    public LocalizedString(string key, params object[] parameters)
+    {
+        Parameters = parameters;
+        Key = key;
+    }
+
+    public override string ToString()
+    {
+        return Value;
+    }
+
+    private string GetValue(string localizationKey)
+    {
+        if (string.IsNullOrEmpty(localizationKey))
+        {
+            return localizationKey;
+        }
+        
+        ILocalizationProvider localizationProvider = ILocalizationProvider.Current;
+        if (localizationProvider?.LocalizationData == null)
+        {
+            return localizationKey;
+        }
+
+        if (!localizationProvider.CurrentLanguage.Locale.ContainsKey(localizationKey))
+        {
+            Language defaultLanguage = localizationProvider.DefaultLanguage;
+
+            if (localizationProvider.CurrentLanguage == defaultLanguage || !defaultLanguage.Locale.ContainsKey(localizationKey))
+            {
+                return localizationKey;
+            }
+
+            return ApplyParameters(defaultLanguage.Locale[localizationKey]);
+        }
+
+
+        return ApplyParameters(ILocalizationProvider.Current.CurrentLanguage.Locale[localizationKey]);
+    }
+
+    private string GetLongString(int length) => string.Join(' ', Enumerable.Repeat("LaLaLaLaLa", length));
+
+    private string ApplyParameters(string value)
+    {
+        if (Parameters == null || Parameters.Length == 0)
+        {
+            return value;
+        }
+
+        try
+        {
+            var executedParameters = new object[Parameters.Length];
+            for (var i = 0; i < Parameters.Length; i++)
+            {
+                var parameter = Parameters[i];
+                object objToExecute = parameter;
+                if (parameter is LocalizedString str)
+                {
+                    objToExecute = new LocalizedString(str.Key, str.Parameters).Value;
+                }
+
+                executedParameters[i] = objToExecute;
+            }
+
+            return string.Format(value, executedParameters);
+        }
+        catch (FormatException)
+        {
+            return value;
+        }
+    }
+
+    public static implicit operator LocalizedString(string key) => new(key);
+    public static implicit operator string(LocalizedString localizedString) => localizedString.Value;
+}

Some files were not shown because too many files changed in this diff