Browse Source

Merge branch 'master' into development

Krzysztof Krysiński 3 weeks ago
parent
commit
a4c0afdf1a
43 changed files with 1919 additions and 1353 deletions
  1. 28 19
      src/ChunkyImageLib/Operations/EllipseOperation.cs
  2. 86 22
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  3. 1 1
      src/ColorPicker
  4. 1 1
      src/Directory.Build.props
  5. 1 1
      src/Drawie
  6. 1 1
      src/PixiDocks
  7. 166 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs
  8. 2 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  10. 56 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/SliceTextNode.cs
  11. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextIndexOfNode.cs
  12. 39 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextInfoNode.cs
  13. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs
  14. 28 31
      src/PixiEditor.PixiAuth/PixiAuthClient.cs
  15. BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  16. 3 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  17. 3 0
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  18. 2 2
      src/PixiEditor.UI.Common/Fonts/defs.svg
  19. 1146 1133
      src/PixiEditor/Data/Localization/Languages/en.json
  20. 6 0
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  21. 11 1
      src/PixiEditor/Models/Commands/ShortcutsTemplate.cs
  22. 3 0
      src/PixiEditor/Models/Commands/Templates/Providers/Parsers/KeyParser.cs
  23. 1 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  24. 3 0
      src/PixiEditor/Models/EnumTranslations.cs
  25. 7 1
      src/PixiEditor/Models/IO/CustomDocumentFormats/FontDocumentBuilder.cs
  26. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  27. 3 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  28. 1 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  29. 11 0
      src/PixiEditor/ViewModels/Document/Nodes/Effects/PosterizationNodeViewModel.cs
  30. 24 0
      src/PixiEditor/ViewModels/Document/Nodes/Text/SliceTextNodeViewModel.cs
  31. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/Text/TextIndexOfNodeViewModel.cs
  32. 7 0
      src/PixiEditor/ViewModels/Document/Nodes/Text/TextInfoNodeViewModel.cs
  33. 43 75
      src/PixiEditor/ViewModels/SettingsWindowViewModel.cs
  34. 19 3
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  35. 19 2
      src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs
  36. 36 26
      src/PixiEditor/Views/Main/DocumentPreview.axaml
  37. 1 1
      src/PixiEditor/Views/Nodes/ConnectionRenderer.cs
  38. 1 0
      src/PixiEditor/Views/Nodes/Properties/BooleanPropertyView.axaml
  39. 11 0
      src/PixiEditor/Views/Overlays/Overlay.cs
  40. 22 19
      src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs
  41. 20 0
      src/PixiEditor/Views/Rendering/Scene.cs
  42. 44 5
      src/PixiEditor/Views/Shortcuts/ImportShortcutTemplatePopup.axaml.cs
  43. 1 1
      tests/Directory.Build.props

+ 28 - 19
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -8,6 +8,7 @@ using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;
+
 internal class EllipseOperation : IMirroredDrawOperation
 internal class EllipseOperation : IMirroredDrawOperation
 {
 {
     public bool IgnoreEmptyChunks => false;
     public bool IgnoreEmptyChunks => false;
@@ -21,14 +22,15 @@ internal class EllipseOperation : IMirroredDrawOperation
     private bool init = false;
     private bool init = false;
     private VectorPath? outerPath;
     private VectorPath? outerPath;
     private VectorPath? innerPath;
     private VectorPath? innerPath;
-    
+
     private VectorPath? ellipseOutline;
     private VectorPath? ellipseOutline;
     private VecF[]? ellipse;
     private VecF[]? ellipse;
     private VecF[]? ellipseFill;
     private VecF[]? ellipseFill;
     private RectI? ellipseFillRect;
     private RectI? ellipseFillRect;
     private bool antialiased;
     private bool antialiased;
 
 
-    public EllipseOperation(RectD location, Paintable strokePaintable, Paintable fillPaintable, float strokeWidth, double rotationRad,
+    public EllipseOperation(RectD location, Paintable strokePaintable, Paintable fillPaintable, float strokeWidth,
+        double rotationRad,
         bool antiAliased, Paint? paint = null)
         bool antiAliased, Paint? paint = null)
     {
     {
         this.location = location;
         this.location = location;
@@ -92,7 +94,7 @@ internal class EllipseOperation : IMirroredDrawOperation
 
 
         if (antialiased)
         if (antialiased)
         {
         {
-            DrawAntiAliased(surf);   
+            DrawAntiAliased(surf);
         }
         }
         else
         else
         {
         {
@@ -109,22 +111,26 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
         {
             if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             {
             {
+                RectD rect = (RectD)ellipseFillRect!.Value;
+                fillPaintable.Bounds = location;
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 {
                 {
                     paint.SetPaintable(fillPaintable);
                     paint.SetPaintable(fillPaintable);
                     surf.Canvas.DrawPoints(PointMode.Lines, ellipseFill!, paint);
                     surf.Canvas.DrawPoints(PointMode.Lines, ellipseFill!, paint);
-                    surf.Canvas.DrawRect((RectD)ellipseFillRect!.Value, paint);
+                    surf.Canvas.DrawRect(rect, paint);
                 }
                 }
-                
+
                 paint.SetPaintable(strokeWidth <= 0 ? fillPaintable : strokePaintable);
                 paint.SetPaintable(strokeWidth <= 0 ? fillPaintable : strokePaintable);
                 paint.StrokeWidth = 1f;
                 paint.StrokeWidth = 1f;
                 surf.Canvas.DrawPoints(PointMode.Points, ellipse!, paint);
                 surf.Canvas.DrawPoints(PointMode.Points, ellipse!, paint);
+
+                fillPaintable.Bounds = null;
             }
             }
             else
             else
             {
             {
                 surf.Canvas.Save();
                 surf.Canvas.Save();
                 surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
                 surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
-                
+
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 if (fillPaintable.AnythingVisible || paint.BlendMode != BlendMode.SrcOver)
                 {
                 {
                     paint.SetPaintable(fillPaintable);
                     paint.SetPaintable(fillPaintable);
@@ -151,14 +157,15 @@ internal class EllipseOperation : IMirroredDrawOperation
                 surf.Canvas.Save();
                 surf.Canvas.Save();
                 surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
                 surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
                 surf.Canvas.ClipPath(innerPath!);
                 surf.Canvas.ClipPath(innerPath!);
-                surf.Canvas.DrawPaintable(fillPaintable, paint.BlendMode);
+                surf.Canvas.DrawPaintable(fillPaintable, paint.BlendMode, location);
                 surf.Canvas.Restore();
                 surf.Canvas.Restore();
             }
             }
+
             surf.Canvas.Save();
             surf.Canvas.Save();
             surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
             surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
             surf.Canvas.ClipPath(outerPath!);
             surf.Canvas.ClipPath(outerPath!);
             surf.Canvas.ClipPath(innerPath!, ClipOperation.Difference);
             surf.Canvas.ClipPath(innerPath!, ClipOperation.Difference);
-            surf.Canvas.DrawPaintable(strokePaintable, paint.BlendMode);
+            surf.Canvas.DrawPaintable(strokePaintable, paint.BlendMode, location);
             surf.Canvas.Restore();
             surf.Canvas.Restore();
         }
         }
     }
     }
@@ -167,24 +174,24 @@ internal class EllipseOperation : IMirroredDrawOperation
     {
     {
         surf.Canvas.Save();
         surf.Canvas.Save();
         surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
         surf.Canvas.RotateRadians((float)rotation, (float)location.Center.X, (float)location.Center.Y);
-        
+
         paint.IsAntiAliased = false;
         paint.IsAntiAliased = false;
         paint.SetPaintable(fillPaintable);
         paint.SetPaintable(fillPaintable);
         paint.Style = PaintStyle.Fill;
         paint.Style = PaintStyle.Fill;
-        
+
         RectD fillRect = ((RectD)location).Inflate(-strokeWidth / 2f);
         RectD fillRect = ((RectD)location).Inflate(-strokeWidth / 2f);
-        
+
         surf.Canvas.DrawOval(fillRect.Center, fillRect.Size / 2f, paint);
         surf.Canvas.DrawOval(fillRect.Center, fillRect.Size / 2f, paint);
 
 
         paint.IsAntiAliased = true;
         paint.IsAntiAliased = true;
         paint.SetPaintable(strokeWidth <= 0 ? fillPaintable : strokePaintable);
         paint.SetPaintable(strokeWidth <= 0 ? fillPaintable : strokePaintable);
         paint.Style = PaintStyle.Stroke;
         paint.Style = PaintStyle.Stroke;
         paint.StrokeWidth = strokeWidth <= 0 ? 1f : strokeWidth;
         paint.StrokeWidth = strokeWidth <= 0 ? 1f : strokeWidth;
-        
+
         RectD strokeRect = ((RectD)location).Inflate((-strokeWidth / 2f));
         RectD strokeRect = ((RectD)location).Inflate((-strokeWidth / 2f));
-        
+
         surf.Canvas.DrawOval(strokeRect.Center, strokeRect.Size / 2f, paint);
         surf.Canvas.DrawOval(strokeRect.Center, strokeRect.Size / 2f, paint);
-        
+
         surf.Canvas.Restore();
         surf.Canvas.Restore();
     }
     }
 
 
@@ -193,14 +200,15 @@ internal class EllipseOperation : IMirroredDrawOperation
         ShapeCorners corners = new((RectD)location);
         ShapeCorners corners = new((RectD)location);
         corners = corners.AsRotated(rotation, (VecD)location.Center);
         corners = corners.AsRotated(rotation, (VecD)location.Center);
         RectI bounds = (RectI)corners.AABBBounds.RoundOutwards();
         RectI bounds = (RectI)corners.AABBBounds.RoundOutwards();
-        
+
         var chunks = OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
         var chunks = OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
         if (!fillPaintable?.AnythingVisible ?? false)
         if (!fillPaintable?.AnythingVisible ?? false)
         {
         {
-             chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
-                (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2, ChunkyImage.FullChunkSize, rotation));
+            chunks.ExceptWith(OperationHelper.FindChunksFullyInsideEllipse
+            (location.Center, location.Width / 2.0 - strokeWidth * 2, location.Height / 2.0 - strokeWidth * 2,
+                ChunkyImage.FullChunkSize, rotation));
         }
         }
-        
+
         return new AffectedArea(chunks, bounds);
         return new AffectedArea(chunks, bounds);
     }
     }
 
 
@@ -226,7 +234,8 @@ internal class EllipseOperation : IMirroredDrawOperation
             ((IPositionPaintable)finalStrokePaintable).Position = newLocation.Center;
             ((IPositionPaintable)finalStrokePaintable).Position = newLocation.Center;
         }
         }
 
 
-        return new EllipseOperation(newLocation, finalStrokePaintable, finalFillPaintable, strokeWidth, rotation, antialiased, paint);
+        return new EllipseOperation(newLocation, finalStrokePaintable, finalFillPaintable, strokeWidth, rotation,
+            antialiased, paint);
     }
     }
 
 
     public void Dispose()
     public void Dispose()

+ 86 - 22
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -1,7 +1,9 @@
 using ChunkyImageLib.DataHolders;
 using ChunkyImageLib.DataHolders;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Utils;
 using Drawie.Numerics;
 using Drawie.Numerics;
 
 
 namespace ChunkyImageLib.Operations;
 namespace ChunkyImageLib.Operations;
@@ -44,7 +46,7 @@ internal class RectangleOperation : IMirroredDrawOperation
 
 
         if (Data.AntiAliasing)
         if (Data.AntiAliasing)
         {
         {
-            DrawAntiAliased(surf, rect, innerRect, radiusInPx);
+            DrawAntiAliased(surf, rect, radiusInPx);
         }
         }
         else
         else
         {
         {
@@ -56,6 +58,7 @@ internal class RectangleOperation : IMirroredDrawOperation
 
 
     private void DrawPixelPerfect(DrawingSurface surf, RectD rect, RectD innerRect, double radius)
     private void DrawPixelPerfect(DrawingSurface surf, RectD rect, RectD innerRect, double radius)
     {
     {
+        VecD vecInnerRadius = new VecD(Math.Max(0, radius - Data.StrokeWidth));
         // draw fill
         // draw fill
         if (Data.FillPaintable.AnythingVisible)
         if (Data.FillPaintable.AnythingVisible)
         {
         {
@@ -66,10 +69,10 @@ internal class RectangleOperation : IMirroredDrawOperation
             }
             }
             else
             else
             {
             {
-                surf.Canvas.ClipRoundRect(innerRect, new VecD(radius), ClipOperation.Intersect);
+                surf.Canvas.ClipRoundRect(innerRect, vecInnerRadius, ClipOperation.Intersect);
             }
             }
 
 
-            surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode);
+            surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode, rect);
             surf.Canvas.RestoreToCount(saved);
             surf.Canvas.RestoreToCount(saved);
         }
         }
 
 
@@ -84,31 +87,26 @@ internal class RectangleOperation : IMirroredDrawOperation
         {
         {
             VecD vecRadius = new VecD(radius);
             VecD vecRadius = new VecD(radius);
             surf.Canvas.ClipRoundRect(rect, vecRadius, ClipOperation.Intersect);
             surf.Canvas.ClipRoundRect(rect, vecRadius, ClipOperation.Intersect);
-            surf.Canvas.ClipRoundRect(innerRect, vecRadius, ClipOperation.Difference);
+            surf.Canvas.ClipRoundRect(innerRect, vecInnerRadius, ClipOperation.Difference);
         }
         }
 
 
-        surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode);
+        surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode, rect);
     }
     }
 
 
-    private void DrawAntiAliased(DrawingSurface surf, RectD rect, RectD innerRect, double radius)
+    private void DrawAntiAliased(DrawingSurface surf, RectD rect, double radius)
     {
     {
-        surf.Canvas.Save();
-        paint.StrokeWidth = Data.StrokeWidth > 0 ? Data.StrokeWidth : 1;
-        paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
-        paint.Style = PaintStyle.Fill;
-
-        if (radius == 0)
+        // shrink radius too so corners match inner curve
+        // Draw fill first
+        if (Data.FillPaintable != null)
         {
         {
-            surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
-                (float)rect.Height, paint);
+            Data.FillPaintable.Bounds = rect;
         }
         }
-        else
+
+        if (Data.Stroke != null)
         {
         {
-            surf.Canvas.DrawRoundRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
-                (float)rect.Height, (float)radius, (float)radius, paint);
+            Data.Stroke.Bounds = rect;
         }
         }
 
 
-        // draw fill
         if (Data.FillPaintable.AnythingVisible)
         if (Data.FillPaintable.AnythingVisible)
         {
         {
             int saved = surf.Canvas.Save();
             int saved = surf.Canvas.Save();
@@ -116,18 +114,80 @@ internal class RectangleOperation : IMirroredDrawOperation
             paint.StrokeWidth = 0;
             paint.StrokeWidth = 0;
             paint.SetPaintable(Data.FillPaintable);
             paint.SetPaintable(Data.FillPaintable);
             paint.Style = PaintStyle.Fill;
             paint.Style = PaintStyle.Fill;
+            RectD fillRect = rect;
+            double innerRadius = Math.Max(0, radius - Data.StrokeWidth);
+            bool hasStroke = Data is { StrokeWidth: > 0, Stroke.AnythingVisible: true };
+            if (hasStroke)
+            {
+                paint.IsAntiAliased = false;
+                fillRect = rect.Inflate(-Data.StrokeWidth + 0.5);
+                surf.Canvas.ClipRoundRect(fillRect, new VecD(innerRadius), ClipOperation.Intersect);
+            }
+
             if (radius == 0)
             if (radius == 0)
             {
             {
-                surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width, (float)innerRect.Height, paint);
+                surf.Canvas.DrawRect((float)fillRect.Left, (float)fillRect.Top,
+                    (float)fillRect.Width, (float)fillRect.Height, paint);
             }
             }
             else
             else
             {
             {
-                surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
-                    (float)innerRect.Height, (float)radius, (float)radius, paint);
+                if (hasStroke)
+                {
+                    surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode);
+                }
+                else
+                {
+                    surf.Canvas.DrawRoundRect((float)fillRect.Left, (float)fillRect.Top,
+                        (float)fillRect.Width, (float)fillRect.Height,
+                        (float)innerRadius, (float)innerRadius, paint);
+                }
             }
             }
 
 
             surf.Canvas.RestoreToCount(saved);
             surf.Canvas.RestoreToCount(saved);
         }
         }
+
+        bool hasFill = Data.FillPaintable.AnythingVisible;
+
+        // Draw stroke fully inside
+        if (Data.StrokeWidth > 0)
+        {
+            surf.Canvas.Save();
+
+            paint.StrokeWidth = Data.StrokeWidth;
+            paint.SetPaintable(Data.Stroke);
+            paint.Style = PaintStyle.Stroke;
+            paint.IsAntiAliased = Data.AntiAliasing;
+
+            // shrink rect so stroke is fully inside
+            RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
+
+            double innerRadius = Math.Max(0, radius - Data.StrokeWidth / 2f);
+
+            if (radius > 0 && innerRadius <= 0)
+            {
+                innerRadius = 0.0001;
+            }
+
+            if (innerRadius == 0)
+            {
+                surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top,
+                    (float)innerRect.Width, (float)innerRect.Height, paint);
+            }
+            else
+            {
+                surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top,
+                    (float)innerRect.Width, (float)innerRect.Height,
+                    (float)innerRadius, (float)innerRadius, paint);
+            }
+
+            if(Data.FillPaintable != null)
+                Data.FillPaintable.Bounds = null;
+
+            if(Data.Stroke != null)
+                Data.Stroke.Bounds = null;
+
+            surf.Canvas.Restore();
+        }
     }
     }
 
 
     public AffectedArea FindAffectedArea(VecI imageSize)
     public AffectedArea FindAffectedArea(VecI imageSize)
@@ -147,10 +207,14 @@ internal class RectangleOperation : IMirroredDrawOperation
         var chunks =
         var chunks =
             OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle,
             OperationHelper.FindChunksTouchingRectangle(Data.Center, Data.Size.Abs(), Data.Angle,
                 ChunkPool.FullChunkSize);
                 ChunkPool.FullChunkSize);
+
+        VecD radiusShrink = new VecD(Data.CornerRadius * Math.Min(Data.Size.X, Data.Size.Y),
+            Data.CornerRadius * Math.Min(Data.Size.X, Data.Size.Y));
+        VecD innerSize = Data.Size.Abs() - radiusShrink;
         chunks.ExceptWith(
         chunks.ExceptWith(
             OperationHelper.FindChunksFullyInsideRectangle(
             OperationHelper.FindChunksFullyInsideRectangle(
                 Data.Center,
                 Data.Center,
-                Data.Size.Abs() - new VecD(Data.StrokeWidth * 2, Data.StrokeWidth * 2),
+                innerSize - new VecD(Data.StrokeWidth * 2, Data.StrokeWidth * 2),
                 Data.Angle,
                 Data.Angle,
                 ChunkPool.FullChunkSize));
                 ChunkPool.FullChunkSize));
         return new(chunks, affRect);
         return new(chunks, affRect);

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 943e9abbb60b73c4965b947e987dc2696e0b08f8
+Subproject commit f39e02c617d9a4a57b1a911fc4f03bbf7a7fd987

+ 1 - 1
src/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
 <Project>
     <PropertyGroup>
     <PropertyGroup>
         <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
         <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)Custom.ruleset</CodeAnalysisRuleSet>
-		    <AvaloniaVersion>11.3.0</AvaloniaVersion>
+		    <AvaloniaVersion>11.3.5</AvaloniaVersion>
     </PropertyGroup>
     </PropertyGroup>
   
   
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">
   <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(Platform)' == 'x64'">

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit b6c34c96ac5b01abad69604465445270270270d2
+Subproject commit 9e5f6dc3ab03cd67fa157b0952e7bb510c465e8b

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 6e745d0309ad7a00a53f62f2aa362be77903a5fd
+Subproject commit af479aac479fd1a0494cb61fa49a03a3713c96cf

+ 166 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Effects/PosterizationNode.cs

@@ -0,0 +1,166 @@
+using Drawie.Backend.Core;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Shaders;
+using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.ColorSpaces;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
+
+[NodeInfo("Posterization")]
+public class PosterizationNode : RenderNode, IRenderInput
+{
+    public RenderInputProperty Background { get; }
+    public InputProperty<PosterizationMode> Mode { get; }
+    public InputProperty<int> Levels { get; }
+    public InputProperty<ColorSpaceType> PosterizationColorSpace { get; }
+    
+    private Paint paint;
+    private Shader shader;
+    
+    private Shader? lastImageShader;
+    private VecI lastDocumentSize;
+    
+    private string shaderCode = """
+                                 uniform shader iImage;
+                                 uniform int iMode;
+                                 uniform float iLevels;
+                                 
+                                 half posterize(half value, float levels) {
+                                     return clamp(floor(value * (levels - 1) + 0.5) / (levels - 1), 0.0, 1.0);
+                                 }
+                                 
+                                 half4 posterizeRgb(half4 color, float levels) {
+                                     return half4(
+                                         posterize(color.r, levels),
+                                         posterize(color.g, levels),
+                                         posterize(color.b, levels),
+                                         color.a
+                                     );
+                                 }
+                                 
+                                 half4 posterizeLuminance(half4 color, float levels) {
+                                     half lum = dot(color.rgb, half3(0.299, 0.587, 0.114));
+                                     half posterizedLum = posterize(lum, levels);
+                                     return half4(posterizedLum, posterizedLum, posterizedLum, color.a);
+                                 }
+                                 
+                                 half4 main(float2 uv)
+                                 {
+                                    half4 color = iImage.eval(uv);
+                                    half4 result;
+                                    
+                                    if(iMode == 0) {
+                                        result = posterizeRgb(color, iLevels);
+                                    } else if(iMode == 1) {
+                                        result = posterizeLuminance(color, iLevels);
+                                    } 
+                                    
+                                    return result;
+                                 }
+                                 """;
+
+    protected override bool ExecuteOnlyOnCacheChange => true;
+
+    public PosterizationNode()
+    {
+        Background = CreateRenderInput("Background", "BACKGROUND");
+        Mode = CreateInput("Mode", "MODE", PosterizationMode.Rgb);
+        Levels = CreateInput("Levels", "LEVELS", 8)
+            .WithRules(v => v.Min(2).Max(256));
+        PosterizationColorSpace = CreateInput("PosterizationColorSpace", "COLOR_SPACE", ColorSpaceType.Srgb);
+        
+        paint = new Paint();
+        paint.BlendMode = BlendMode.Src;
+        Output.FirstInChain = null;
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        base.OnExecute(context);
+        lastDocumentSize = context.RenderOutputSize;
+        
+        Uniforms uniforms = new Uniforms();
+        uniforms.Add("iImage", new Uniform("iImage", lastImageShader));
+        uniforms.Add("iMode", new Uniform("iMode", (int)Mode.Value));
+        uniforms.Add("iLevels", new Uniform("iLevels", (float)Levels.Value));
+        shader?.Dispose();
+        shader = Shader.Create(shaderCode, uniforms, out _);
+    }
+    
+    protected override void OnPaint(RenderContext context, DrawingSurface surface)
+    {
+        if (Background.Value == null)
+        {
+            return;
+        }
+
+        ColorSpace colorSpace;
+        switch (PosterizationColorSpace.Value)
+        {
+            case ColorSpaceType.Srgb:
+                colorSpace = ColorSpace.CreateSrgb();
+                break;
+            case ColorSpaceType.LinearSrgb:
+                colorSpace = ColorSpace.CreateSrgbLinear();
+                break;
+            case ColorSpaceType.Inherit:
+                colorSpace = context.ProcessingColorSpace;
+                break;
+            default:
+                colorSpace = ColorSpace.CreateSrgb();
+                break;
+        }
+        
+        using Texture temp = Texture.ForProcessing(surface, colorSpace);
+        Background.Value.Paint(context, temp.DrawingSurface);
+        var snapshot = temp.DrawingSurface.Snapshot();
+        
+        lastImageShader?.Dispose();
+        lastImageShader = snapshot.ToShader();
+
+        Uniforms uniforms = new Uniforms();
+        uniforms.Add("iImage", new Uniform("iImage", lastImageShader));
+        uniforms.Add("iMode", new Uniform("iMode", (int)Mode.Value));
+        uniforms.Add("iLevels", new Uniform("iLevels", (float)Levels.Value));
+        shader = shader.WithUpdatedUniforms(uniforms);
+        paint.Shader = shader;
+        snapshot.Dispose();
+        
+        var savedTemp = temp.DrawingSurface.Canvas.Save();
+        temp.DrawingSurface.Canvas.SetMatrix(Matrix3X3.Identity);
+        temp.DrawingSurface.Canvas.DrawRect(0, 0, context.RenderOutputSize.X, context.RenderOutputSize.Y, paint);
+        temp.DrawingSurface.Canvas.RestoreToCount(savedTemp);
+        
+        var saved = surface.Canvas.Save();
+        surface.Canvas.SetMatrix(Matrix3X3.Identity);
+        surface.Canvas.DrawSurface(temp.DrawingSurface, 0, 0);
+        surface.Canvas.RestoreToCount(saved);
+    }
+    
+    public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
+    {
+        return new RectD(0, 0, lastDocumentSize.X, lastDocumentSize.Y);
+    }
+
+    public override bool RenderPreview(DrawingSurface renderOn, RenderContext context, string elementToRenderName)
+    {
+        OnPaint(context, renderOn);
+        return true;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new PosterizationNode();
+    }
+}
+
+public enum PosterizationMode
+{
+    Rgb = 0,
+    Luminance = 1,
+}

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/SampleImageNode.cs

@@ -58,11 +58,11 @@ public class SampleImageNode : Node
 
 
         if (SampleMode.Value == ColorSampleMode.ColorManaged)
         if (SampleMode.Value == ColorSampleMode.ColorManaged)
         {
         {
-            color = Image.Value.GetSRGBPixel(pixelCoordinate);
+            color = Image.Value.GetSrgbPixel(pixelCoordinate);
         }
         }
         else
         else
         {
         {
-            color = Image.Value.GetPixel(pixelCoordinate);
+            color = Image.Value.GetRawPixel(pixelCoordinate);
         }
         }
 
 
         return new Half4("") { ConstantValue = color };
         return new Half4("") { ConstantValue = color };

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -15,7 +15,7 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
 
     public override ShapeCorners TransformationCorners =>
     public override ShapeCorners TransformationCorners =>
-        new ShapeCorners(VisualAABB).WithMatrix(TransformationMatrix);
+        new ShapeCorners(Path.TightBounds).WithMatrix(TransformationMatrix);
 
 
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
     public StrokeCap StrokeLineCap { get; set; } = StrokeCap.Round;
 
 

+ 56 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/SliceTextNode.cs

@@ -0,0 +1,56 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Text;
+
+[NodeInfo("SliceText")]
+public class SliceTextNode : Node
+{
+    public OutputProperty<string> SlicedText { get; }
+    
+    public InputProperty<bool> UseLength { get; }
+    
+    public InputProperty<string?> Text { get; }
+    
+    public InputProperty<int> Index { get; }
+    
+    public InputProperty<int> Length { get; }
+    
+    public SliceTextNode()
+    {
+        SlicedText = CreateOutput("SlicedText", "TEXT", string.Empty);
+        Text = CreateInput("Text", "TEXT", string.Empty);
+        UseLength = CreateInput("UseLength", "TEXT_SLICE_USE_LENGTH", true);
+        
+        Index = CreateInput("Index", "INDEX_START_AT", 0)
+            .WithRules(x => x.Min(0));
+        
+        Length = CreateInput("Length", "LENGTH", 1)
+            .WithRules(x => x.Min(0));
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        var text = Text.Value;
+
+        if (text == null)
+        {
+            SlicedText.Value = string.Empty;
+            return;
+        }
+
+        var startIndex = Math.Clamp(Index.Value, 0, text.Length);
+
+        if (!UseLength.Value)
+        {
+            SlicedText.Value = text.Substring(startIndex);
+            return;
+        }
+
+        var length = Math.Clamp(Length.Value, 0, text.Length - startIndex);
+        
+        SlicedText.Value = text.Substring(startIndex, length);
+    }
+
+    public override Node CreateCopy() =>
+        new SliceTextNode();
+}

+ 51 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextIndexOfNode.cs

@@ -0,0 +1,51 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Text;
+
+[NodeInfo("CharacterPosition")]
+public class TextIndexOfNode : Node
+{
+    public OutputProperty<int> FirstIndex { get; }
+    
+    public OutputProperty<int> LastIndex { get; }
+    
+    public InputProperty<bool> MatchCase { get; }
+    
+    public InputProperty<string?> Text { get; }
+    
+    public InputProperty<string?> SearchText { get; }
+
+    public TextIndexOfNode()
+    {
+        FirstIndex = CreateOutput("FirstIndex", "FIRST_POSITION", -1);
+        LastIndex = CreateOutput("LastIndex", "LAST_POSITION", -1);
+        
+        MatchCase = CreateInput("MatchCase", "MATCH_CASE", false);
+        Text = CreateInput("Text", "TEXT", string.Empty);
+        SearchText = CreateInput("SearchText", "SEARCH_TEXT", string.Empty);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        var comparisonMode = MatchCase.Value
+            ? StringComparison.InvariantCulture
+            : StringComparison.InvariantCultureIgnoreCase;
+
+        var text = Text.Value;
+        var searchText = SearchText.Value;
+
+        if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(searchText))
+        {
+            FirstIndex.Value = -1;
+            LastIndex.Value = -1;
+            
+            return;
+        }
+
+        FirstIndex.Value = text.IndexOf(searchText, comparisonMode);
+        LastIndex.Value = text.LastIndexOf(searchText, comparisonMode);
+    }
+
+    public override Node CreateCopy() =>
+        new TextIndexOfNode();
+}

+ 39 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Text/TextInfoNode.cs

@@ -0,0 +1,39 @@
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Text;
+
+[NodeInfo("TextInfo")]
+public class TextInfoNode : Node
+{
+    public OutputProperty<int> Length { get; }
+    
+    public OutputProperty<int> LineCount { get; }
+    
+    public InputProperty<string> Text { get; }
+
+    public TextInfoNode()
+    {
+        Length = CreateOutput("Length", "TEXT_LENGTH", 0);
+        LineCount = CreateOutput("LineCount", "TEXT_LINE_COUNT", 0);
+        
+        Text = CreateInput("Text", "TEXT", string.Empty);
+    }
+    
+    protected override void OnExecute(RenderContext context)
+    {
+        var text = Text.Value;
+
+        if (string.IsNullOrEmpty(text))
+        {
+            Length.Value = 0;
+            LineCount.Value = 0;
+            return;
+        }
+        
+        Length.Value = text.Length;
+        LineCount.Value = text.AsSpan().Count('\n');
+    }
+
+    public override Node CreateCopy() =>
+        new TextInfoNode();
+}

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelected_UpdateableChange.cs

@@ -91,7 +91,7 @@ internal class TransformSelected_UpdateableChange : InterruptableUpdateableChang
 
 
         if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
         if (memberData.Count == 1 && firstLayer is VectorLayerNode vectorLayer)
         {
         {
-            tightBounds = vectorLayer.EmbeddedShapeData?.VisualAABB ?? default;
+            tightBounds = vectorLayer.EmbeddedShapeData?.GeometryAABB ?? default;
         }
         }
 
 
         for (var i = 1; i < memberData.Count; i++)
         for (var i = 1; i < memberData.Count; i++)

+ 28 - 31
src/PixiEditor.PixiAuth/PixiAuthClient.cs

@@ -42,10 +42,6 @@ public class PixiAuthClient
                 return sessionId;
                 return sessionId;
             }
             }
         }
         }
-        else if (response.StatusCode == HttpStatusCode.BadRequest)
-        {
-            throw new BadRequestException(await response.Content.ReadAsStringAsync());
-        }
         else if (response.StatusCode >= HttpStatusCode.InternalServerError)
         else if (response.StatusCode >= HttpStatusCode.InternalServerError)
         {
         {
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
@@ -68,6 +64,10 @@ public class PixiAuthClient
         {
         {
             throw new ForbiddenException("FORBIDDEN");
             throw new ForbiddenException("FORBIDDEN");
         }
         }
+        else if (response.StatusCode >= HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
 
 
         return null;
         return null;
     }
     }
@@ -97,10 +97,6 @@ public class PixiAuthClient
                 return (token, expirationDate);
                 return (token, expirationDate);
             }
             }
         }
         }
-        else if (response.StatusCode == HttpStatusCode.BadRequest)
-        {
-            throw new BadRequestException(await response.Content.ReadAsStringAsync());
-        }
         else if (response.StatusCode >= HttpStatusCode.InternalServerError)
         else if (response.StatusCode >= HttpStatusCode.InternalServerError)
         {
         {
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
@@ -113,6 +109,10 @@ public class PixiAuthClient
         {
         {
             throw new ForbiddenException("FORBIDDEN");
             throw new ForbiddenException("FORBIDDEN");
         }
         }
+        else if (response.StatusCode >= HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
 
 
         return (null, null);
         return (null, null);
     }
     }
@@ -157,18 +157,14 @@ public class PixiAuthClient
                 return (token, expirationDate);
                 return (token, expirationDate);
             }
             }
         }
         }
-        else if (response.StatusCode == HttpStatusCode.Forbidden)
-        {
-            throw new ForbiddenException("SESSION_NOT_VALID");
-        }
-        else if (response.StatusCode == HttpStatusCode.BadRequest)
-        {
-            throw new BadRequestException(await response.Content.ReadAsStringAsync());
-        }
         else if ((int)response.StatusCode >= 500)
         else if ((int)response.StatusCode >= 500)
         {
         {
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
         }
         }
+        else if (response.StatusCode == HttpStatusCode.Forbidden)
+        {
+            throw new ForbiddenException("SESSION_NOT_VALID");
+        }
         else if (response.StatusCode == HttpStatusCode.Unauthorized)
         else if (response.StatusCode == HttpStatusCode.Unauthorized)
         {
         {
             throw new UnauthorizedAccessException("UNAUTHORIZED");
             throw new UnauthorizedAccessException("UNAUTHORIZED");
@@ -177,6 +173,10 @@ public class PixiAuthClient
         {
         {
             throw new ForbiddenException("FORBIDDEN");
             throw new ForbiddenException("FORBIDDEN");
         }
         }
+        else if (response.StatusCode >= HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
 
 
         return (null, null);
         return (null, null);
     }
     }
@@ -222,15 +222,14 @@ public class PixiAuthClient
 
 
         var response = await httpClient.SendAsync(request);
         var response = await httpClient.SendAsync(request);
 
 
-        if (response.StatusCode == HttpStatusCode.BadRequest)
-        {
-            throw new BadRequestException(await response.Content.ReadAsStringAsync());
-        }
-
         if (response.StatusCode >= HttpStatusCode.InternalServerError)
         if (response.StatusCode >= HttpStatusCode.InternalServerError)
         {
         {
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
         }
         }
+        if (response.StatusCode >= HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
 
 
         if (response.StatusCode == HttpStatusCode.OK)
         if (response.StatusCode == HttpStatusCode.OK)
         {
         {
@@ -256,15 +255,14 @@ public class PixiAuthClient
 
 
         var response = await httpClient.SendAsync(request);
         var response = await httpClient.SendAsync(request);
 
 
-        if (response.StatusCode == HttpStatusCode.BadRequest)
-        {
-            throw new BadRequestException(await response.Content.ReadAsStringAsync());
-        }
-
         if (response.StatusCode >= HttpStatusCode.InternalServerError)
         if (response.StatusCode >= HttpStatusCode.InternalServerError)
         {
         {
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
         }
         }
+        if (response.StatusCode >= HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
 
 
         if (response.StatusCode == HttpStatusCode.OK)
         if (response.StatusCode == HttpStatusCode.OK)
         {
         {
@@ -294,15 +292,14 @@ public class PixiAuthClient
 
 
         var response = await httpClient.SendAsync(request);
         var response = await httpClient.SendAsync(request);
 
 
-        if (response.StatusCode == HttpStatusCode.BadRequest)
-        {
-            throw new BadRequestException(await response.Content.ReadAsStringAsync());
-        }
-
         if (response.StatusCode >= HttpStatusCode.InternalServerError)
         if (response.StatusCode >= HttpStatusCode.InternalServerError)
         {
         {
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
             throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
         }
         }
+        if (response.StatusCode >= HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
 
 
         if (response.StatusCode == HttpStatusCode.OK)
         if (response.StatusCode == HttpStatusCode.OK)
         {
         {

BIN
src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf


+ 3 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -159,7 +159,9 @@
             <system:String x:Key="icon-select-all">&#xE970;</system:String>
             <system:String x:Key="icon-select-all">&#xE970;</system:String>
             <system:String x:Key="icon-separate-vector">&#xE91C;</system:String>
             <system:String x:Key="icon-separate-vector">&#xE91C;</system:String>
             <system:String x:Key="icon-settings">&#xE971;</system:String>
             <system:String x:Key="icon-settings">&#xE971;</system:String>
+            <system:String x:Key="icon-shredder">&#xE9CA;</system:String>
             <system:String x:Key="icon-shuffle">&#xE992;</system:String>
             <system:String x:Key="icon-shuffle">&#xE992;</system:String>
+            <system:String x:Key="icon-slice">&#xE9CC;</system:String>
             <system:String x:Key="icon-sliders">&#xE972;</system:String>
             <system:String x:Key="icon-sliders">&#xE972;</system:String>
             <system:String x:Key="icon-snapping">&#xE9A7;</system:String>
             <system:String x:Key="icon-snapping">&#xE9A7;</system:String>
             <system:String x:Key="icon-spline-chart">&#xE9BE;</system:String>
             <system:String x:Key="icon-spline-chart">&#xE9BE;</system:String>
@@ -198,6 +200,7 @@
             <system:String x:Key="icon-upload-cloud">&#xE98E;</system:String>
             <system:String x:Key="icon-upload-cloud">&#xE98E;</system:String>
             <system:String x:Key="icon-user">&#xE97B;</system:String>
             <system:String x:Key="icon-user">&#xE97B;</system:String>
             <system:String x:Key="icon-vector-pen">&#xE965;</system:String>
             <system:String x:Key="icon-vector-pen">&#xE965;</system:String>
+            <system:String x:Key="icon-whole-word">&#xE9CB;</system:String>
             <system:String x:Key="icon-write">&#xE988;</system:String>
             <system:String x:Key="icon-write">&#xE988;</system:String>
             <system:String x:Key="icon-x-flip">&#xE900;</system:String>
             <system:String x:Key="icon-x-flip">&#xE900;</system:String>
             <system:String x:Key="icon-x-symmetry">&#xE980;</system:String>
             <system:String x:Key="icon-x-symmetry">&#xE980;</system:String>

+ 3 - 0
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -155,7 +155,9 @@ public static partial class PixiPerfectIcons
     public const string SelectAll = "\uE970";
     public const string SelectAll = "\uE970";
     public const string SeparateVector = "\uE91C";
     public const string SeparateVector = "\uE91C";
     public const string Settings = "\uE971";
     public const string Settings = "\uE971";
+    public const string Shredder = "\uE9CA";
     public const string Shuffle = "\uE992";
     public const string Shuffle = "\uE992";
+    public const string Slice = "\uE9CC";
     public const string Sliders = "\uE972";
     public const string Sliders = "\uE972";
     public const string Snapping = "\uE9A7";
     public const string Snapping = "\uE9A7";
     public const string SplineChart = "\uE9BE";
     public const string SplineChart = "\uE9BE";
@@ -194,6 +196,7 @@ public static partial class PixiPerfectIcons
     public const string UploadCloud = "\uE98E";
     public const string UploadCloud = "\uE98E";
     public const string User = "\uE97B";
     public const string User = "\uE97B";
     public const string VectorPen = "\uE965";
     public const string VectorPen = "\uE965";
+    public const string WholeWord = "\uE9CB";
     public const string Write = "\uE988";
     public const string Write = "\uE988";
     public const string XFlip = "\uE900";
     public const string XFlip = "\uE900";
     public const string XSymmetry = "\uE980";
     public const string XSymmetry = "\uE980";

File diff suppressed because it is too large
+ 2 - 2
src/PixiEditor.UI.Common/Fonts/defs.svg


+ 1146 - 1133
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1,1135 +1,1148 @@
 {
 {
-    "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",
-    "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",
-    "OPEN_DOCUMENTATION": "Open documentation",
-    "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",
-    "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
-    "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
-    "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
-    "VERSION": "Version {0}",
-    "BUILD_ID": "Build ID: {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_ALL_SELECTED": "Delete all selected layers/folders",
-    "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Delete all selected layers and/or folders",
-    "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_SELECTED": "Delete selected",
-    "DELETE_SELECTED_DESCRIPTIVE": "Delete selected element (layer, pixels, etc.)",
-    "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_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",
-    "GRIDLINES_SIZE": "Grid Size",
-    "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_PREVIEW_WINDOW": "Open preview 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": "Select and transform layers ({0}).",
-    "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",
-    "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",
-    "PREVIEW_TITLE": "Preview",
-    "NORMAL_BLEND_MODE": "Normal",
-    "ERASE_BLEND_MODE": "Erase",
-    "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",
-    "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 member 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_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?",
-    "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",
-    "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "Click and hold mouse to move pixels in selected 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.",
-    "COULD_NOT_SAVE_PALETTE": "There was an error while saving the palette.",
-    "NO_COLORS_TO_SAVE": "There are no colors to save.",
-    "CANVAS": "Canvas",
-    "SINGLE_LAYER": "Single Layer",
-    "CHOOSE": "Choose",
-    "REMOVE": "Remove",
-    "FILE_FORMAT_NOT_ASEPRITE_KEYS": "File is not a \".aseprite-keys\" file",
-    "FILE_HAS_INVALID_SHORTCUT": "The file contains an invalid shortcut",
-    "FILE_EXTENSION_NOT_SUPPORTED": "The file type '{0}' is not supported",
-    "ERROR_READING_FILE": "Error while reading the file",
-    "DISCARD_PALETTE": "Discard palette",
-    "DISCARD_PALETTE_CONFIRMATION": "Are you sure you want to discard current palette? This cannot be undone.",
-    "IMPORT_AS_NEW_LAYER": "Import as new layer",
-    "PASTE_AS_PRIMARY_COLOR": "Paste as primary color",
-    "IMPORT_AS_NEW_FILE": "Import as new file",
-    "IMPORT_PALETTE_FILE": "Import palette file",
-    "IMPORT_MULTIPLE_PALETTE_COLORS": "Import colors into palette",
-    "IMPORT_SINGLE_PALETTE_COLOR": "Import color into palette",
-    "IMPORT_AS_REFERENCE_LAYER": "Import as reference layer",
-    "NAVIGATOR_PICK_ACTION_DISPLAY": "Right-click to pick color, Shift-right-click to copy color to clipboard",
-    "OPEN_FILE_FROM_CLIPBOARD": "Open from clipboard",
-    "OPEN_FILE_FROM_CLIPBOARD_DESCRIPTIVE": "Open from clipboard",
-    "OPEN_LOCALIZATION_DATA": "Do you want to open the LocalizationData.json?\nThe updated date has been put in the clipboard.\nNote that changes wont be applied until a restart",
-    "DOWNLOADING_LANGUAGE_FAILED": "Downloading language failed.\nAPI Key might have been overused.",
-    "LOCALIZATION_DATA_NOT_FOUND": "Localization data path not found",
-    "APPLY": "Apply",
-    "UPDATE_SOURCE": "Update source",
-    "COPY_TO_CLIPBOARD": "Copy to clipboard",
-    "LANGUAGE_FILE_NOT_FOUND": "Language file not found.\nLooking for {0}",
-    "PROJECT_ROOT_NOT_FOUND": "PixiEditor Project root not found.\nLooking for PixiEditor.csproj",
-    "LOCALIZATION_FOLDER_NOT_FOUND": "Localization folder not found.\nLooking for /Data/Localization",
-    "SELECT_A_LANGUAGE": "Select a language",
-    "DONE": "Done",
-    "SOURCE_UNSET_OR_MISSING": "Source missing/unset",
-    "SOURCE_NEWER": "Source newer",
-    "SOURCE_UP_TO_DATE": "Source is up to date",
-    "SOURCE_OLDER": "Cloud newer",
-    "COLOR_PICKER_ACTION_DISPLAY_REFERENCE_ONLY": "Click to pick colors from the reference layer.",
-    "COLOR_PICKER_ACTION_DISPLAY_CANVAS_ONLY": "Click to pick colors from the canvas.",
-    "LOCALIZATION_DEBUG_WINDOW_TITLE": "Localization Debug Window",
-    "SHORTCUTS_TITLE": "Shortcuts",
-    "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_PERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
-    "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
-    "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally. Drag outside handles to rotate.",
-    "TRANSFORM_ACTION_DISPLAY_SCALE_NOROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally.",
-    "LOCAL_PALETTE_SOURCE_NAME": "Local",
-    "ERROR_FORBIDDEN_UNIQUE_NAME": "Extension unique name cannot start with 'pixieditor'.",
-    "ERROR_MISSING_METADATA": "Extension metadata key '{0}' is missing.",
-    "ERROR_NO_CLASS_ENTRY": "Extension class entry is missing on path '{0}'.",
-    "ERROR_NO_ENTRY_ASSEMBLY": "Extension entry assembly is missing on path '{0}'.",
-    "ERROR_MISSING_ADDITIONAL_CONTENT": "Your current setup doesn't allow loading this extension. Perhaps you don't own it or don't have it installed. You can purchase it here '{0}'.",
-    "BUY_SUPPORTER_PACK": "Buy Supporter Pack",
-    "NEWS": "News",
-    "DISABLE_NEWS_PANEL": "Disable News panel in startup window",
-    "FAILED_FETCH_NEWS": "Failed to fetch news",
-    "CROP_TO_SELECTION": "Crop to selection",
-    "CROP_TO_SELECTION_DESCRIPTIVE": "Crop image to selection",
-    "SHOW_CONTEXT_MENU": "Show context menu",
-    "ERASE": "Erase",
-    "USE_SECONDARY_COLOR": "Use secondary color",
-    "RIGHT_CLICK_MODE": "Right click mode",
-    "ADD_PRIMARY_COLOR_TO_PALETTE": "Add primary color to palette",
-    "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette",
-    "EXPORT_SAVE_TITLE": "Choose a location to save the image",
-    "BROWSE_DIRECTORY": "Browse Directory",
-    "CRASH_NOT_ALL_DOCUMENTS_RECOVERED_TITLE": "Not all documents were recovered",
-    "CRASH_NOT_ALL_DOCUMENTS_RECOVERED": "Could not recover all documents. Git gud at saving your work.",
-    "SEND": "Send report",
-    "OPEN_DOCKABLE_MENU": "Open Tab",
-    "TIMELINE_TITLE": "Timeline",
-    "EXPORT_IMAGE_HEADER": "Image",
-    "EXPORT_ANIMATION_HEADER": "Animation",
-    "EXPORT_SPRITESHEET_HEADER": "Spritesheet",
-    "PIXI_FILE": "PixiEditor Files",
-    "PNG_FILE": "PNG Images",
-    "JPEG_FILE": "JPEG Images",
-    "WEBP_FILE": "WebP Images",
-    "GIF_FILE": "GIFs",
-    "BMP_FILE": "BMP Images",
-    "IMAGE_FILES": "Image Files",
-    "VIDEO_FILES": "Video Files",
-    "OPEN_TYPE_FONT": "OpenType Fonts",
-    "TRUE_TYPE_FONT": "TrueType Fonts",
-    "SVG_FILE": "Scalable Vector Graphics",
-    "MP4_FILE": "MP4 Videos",
-    "COLUMNS": "Columns",
-    "ROWS": "Rows",
-    "BACKGROUND": "Background",
-    "OPACITY": "Opacity",
-    "IS_VISIBLE": "Is visible",
-    "BLEND_MODE": "Blend mode",
-    "MASK": "Mask",
-    "MASK_IS_VISIBLE": "Mask is visible",
-    "OUTPUT": "Output",
-    "INPUT": "Input",
-    "NODE_GRAPH_TITLE": "Graph View",
-    "CONTENT": "Content",
-    "RADIUS": "Radius",
-    "STROKE_COLOR": "Stroke color",
-    "STROKE_WIDTH": "Stroke width",
-    "FILL_COLOR": "Fill color",
-    "TOP": "Top",
-    "BOTTOM": "Bottom",
-    "CHANNELS_DOCK_TITLE": "Channels",
-    "RED": "Red",
-    "GREEN": "Green",
-    "BLUE": "Blue",
-    "ALPHA": "Alpha",
-    "COLOR": "Color",
-    "COORDINATE": "Coordinate",
-    "VECTOR": "Vector",
-    "MATRIX": "Matrix",
-    "TRANSFORMED": "Transformed",
-    "GRAYSCALE": "Grayscale",
-    "CLAMP": "Clamp",
-    "SIZE": "Size",
-    "NOISE": "Noise",
-    "SCALE": "Scale",
-    "SEED": "Seed",
-    "KERNEL": "Kernel",
-    "KERNEL_VIEW_SUM": "Sum:",
-    "KERNEL_VIEW_SUM_TOOLTIP": "The sum of all values. You likely want to aim for a value of 1 or 0",
-    "GAIN": "Gain",
-    "BIAS": "Bias",
-    "TILE_MODE": "Tile Mode",
-    "ON_ALPHA": "On Alpha",
-    "OUTPUT_NODE": "Output",
-    "NOISE_NODE": "Noise",
-    "ELLIPSE_NODE": "Ellipse",
-    "LINE_NODE": "Line",
-    "LINE_START": "Start",
-    "LINE_END": "End",
-    "RECTANGLE_NODE": "Rectangle",
-    "CREATE_IMAGE_NODE": "Create Image",
-    "FOLDER_NODE": "Folder",
-    "IMAGE_LAYER_NODE": "Image Layer",
-    "KERNEL_FILTER_NODE": "Kernel Filter",
-    "MATH_NODE": "Math",
-    "COLOR_MATRIX_TRANSFORM_FILTER_NODE": "Matrix Transform Filter",
-    "MERGE_NODE": "Merge",
-    "MODIFY_IMAGE_LEFT_NODE": "Begin Modify Image",
-    "MODIFY_IMAGE_RIGHT_NODE": "End Modify Image",
-    "COMBINE_CHANNELS_NODE": "Combine Channels",
-    "COMBINE_COLOR_NODE": "Combine Color",
-    "COMBINE_VECD_NODE": "Combine Vector",
-    "COMBINE_VECI_NODE": "Combine Integer Vector",
-    "SEPARATE_CHANNELS_NODE": "Separate Channels",
-    "SEPARATE_VECD_NODE": "Separate Vector",
-    "SEPARATE_VECI_NODE": "Separate Integer Vector",
-    "SEPARATE_COLOR_NODE": "Separate Color",
-    "TIME_NODE": "Time",
-    "FILTERS": "Filters",
-    "PREVIOUS": "Previous",
-    "FILL": "Fill",
-    "MATH_MODE": "Math Mode",
-    "NOISE_TYPE": "Noise Type",
-    "OCTAVES": "Octaves",
-    "ACTIVE_FRAME": "Active Frame",
-    "NORMALIZED_TIME": "Normalized Time",
-    "WITHOUT_FILTERS": "Without filters",
-    "RAW_LAYER_OUTPUT": "Raw",
-    "EXAMPLE_FILES": "Example Files",
-    "PROCEDURAL_GENERATION": "Procedural Animation",
-    "POND_EXAMPLE": "Pond",
-    "TREE_EXAMPLE": "Windy Tree",
-    "OUTLINE_EXAMPLE": "Automatic Outline",
-    "BETA_ANIMATIONS": "Animations",
-    "SLIME_EXAMPLE": "Animated Slime",
-    "APPLY_FILTER_NODE": "Apply Filter",
-    "FILTER": "Filter",
-    "LERP_NODE": "Lerp",
-    "GRAYSCALE_FILTER_NODE": "Grayscale Filter",
-    "FROM": "From",
-    "TO": "To",
-    "TIME": "Time",
-    "WARMING_UP": "Warming up",
-    "RENDERING_FRAME": "Generating Frame {0}/{1}",
-    "RENDERING_VIDEO": "Rendering Video",
-    "FINISHED": "Finished",
-    "GENERATING_SPRITE_SHEET": "Generating Sprite Sheet",
-    "RENDERING_IMAGE": "Rendering Image",
-    "PROGRESS_POPUP_TITLE": "Progress",
-    "POINTS": "Points",
-    "MIN_DISTANCE": "Min. Distance",
-    "MAX_POINTS": "Max. Points",
-    "DISTRIBUTE_POINTS": "Distribute points",
-    "REMOVE_CLOSE_POINTS": "Remove close points",
-    "RASTERIZE_SHAPE": "Rasterize Shape",
-    "MODE": "Mode",
-    "Factor": "Factor",
-    "NORMALIZE": "Normalize",
-    "WEIGHT_FACTOR": "Weight",
-    "STARS_EXAMPLE": "Stars",
-    "ADD_EMPTY_FRAME": "Add empty frame",
-    "DUPLICATE_FRAME": "Duplicate frame",
-    "DELETE_FRAME": "Remove frame",
-    "DEFAULT_MEMBER_NAME": "New Element",
-    "NO_PARSER_FOUND": "No file parser found for extension '{0}'",
-    "SELECT_FILE_FORMAT": "Select file format",
-    "SELECT_FILE_FORMAT_DESCRIPTION": "Multiple file types of the same format are supported. Please select the one you want to use.",
-    "NEW_PALETTE_FILE": "palette",
-    "ISLAND_EXAMPLE": "Islands",
-    "ONION_FRAMES_COUNT": "Onion frames",
-    "ONION_OPACITY": "Onion opacity",
-    "TOGGLE_ONION_SKINNING": "Toggle onion skinning",
-    "CHANGE_ACTIVE_FRAME_PREVIOUS": "Change active frame to previous",
-    "CHANGE_ACTIVE_FRAME_NEXT": "Change active frame to next",
-    "TOGGLE_ANIMATION": "Toggle animation",
-    "NEW_FROM_CLIPBOARD": "New from clipboard",
-    "OFFSET": "Offset",
-    "SHAPE": "Shape",
-    "STRUCTURE": "Structure",
-    "NUMBERS": "Numbers",
-    "OPERATIONS": "Operations",
-    "GENERATION": "Generation",
-    "NUMBER": "Number",
-    "ANIMATION": "Animation",
-    "SAMPLE_IMAGE": "Sample Image",
-    "POSITION": "Position",
-    "MATH_ADD": "Add",
-    "MATH_SUBTRACT": "Subtract",
-    "MULTIPLY": "Multiply",
-    "DIVIDE": "Divide",
-    "SIN": "Sin",
-    "COS": "Cos",
-    "TAN": "Tan",
-    "GREATER_THAN": "Greater than",
-    "LESS_THAN": "Less than",
-    "LESS_THAN_OR_EQUAL": "Less than or equal",
-    "COMPARE": "Compare",
-    "MATH_POWER": "Power",
-    "LOGARITHM": "Logarithm",
-    "NATURAL_LOGARITHM": "Natural logarithm",
-    "ROOT": "Root",
-    "INVERSE_ROOT": "Inverse root",
-    "FRACTION": "Fraction",
-    "NEGATE": "Negate",
-    "FLOOR": "Floor",
-    "CEIL": "Ceil",
-    "ROUND": "Round",
-    "MODULO": "Modulo",
-    "STEP": "Step",
-    "SMOOTH_STEP": "Smoothstep",
-    "PIXEL_ART_TOOLSET": "Pixel Art",
-    "VECTOR_TOOLSET": "Vector",
-    "VECTOR_LAYER": "Vector Layer",
-    "STROKE_COLOR_LABEL": "Stroke",
-    "SYNC_WITH_PRIMARY_COLOR_LABEL": "Sync with primary color",
-    "RASTERIZE": "Rasterize",
-    "RASTERIZE_ACTIVE_LAYER": "Rasterize active layer",
-    "RASTERIZE_ACTIVE_LAYER_DESCRIPTIVE": "Convert/Rasterize the active layer into a image (raster) layer.",
-    "NEW_ELLIPSE_LAYER_NAME": "Ellipse",
-    "NEW_RECTANGLE_LAYER_NAME": "Rectangle",
-    "NEW_LINE_LAYER_NAME": "Line",
-    "RENDER_OUTPUT": "Render Output",
-    "PAINT_TOOLSET": "Painting",
-    "HARDNESS_SETTING": "Hardness",
-    "SPACING_SETTING": "Spacing",
-    "ANTI_ALIASING_SETTING": "Anti-aliasing",
-    "TOLERANCE_LABEL": "Tolerance",
-    "TOGGLE_SNAPPING": "Toggle snapping",
-    "HIGH_RES_PREVIEW": "High Resolution Preview",
-    "LOW_RES_PREVIEW": "Document Resolution Preview",
-    "TOGGLE_HIGH_RES_PREVIEW": "Toggle high resolution preview",
-    "FACTOR": "Factor",
-    "PATH_TOOL": "Path",
-    "PATH_TOOL_TOOLTIP": "Create vector paths and curves ({0}).",
-    "PATH_TOOL_ACTION_DISPLAY": "Click to add a point.",
-    "PATH_TOOL_ACTION_DISPLAY_CTRL": "Click on existing point and drag to make it a curve. Tap on a control point to select it.",
-    "PATH_TOOL_ACTION_DISPLAY_SHIFT": "Click to create a new layer.",
-    "PATH_TOOL_ACTION_DISPLAY_CTRL_SHIFT": "Tap on a control point to add it to the selection.",
-    "PATH_TOOL_ACTION_DISPLAY_ALT": "Click on a control point and move to adjust only one side of the curve.",
-    "DEFAULT_PATH_LAYER_NAME": "Path",
-    "DELETE_NODES": "Delete nodes",
-    "DELETE_NODES_DESCRIPTIVE": "Delete selected nodes",
-    "DELETE_CELS": "Delete cels",
-    "DELETE_CELS_DESCRIPTIVE": "Delete selected cels",
-    "COPY_COLOR_TO_CLIPBOARD": "Copy color to clipboard",
-    "VIEWPORT_ROTATION": "Viewport rotation",
-    "NEXT_TOOL_SET": "Next tool set",
-    "PREVIOUS_TOOL_SET": "Previous tool set",
-    "FILL_MODE": "Fill mode",
-    "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
-    "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using sRGB blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
-    "FILL_TYPE_WINDING": "Winding",
-    "FILL_TYPE_EVEN_ODD": "Even Odd",
-    "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
-    "FILL_TYPE_INVERSE_EVEN_ODD": "Inverse Even Odd",
-    "STROKE_CAP": "Stroke Cap",
-    "STROKE_JOIN": "Stroke Join",
-    "COPY_VISIBLE": "Copy visible",
-    "COPY_VISIBLE_DESCRIPTIVE": "Copy visible pixels",
-    "COLOR_SAMPLE_MODE": "Sample mode",
-    "CREATE_CEL": "Create cel",
-    "CREATE_CEL_DESCRIPTIVE": "Create a new cel",
-    "DUPLICATE_CEL": "Duplicate cel",
-    "DUPLICATE_CEL_DESCRIPTIVE": "Duplicate cel in the current frame",
-    "RENDER_PREVIEW": "Render preview",
-    "OUTPUT_NAME": "Output name",
-    "CUSTOM_OUTPUT_NODE": "Custom Output",
-    "TOGGLE_HUD": "Toggle HUD",
-    "OPEN_TIMELINE": "Open timeline",
-    "OPEN_NODE_GRAPH": "Open node graph",
-    "TOGGLE_PLAY": "Play/Pause animation",
-    "COPY_NODES": "Copy nodes",
-    "COPY_NODES_DESCRIPTIVE": "Copy selected nodes",
-    "PASTE_NODES": "Paste nodes",
-    "PASTE_NODES_DESCRIPTIVE": "Paste copied nodes",
-    "COPY_CELS": "Copy cels",
-    "COPY_CELS_DESCRIPTIVE": "Copy selected cels",
-    "TOGGLE_ONION_SKINNING_DESCRIPTIVE": "Toggle onion skinning",
-    "VALUE": "Value",
-    "TARGET": "Target",
-    "EPSILON": "Epsilon",
-    "PRESERVE_ALPHA": "Preserve alpha",
-    "BLUR_FILTER_NODE": "Gaussian Blur Filter",
-    "LENGTH": "Length",
-    "GREATER_THAN_OR_EQUAL": "Greater than or equal",
-    "COLOR_NODE": "Color",
-    "CONVERT_TO_CURVE": "Convert to curve",
-    "CONVERT_TO_CURVE_DESCRIPTIVE": "Convert selected vector layer to a curve/path",
-    "FONT_FILES": "Font Files",
-    "UNIT_PT": "pt",
-    "FONT_LABEL": "Family",
-    "FONT_SIZE_LABEL": "Size",
-    "SPACING_LABEL": "Spacing",
-    "TEXT_TOOL": "Text",
-    "MISSING_FONT": "Missing font",
-    "TEXT_LAYER_NAME": "Text",
-    "TEXT_TOOL_TOOLTIP": "Create text ({0}).",
-    "BOLD_TOOLTIP": "Bold",
-    "ITALIC_TOOLTIP": "Italic",
-    "CUSTOM_FONT": "Custom font",
-    "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics",
-    "USE_SRGB_PROCESSING": "Use sRGB for processing colors",
-    "USE_SRGB_PROCESSING_DESC": "Convert document using linear sRGB to sRGB for processing colors. This will affect the colors of the document.",
-    "TEXT_NODE": "Text",
-    "TEXT_LABEL": "Text",
-    "TEXT_ON_PATH_NODE": "Text on Path",
-    "HIGH_DPI_RENDERING": "High DPI Rendering",
-    "THICKNESS": "Thickness",
-    "TYPE": "Type",
-    "EFFECTS": "Effects",
-    "OUTLINE_NODE": "Outline",
-    "SHADER_CODE": "Shader Code",
-    "SHADER_NODE": "Shader",
-    "FAILED_TO_OPEN_EDITABLE_STRING_TITLE": "Failed to open file",
-    "FAILED_TO_OPEN_EDITABLE_STRING_MESSAGE": "Failed to edit this string in external editor. Reason: {0}",
-    "STRING_EDIT_IN_DEFAULT_APP": "Edit in default app",
-    "STRING_OPEN_IN_FOLDER": "Open in folder",
-    "DISCO_BALL_EXAMPLE": "Disco Ball",
-    "COLOR_SPACE": "Color Space",
-    "PHOTO_EXAMPLES": "Photo",
-    "MASK_EXAMPLE": "Mask",
-    "SHADOW_NODE": "Shadow Filter",
-    "INPUT_MATRIX": "Input Matrix",
-    "OUTPUT_MATRIX": "Output Matrix",
-    "CENTER": "Center",
-    "CANVAS_POSITION": "Canvas Position",
-    "CENTER_POSITION": "Center Position",
-    "TILE_MODE_X": "Tile Mode X",
-    "TILE_MODE_Y": "Tile Mode Y",
-    "TILE_NODE": "Tile",
-    "SKEW": "Skew",
-    "OFFSET_NODE": "Offset",
-    "SKEW_NODE": "Skew",
-    "SCALE_NODE": "Scale",
-    "ROTATE_NODE": "Rotate",
-    "TRANSFORM_NODE": "Transform",
-    "UNIT": "Unit",
-    "ANGLE": "Angle",
-    "DOCUMENT_INFO_NODE": "Document Info",
-    "MASK_NODE": "Mask",
-    "SEPIA_FILTER_NODE": "Sepia Filter",
-    "INTENSITY": "Intensity",
-    "INVERT_FILTER_NODE": "Invert Filter",
-    "COLOR_ADJUSTMENTS_FILTER": "Color Adjustments Filter",
-    "ADJUST_BRIGHTNESS": "Adjust Brightness",
-    "ADJUST_CONTRAST": "Adjust Contrast",
-    "ADJUST_SATURATION": "Adjust Saturation",
-    "ADJUST_TEMPERATURE": "Adjust Temperature",
-    "ADJUST_TINT": "Adjust Tint",
-    "ADJUST_HUE": "Adjust Hue",
-    "HUE_VALUE": "Hue",
-    "SATURATION_VALUE": "Saturation",
-    "BRIGHTNESS_VALUE": "Brightness",
-    "CONTRAST_VALUE": "Contrast",
-    "TEMPERATURE_VALUE": "Temperature",
-    "TINT_VALUE": "Tint",
-    "UNEXPECTED_SHUTDOWN": "Unexpected shutdown",
-    "UNEXPECTED_SHUTDOWN_MSG": "PixiEditor was unexpectedly shut down. We've loaded latest autosave of your files.",
-    "OK": "OK",
-    "OPEN_AUTOSAVES": "Browse Autosaves",
-    "AUTOSAVE_SETTINGS_HEADER": "Autosave",
-    "AUTOSAVE_SETTINGS_SAVE_STATE": "Reopen last files on startup",
-    "AUTOSAVE_SETTINGS_PERIOD": "Autosave period",
-    "AUTOSAVE_ENABLED": "Autosave enabled",
-    "MINUTE_UNIVERSAL": "min",
-    "AUTOSAVE_SETTINGS_SAVE_USER_FILE": "Autosave to selected file",
-    "LOAD_LAZY_FILE_MESSAGE": "To improve startup time, PixiEditor didn't load this file. Click the button below to load it.",
-    "EASING_NODE": "Easing",
-    "EASING_TYPE": "Easing Type",
-    "OPEN_DIRECTORY_ON_EXPORT": "Open directory on export",
-    "ERROR_LOOP_DETECTED_MESSAGE": "Moving this layer will create a loop. Fix it in the Node Graph.",
-    "LINEAR_EASING_TYPE": "Linear",
-    "IN_SINE_EASING_TYPE": "In Sine",
-    "OUT_SINE_EASING_TYPE": "Out Sine",
-    "IN_OUT_SINE_EASING_TYPE": "In Out Sine",
-    "IN_QUAD_EASING_TYPE": "In Quad",
-    "OUT_QUAD_EASING_TYPE": "Out Quad",
-    "IN_OUT_QUAD_EASING_TYPE": "In Out Quad",
-    "IN_CUBIC_EASING_TYPE": "In Cubic",
-    "OUT_CUBIC_EASING_TYPE": "Out Cubic",
-    "IN_OUT_CUBIC_EASING_TYPE": "In Out Cubic",
-    "IN_QUART_EASING_TYPE": "In Quart",
-    "OUT_QUART_EASING_TYPE": "Out Quart",
-    "IN_OUT_QUART_EASING_TYPE": "In Out Quart",
-    "IN_QUINT_EASING_TYPE": "In Quint",
-    "OUT_QUINT_EASING_TYPE": "Out Quint",
-    "IN_OUT_QUINT_EASING_TYPE": "In Out Quint",
-    "IN_EXPO_EASING_TYPE": "In Expo",
-    "OUT_EXPO_EASING_TYPE": "Out Expo",
-    "IN_OUT_EXPO_EASING_TYPE": "In Out Expo",
-    "IN_CIRC_EASING_TYPE": "In Circ",
-    "OUT_CIRC_EASING_TYPE": "Out Circ",
-    "IN_OUT_CIRC_EASING_TYPE": "In Out Circ",
-    "IN_BACK_EASING_TYPE": "In Back",
-    "OUT_BACK_EASING_TYPE": "Out Back",
-    "IN_OUT_BACK_EASING_TYPE": "In Out Back",
-    "IN_ELASTIC_EASING_TYPE": "In Elastic",
-    "OUT_ELASTIC_EASING_TYPE": "Out Elastic",
-    "IN_OUT_ELASTIC_EASING_TYPE": "In Out Elastic",
-    "IN_BOUNCE_EASING_TYPE": "In Bounce",
-    "OUT_BOUNCE_EASING_TYPE": "Out Bounce",
-    "IN_OUT_BOUNCE_EASING_TYPE": "In Out Bounce",
-    "R_G_B_COMBINE_SEPARATE_COLOR_MODE": "RGB",
-    "H_S_V_COMBINE_SEPARATE_COLOR_MODE": "HSV",
-    "H_S_L_COMBINE_SEPARATE_COLOR_MODE": "HSL",
-    "COLOR_MANAGED_COLOR_SAMPLE_MODE": "Color Managed",
-    "RAW_COLOR_SAMPLE_MODE": "Raw",
-    "FRACTAL_PERLIN_NOISE_TYPE": "Perlin",
-    "TURBULENCE_PERLIN_NOISE_TYPE": "Turbulence",
-    "VORONOI_NOISE_TYPE": "Voronoi",
-    "VORONOI_FEATURE": "Feature",
-    "F1_VORONOI_FEATURE": "F1",
-    "F2_VORONOI_FEATURE": "F2",
-    "F2_MINUS_F1_VORONOI_FEATURE": "F2-F1",
-    "RANDOMNESS": "Randomness",
-    "ANGLE_OFFSET": "Angle Offset",
-    "INHERIT_COLOR_SPACE_TYPE": "Inherit",
-    "SRGB_COLOR_SPACE_TYPE": "sRGB",
-    "LINEAR_SRGB_COLOR_SPACE_TYPE": "Linear sRGB",
-    "SIMPLE_OUTLINE_TYPE": "Simple",
-    "GAUSSIAN_OUTLINE_TYPE": "Gaussian",
-    "PIXEL_PERFECT_OUTLINE_TYPE": "Pixel Perfect",
-    "DEGREES_ROTATION_TYPE": "Degrees",
-    "RADIANS_ROTATION_TYPE": "Radians",
-    "WEIGHTED_GRAYSCALE_MODE": "Weighted",
-    "AVERAGE_GRAYSCALE_MODE": "Average",
-    "CUSTOM_GRAYSCALE_MODE": "Custom",
-    "CLAMP_TILE_MODE": "Clamp",
-    "REPEAT_TILE_MODE": "Repeat",
-    "MIRROR_TILE_MODE": "Mirror",
-    "DECAL_TILE_MODE": "Decal",
-    "ERR_UNKNOWN_FILE_FORMAT": "Unknown file format",
-    "ERR_EXPORT_SIZE_INVALID": "Invalid export size. Values must be greater than 0.",
-    "ERR_UNKNOWN_IMG_FORMAT": "Unknown image format '{0}'.",
-    "ERR_FAILED_GENERATE_SPRITE_SHEET": "Failed generating sprite sheet",
-    "ERR_NO_RENDERER": "Animation renderer not found.",
-    "ERR_RENDERING_FAILED": "Rendering failed",
-    "ENABLE_ANALYTICS": "Send anonymous analytics",
-    "ANALYTICS_INFO": "We collect anonymous usage data to improve PixiEditor. No personal data is collected.",
-    "LANGUAGE_INFO": "All translations are community-driven. Join our Discord server for more information.",
-    "UP_TO_DATE_UNKNOWN": "Couldn't check for updates",
-    "UP_TO_DATE": "PixiEditor is up to date",
-    "UPDATE_AVAILABLE": "Update {0} is available",
-    "UPDATE_FAILED_DOWNLOAD": "Failed to download the update",
-    "UPDATE_READY_TO_INSTALL": "Update is ready. Switch to {0}?",
-    "SWITCH_TO_NEW_VERSION": "Switch",
-    "DOWNLOAD_UPDATE": "Download",
-    "DOWNLOADING_UPDATE": "Downloading update...",
-    "CHECKING_FOR_UPDATES": "Checking for updates...",
-    "PAINT_SHAPE_SETTING": "Brush shape",
-    "BOOL_OPERATION_NODE": "Boolean Operation",
-    "FIRST_SHAPE": "First shape",
-    "SECOND_SHAPE": "Second shape",
-    "OPERATION": "Operation",
-    "UNION_VECTOR_PATH_OP": "Union",
-    "DIFFERENCE_VECTOR_PATH_OP": "Difference",
-    "INTERSECT_VECTOR_PATH_OP": "Intersect",
-    "XOR_VECTOR_PATH_OP": "XOR",
-    "REVERSE_DIFFERENCE_VECTOR_PATH_OP": "Reverse Difference",
-    "NO_DOCUMENT_OPEN": "Nothing's here",
-    "EMPTY_DOCUMENT_ACTION_BTN": "Start creating",
-    "ONBOARDING_TITLE": "Welcome to",
-    "ONBOARDING_DESCRIPTION": "Let's set up your workspace!",
-    "ONBOARDING_SKIP_BTN": "Skip",
-    "ONBOARDING_ACTION_BTN": "Let's begin",
-    "ONB_SELECT_PRIMARY_TOOLSET": "Select Your Primary Toolset",
-    "ONB_NEXT_BTN": "Next",
-    "ONB_FINISH_BTN": "Finish",
-    "ONB_BACK_BTN": "Previous",
-    "ONB_ANALYTICS": "Anonymous Analytics",
-    "ONB_ALL_SET": "You are all set!",
-    "ONB_ALL_SET_BTN": "Start creating",
-    "ANALYTICS_INFO_DETAILED": "PixiEditor collects anonymous usage data to improve the app. The data does not contain any personal information. Among other things, PixiEditor tracks the following:\n- Which and how the tools are used\n- How long you've used the app for\n- Which commands are used\n- Performance data\n\n You can opt out of analytics at any time in the settings.",
-    "PRIVACY_POLICY": "Privacy Policy",
-    "ONB_SHORTCUTS": "Select Your Shortcuts",
-    "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER": "Current Node Graph setup disallows creation of a new layer next to the selected one.",
-    "PRIMARY_TOOLSET": "Primary Toolset",
-    "OPEN_ONBOARDING_WINDOW": "Open onboarding window",
-    "USER_NOT_FOUND": "Please enter the email you used to purchase the Founder's Edition.",
-    "SESSION_NOT_VALID": "Session is not valid, please log in again",
-    "SESSION_NOT_FOUND": "Session not found, try logging in again",
-    "INTERNAL_SERVER_ERROR": "There was an internal server error. Please try again later.",
-    "TOO_MANY_REQUESTS": "Too many requests. Try again in {0} seconds.",
-    "SESSION_EXPIRED": "Session expired. Please log in again.",
-    "CONNECTION_ERROR": "Connection error. Please check your internet connection.",
-    "FAIL_LOAD_USER_DATA": "Failed to load saved user data",
-    "LOGOUT": "Logout",
-    "LOGGED_IN_AS": "Hello",
-    "EMAIL_SENT": "Email sent! Check your inbox.",
-    "RESEND_ACTIVATION": "Resend",
-    "INVALID_TOKEN": "Session is invalid or expired. Please log in again.",
-    "ENTER_EMAIL": "Enter your email",
-    "LOGIN_LINK": "Send Login Link",
-    "LOGIN_LINK_INFO": "We'll email you a secure link to log in. No password needed.",
-    "ACCOUNT_WINDOW_TITLE": "Account",
-    "CONNECTION_TIMEOUT": "Connection timed out. Please try again.",
-    "OPEN_ACCOUNT_WINDOW": "Manage Account",
-    "AUTO_SCALE_BACKGROUND": "Auto scale background",
-    "UPDATES": "Updates",
-    "SCENE": "Scene",
-    "CUSTOM_BACKGROUND_SCALE": "Custom background scale",
-    "PRIMARY_BG_COLOR": "Primary background color",
-    "SECONDARY_BG_COLOR": "Secondary background color",
-    "RESET": "Reset",
-    "INSTALL": "Install",
-    "OWNED_PRODUCTS": "Owned Content",
-    "INSTALLING": "Installing",
-    "INSTALLED": "Installed",
-    "ACCOUNT_PROVIDER_INFO": "Account handled by",
-    "UPDATE": "Update",
-    "AUTOSAVE_OPEN_FOLDER": "Open autosave folder",
-    "AUTOSAVE_OPEN_FOLDER_DESCRIPTIVE": "Open the folder where autosaves are stored",
-    "AUTOSAVE_TOGGLE_DESCRIPTIVE": "Enable/disable autosave",
-    "FOUNDERS_BUNDLE": "Founder's Bundle",
-    "FOUNDERS_BUNDLE_SUBTEXT": "Support PixiEditor and boost your productivity!",
-    "BECOME_A_FOUNDER": "Become a Founder",
-    "LOGIN": "Login",
-    "NOT_FOUNDER_YET": "Not a Founder yet?",
-    "ERROR_GRAPH": "Graph setup produced an error. Fix it in the node graph",
-    "COLOR_MATRIX_FILTER_NODE": "Color Matrix Filter",
-    "WORKSPACE": "Workspace",
-    "IS_DEFAULT_EXPORT": "Is Default Export",
-    "EXPORT_OUTPUT": "Export Output",
-    "RENDER_OUTPUT_SIZE": "Render Output Size",
-    "RENDER_OUTPUT_CENTER": "Render Output Center",
-    "COLOR_PICKER": "Color Picker",
-    "UNAUTHORIZED_ACCESS": "Unauthorized access",
-    "SEPARATE_SHAPES": "Separate Shapes",
-    "SEPARATE_SHAPES_DESCRIPTIVE": "Separate shapes from current vector into individual layers",
-    "TEXT": "Text",
-    "EXTRACT_SELECTED_TEXT": "Extract selected text",
-    "EXTRACT_SELECTED_TEXT_DESCRIPTIVE": "Extract selected text into new layer.",
-    "EXTRACT_SELECTED_CHARACTERS": "Extract selected characters",
-    "EXTRACT_SELECTED_CHARACTERS_DESCRIPTIVE": "Extract individual characters from selection into new layers.",
-    "STEP_START": "Step back to closest cel",
-    "STEP_END": "Step forward to closest cel",
-    "STEP_FORWARD": "Step forward one frame",
-    "STEP_BACK": "Step back one frame",
-    "ANIMATION_QUALITY_PRESET": "Quality Preset",
-    "VERY_LOW_QUALITY_PRESET": "Very Low",
-    "LOW_QUALITY_PRESET": "Low",
-    "MEDIUM_QUALITY_PRESET": "Medium",
-    "HIGH_QUALITY_PRESET": "High",
-    "VERY_HIGH_QUALITY_PRESET": "Very High",
-    "EXPORT_FRAMES": "Export Frames",
-    "NORMALIZE_OFFSET": "Normalize Offset",
-    "TANGENT": "Tangent",
-    "EVALUATE_PATH_NODE": "Evaluate Path",
-    "OLD_MIN": "Old Min",
-    "OLD_MAX": "Old Max",
-    "NEW_MIN": "New Min",
-    "NEW_MAX": "New Max",
-    "REMAP_NODE": "Remap",
-    "TEXT_TOOL_ACTION_DISPLAY": "Click on the canvas to add a new text (drag while clicking to set the size). Click on existing text to edit it.",
-    "PASTE_CELS": "Paste cels",
-    "PASTE_CELS_DESCRIPTIVE": "Paste cels from clipboard into the current frame",
-    "SCALE_X": "Scale X",
-    "SCALE_Y": "Scale Y",
-    "TRANSLATE_X": "Translate X",
-    "TRANSLATE_Y": "Translate Y",
-    "SKEW_X": "Skew X",
-    "SKEW_Y": "Skew Y",
-    "PERSPECTIVE_0": "Perspective 0",
-    "PERSPECTIVE_1": "Perspective 1",
-    "PERSPECTIVE_2": "Perspective 2",
-    "COMPOSE_MATRIX": "Compose Matrix",
-    "DECOMPOSE_MATRIX": "Decompose Matrix",
-    "NORMALIZE_COORDINATES": "Normalize Coordinates",
-    "TRANSFORMED_POSITION": "Transformed Position",
-    "ACCOUNT_PROVIDER_NOT_AVAILABLE": "This build of PixiEditor does not support accounts. Use the official build from pixieditor.net to manage your account.",
-    "STEAM_OFFLINE": "Cannot validate the account. Steam is offline. Make sure Steam client is running and you are logged in.",
-    "ERROR_GPU_RESOURCES_CREATION": "Failed to create resources: Try updating your GPU drivers or try setting different rendering api in settings. \nError: '{0}'",
-    "ERROR_SAVING_PREFERENCES_DESC": "Failed to save preferences with error: '{0}'. Please check if you have write permissions to the PixiEditor data folder.",
-    "ERROR_SAVING_PREFERENCES": "Failed to save preferences",
-    "PREFERRED_RENDERER": "Preferred Render Api",
-    "PERFORMANCE": "Performance",
-    "DISABLE_PREVIEWS": "Disable Previews",
-    "MAX_BILINEAR_CANVAS_SIZE": "Max Bilinear Canvas Size",
-    "MAX_BILINEAR_CANVAS_SIZE_DESC": "Maximum canvas size for bilinear filtering. Set to 0 to disable bilinear filtering. Bilinear filtering improves the quality of the canvas, but can cause performance issues on large canvases.",
-    "INVERT_MASK": "Invert mask",
-    "TOGGLE_TINTING_SELECTION": "Toggle selection tinting",
-    "TOGGLE_TINTING_SELECTION_DESCRIPTIVE": "Toggle selection tinting",
-    "TINT_SELECTION": "Selection tinting",
-    "PAINT_BRUSH_SHAPE_CIRCLE": "Circle",
-    "PAINT_BRUSH_SHAPE_SQUARE": "Square",
-    "BRIGHTNESS_MODE_DEFAULT": "Default",
-    "BRIGHTNESS_MODE_REPEAT": "Repeat",
-    "ROUND_STROKE_CAP": "Round",
-    "BUTT_STROKE_CAP": "Butt",
-    "SQUARE_STROKE_CAP": "Square",
-    "ROUND_STROKE_JOIN": "Round",
-    "MITER_STROKE_JOIN": "Miter",
-    "BEVEL_STROKE_JOIN": "Bevel",
-    "TOGGLE_FULLSCREEN": "Toggle Fullscreen",
-    "TOGGLE_FULLSCREEN_DESCRIPTIVE": "Enter or Exit Fullscreen Mode"
+  "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",
+  "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",
+  "OPEN_DOCUMENTATION": "Open documentation",
+  "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",
+  "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
+  "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
+  "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
+  "VERSION": "Version {0}",
+  "BUILD_ID": "Build ID: {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_ALL_SELECTED": "Delete all selected layers/folders",
+  "LAYER_DELETE_ALL_SELECTED_DESCRIPTIVE": "Delete all selected layers and/or folders",
+  "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_SELECTED": "Delete selected",
+  "DELETE_SELECTED_DESCRIPTIVE": "Delete selected element (layer, pixels, etc.)",
+  "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_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",
+  "GRIDLINES_SIZE": "Grid Size",
+  "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_PREVIEW_WINDOW": "Open preview 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": "Select and transform layers ({0}).",
+  "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",
+  "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",
+  "PREVIEW_TITLE": "Preview",
+  "NORMAL_BLEND_MODE": "Normal",
+  "ERASE_BLEND_MODE": "Erase",
+  "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",
+  "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 member 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_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?",
+  "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",
+  "MOVE_TOOL_ACTION_DISPLAY_TRANSFORMING": "Click and hold mouse to move pixels in selected 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.",
+  "COULD_NOT_SAVE_PALETTE": "There was an error while saving the palette.",
+  "NO_COLORS_TO_SAVE": "There are no colors to save.",
+  "CANVAS": "Canvas",
+  "SINGLE_LAYER": "Single Layer",
+  "CHOOSE": "Choose",
+  "REMOVE": "Remove",
+  "FILE_FORMAT_NOT_ASEPRITE_KEYS": "File is not a \".aseprite-keys\" file",
+  "FILE_HAS_INVALID_SHORTCUT": "The file contains an invalid shortcut",
+  "FILE_EXTENSION_NOT_SUPPORTED": "The file type '{0}' is not supported",
+  "ERROR_READING_FILE": "Error while reading the file",
+  "DISCARD_PALETTE": "Discard palette",
+  "DISCARD_PALETTE_CONFIRMATION": "Are you sure you want to discard current palette? This cannot be undone.",
+  "IMPORT_AS_NEW_LAYER": "Import as new layer",
+  "PASTE_AS_PRIMARY_COLOR": "Paste as primary color",
+  "IMPORT_AS_NEW_FILE": "Import as new file",
+  "IMPORT_PALETTE_FILE": "Import palette file",
+  "IMPORT_MULTIPLE_PALETTE_COLORS": "Import colors into palette",
+  "IMPORT_SINGLE_PALETTE_COLOR": "Import color into palette",
+  "IMPORT_AS_REFERENCE_LAYER": "Import as reference layer",
+  "NAVIGATOR_PICK_ACTION_DISPLAY": "Right-click to pick color, Shift-right-click to copy color to clipboard",
+  "OPEN_FILE_FROM_CLIPBOARD": "Open from clipboard",
+  "OPEN_FILE_FROM_CLIPBOARD_DESCRIPTIVE": "Open from clipboard",
+  "OPEN_LOCALIZATION_DATA": "Do you want to open the LocalizationData.json?\nThe updated date has been put in the clipboard.\nNote that changes wont be applied until a restart",
+  "DOWNLOADING_LANGUAGE_FAILED": "Downloading language failed.\nAPI Key might have been overused.",
+  "LOCALIZATION_DATA_NOT_FOUND": "Localization data path not found",
+  "APPLY": "Apply",
+  "UPDATE_SOURCE": "Update source",
+  "COPY_TO_CLIPBOARD": "Copy to clipboard",
+  "LANGUAGE_FILE_NOT_FOUND": "Language file not found.\nLooking for {0}",
+  "PROJECT_ROOT_NOT_FOUND": "PixiEditor Project root not found.\nLooking for PixiEditor.csproj",
+  "LOCALIZATION_FOLDER_NOT_FOUND": "Localization folder not found.\nLooking for /Data/Localization",
+  "SELECT_A_LANGUAGE": "Select a language",
+  "DONE": "Done",
+  "SOURCE_UNSET_OR_MISSING": "Source missing/unset",
+  "SOURCE_NEWER": "Source newer",
+  "SOURCE_UP_TO_DATE": "Source is up to date",
+  "SOURCE_OLDER": "Cloud newer",
+  "COLOR_PICKER_ACTION_DISPLAY_REFERENCE_ONLY": "Click to pick colors from the reference layer.",
+  "COLOR_PICKER_ACTION_DISPLAY_CANVAS_ONLY": "Click to pick colors from the canvas.",
+  "LOCALIZATION_DEBUG_WINDOW_TITLE": "Localization Debug Window",
+  "SHORTCUTS_TITLE": "Shortcuts",
+  "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_PERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
+  "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_SHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally. Hold Alt and drag a side handle to shear. Drag outside handles to rotate.",
+  "TRANSFORM_ACTION_DISPLAY_SCALE_ROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally. Drag outside handles to rotate.",
+  "TRANSFORM_ACTION_DISPLAY_SCALE_NOROTATE_NOSHEAR_NOPERSPECTIVE": "Drag handles to scale transform. Hold Ctrl and drag a handle to scale from center. Hold Shift to scale proportionally.",
+  "LOCAL_PALETTE_SOURCE_NAME": "Local",
+  "ERROR_FORBIDDEN_UNIQUE_NAME": "Extension unique name cannot start with 'pixieditor'.",
+  "ERROR_MISSING_METADATA": "Extension metadata key '{0}' is missing.",
+  "ERROR_NO_CLASS_ENTRY": "Extension class entry is missing on path '{0}'.",
+  "ERROR_NO_ENTRY_ASSEMBLY": "Extension entry assembly is missing on path '{0}'.",
+  "ERROR_MISSING_ADDITIONAL_CONTENT": "Your current setup doesn't allow loading this extension. Perhaps you don't own it or don't have it installed. You can purchase it here '{0}'.",
+  "BUY_SUPPORTER_PACK": "Buy Supporter Pack",
+  "NEWS": "News",
+  "DISABLE_NEWS_PANEL": "Disable News panel in startup window",
+  "FAILED_FETCH_NEWS": "Failed to fetch news",
+  "CROP_TO_SELECTION": "Crop to selection",
+  "CROP_TO_SELECTION_DESCRIPTIVE": "Crop image to selection",
+  "SHOW_CONTEXT_MENU": "Show context menu",
+  "ERASE": "Erase",
+  "USE_SECONDARY_COLOR": "Use secondary color",
+  "RIGHT_CLICK_MODE": "Right click mode",
+  "ADD_PRIMARY_COLOR_TO_PALETTE": "Add primary color to palette",
+  "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette",
+  "EXPORT_SAVE_TITLE": "Choose a location to save the image",
+  "BROWSE_DIRECTORY": "Browse Directory",
+  "CRASH_NOT_ALL_DOCUMENTS_RECOVERED_TITLE": "Not all documents were recovered",
+  "CRASH_NOT_ALL_DOCUMENTS_RECOVERED": "Could not recover all documents. Git gud at saving your work.",
+  "SEND": "Send report",
+  "OPEN_DOCKABLE_MENU": "Open Tab",
+  "TIMELINE_TITLE": "Timeline",
+  "EXPORT_IMAGE_HEADER": "Image",
+  "EXPORT_ANIMATION_HEADER": "Animation",
+  "EXPORT_SPRITESHEET_HEADER": "Spritesheet",
+  "PIXI_FILE": "PixiEditor Files",
+  "PNG_FILE": "PNG Images",
+  "JPEG_FILE": "JPEG Images",
+  "WEBP_FILE": "WebP Images",
+  "GIF_FILE": "GIFs",
+  "BMP_FILE": "BMP Images",
+  "IMAGE_FILES": "Image Files",
+  "VIDEO_FILES": "Video Files",
+  "OPEN_TYPE_FONT": "OpenType Fonts",
+  "TRUE_TYPE_FONT": "TrueType Fonts",
+  "SVG_FILE": "Scalable Vector Graphics",
+  "MP4_FILE": "MP4 Videos",
+  "COLUMNS": "Columns",
+  "ROWS": "Rows",
+  "BACKGROUND": "Background",
+  "OPACITY": "Opacity",
+  "IS_VISIBLE": "Is visible",
+  "BLEND_MODE": "Blend mode",
+  "MASK": "Mask",
+  "MASK_IS_VISIBLE": "Mask is visible",
+  "OUTPUT": "Output",
+  "INPUT": "Input",
+  "NODE_GRAPH_TITLE": "Graph View",
+  "CONTENT": "Content",
+  "RADIUS": "Radius",
+  "STROKE_COLOR": "Stroke color",
+  "STROKE_WIDTH": "Stroke width",
+  "FILL_COLOR": "Fill color",
+  "TOP": "Top",
+  "BOTTOM": "Bottom",
+  "CHANNELS_DOCK_TITLE": "Channels",
+  "RED": "Red",
+  "GREEN": "Green",
+  "BLUE": "Blue",
+  "ALPHA": "Alpha",
+  "COLOR": "Color",
+  "COORDINATE": "Coordinate",
+  "VECTOR": "Vector",
+  "MATRIX": "Matrix",
+  "TRANSFORMED": "Transformed",
+  "GRAYSCALE": "Grayscale",
+  "CLAMP": "Clamp",
+  "SIZE": "Size",
+  "NOISE": "Noise",
+  "SCALE": "Scale",
+  "SEED": "Seed",
+  "KERNEL": "Kernel",
+  "KERNEL_VIEW_SUM": "Sum:",
+  "KERNEL_VIEW_SUM_TOOLTIP": "The sum of all values. You likely want to aim for a value of 1 or 0",
+  "GAIN": "Gain",
+  "BIAS": "Bias",
+  "TILE_MODE": "Tile Mode",
+  "ON_ALPHA": "On Alpha",
+  "OUTPUT_NODE": "Output",
+  "NOISE_NODE": "Noise",
+  "ELLIPSE_NODE": "Ellipse",
+  "LINE_NODE": "Line",
+  "LINE_START": "Start",
+  "LINE_END": "End",
+  "RECTANGLE_NODE": "Rectangle",
+  "CREATE_IMAGE_NODE": "Create Image",
+  "FOLDER_NODE": "Folder",
+  "IMAGE_LAYER_NODE": "Image Layer",
+  "KERNEL_FILTER_NODE": "Kernel Filter",
+  "MATH_NODE": "Math",
+  "COLOR_MATRIX_TRANSFORM_FILTER_NODE": "Matrix Transform Filter",
+  "MERGE_NODE": "Merge",
+  "MODIFY_IMAGE_LEFT_NODE": "Begin Modify Image",
+  "MODIFY_IMAGE_RIGHT_NODE": "End Modify Image",
+  "COMBINE_CHANNELS_NODE": "Combine Channels",
+  "COMBINE_COLOR_NODE": "Combine Color",
+  "COMBINE_VECD_NODE": "Combine Vector",
+  "COMBINE_VECI_NODE": "Combine Integer Vector",
+  "SEPARATE_CHANNELS_NODE": "Separate Channels",
+  "SEPARATE_VECD_NODE": "Separate Vector",
+  "SEPARATE_VECI_NODE": "Separate Integer Vector",
+  "SEPARATE_COLOR_NODE": "Separate Color",
+  "TIME_NODE": "Time",
+  "FILTERS": "Filters",
+  "PREVIOUS": "Previous",
+  "FILL": "Fill",
+  "MATH_MODE": "Math Mode",
+  "NOISE_TYPE": "Noise Type",
+  "OCTAVES": "Octaves",
+  "ACTIVE_FRAME": "Active Frame",
+  "NORMALIZED_TIME": "Normalized Time",
+  "WITHOUT_FILTERS": "Without filters",
+  "RAW_LAYER_OUTPUT": "Raw",
+  "EXAMPLE_FILES": "Example Files",
+  "PROCEDURAL_GENERATION": "Procedural Animation",
+  "POND_EXAMPLE": "Pond",
+  "TREE_EXAMPLE": "Windy Tree",
+  "OUTLINE_EXAMPLE": "Automatic Outline",
+  "BETA_ANIMATIONS": "Animations",
+  "SLIME_EXAMPLE": "Animated Slime",
+  "APPLY_FILTER_NODE": "Apply Filter",
+  "FILTER": "Filter",
+  "LERP_NODE": "Lerp",
+  "GRAYSCALE_FILTER_NODE": "Grayscale Filter",
+  "FROM": "From",
+  "TO": "To",
+  "TIME": "Time",
+  "WARMING_UP": "Warming up",
+  "RENDERING_FRAME": "Generating Frame {0}/{1}",
+  "RENDERING_VIDEO": "Rendering Video",
+  "FINISHED": "Finished",
+  "GENERATING_SPRITE_SHEET": "Generating Sprite Sheet",
+  "RENDERING_IMAGE": "Rendering Image",
+  "PROGRESS_POPUP_TITLE": "Progress",
+  "POINTS": "Points",
+  "MIN_DISTANCE": "Min. Distance",
+  "MAX_POINTS": "Max. Points",
+  "DISTRIBUTE_POINTS": "Distribute points",
+  "REMOVE_CLOSE_POINTS": "Remove close points",
+  "RASTERIZE_SHAPE": "Rasterize Shape",
+  "MODE": "Mode",
+  "Factor": "Factor",
+  "NORMALIZE": "Normalize",
+  "WEIGHT_FACTOR": "Weight",
+  "STARS_EXAMPLE": "Stars",
+  "ADD_EMPTY_FRAME": "Add empty frame",
+  "DUPLICATE_FRAME": "Duplicate frame",
+  "DELETE_FRAME": "Remove frame",
+  "DEFAULT_MEMBER_NAME": "New Element",
+  "NO_PARSER_FOUND": "No file parser found for extension '{0}'",
+  "SELECT_FILE_FORMAT": "Select file format",
+  "SELECT_FILE_FORMAT_DESCRIPTION": "Multiple file types of the same format are supported. Please select the one you want to use.",
+  "NEW_PALETTE_FILE": "palette",
+  "ISLAND_EXAMPLE": "Islands",
+  "ONION_FRAMES_COUNT": "Onion frames",
+  "ONION_OPACITY": "Onion opacity",
+  "TOGGLE_ONION_SKINNING": "Toggle onion skinning",
+  "CHANGE_ACTIVE_FRAME_PREVIOUS": "Change active frame to previous",
+  "CHANGE_ACTIVE_FRAME_NEXT": "Change active frame to next",
+  "TOGGLE_ANIMATION": "Toggle animation",
+  "NEW_FROM_CLIPBOARD": "New from clipboard",
+  "OFFSET": "Offset",
+  "SHAPE": "Shape",
+  "STRUCTURE": "Structure",
+  "NUMBERS": "Numbers",
+  "OPERATIONS": "Operations",
+  "GENERATION": "Generation",
+  "NUMBER": "Number",
+  "ANIMATION": "Animation",
+  "SAMPLE_IMAGE": "Sample Image",
+  "POSITION": "Position",
+  "MATH_ADD": "Add",
+  "MATH_SUBTRACT": "Subtract",
+  "MULTIPLY": "Multiply",
+  "DIVIDE": "Divide",
+  "SIN": "Sin",
+  "COS": "Cos",
+  "TAN": "Tan",
+  "GREATER_THAN": "Greater than",
+  "LESS_THAN": "Less than",
+  "LESS_THAN_OR_EQUAL": "Less than or equal",
+  "COMPARE": "Compare",
+  "MATH_POWER": "Power",
+  "LOGARITHM": "Logarithm",
+  "NATURAL_LOGARITHM": "Natural logarithm",
+  "ROOT": "Root",
+  "INVERSE_ROOT": "Inverse root",
+  "FRACTION": "Fraction",
+  "NEGATE": "Negate",
+  "FLOOR": "Floor",
+  "CEIL": "Ceil",
+  "ROUND": "Round",
+  "MODULO": "Modulo",
+  "STEP": "Step",
+  "SMOOTH_STEP": "Smoothstep",
+  "PIXEL_ART_TOOLSET": "Pixel Art",
+  "VECTOR_TOOLSET": "Vector",
+  "VECTOR_LAYER": "Vector Layer",
+  "STROKE_COLOR_LABEL": "Stroke",
+  "SYNC_WITH_PRIMARY_COLOR_LABEL": "Sync with primary color",
+  "RASTERIZE": "Rasterize",
+  "RASTERIZE_ACTIVE_LAYER": "Rasterize active layer",
+  "RASTERIZE_ACTIVE_LAYER_DESCRIPTIVE": "Convert/Rasterize the active layer into a image (raster) layer.",
+  "NEW_ELLIPSE_LAYER_NAME": "Ellipse",
+  "NEW_RECTANGLE_LAYER_NAME": "Rectangle",
+  "NEW_LINE_LAYER_NAME": "Line",
+  "RENDER_OUTPUT": "Render Output",
+  "PAINT_TOOLSET": "Painting",
+  "HARDNESS_SETTING": "Hardness",
+  "SPACING_SETTING": "Spacing",
+  "ANTI_ALIASING_SETTING": "Anti-aliasing",
+  "TOLERANCE_LABEL": "Tolerance",
+  "TOGGLE_SNAPPING": "Toggle snapping",
+  "HIGH_RES_PREVIEW": "High Resolution Preview",
+  "LOW_RES_PREVIEW": "Document Resolution Preview",
+  "TOGGLE_HIGH_RES_PREVIEW": "Toggle high resolution preview",
+  "FACTOR": "Factor",
+  "PATH_TOOL": "Path",
+  "PATH_TOOL_TOOLTIP": "Create vector paths and curves ({0}).",
+  "PATH_TOOL_ACTION_DISPLAY": "Click to add a point.",
+  "PATH_TOOL_ACTION_DISPLAY_CTRL": "Click on existing point and drag to make it a curve. Tap on a control point to select it.",
+  "PATH_TOOL_ACTION_DISPLAY_SHIFT": "Click to create a new layer.",
+  "PATH_TOOL_ACTION_DISPLAY_CTRL_SHIFT": "Tap on a control point to add it to the selection.",
+  "PATH_TOOL_ACTION_DISPLAY_ALT": "Click on a control point and move to adjust only one side of the curve.",
+  "DEFAULT_PATH_LAYER_NAME": "Path",
+  "DELETE_NODES": "Delete nodes",
+  "DELETE_NODES_DESCRIPTIVE": "Delete selected nodes",
+  "DELETE_CELS": "Delete cels",
+  "DELETE_CELS_DESCRIPTIVE": "Delete selected cels",
+  "COPY_COLOR_TO_CLIPBOARD": "Copy color to clipboard",
+  "VIEWPORT_ROTATION": "Viewport rotation",
+  "NEXT_TOOL_SET": "Next tool set",
+  "PREVIOUS_TOOL_SET": "Previous tool set",
+  "FILL_MODE": "Fill mode",
+  "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
+  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using sRGB blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
+  "FILL_TYPE_WINDING": "Winding",
+  "FILL_TYPE_EVEN_ODD": "Even Odd",
+  "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
+  "FILL_TYPE_INVERSE_EVEN_ODD": "Inverse Even Odd",
+  "STROKE_CAP": "Stroke Cap",
+  "STROKE_JOIN": "Stroke Join",
+  "COPY_VISIBLE": "Copy visible",
+  "COPY_VISIBLE_DESCRIPTIVE": "Copy visible pixels",
+  "COLOR_SAMPLE_MODE": "Sample mode",
+  "CREATE_CEL": "Create cel",
+  "CREATE_CEL_DESCRIPTIVE": "Create a new cel",
+  "DUPLICATE_CEL": "Duplicate cel",
+  "DUPLICATE_CEL_DESCRIPTIVE": "Duplicate cel in the current frame",
+  "RENDER_PREVIEW": "Render preview",
+  "OUTPUT_NAME": "Output name",
+  "CUSTOM_OUTPUT_NODE": "Custom Output",
+  "TOGGLE_HUD": "Toggle HUD",
+  "OPEN_TIMELINE": "Open timeline",
+  "OPEN_NODE_GRAPH": "Open node graph",
+  "TOGGLE_PLAY": "Play/Pause animation",
+  "COPY_NODES": "Copy nodes",
+  "COPY_NODES_DESCRIPTIVE": "Copy selected nodes",
+  "PASTE_NODES": "Paste nodes",
+  "PASTE_NODES_DESCRIPTIVE": "Paste copied nodes",
+  "COPY_CELS": "Copy cels",
+  "COPY_CELS_DESCRIPTIVE": "Copy selected cels",
+  "TOGGLE_ONION_SKINNING_DESCRIPTIVE": "Toggle onion skinning",
+  "VALUE": "Value",
+  "TARGET": "Target",
+  "EPSILON": "Epsilon",
+  "PRESERVE_ALPHA": "Preserve alpha",
+  "BLUR_FILTER_NODE": "Gaussian Blur Filter",
+  "LENGTH": "Length",
+  "GREATER_THAN_OR_EQUAL": "Greater than or equal",
+  "COLOR_NODE": "Color",
+  "CONVERT_TO_CURVE": "Convert to curve",
+  "CONVERT_TO_CURVE_DESCRIPTIVE": "Convert selected vector layer to a curve/path",
+  "FONT_FILES": "Font Files",
+  "UNIT_PT": "pt",
+  "FONT_LABEL": "Family",
+  "FONT_SIZE_LABEL": "Size",
+  "SPACING_LABEL": "Spacing",
+  "TEXT_TOOL": "Text",
+  "MISSING_FONT": "Missing font",
+  "TEXT_LAYER_NAME": "Text",
+  "TEXT_TOOL_TOOLTIP": "Create text ({0}).",
+  "BOLD_TOOLTIP": "Bold",
+  "ITALIC_TOOLTIP": "Italic",
+  "CUSTOM_FONT": "Custom font",
+  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics",
+  "USE_SRGB_PROCESSING": "Use sRGB for processing colors",
+  "USE_SRGB_PROCESSING_DESC": "Convert document using linear sRGB to sRGB for processing colors. This will affect the colors of the document.",
+  "TEXT_NODE": "Text",
+  "TEXT_LABEL": "Text",
+  "TEXT_ON_PATH_NODE": "Text on Path",
+  "HIGH_DPI_RENDERING": "High DPI Rendering",
+  "THICKNESS": "Thickness",
+  "TYPE": "Type",
+  "EFFECTS": "Effects",
+  "OUTLINE_NODE": "Outline",
+  "SHADER_CODE": "Shader Code",
+  "SHADER_NODE": "Shader",
+  "FAILED_TO_OPEN_EDITABLE_STRING_TITLE": "Failed to open file",
+  "FAILED_TO_OPEN_EDITABLE_STRING_MESSAGE": "Failed to edit this string in external editor. Reason: {0}",
+  "STRING_EDIT_IN_DEFAULT_APP": "Edit in default app",
+  "STRING_OPEN_IN_FOLDER": "Open in folder",
+  "DISCO_BALL_EXAMPLE": "Disco Ball",
+  "COLOR_SPACE": "Color Space",
+  "PHOTO_EXAMPLES": "Photo",
+  "MASK_EXAMPLE": "Mask",
+  "SHADOW_NODE": "Shadow Filter",
+  "INPUT_MATRIX": "Input Matrix",
+  "OUTPUT_MATRIX": "Output Matrix",
+  "CENTER": "Center",
+  "CANVAS_POSITION": "Canvas Position",
+  "CENTER_POSITION": "Center Position",
+  "TILE_MODE_X": "Tile Mode X",
+  "TILE_MODE_Y": "Tile Mode Y",
+  "TILE_NODE": "Tile",
+  "SKEW": "Skew",
+  "OFFSET_NODE": "Offset",
+  "SKEW_NODE": "Skew",
+  "SCALE_NODE": "Scale",
+  "ROTATE_NODE": "Rotate",
+  "TRANSFORM_NODE": "Transform",
+  "UNIT": "Unit",
+  "ANGLE": "Angle",
+  "DOCUMENT_INFO_NODE": "Document Info",
+  "MASK_NODE": "Mask",
+  "SEPIA_FILTER_NODE": "Sepia Filter",
+  "INTENSITY": "Intensity",
+  "INVERT_FILTER_NODE": "Invert Filter",
+  "COLOR_ADJUSTMENTS_FILTER": "Color Adjustments Filter",
+  "ADJUST_BRIGHTNESS": "Adjust Brightness",
+  "ADJUST_CONTRAST": "Adjust Contrast",
+  "ADJUST_SATURATION": "Adjust Saturation",
+  "ADJUST_TEMPERATURE": "Adjust Temperature",
+  "ADJUST_TINT": "Adjust Tint",
+  "ADJUST_HUE": "Adjust Hue",
+  "HUE_VALUE": "Hue",
+  "SATURATION_VALUE": "Saturation",
+  "BRIGHTNESS_VALUE": "Brightness",
+  "CONTRAST_VALUE": "Contrast",
+  "TEMPERATURE_VALUE": "Temperature",
+  "TINT_VALUE": "Tint",
+  "UNEXPECTED_SHUTDOWN": "Unexpected shutdown",
+  "UNEXPECTED_SHUTDOWN_MSG": "PixiEditor was unexpectedly shut down. We've loaded latest autosave of your files.",
+  "OK": "OK",
+  "OPEN_AUTOSAVES": "Browse Autosaves",
+  "AUTOSAVE_SETTINGS_HEADER": "Autosave",
+  "AUTOSAVE_SETTINGS_SAVE_STATE": "Reopen last files on startup",
+  "AUTOSAVE_SETTINGS_PERIOD": "Autosave period",
+  "AUTOSAVE_ENABLED": "Autosave enabled",
+  "MINUTE_UNIVERSAL": "min",
+  "AUTOSAVE_SETTINGS_SAVE_USER_FILE": "Autosave to selected file",
+  "LOAD_LAZY_FILE_MESSAGE": "To improve startup time, PixiEditor didn't load this file. Click the button below to load it.",
+  "EASING_NODE": "Easing",
+  "EASING_TYPE": "Easing Type",
+  "OPEN_DIRECTORY_ON_EXPORT": "Open directory on export",
+  "ERROR_LOOP_DETECTED_MESSAGE": "Moving this layer will create a loop. Fix it in the Node Graph.",
+  "LINEAR_EASING_TYPE": "Linear",
+  "IN_SINE_EASING_TYPE": "In Sine",
+  "OUT_SINE_EASING_TYPE": "Out Sine",
+  "IN_OUT_SINE_EASING_TYPE": "In Out Sine",
+  "IN_QUAD_EASING_TYPE": "In Quad",
+  "OUT_QUAD_EASING_TYPE": "Out Quad",
+  "IN_OUT_QUAD_EASING_TYPE": "In Out Quad",
+  "IN_CUBIC_EASING_TYPE": "In Cubic",
+  "OUT_CUBIC_EASING_TYPE": "Out Cubic",
+  "IN_OUT_CUBIC_EASING_TYPE": "In Out Cubic",
+  "IN_QUART_EASING_TYPE": "In Quart",
+  "OUT_QUART_EASING_TYPE": "Out Quart",
+  "IN_OUT_QUART_EASING_TYPE": "In Out Quart",
+  "IN_QUINT_EASING_TYPE": "In Quint",
+  "OUT_QUINT_EASING_TYPE": "Out Quint",
+  "IN_OUT_QUINT_EASING_TYPE": "In Out Quint",
+  "IN_EXPO_EASING_TYPE": "In Expo",
+  "OUT_EXPO_EASING_TYPE": "Out Expo",
+  "IN_OUT_EXPO_EASING_TYPE": "In Out Expo",
+  "IN_CIRC_EASING_TYPE": "In Circ",
+  "OUT_CIRC_EASING_TYPE": "Out Circ",
+  "IN_OUT_CIRC_EASING_TYPE": "In Out Circ",
+  "IN_BACK_EASING_TYPE": "In Back",
+  "OUT_BACK_EASING_TYPE": "Out Back",
+  "IN_OUT_BACK_EASING_TYPE": "In Out Back",
+  "IN_ELASTIC_EASING_TYPE": "In Elastic",
+  "OUT_ELASTIC_EASING_TYPE": "Out Elastic",
+  "IN_OUT_ELASTIC_EASING_TYPE": "In Out Elastic",
+  "IN_BOUNCE_EASING_TYPE": "In Bounce",
+  "OUT_BOUNCE_EASING_TYPE": "Out Bounce",
+  "IN_OUT_BOUNCE_EASING_TYPE": "In Out Bounce",
+  "R_G_B_COMBINE_SEPARATE_COLOR_MODE": "RGB",
+  "H_S_V_COMBINE_SEPARATE_COLOR_MODE": "HSV",
+  "H_S_L_COMBINE_SEPARATE_COLOR_MODE": "HSL",
+  "COLOR_MANAGED_COLOR_SAMPLE_MODE": "Color Managed",
+  "RAW_COLOR_SAMPLE_MODE": "Raw",
+  "FRACTAL_PERLIN_NOISE_TYPE": "Perlin",
+  "TURBULENCE_PERLIN_NOISE_TYPE": "Turbulence",
+  "VORONOI_NOISE_TYPE": "Voronoi",
+  "VORONOI_FEATURE": "Feature",
+  "F1_VORONOI_FEATURE": "F1",
+  "F2_VORONOI_FEATURE": "F2",
+  "F2_MINUS_F1_VORONOI_FEATURE": "F2-F1",
+  "RANDOMNESS": "Randomness",
+  "ANGLE_OFFSET": "Angle Offset",
+  "INHERIT_COLOR_SPACE_TYPE": "Inherit",
+  "SRGB_COLOR_SPACE_TYPE": "sRGB",
+  "LINEAR_SRGB_COLOR_SPACE_TYPE": "Linear sRGB",
+  "SIMPLE_OUTLINE_TYPE": "Simple",
+  "GAUSSIAN_OUTLINE_TYPE": "Gaussian",
+  "PIXEL_PERFECT_OUTLINE_TYPE": "Pixel Perfect",
+  "DEGREES_ROTATION_TYPE": "Degrees",
+  "RADIANS_ROTATION_TYPE": "Radians",
+  "WEIGHTED_GRAYSCALE_MODE": "Weighted",
+  "AVERAGE_GRAYSCALE_MODE": "Average",
+  "CUSTOM_GRAYSCALE_MODE": "Custom",
+  "CLAMP_TILE_MODE": "Clamp",
+  "REPEAT_TILE_MODE": "Repeat",
+  "MIRROR_TILE_MODE": "Mirror",
+  "DECAL_TILE_MODE": "Decal",
+  "ERR_UNKNOWN_FILE_FORMAT": "Unknown file format",
+  "ERR_EXPORT_SIZE_INVALID": "Invalid export size. Values must be greater than 0.",
+  "ERR_UNKNOWN_IMG_FORMAT": "Unknown image format '{0}'.",
+  "ERR_FAILED_GENERATE_SPRITE_SHEET": "Failed generating sprite sheet",
+  "ERR_NO_RENDERER": "Animation renderer not found.",
+  "ERR_RENDERING_FAILED": "Rendering failed",
+  "ENABLE_ANALYTICS": "Send anonymous analytics",
+  "ANALYTICS_INFO": "We collect anonymous usage data to improve PixiEditor. No personal data is collected.",
+  "LANGUAGE_INFO": "All translations are community-driven. Join our Discord server for more information.",
+  "UP_TO_DATE_UNKNOWN": "Couldn't check for updates",
+  "UP_TO_DATE": "PixiEditor is up to date",
+  "UPDATE_AVAILABLE": "Update {0} is available",
+  "UPDATE_FAILED_DOWNLOAD": "Failed to download the update",
+  "UPDATE_READY_TO_INSTALL": "Update is ready. Switch to {0}?",
+  "SWITCH_TO_NEW_VERSION": "Switch",
+  "DOWNLOAD_UPDATE": "Download",
+  "DOWNLOADING_UPDATE": "Downloading update...",
+  "CHECKING_FOR_UPDATES": "Checking for updates...",
+  "PAINT_SHAPE_SETTING": "Brush shape",
+  "BOOL_OPERATION_NODE": "Boolean Operation",
+  "FIRST_SHAPE": "First shape",
+  "SECOND_SHAPE": "Second shape",
+  "OPERATION": "Operation",
+  "UNION_VECTOR_PATH_OP": "Union",
+  "DIFFERENCE_VECTOR_PATH_OP": "Difference",
+  "INTERSECT_VECTOR_PATH_OP": "Intersect",
+  "XOR_VECTOR_PATH_OP": "XOR",
+  "REVERSE_DIFFERENCE_VECTOR_PATH_OP": "Reverse Difference",
+  "NO_DOCUMENT_OPEN": "Nothing's here",
+  "EMPTY_DOCUMENT_ACTION_BTN": "Start creating",
+  "ONBOARDING_TITLE": "Welcome to",
+  "ONBOARDING_DESCRIPTION": "Let's set up your workspace!",
+  "ONBOARDING_SKIP_BTN": "Skip",
+  "ONBOARDING_ACTION_BTN": "Let's begin",
+  "ONB_SELECT_PRIMARY_TOOLSET": "Select Your Primary Toolset",
+  "ONB_NEXT_BTN": "Next",
+  "ONB_FINISH_BTN": "Finish",
+  "ONB_BACK_BTN": "Previous",
+  "ONB_ANALYTICS": "Anonymous Analytics",
+  "ONB_ALL_SET": "You are all set!",
+  "ONB_ALL_SET_BTN": "Start creating",
+  "ANALYTICS_INFO_DETAILED": "PixiEditor collects anonymous usage data to improve the app. The data does not contain any personal information. Among other things, PixiEditor tracks the following:\n- Which and how the tools are used\n- How long you've used the app for\n- Which commands are used\n- Performance data\n\n You can opt out of analytics at any time in the settings.",
+  "PRIVACY_POLICY": "Privacy Policy",
+  "ONB_SHORTCUTS": "Select Your Shortcuts",
+  "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER": "Current Node Graph setup disallows creation of a new layer next to the selected one.",
+  "PRIMARY_TOOLSET": "Primary Toolset",
+  "OPEN_ONBOARDING_WINDOW": "Open onboarding window",
+  "USER_NOT_FOUND": "Please enter the email you used to purchase the Founder's Edition.",
+  "SESSION_NOT_VALID": "Session is not valid, please log in again",
+  "SESSION_NOT_FOUND": "Session not found, try logging in again",
+  "INTERNAL_SERVER_ERROR": "There was an internal server error. Please try again later.",
+  "TOO_MANY_REQUESTS": "Too many requests. Try again in {0} seconds.",
+  "SESSION_EXPIRED": "Session expired. Please log in again.",
+  "CONNECTION_ERROR": "Connection error. Please check your internet connection.",
+  "FAIL_LOAD_USER_DATA": "Failed to load saved user data",
+  "LOGOUT": "Logout",
+  "LOGGED_IN_AS": "Hello",
+  "EMAIL_SENT": "Email sent! Check your inbox.",
+  "RESEND_ACTIVATION": "Resend",
+  "INVALID_TOKEN": "Session is invalid or expired. Please log in again.",
+  "ENTER_EMAIL": "Enter your email",
+  "LOGIN_LINK": "Send Login Link",
+  "LOGIN_LINK_INFO": "We'll email you a secure link to log in. No password needed.",
+  "ACCOUNT_WINDOW_TITLE": "Account",
+  "CONNECTION_TIMEOUT": "Connection timed out. Please try again.",
+  "OPEN_ACCOUNT_WINDOW": "Manage Account",
+  "AUTO_SCALE_BACKGROUND": "Auto scale background",
+  "UPDATES": "Updates",
+  "SCENE": "Scene",
+  "CUSTOM_BACKGROUND_SCALE": "Custom background scale",
+  "PRIMARY_BG_COLOR": "Primary background color",
+  "SECONDARY_BG_COLOR": "Secondary background color",
+  "RESET": "Reset",
+  "INSTALL": "Install",
+  "OWNED_PRODUCTS": "Owned Content",
+  "INSTALLING": "Installing",
+  "INSTALLED": "Installed",
+  "ACCOUNT_PROVIDER_INFO": "Account handled by",
+  "UPDATE": "Update",
+  "AUTOSAVE_OPEN_FOLDER": "Open autosave folder",
+  "AUTOSAVE_OPEN_FOLDER_DESCRIPTIVE": "Open the folder where autosaves are stored",
+  "AUTOSAVE_TOGGLE_DESCRIPTIVE": "Enable/disable autosave",
+  "FOUNDERS_BUNDLE": "Founder's Bundle",
+  "FOUNDERS_BUNDLE_SUBTEXT": "Support PixiEditor and boost your productivity!",
+  "BECOME_A_FOUNDER": "Become a Founder",
+  "LOGIN": "Login",
+  "NOT_FOUNDER_YET": "Not a Founder yet?",
+  "ERROR_GRAPH": "Graph setup produced an error. Fix it in the node graph",
+  "COLOR_MATRIX_FILTER_NODE": "Color Matrix Filter",
+  "WORKSPACE": "Workspace",
+  "IS_DEFAULT_EXPORT": "Is Default Export",
+  "EXPORT_OUTPUT": "Export Output",
+  "RENDER_OUTPUT_SIZE": "Render Output Size",
+  "RENDER_OUTPUT_CENTER": "Render Output Center",
+  "COLOR_PICKER": "Color Picker",
+  "UNAUTHORIZED_ACCESS": "Unauthorized access",
+  "SEPARATE_SHAPES": "Separate Shapes",
+  "SEPARATE_SHAPES_DESCRIPTIVE": "Separate shapes from current vector into individual layers",
+  "TEXT": "Text",
+  "EXTRACT_SELECTED_TEXT": "Extract selected text",
+  "EXTRACT_SELECTED_TEXT_DESCRIPTIVE": "Extract selected text into new layer.",
+  "EXTRACT_SELECTED_CHARACTERS": "Extract selected characters",
+  "EXTRACT_SELECTED_CHARACTERS_DESCRIPTIVE": "Extract individual characters from selection into new layers.",
+  "STEP_START": "Step back to closest cel",
+  "STEP_END": "Step forward to closest cel",
+  "STEP_FORWARD": "Step forward one frame",
+  "STEP_BACK": "Step back one frame",
+  "ANIMATION_QUALITY_PRESET": "Quality Preset",
+  "VERY_LOW_QUALITY_PRESET": "Very Low",
+  "LOW_QUALITY_PRESET": "Low",
+  "MEDIUM_QUALITY_PRESET": "Medium",
+  "HIGH_QUALITY_PRESET": "High",
+  "VERY_HIGH_QUALITY_PRESET": "Very High",
+  "EXPORT_FRAMES": "Export Frames",
+  "NORMALIZE_OFFSET": "Normalize Offset",
+  "TANGENT": "Tangent",
+  "EVALUATE_PATH_NODE": "Evaluate Path",
+  "OLD_MIN": "Old Min",
+  "OLD_MAX": "Old Max",
+  "NEW_MIN": "New Min",
+  "NEW_MAX": "New Max",
+  "REMAP_NODE": "Remap",
+  "TEXT_TOOL_ACTION_DISPLAY": "Click on the canvas to add a new text (drag while clicking to set the size). Click on existing text to edit it.",
+  "PASTE_CELS": "Paste cels",
+  "PASTE_CELS_DESCRIPTIVE": "Paste cels from clipboard into the current frame",
+  "SCALE_X": "Scale X",
+  "SCALE_Y": "Scale Y",
+  "TRANSLATE_X": "Translate X",
+  "TRANSLATE_Y": "Translate Y",
+  "SKEW_X": "Skew X",
+  "SKEW_Y": "Skew Y",
+  "PERSPECTIVE_0": "Perspective 0",
+  "PERSPECTIVE_1": "Perspective 1",
+  "PERSPECTIVE_2": "Perspective 2",
+  "COMPOSE_MATRIX": "Compose Matrix",
+  "DECOMPOSE_MATRIX": "Decompose Matrix",
+  "NORMALIZE_COORDINATES": "Normalize Coordinates",
+  "TRANSFORMED_POSITION": "Transformed Position",
+  "ACCOUNT_PROVIDER_NOT_AVAILABLE": "This build of PixiEditor does not support accounts. Use the official build from pixieditor.net to manage your account.",
+  "STEAM_OFFLINE": "Cannot validate the account. Steam is offline. Make sure Steam client is running and you are logged in.",
+  "ERROR_GPU_RESOURCES_CREATION": "Failed to create resources: Try updating your GPU drivers or try setting different rendering api in settings. \nError: '{0}'",
+  "ERROR_SAVING_PREFERENCES_DESC": "Failed to save preferences with error: '{0}'. Please check if you have write permissions to the PixiEditor data folder.",
+  "ERROR_SAVING_PREFERENCES": "Failed to save preferences",
+  "PREFERRED_RENDERER": "Preferred Render Api",
+  "PERFORMANCE": "Performance",
+  "DISABLE_PREVIEWS": "Disable Previews",
+  "MAX_BILINEAR_CANVAS_SIZE": "Max Bilinear Canvas Size",
+  "MAX_BILINEAR_CANVAS_SIZE_DESC": "Maximum canvas size for bilinear filtering. Set to 0 to disable bilinear filtering. Bilinear filtering improves the quality of the canvas, but can cause performance issues on large canvases.",
+  "INVERT_MASK": "Invert mask",
+  "SLICE_TEXT_NODE": "Slice text",
+  "INDEX_START_AT": "Start at",
+  "CHARACTER_POSITION_NODE": "Character Position",
+  "FIRST_POSITION": "First position",
+  "LAST_POSITION": "Last position",
+  "MATCH_CASE": "Match case",
+  "SEARCH_TEXT": "Search text",
+  "TEXT_INFO_NODE": "Text info",
+  "TEXT_LENGTH": "Length",
+  "TEXT_LINE_COUNT": "Line count",
+  "TEXT_SLICE_USE_LENGTH": "Use length",
+  "TOGGLE_TINTING_SELECTION": "Toggle selection tinting",
+  "TOGGLE_TINTING_SELECTION_DESCRIPTIVE": "Toggle selection tinting",
+  "TINT_SELECTION": "Selection tinting",
+  "PAINT_BRUSH_SHAPE_CIRCLE": "Circle",
+  "PAINT_BRUSH_SHAPE_SQUARE": "Square",
+  "BRIGHTNESS_MODE_DEFAULT": "Default",
+  "BRIGHTNESS_MODE_REPEAT": "Repeat",
+  "ROUND_STROKE_CAP": "Round",
+  "BUTT_STROKE_CAP": "Butt",
+  "SQUARE_STROKE_CAP": "Square",
+  "ROUND_STROKE_JOIN": "Round",
+  "MITER_STROKE_JOIN": "Miter",
+  "BEVEL_STROKE_JOIN": "Bevel",
+  "TOGGLE_FULLSCREEN": "Toggle Fullscreen",
+  "TOGGLE_FULLSCREEN_DESCRIPTIVE": "Enter or Exit Fullscreen Mode",
+  "SHORTCUTS_IMPORTED_WITH_ERRORS": "Importing shortcuts finished with errors.\nSome shortcuts were not recognized and have been skipped.",
+  "OPEN_ERROR_LOG": "Open error log"
 }
 }

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

@@ -86,6 +86,12 @@ internal class SupportedFilesHelper
         string? localPath = file.TryGetLocalPath();
         string? localPath = file.TryGetLocalPath();
 
 
         string extension = Path.GetExtension(localPath ?? file.Name);
         string extension = Path.GetExtension(localPath ?? file.Name);
+
+        if (string.IsNullOrEmpty(extension))
+        {
+            return allSupportedExtensions.First(i => i.CanSave);
+        }
+
         return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
         return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
     }
 
 

+ 11 - 1
src/PixiEditor/Models/Commands/ShortcutsTemplate.cs

@@ -11,6 +11,9 @@ public sealed class ShortcutsTemplate
 {
 {
     public List<Shortcut> Shortcuts { get; set; }
     public List<Shortcut> Shortcuts { get; set; }
 
 
+    [NonSerialized]
+    public List<string> Errors = new List<string>();
+
     public ShortcutsTemplate()
     public ShortcutsTemplate()
     {
     {
         Shortcuts = new List<Shortcut>();
         Shortcuts = new List<Shortcut>();
@@ -23,7 +26,14 @@ public sealed class ShortcutsTemplate
         {
         {
             foreach (string command in keyDefinition.Commands)
             foreach (string command in keyDefinition.Commands)
             {
             {
-                template.Shortcuts.Add(new Shortcut(keyDefinition.DefaultShortcut.ToKeyCombination(), command));
+                try
+                {
+                    template.Shortcuts.Add(new Shortcut(keyDefinition.DefaultShortcut.ToKeyCombination(), command));
+                }
+                catch (ArgumentException) // Invalid key
+                {
+                    template.Errors.Add($"Error for command {command}: Invalid key '{keyDefinition.DefaultShortcut.key}' with modifiers '{string.Join(", ", keyDefinition.DefaultShortcut.modifiers ?? [])}'");
+                }
             }
             }
         }
         }
 
 

+ 3 - 0
src/PixiEditor/Models/Commands/Templates/Providers/Parsers/KeyParser.cs

@@ -101,6 +101,9 @@ public static class KeyParser
             case ")":
             case ")":
                 parsed = Key.D0;
                 parsed = Key.D0;
                 return true;
                 return true;
+            case "|":
+                parsed = Key.Oem5;
+                return true;
             default:
             default:
                 parsed = Key.None;
                 parsed = Key.None;
                 return false;
                 return false;

+ 1 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -283,6 +283,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
                 new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering));
                 new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering));
         }
         }
 
 
+        document.TextOverlayHandler.Font = null; // Forces refreshing glyphs
         document.TextOverlayHandler.Font = constructedText.Font;
         document.TextOverlayHandler.Font = constructedText.Font;
         document.TextOverlayHandler.Spacing = toolbar.Spacing;
         document.TextOverlayHandler.Spacing = toolbar.Spacing;
     }
     }

+ 3 - 0
src/PixiEditor/Models/EnumTranslations.cs

@@ -99,6 +99,9 @@ using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
 [assembly: LocalizeEnum<OutlineType>(OutlineType.Gaussian, "GAUSSIAN_OUTLINE_TYPE")]
 [assembly: LocalizeEnum<OutlineType>(OutlineType.Gaussian, "GAUSSIAN_OUTLINE_TYPE")]
 [assembly: LocalizeEnum<OutlineType>(OutlineType.PixelPerfect, "PIXEL_PERFECT_OUTLINE_TYPE")]
 [assembly: LocalizeEnum<OutlineType>(OutlineType.PixelPerfect, "PIXEL_PERFECT_OUTLINE_TYPE")]
 
 
+[assembly: LocalizeEnum<PosterizationMode>(PosterizationMode.Rgb, "RGB_POSTERIZATION_MODE")]
+[assembly: LocalizeEnum<PosterizationMode>(PosterizationMode.Luminance, "LUMINANCE_POSTERIZATION_MODE")]
+
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F1, "F1_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F1, "F1_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2, "F2_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2, "F2_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2MinusF1, "F2_MINUS_F1_VORONOI_FEATURE")]
 [assembly: LocalizeEnum<VoronoiFeature>(VoronoiFeature.F2MinusF1, "F2_MINUS_F1_VORONOI_FEATURE")]

+ 7 - 1
src/PixiEditor/Models/IO/CustomDocumentFormats/FontDocumentBuilder.cs

@@ -14,7 +14,13 @@ internal class FontDocumentBuilder : IDocumentBuilder
 
 
     public void Build(DocumentViewModelBuilder builder, string path)
     public void Build(DocumentViewModelBuilder builder, string path)
     {
     {
-        Font font = Font.FromFontFamily(new FontFamilyName(new Uri(path), Path.GetFileNameWithoutExtension(path)));
+        Font? font = Font.FromFontFamily(new FontFamilyName(new Uri(path), Path.GetFileNameWithoutExtension(path)));
+
+        if (font is null)
+        {
+            throw new Exception("Failed to load font");
+        }
+
         font.Size = 12;
         font.Size = 12;
 
 
         List<char> glyphs = new();
         List<char> glyphs = new();

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -43,5 +43,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.1.13")]
-[assembly: AssemblyFileVersion("2.0.1.13")]
+[assembly: AssemblyVersion("2.0.1.15")]
+[assembly: AssemblyFileVersion("2.0.1.15")]

+ 3 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -171,7 +171,9 @@ internal partial class DocumentViewModel
         if (previousElement != null)
         if (previousElement != null)
         {
         {
             var clone = previousElement.Clone();
             var clone = previousElement.Clone();
-            (clone as SvgPrimitive).ClipPath.Unit = null;
+            if(clone is not IClipable clipable)
+                return;
+            clipable.ClipPath.Unit = null;
             clone.Id.Unit = null;
             clone.Id.Unit = null;
             defs.Children.Add(new SvgClipPath()
             defs.Children.Add(new SvgClipPath()
             {
             {

+ 1 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -952,7 +952,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         if (transformed.X < 0 || transformed.Y < 0 || transformed.X >= bitmap.Size.X || transformed.Y >= bitmap.Size.Y)
         if (transformed.X < 0 || transformed.Y < 0 || transformed.X >= bitmap.Size.X || transformed.Y >= bitmap.Size.Y)
             return null;
             return null;
 
 
-        return bitmap.GetSRGBPixel(new VecI((int)transformed.X, (int)transformed.Y));
+        return bitmap.GetSrgbPixel(new VecI((int)transformed.X, (int)transformed.Y));
     }
     }
 
 
     public void SuppressAllOverlayEvents(string suppressor)
     public void SuppressAllOverlayEvents(string suppressor)

+ 11 - 0
src/PixiEditor/ViewModels/Document/Nodes/Effects/PosterizationNodeViewModel.cs

@@ -0,0 +1,11 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Effects;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Effects;
+
+[NodeViewModel("POSTERIZATION_NODE", "EFFECTS", PixiPerfectIcons.Swatches)]
+internal class PosterizationNodeViewModel : NodeViewModel<PosterizationNode>
+{
+    
+}
+

+ 24 - 0
src/PixiEditor/ViewModels/Document/Nodes/Text/SliceTextNodeViewModel.cs

@@ -0,0 +1,24 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Text;
+using PixiEditor.Models.Events;
+using PixiEditor.Models.Handlers;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Text;
+
+[NodeViewModel("SLICE_TEXT_NODE", "SHAPE", PixiPerfectIcons.Slice)]
+internal class SliceTextNodeViewModel : NodeViewModel<SliceTextNode>
+{
+    private NodePropertyViewModel<bool> _useLengthProperty;
+    private NodePropertyViewModel _lengthProperty;
+    
+    public override void OnInitialized()
+    {
+        _useLengthProperty = FindInputProperty<bool>("UseLength");
+        _lengthProperty = FindInputProperty("Length");
+        
+        _useLengthProperty.ValueChanged += UseLengthValueChanged;
+    }
+
+    private void UseLengthValueChanged(INodePropertyHandler property, NodePropertyValueChangedArgs args) =>
+        _lengthProperty.IsVisible = _useLengthProperty.Value;
+}

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/Text/TextIndexOfNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Text;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Text;
+
+[NodeViewModel("CHARACTER_POSITION_NODE", "SHAPE", PixiPerfectIcons.WholeWord)]
+internal class TextIndexOfNodeViewModel : NodeViewModel<TextIndexOfNode>;

+ 7 - 0
src/PixiEditor/ViewModels/Document/Nodes/Text/TextInfoNodeViewModel.cs

@@ -0,0 +1,7 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Text;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Text;
+
+[NodeViewModel("TEXT_INFO_NODE", "SHAPE", PixiPerfectIcons.Shredder)]
+internal class TextInfoNodeViewModel : NodeViewModel<TextInfoNode>;

+ 43 - 75
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -41,12 +41,10 @@ internal class SettingsPage : ObservableObject
 internal partial class SettingsWindowViewModel : ViewModelBase
 internal partial class SettingsWindowViewModel : ViewModelBase
 {
 {
     private string searchTerm;
     private string searchTerm;
-    
-    [ObservableProperty]
-    private int visibleGroups;
-    
-    [ObservableProperty]
-    private int currentPage;
+
+    [ObservableProperty] private int visibleGroups;
+
+    [ObservableProperty] private int currentPage;
 
 
     public bool ShowUpdateTab
     public bool ShowUpdateTab
     {
     {
@@ -85,7 +83,8 @@ internal partial class SettingsWindowViewModel : ViewModelBase
     [Command.Internal("PixiEditor.Shortcuts.Reset")]
     [Command.Internal("PixiEditor.Shortcuts.Reset")]
     public static async Task ResetCommand()
     public static async Task ResetCommand()
     {
     {
-        await new OptionsDialog<string>("ARE_YOU_SURE", new LocalizedString("WARNING_RESET_SHORTCUTS_DEFAULT"), MainWindow.Current!)
+        await new OptionsDialog<string>("ARE_YOU_SURE", new LocalizedString("WARNING_RESET_SHORTCUTS_DEFAULT"),
+            MainWindow.Current!)
         {
         {
             { new LocalizedString("YES"), x => CommandController.Current.ResetShortcuts() },
             { new LocalizedString("YES"), x => CommandController.Current.ResetShortcuts() },
             new LocalizedString("CANCEL"),
             new LocalizedString("CANCEL"),
@@ -112,31 +111,13 @@ internal partial class SettingsWindowViewModel : ViewModelBase
             SuggestedStartLocation = suggestedStartLocation,
             SuggestedStartLocation = suggestedStartLocation,
             FileTypeChoices = new List<FilePickerFileType>()
             FileTypeChoices = new List<FilePickerFileType>()
             {
             {
-                new FilePickerFileType("PixiShorts (*.pixisc)")
-                {
-                    Patterns = new List<string>
-                    {
-                        "*.pixisc"
-                    },
-                },
-                new FilePickerFileType("json (*.json)")
-                {
-                    Patterns = new List<string>
-                    {
-                        "*.json"
-                    },
-                },
-                new FilePickerFileType("All files (*.*)")
-                {
-                    Patterns = new List<string>
-                    {
-                        "*.*"
-                    },
-                },
+                new FilePickerFileType("PixiShorts (*.pixisc)") { Patterns = new List<string> { "*.pixisc" }, },
+                new FilePickerFileType("json (*.json)") { Patterns = new List<string> { "*.json" }, },
+                new FilePickerFileType("All files (*.*)") { Patterns = new List<string> { "*.*" }, },
             },
             },
         });
         });
-        
-        
+
+
         if (file is not null)
         if (file is not null)
         {
         {
             try
             try
@@ -146,10 +127,11 @@ internal partial class SettingsWindowViewModel : ViewModelBase
             catch (Exception ex)
             catch (Exception ex)
             {
             {
                 string errMessageTrimmed = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
                 string errMessageTrimmed = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
-                NoticeDialog.Show(title: "ERROR", message: new LocalizedString("UNKNOWN_ERROR_SAVING").Value + $" {errMessageTrimmed}");
+                NoticeDialog.Show(title: "ERROR",
+                    message: new LocalizedString("UNKNOWN_ERROR_SAVING").Value + $" {errMessageTrimmed}");
             }
             }
         }
         }
-        
+
         // Sometimes, focus was brought back to the last edited shortcut
         // Sometimes, focus was brought back to the last edited shortcut
         // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
         // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
     }
     }
@@ -159,37 +141,20 @@ internal partial class SettingsWindowViewModel : ViewModelBase
     {
     {
         List<FilePickerFileType> fileTypes = new List<FilePickerFileType>
         List<FilePickerFileType> fileTypes = new List<FilePickerFileType>
         {
         {
-            new("PixiShorts (*.pixisc)")
-            {
-                Patterns = new List<string>
-                {
-                    "*.pixisc"
-                },
-            },
-            new("json (*.json)")
-            {
-                Patterns = new List<string>
-                {
-                    "*.json"
-                },
-            },
+            new("PixiShorts (*.pixisc)") { Patterns = new List<string> { "*.pixisc" }, },
+            new("json (*.json)") { Patterns = new List<string> { "*.json" }, },
         };
         };
-        
+
         customShortcutFormats ??= ShortcutProvider.GetProviders().OfType<ICustomShortcutFormat>().ToList();
         customShortcutFormats ??= ShortcutProvider.GetProviders().OfType<ICustomShortcutFormat>().ToList();
         AddCustomParsersFormat(customShortcutFormats, fileTypes);
         AddCustomParsersFormat(customShortcutFormats, fileTypes);
-        
-        fileTypes.Add(new FilePickerFileType("All files (*.*)")
-        {
-            Patterns = new List<string>
+
+        fileTypes.Add(new FilePickerFileType("All files (*.*)") { Patterns = new List<string> { "*.*" }, });
+
+        fileTypes.Insert(0,
+            new FilePickerFileType($"All Shortcut files {string.Join(",", fileTypes.SelectMany(a => a.Patterns))}")
             {
             {
-                "*.*"
-            },
-        });
-        
-        fileTypes.Insert(0, new FilePickerFileType($"All Shortcut files {string.Join(",", fileTypes.SelectMany(a => a.Patterns))}")
-        {
-            Patterns = fileTypes.SelectMany(a => a.Patterns).ToList(),
-        });
+                Patterns = fileTypes.SelectMany(a => a.Patterns).ToList(),
+            });
 
 
         IStorageFolder? suggestedLocation = null;
         IStorageFolder? suggestedLocation = null;
         try
         try
@@ -205,34 +170,35 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
 
         IReadOnlyList<IStorageFile> files = await MainWindow.Current!.StorageProvider.OpenFilePickerAsync(new()
         IReadOnlyList<IStorageFile> files = await MainWindow.Current!.StorageProvider.OpenFilePickerAsync(new()
         {
         {
-            AllowMultiple = false,
-            SuggestedStartLocation = suggestedLocation,
-            FileTypeFilter = fileTypes,
+            AllowMultiple = false, SuggestedStartLocation = suggestedLocation, FileTypeFilter = fileTypes,
         });
         });
-        
+
         if (files.Count > 0)
         if (files.Count > 0)
         {
         {
             List<Shortcut> shortcuts = new List<Shortcut>();
             List<Shortcut> shortcuts = new List<Shortcut>();
-            if (!TryImport(files[0], ref shortcuts))
+            if (!TryImport(files[0], ref shortcuts, out var errors))
                 return;
                 return;
-            
+
             CommandController.Current.ResetShortcuts();
             CommandController.Current.ResetShortcuts();
             CommandController.Current.Import(shortcuts, false);
             CommandController.Current.Import(shortcuts, false);
             File.Copy(files[0].Path.LocalPath, CommandController.ShortcutsPath, true);
             File.Copy(files[0].Path.LocalPath, CommandController.ShortcutsPath, true);
-            NoticeDialog.Show("SHORTCUTS_IMPORTED_SUCCESS", "SUCCESS");
+            ImportShortcutTemplatePopup.DisplayImportSuccessInfo(null, errors);
         }
         }
-        
+
         // Sometimes, focus was brought back to the last edited shortcut
         // Sometimes, focus was brought back to the last edited shortcut
         // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
         // TODO: Keyboard.ClearFocus(); should be there but I can't find an equivalent from avalonia
     }
     }
 
 
-    private static bool TryImport(IStorageFile file, ref List<Shortcut> shortcuts)
+    private static bool TryImport(IStorageFile file, ref List<Shortcut> shortcuts, out List<string>? errors)
     {
     {
+        errors = null;
         if (file.Name.EndsWith(".pixisc") || file.Name.EndsWith(".json"))
         if (file.Name.EndsWith(".pixisc") || file.Name.EndsWith(".json"))
         {
         {
             try
             try
             {
             {
-                shortcuts = ShortcutFile.LoadTemplate(file.Path.LocalPath)?.Shortcuts.ToList();
+                var template = ShortcutFile.LoadTemplate(file.Path.LocalPath);
+                errors = template.Errors;
+                shortcuts = template.Shortcuts.ToList();
             }
             }
             catch (Exception)
             catch (Exception)
             {
             {
@@ -258,7 +224,9 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
 
             try
             try
             {
             {
-                shortcuts = provider.KeysParser.Parse(file.Path.LocalPath, false)?.Shortcuts.ToList();
+                var template = provider.KeysParser.Parse(file.Path.LocalPath, false);
+                shortcuts = template?.Shortcuts.ToList();
+                errors = template?.Errors;
             }
             }
             catch (RecoverableException e)
             catch (RecoverableException e)
             {
             {
@@ -270,7 +238,8 @@ internal partial class SettingsWindowViewModel : ViewModelBase
         return true;
         return true;
     }
     }
 
 
-    private static void AddCustomParsersFormat(IList<ICustomShortcutFormat>? customFormats, List<FilePickerFileType> listToAddTo)
+    private static void AddCustomParsersFormat(IList<ICustomShortcutFormat>? customFormats,
+        List<FilePickerFileType> listToAddTo)
     {
     {
         if (customFormats is null || customFormats.Count == 0)
         if (customFormats is null || customFormats.Count == 0)
             return;
             return;
@@ -355,6 +324,7 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
 
                 group.IsVisible = visibleCommands > 0;
                 group.IsVisible = visibleCommands > 0;
             }
             }
+
             return;
             return;
         }
         }
 
 
@@ -381,8 +351,7 @@ internal partial class SettingsWindowViewModel : ViewModelBase
 
 
 internal partial class GroupSearchResult : ObservableObject
 internal partial class GroupSearchResult : ObservableObject
 {
 {
-    [ObservableProperty]
-    private bool isVisible;
+    [ObservableProperty] private bool isVisible;
 
 
     public LocalizedString DisplayName { get; set; }
     public LocalizedString DisplayName { get; set; }
 
 
@@ -397,8 +366,7 @@ internal partial class GroupSearchResult : ObservableObject
 
 
 internal partial class CommandSearchResult : ObservableObject
 internal partial class CommandSearchResult : ObservableObject
 {
 {
-    [ObservableProperty]
-    private bool isVisible;
+    [ObservableProperty] private bool isVisible;
 
 
     public Models.Commands.Commands.Command Command { get; set; }
     public Models.Commands.Commands.Command Command { get; set; }
 
 

+ 19 - 3
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -3,6 +3,7 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
+using System.Net.Sockets;
 using System.Reflection;
 using System.Reflection;
 using System.Text;
 using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -212,9 +213,24 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
 
     public async Task Download()
     public async Task Download()
     {
     {
-        bool updateCompatible = await UpdateChecker.IsUpdateCompatible();
-        bool updateFileDoesNotExists = !AutoUpdateFileExists();
-        bool updateExeDoesNotExists = !UpdateInstallerFileExists();
+        bool updateCompatible, updateFileDoesNotExists, updateExeDoesNotExists;
+        try
+        {
+            updateCompatible = await UpdateChecker.IsUpdateCompatible();
+            updateFileDoesNotExists = !AutoUpdateFileExists();
+            updateExeDoesNotExists = !UpdateInstallerFileExists();
+        }
+        catch (Exception ex)
+        {
+            UpdateState = UpdateState.UnableToCheck;
+            if (ex is not IOException && ex is not SocketException)
+            {
+                CrashHelper.SendExceptionInfo(ex);
+            }
+
+            return;
+        }
+
 
 
         if (!updateExeDoesNotExists || !updateFileDoesNotExists)
         if (!updateExeDoesNotExists || !updateFileDoesNotExists)
         {
         {

+ 19 - 2
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -54,12 +54,17 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     {
     {
         get
         get
         {
         {
-            if (TimeToEndTimeout == null)
+            if (TimeToEndTimeout == null || !EmailEqualsLastSentMail)
             {
             {
                 return string.Empty;
                 return string.Empty;
             }
             }
 
 
             TimeSpan timeLeft = TimeToEndTimeout.Value - DateTime.Now;
             TimeSpan timeLeft = TimeToEndTimeout.Value - DateTime.Now;
+            if(timeLeft.TotalHours > 1)
+                return $"({timeLeft:hh\\:mm\\:ss})";
+            if(timeLeft.TotalMinutes > 1)
+                return $"({timeLeft:mm\\:ss})";
+
             return timeLeft.TotalSeconds > 0 ? $"({timeLeft:ss})" : string.Empty;
             return timeLeft.TotalSeconds > 0 ? $"({timeLeft:ss})" : string.Empty;
         }
         }
     }
     }
@@ -194,6 +199,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             LastError = null;
             LastError = null;
             try
             try
             {
             {
+                lastSentHash = EmailUtility.GetEmailHash(email);
                 await pixiAuthIdentityProvider.RequestLogin(email);
                 await pixiAuthIdentityProvider.RequestLogin(email);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
@@ -206,7 +212,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     public bool CanRequestLogin(string email)
     public bool CanRequestLogin(string email)
     {
     {
         return IdentityProvider is PixiAuthIdentityProvider && !string.IsNullOrEmpty(email) && email.Contains('@') &&
         return IdentityProvider is PixiAuthIdentityProvider && !string.IsNullOrEmpty(email) && email.Contains('@') &&
-               !HasTimeout();
+               !(HasTimeout() && EmailEqualsLastSentMail);
     }
     }
 
 
     public async Task ResendActivation(string email)
     public async Task ResendActivation(string email)
@@ -240,6 +246,10 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
         DispatcherTimer.RunOnce(
         DispatcherTimer.RunOnce(
             () =>
             () =>
             {
             {
+                if (TimeToEndTimeout.HasValue && TimeToEndTimeout.Value > DateTime.Now)
+                {
+                    return;
+                }
                 TimeToEndTimeout = null;
                 TimeToEndTimeout = null;
                 LastError = null;
                 LastError = null;
                 NotifyProperties();
                 NotifyProperties();
@@ -376,6 +386,13 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
 
 
     private void OnError(string error, object? arg = null)
     private void OnError(string error, object? arg = null)
     {
     {
+        if (error != "TOO_MANY_REQUESTS")
+        {
+            TimeToEndTimeout = null;
+            timerCancelable?.Dispose();
+            timerCancelable = null;
+            NotifyProperties();
+        }
         if (error == "SESSION_NOT_VALIDATED")
         if (error == "SESSION_NOT_VALIDATED")
         {
         {
             LastError = null;
             LastError = null;

+ 36 - 26
src/PixiEditor/Views/Main/DocumentPreview.axaml

@@ -11,11 +11,11 @@
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              Name="uc"
              Name="uc"
              x:Class="PixiEditor.Views.Main.DocumentPreview">
              x:Class="PixiEditor.Views.Main.DocumentPreview">
-     <Grid>
+    <Grid>
         <Grid.RowDefinitions>
         <Grid.RowDefinitions>
-            <RowDefinition Height="*"/>
-            <RowDefinition Height="5"/>
-            <RowDefinition Height="Auto"/>
+            <RowDefinition Height="*" />
+            <RowDefinition Height="5" />
+            <RowDefinition Height="Auto" />
         </Grid.RowDefinitions>
         </Grid.RowDefinitions>
 
 
         <Grid x:Name="imageGrid" RenderOptions.BitmapInterpolationMode="None"
         <Grid x:Name="imageGrid" RenderOptions.BitmapInterpolationMode="None"
@@ -28,78 +28,88 @@
                 x:Name="viewport"
                 x:Name="viewport"
                 RenderInDocSize="{Binding ElementName=highDpiButton, Path=IsChecked}"
                 RenderInDocSize="{Binding ElementName=highDpiButton, Path=IsChecked}"
                 Document="{Binding Document, ElementName=uc}"
                 Document="{Binding Document, ElementName=uc}"
-                Background="{Binding ActiveItem.Value, ElementName=backgroundButton}"/>
+                Background="{Binding ActiveItem.Value, ElementName=backgroundButton}" />
         </Grid>
         </Grid>
 
 
         <Grid Grid.Row="1">
         <Grid Grid.Row="1">
             <Grid.Background>
             <Grid.Background>
-                <SolidColorBrush Color="{Binding ColorCursorColor, ElementName=uc, FallbackValue=Black}"/>
+                <SolidColorBrush Color="{Binding ColorCursorColor, ElementName=uc, FallbackValue=Black}" />
             </Grid.Background>
             </Grid.Background>
         </Grid>
         </Grid>
         <StackPanel Margin="10, 0, 0, 0" Grid.Row="2" Orientation="Horizontal" Height="30"
         <StackPanel Margin="10, 0, 0, 0" Grid.Row="2" Orientation="Horizontal" Height="30"
                     Background="{DynamicResource ThemeBackgroundBrush}">
                     Background="{DynamicResource ThemeBackgroundBrush}">
             <StackPanel.Styles>
             <StackPanel.Styles>
                 <Style Selector="TextBlock">
                 <Style Selector="TextBlock">
-                    <Setter Property="VerticalAlignment" Value="Center"/>
+                    <Setter Property="VerticalAlignment" Value="Center" />
                 </Style>
                 </Style>
             </StackPanel.Styles>
             </StackPanel.Styles>
 
 
-            <TextBlock Text="{Binding ColorCursorPosition.X, ElementName=uc, StringFormat='X: {0}'}"/>
-            <TextBlock Text="{Binding ColorCursorPosition.Y, ElementName=uc, StringFormat='Y: {0}'}"/>
+            <TextBlock>
+                <Run Text="{Binding ColorCursorPosition.X, ElementName=uc, StringFormat='X: {0}'}" />
+                <Run Text="{Binding ColorCursorPosition.Y, ElementName=uc, StringFormat='Y: {0}'}" />
+            </TextBlock>
 
 
             <TextBlock VerticalAlignment="Center" Margin="10, 0, 0, 0">
             <TextBlock VerticalAlignment="Center" Margin="10, 0, 0, 0">
                 <TextBlock.Text>
                 <TextBlock.Text>
                     <MultiBinding Converter="{converters:FormattedColorConverter}">
                     <MultiBinding Converter="{converters:FormattedColorConverter}">
-                        <Binding Path="ColorCursorColor" ElementName="uc"/>
-                        <Binding Path="ActiveItem.Value" ElementName="formatButton"/>
+                        <Binding Path="ColorCursorColor" ElementName="uc" />
+                        <Binding Path="ActiveItem.Value" ElementName="formatButton" />
                     </MultiBinding>
                     </MultiBinding>
                 </TextBlock.Text>
                 </TextBlock.Text>
             </TextBlock>
             </TextBlock>
         </StackPanel>
         </StackPanel>
-        <Grid Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,5,0" ui:RenderOptionsBindable.BitmapInterpolationMode="{Binding ElementName=backgroundButton, Path=ActiveItem.ScalingMode}">
+        <Grid Grid.Row="2" HorizontalAlignment="Right" Margin="0,0,5,0"
+              ui:RenderOptionsBindable.BitmapInterpolationMode="{Binding ElementName=backgroundButton, Path=ActiveItem.ScalingMode}">
             <StackPanel Spacing="5" Orientation="Horizontal">
             <StackPanel Spacing="5" Orientation="Horizontal">
                 <StackPanel.Styles>
                 <StackPanel.Styles>
                     <Style Selector="ToggleButton#highDpiButton">
                     <Style Selector="ToggleButton#highDpiButton">
-                        <Setter Property="Content" Value="{DynamicResource icon-circle}"/>
+                        <Setter Property="Content" Value="{DynamicResource icon-circle}" />
                     </Style>
                     </Style>
                     <Style Selector="ToggleButton#highDpiButton:checked">
                     <Style Selector="ToggleButton#highDpiButton:checked">
-                        <Setter Property="Content" Value="{DynamicResource icon-lowres-circle}"/>
-                        <Setter Property="Background" Value="Transparent"/>
-                        <Setter Property="BorderThickness" Value="0"/>
+                        <Setter Property="Content" Value="{DynamicResource icon-lowres-circle}" />
+                        <Setter Property="Background" Value="Transparent" />
+                        <Setter Property="BorderThickness" Value="0" />
                     </Style>
                     </Style>
                 </StackPanel.Styles>
                 </StackPanel.Styles>
 
 
-                <ToggleButton x:Name="highDpiButton" Classes="pixi-icon" localization:Translator.TooltipKey="TOGGLE_HIGH_RES_PREVIEW"/>
+                <ToggleButton x:Name="highDpiButton" Classes="pixi-icon"
+                              localization:Translator.TooltipKey="TOGGLE_HIGH_RES_PREVIEW" />
                 <input:ListSwitchButton x:Name="formatButton" Height="20">
                 <input:ListSwitchButton x:Name="formatButton" Height="20">
                     <input:ListSwitchButton.Items>
                     <input:ListSwitchButton.Items>
                         <input:SwitchItemObservableCollection>
                         <input:SwitchItemObservableCollection>
-                            <input:SwitchItem Content="RGBA" Background="{DynamicResource ThemeControlMidBrush}" Value="RGBA"/>
-                            <input:SwitchItem Content="HEX" Background="{DynamicResource ThemeControlMidBrush}" Value="HEX"/>
+                            <input:SwitchItem Content="RGBA" Background="{DynamicResource ThemeControlMidBrush}"
+                                              Value="RGBA" />
+                            <input:SwitchItem Content="HEX" Background="{DynamicResource ThemeControlMidBrush}"
+                                              Value="HEX" />
                         </input:SwitchItemObservableCollection>
                         </input:SwitchItemObservableCollection>
                     </input:ListSwitchButton.Items>
                     </input:ListSwitchButton.Items>
                 </input:ListSwitchButton>
                 </input:ListSwitchButton>
-                <input:ListSwitchButton RenderOptions.BitmapInterpolationMode="None" BorderBrush="{DynamicResource ThemeBorderMidBrush}" Width="25" Height="20" x:Name="backgroundButton">
+                <input:ListSwitchButton RenderOptions.BitmapInterpolationMode="None"
+                                        BorderBrush="{DynamicResource ThemeBorderMidBrush}" Width="25" Height="20"
+                                        x:Name="backgroundButton">
                     <input:ListSwitchButton.Items>
                     <input:ListSwitchButton.Items>
                         <input:SwitchItemObservableCollection>
                         <input:SwitchItemObservableCollection>
                             <input:SwitchItem ScalingMode="None">
                             <input:SwitchItem ScalingMode="None">
                                 <input:SwitchItem.Background>
                                 <input:SwitchItem.Background>
-                                    <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile" DestinationRect="0, 0, 25 25"/>
+                                    <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile"
+                                                DestinationRect="0, 0, 25 25" />
                                 </input:SwitchItem.Background>
                                 </input:SwitchItem.Background>
                                 <input:SwitchItem.Value>
                                 <input:SwitchItem.Value>
-                                    <ImageBrush DestinationRect="0, 10, 10, 10" Source="/Images/CheckerTile.png" TileMode="Tile"/>
+                                    <ImageBrush DestinationRect="0, 10, 10, 10" Source="/Images/CheckerTile.png"
+                                                TileMode="Tile" />
                                 </input:SwitchItem.Value>
                                 </input:SwitchItem.Value>
                             </input:SwitchItem>
                             </input:SwitchItem>
                             <input:SwitchItem Value="Transparent">
                             <input:SwitchItem Value="Transparent">
                                 <input:SwitchItem.Background>
                                 <input:SwitchItem.Background>
-                                    <ImageBrush Source="/Images/DiagonalRed.png"/>
+                                    <ImageBrush Source="/Images/DiagonalRed.png" />
                                 </input:SwitchItem.Background>
                                 </input:SwitchItem.Background>
                             </input:SwitchItem>
                             </input:SwitchItem>
-                            <input:SwitchItem Background="White" Value="White"/>
-                            <input:SwitchItem Background="Black" Value="Black"/>
+                            <input:SwitchItem Background="White" Value="White" />
+                            <input:SwitchItem Background="Black" Value="Black" />
                         </input:SwitchItemObservableCollection>
                         </input:SwitchItemObservableCollection>
                     </input:ListSwitchButton.Items>
                     </input:ListSwitchButton.Items>
                 </input:ListSwitchButton>
                 </input:ListSwitchButton>
             </StackPanel>
             </StackPanel>
         </Grid>
         </Grid>
     </Grid>
     </Grid>
-</UserControl>
+</UserControl>

+ 1 - 1
src/PixiEditor/Views/Nodes/ConnectionRenderer.cs

@@ -51,7 +51,7 @@ public class ConnectionRenderer : Control
                     ? socket
                     ? socket
                     : null;
                     : null;
 
 
-            if (inputSocket == null || outputSocket == null)
+            if (inputSocket == null || outputSocket == null || !inputSocket.IsVisible || !outputSocket.IsVisible)
             {
             {
                 continue;
                 continue;
             }
             }

+ 1 - 0
src/PixiEditor/Views/Nodes/Properties/BooleanPropertyView.axaml

@@ -10,5 +10,6 @@
                              x:Class="PixiEditor.Views.Nodes.Properties.BooleanPropertyView">
                              x:Class="PixiEditor.Views.Nodes.Properties.BooleanPropertyView">
     <StackPanel Orientation="Horizontal" HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
     <StackPanel Orientation="Horizontal" HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <CheckBox localization:Translator.Key="{Binding DisplayName}" Margin="0,0,4,0" IsVisible="{Binding ShowInputField}" IsChecked="{Binding Value}"/>
         <CheckBox localization:Translator.Key="{Binding DisplayName}" Margin="0,0,4,0" IsVisible="{Binding ShowInputField}" IsChecked="{Binding Value}"/>
+        <TextBlock localization:Translator.Key="{Binding DisplayName}" Margin="0,0,4,0" IsVisible="{Binding !ShowInputField}" />
     </StackPanel>
     </StackPanel>
 </properties:NodePropertyView>
 </properties:NodePropertyView>

+ 11 - 0
src/PixiEditor/Views/Overlays/Overlay.cs

@@ -160,6 +160,12 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
         if (args.Handled) return;
         if (args.Handled) return;
         PointerPressedOverlay?.Invoke(args);
         PointerPressedOverlay?.Invoke(args);
     }
     }
+    
+    public void TextInput(string text)
+    {
+        if(SuppressEvents) return;
+        OnOverlayTextInput(text);
+    }
 
 
     public void ReleasePointer(OverlayPointerArgs args)
     public void ReleasePointer(OverlayPointerArgs args)
     {
     {
@@ -350,6 +356,11 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     {
     {
     }
     }
 
 
+    protected virtual void OnOverlayTextInput(string text)
+    {
+        
+    }
+
     private static void OnZoomScaleChanged(AvaloniaPropertyChangedEventArgs<double> e)
     private static void OnZoomScaleChanged(AvaloniaPropertyChangedEventArgs<double> e)
     {
     {
         if (e.Sender is Overlay overlay)
         if (e.Sender is Overlay overlay)

+ 22 - 19
src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs

@@ -127,6 +127,8 @@ internal class TextOverlay : Overlay
     private Paint opacityPaint;
     private Paint opacityPaint;
     private Paint sampleTextPaint;
     private Paint sampleTextPaint;
 
 
+    private bool canInsertText;
+
     private int lastXMovementCursorIndex;
     private int lastXMovementCursorIndex;
 
 
     static TextOverlay()
     static TextOverlay()
@@ -209,10 +211,7 @@ internal class TextOverlay : Overlay
         };
         };
 
 
         opacityPaint = new Paint() { Color = Colors.White.WithAlpha(ThemeResources.SelectionFillColor.A) };
         opacityPaint = new Paint() { Color = Colors.White.WithAlpha(ThemeResources.SelectionFillColor.A) };
-        sampleTextPaint = new Paint()
-        {
-            Color = Colors.Black, Style = PaintStyle.Fill, IsAntiAliased = true
-        };
+        sampleTextPaint = new Paint() { Color = Colors.Black, Style = PaintStyle.Fill, IsAntiAliased = true };
     }
     }
 
 
 
 
@@ -472,6 +471,13 @@ internal class TextOverlay : Overlay
         return indexOfClosest;
         return indexOfClosest;
     }
     }
 
 
+    protected override void OnOverlayTextInput(string text)
+    {
+        if (!IsEditing || !canInsertText) return;
+
+        InsertTextAtCursor(text);
+    }
+
     protected override void OnKeyPressed(KeyEventArgs args)
     protected override void OnKeyPressed(KeyEventArgs args)
     {
     {
         if (!IsEditing) return;
         if (!IsEditing) return;
@@ -484,16 +490,23 @@ internal class TextOverlay : Overlay
         if (IsRegisteredExternalShortcut(key, keyModifiers))
         if (IsRegisteredExternalShortcut(key, keyModifiers))
         {
         {
             ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
             ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
+            canInsertText = false;
             return;
             return;
         }
         }
 
 
         if (IsShortcut(key, keyModifiers))
         if (IsShortcut(key, keyModifiers))
         {
         {
             ExecuteShortcut(key, keyModifiers);
             ExecuteShortcut(key, keyModifiers);
+            canInsertText = false;
             return;
             return;
         }
         }
 
 
-        InsertChar(key, args.KeySymbol);
+        if (key == Key.Tab)
+        {
+            args.Handled = true;
+        }
+
+        canInsertText = !TryInsertSpecialChar(key, args.KeySymbol);
     }
     }
 
 
     private bool IsRegisteredExternalShortcut(Key key, KeyModifiers keyModifiers)
     private bool IsRegisteredExternalShortcut(Key key, KeyModifiers keyModifiers)
@@ -506,25 +519,15 @@ internal class TextOverlay : Overlay
         return ctxCommand != null;
         return ctxCommand != null;
     }
     }
 
 
-    private void InsertChar(Key key, string symbol)
+    private bool TryInsertSpecialChar(Key key, string symbol)
     {
     {
         if (key == Key.Enter)
         if (key == Key.Enter)
         {
         {
             InsertTextAtCursor("\n");
             InsertTextAtCursor("\n");
+            return true;
         }
         }
-        else if (key == Key.Space)
-        {
-            InsertTextAtCursor(" ");
-        }
-        else
-        {
-            if (symbol is { Length: 1 })
-            {
-                char symbolChar = symbol[0];
-                if (char.IsControl(symbolChar)) return;
-                InsertTextAtCursor(symbol);
-            }
-        }
+
+        return false;
     }
     }
 
 
     private void InsertTextAtCursor(string toAdd)
     private void InsertTextAtCursor(string toAdd)

+ 20 - 0
src/PixiEditor/Views/Rendering/Scene.cs

@@ -647,6 +647,26 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         }
         }
     }
     }
 
 
+    protected override void OnTextInput(TextInputEventArgs e)
+    {
+        base.OnTextInput(e);
+        try
+        {
+            if (AllOverlays != null)
+            {
+                foreach (Overlay overlay in AllOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.TextInput(e.Text);
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
+        }
+    }
+
     private OverlayPointerArgs ConstructPointerArgs(PointerEventArgs e)
     private OverlayPointerArgs ConstructPointerArgs(PointerEventArgs e)
     {
     {
         return new OverlayPointerArgs
         return new OverlayPointerArgs

+ 44 - 5
src/PixiEditor/Views/Shortcuts/ImportShortcutTemplatePopup.axaml.cs

@@ -6,6 +6,8 @@ using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Commands.Templates;
 using PixiEditor.Models.Commands.Templates;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+using PixiEditor.OperatingSystem;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.UI.Common.Localization;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 
 
@@ -38,7 +40,7 @@ internal partial class ImportShortcutTemplatePopup : PixiEditorPopup
         CommandController.Current.Import(defaults.DefaultShortcuts);
         CommandController.Current.Import(defaults.DefaultShortcuts);
 
 
         if (!quiet)
         if (!quiet)
-            Success(provider);
+            DisplayImportSuccessInfo(provider.Name, null);
     }
     }
 
 
     [Command.Internal("PixiEditor.Shortcuts.Provider.ImportInstallation")]
     [Command.Internal("PixiEditor.Shortcuts.Provider.ImportInstallation")]
@@ -55,10 +57,17 @@ internal partial class ImportShortcutTemplatePopup : PixiEditorPopup
         }
         }
 
 
         CommandController.Current.ResetShortcuts();
         CommandController.Current.ResetShortcuts();
+        List<string> errors = new List<string>();
 
 
         try
         try
         {
         {
-            CommandController.Current.Import(defaults.GetInstalledShortcuts().Shortcuts);
+            var template = defaults.GetInstalledShortcuts();
+            if (template?.Errors?.Count > 0)
+            {
+                errors.AddRange(template.Errors);
+            }
+
+            CommandController.Current.Import(template.Shortcuts);
         }
         }
         catch (RecoverableException e)
         catch (RecoverableException e)
         {
         {
@@ -68,12 +77,42 @@ internal partial class ImportShortcutTemplatePopup : PixiEditorPopup
 
 
         if (!quiet)
         if (!quiet)
         {
         {
-            Success(provider);
+            DisplayImportSuccessInfo(provider.Name, errors);
         }
         }
     }
     }
 
 
-    private static void Success(ShortcutProvider provider) =>
-        NoticeDialog.Show(new LocalizedString("SHORTCUTS_IMPORTED", provider.Name), "SUCCESS");
+    public static void DisplayImportSuccessInfo(string? providerName, List<string>? errors)
+    {
+        string title = providerName != null ? "SHORTCUTS_IMPORTED" : "SHORTCUTS_IMPORTED_SUCCESS";
+        if (errors == null || errors.Count == 0)
+        {
+            NoticeDialog.Show(new LocalizedString(title, providerName), "SUCCESS");
+        }
+        else
+        {
+            string errorMessage = string.Join("\n", errors);
+            string errorLogPath = Path.Combine(Paths.TempFilesPath, "shortcut_import_errors.txt");
+            try
+            {
+                File.WriteAllText(errorLogPath, errorMessage);
+            }
+            catch
+            {
+                // ignored
+            }
+
+            OptionsDialog<string> dialog = new(
+                "WARNING",
+                new LocalizedString("SHORTCUTS_IMPORTED_WITH_ERRORS", providerName),
+                MainWindow.Current!)
+            {
+                { new LocalizedString("OPEN_ERROR_LOG"), x => { IOperatingSystem.Current.OpenUri(errorLogPath); } },
+                { new LocalizedString("CLOSE"), x => { } }
+            };
+
+            dialog.ShowDialog();
+        }
+    }
 
 
     // TODO figure out what these are for
     // TODO figure out what these are for
     /*
     /*

+ 1 - 1
tests/Directory.Build.props

@@ -1,7 +1,7 @@
 <Project>
 <Project>
     <PropertyGroup>
     <PropertyGroup>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
         <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
-		<AvaloniaVersion>11.3.0</AvaloniaVersion>
+		<AvaloniaVersion>11.3.5</AvaloniaVersion>
     </PropertyGroup>
     </PropertyGroup>
     <ItemGroup>
     <ItemGroup>
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
         <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />

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