Browse Source

Merge branch 'master' into pixiauth

Krzysztof Krysiński 2 months ago
parent
commit
9062d4d005
100 changed files with 1044 additions and 692 deletions
  1. 66 0
      .github/workflows/tests-windows.yml
  2. 0 85
      pipelines/Linux/tests-ubuntu.yml
  3. 0 85
      pipelines/MacOS/tests-macos.yml
  4. 0 85
      pipelines/Windows/tests-windows.yml
  5. 13 0
      src/.config/dotnet-tools.json
  6. 6 1
      src/ChunkyImageLib/ChunkyImage.cs
  7. 1 1
      src/ChunkyImageLib/IReadOnlyChunkyImage.cs
  8. 1 1
      src/ColorPicker
  9. 1 1
      src/Drawie
  10. 1 1
      src/PixiDocks
  11. 3 2
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  12. 27 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  13. 3 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs
  14. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/VectorLayerNode.cs
  15. 4 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  16. 37 8
      src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs
  17. 9 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs
  18. 3 0
      src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs
  19. 12 6
      src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.Impl.cs
  20. 3 1
      src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/RightClickMode.cs
  21. 3 1
      src/PixiEditor.Extensions.CommonApi/Utilities/ByteWriter.cs
  22. 101 45
      src/PixiEditor.Extensions.Runtime/ExtensionLoader.cs
  23. 5 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs
  24. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs
  25. 1 1
      src/PixiEditor.Linux/LinuxProcessUtility.cs
  26. 3 39
      src/PixiEditor.MacOs/MacOperatingSystem.cs
  27. 8 4
      src/PixiEditor.MacOs/MacOsProcessUtility.cs
  28. 1 3
      src/PixiEditor.OperatingSystem/IOperatingSystem.cs
  29. 1 1
      src/PixiEditor.OperatingSystem/IProcessUtility.cs
  30. 1 1
      src/PixiEditor.UI.Common/Accents/Base.axaml
  31. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Black.ttf
  32. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-BlackItalic.ttf
  33. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Bold.ttf
  34. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-BoldItalic.ttf
  35. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraBold.ttf
  36. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraBoldItalic.ttf
  37. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraLight.ttf
  38. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraLightItalic.ttf
  39. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Italic.ttf
  40. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Light.ttf
  41. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-LightItalic.ttf
  42. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Medium.ttf
  43. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-MediumItalic.ttf
  44. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Regular.ttf
  45. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-SemiBold.ttf
  46. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-SemiBoldItalic.ttf
  47. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-Thin.ttf
  48. BIN
      src/PixiEditor.UI.Common/Fonts/FiraSans-ThinItalic.ttf
  49. 0 8
      src/PixiEditor.UI.Common/Fonts/FiraSans.axaml
  50. 2 13
      src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj
  51. 2 2
      src/PixiEditor.UpdateInstaller.Exe/PixiEditor.UpdateInstaller.Exe.csproj
  52. 63 8
      src/PixiEditor.UpdateInstaller.Exe/Program.cs
  53. 19 5
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs
  54. 5 0
      src/PixiEditor.UpdateModule/UpdateChecker.cs
  55. 18 14
      src/PixiEditor.UpdateModule/UpdateDownloader.cs
  56. 75 11
      src/PixiEditor.UpdateModule/UpdateInstaller.cs
  57. 2 1
      src/PixiEditor/Data/Configs/ToolSetsConfig.json
  58. 3 1
      src/PixiEditor/Data/Localization/Languages/en.json
  59. 24 77
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  60. 2 2
      src/PixiEditor/Models/Commands/CommandController.cs
  61. 3 0
      src/PixiEditor/Models/Commands/Templates/Providers/Parsers/KeyDefinition.cs
  62. 6 2
      src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveUserFileJob.cs
  63. 1 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs
  64. 9 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs
  65. 9 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs
  66. 1 1
      src/PixiEditor/Models/Handlers/Tools/IEraserToolHandler.cs
  67. 6 2
      src/PixiEditor/Models/IO/Paths.cs
  68. 12 13
      src/PixiEditor/Models/Rendering/SceneRenderer.cs
  69. 4 6
      src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs
  70. 17 8
      src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs
  71. 3 1
      src/PixiEditor/Models/Serialization/Factories/FontFamilySerializationFactory.cs
  72. 17 0
      src/PixiEditor/Models/Serialization/Factories/SerializationFactory.cs
  73. 4 3
      src/PixiEditor/Models/Serialization/Factories/TextSerializationFactory.cs
  74. 1 1
      src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs
  75. 8 5
      src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs
  76. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  77. 8 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  78. 2 2
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  79. 2 1
      src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs
  80. 10 8
      src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs
  81. 12 2
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  82. 23 4
      src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs
  83. 165 30
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  84. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs
  85. 4 4
      src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs
  86. 24 0
      src/PixiEditor/ViewModels/ViewModelMain.cs
  87. 49 44
      src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml
  88. 12 2
      src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml.cs
  89. 3 0
      src/PixiEditor/Views/Main/MainTitleBar.axaml
  90. 21 1
      src/PixiEditor/Views/Main/MainTitleBar.axaml.cs
  91. 4 3
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs
  92. 19 1
      src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs
  93. 1 3
      src/PixiEditor/Views/MainView.axaml
  94. 8 0
      src/PixiEditor/Views/Palettes/PaletteViewer.axaml.cs
  95. 2 1
      src/PixiEditor/Views/Rendering/Scene.cs
  96. 9 2
      src/PixiEditor/Views/Visuals/PixiFilePreviewImage.cs
  97. 1 0
      tests/PixiEditor.Tests/PixiEditor.Tests.csproj
  98. 1 3
      tests/PixiEditor.Tests/PixiEditorTest.cs
  99. 19 0
      tests/PixiEditor.Tests/SerializationTests.cs
  100. 7 7
      tests/PixiEditorTests.sln

+ 66 - 0
.github/workflows/tests-windows.yml

@@ -0,0 +1,66 @@
+name: Tests Windows
+
+on:
+  push:
+    branches: [ "master" ]
+  pull_request:
+    branches: [ "master" ]
+
+env:
+  wasiVer: 'wasi-sdk-25.0-x86_64-windows'
+  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-windows.tar.gz'
+
+jobs:
+
+  build:
+
+    strategy:
+      matrix:
+        configuration: [Release]
+
+    runs-on: windows-latest  # For a list of available runner types, refer to
+                             # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v4
+      with:
+        fetch-depth: 0
+        submodules: 'recursive'
+
+    # Install the .NET Core workload
+    - name: Install .NET Core
+      uses: actions/setup-dotnet@v4
+      with:
+        dotnet-version: 8.0.x
+
+    - name: Install wasi-experimental workload
+      working-directory: tests
+      run: dotnet workload install wasi-experimental
+
+    - name: Download WASI SDK
+      run: |
+        Invoke-WebRequest -Uri "${{ env.wasiUrl }}" -OutFile "${{ env.wasiVer }}.tar.gz"
+
+    - name: Unpack WASI SDK
+      run: |
+              tar -xzf ${{ env.wasiVer }}.tar.gz
+              echo "Contents of directory after extraction:"
+              dir "${{ env.wasiVer }}"
+      shell: pwsh
+
+    - name: Set Environment Path for WASI SDK
+      run: |
+              $env:WASI_SDK_PATH = "${{ github.workspace }}\${{ env.wasiVer }}"
+              echo "WASI_SDK_PATH=$env:WASI_SDK_PATH" >> $env:GITHUB_ENV
+      shell: pwsh
+
+    - name: Verify Environment Path
+      run: |
+              Write-Host "Environment path set to: $env:WASI_SDK_PATH"
+      shell: pwsh
+
+    # Execute all unit tests in the solution
+    - name: Execute unit tests
+      working-directory: tests
+      run: dotnet test

+ 0 - 85
pipelines/Linux/tests-ubuntu.yml

@@ -1,85 +0,0 @@
-trigger:
-  - development
-  - master
-  - 2.0-cicd
-
-pool:
-  vmImage: 'ubuntu-latest'
-
-variables:
-  solution: '**/*.sln'
-  buildPlatform: 'linux-$(arch)'
-  buildConfiguration: 'Release'
-  wasiVer: 'wasi-sdk-24.0-$(wasi-arch)-linux'
-  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
-
-steps:
-  - task: UseDotNet@2
-    displayName: 'Install .NET SDK'
-    inputs:
-      packageType: 'sdk'
-      version: '$(dotnetVersion)'
-  - task: CmdLine@2
-    displayName: 'Download WASI SDK'
-    inputs:
-      script: |
-        curl -L -o $(wasiVer).tar.gz $(wasiUrl)
-
-  - task: CmdLine@2
-    displayName: 'Unpack WASI SDK'
-    inputs:
-      script: |
-        tar -xzf $(wasiVer).tar.gz
-        echo "Contents of directory after extraction:"
-        dir $(wasiVer)
-
-  - task: PowerShell@2
-    displayName: 'Set Environment Path for WASI SDK'
-    inputs:
-      targetType: 'inline'
-      script: |
-        $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
-        Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
-
-  - task: PowerShell@2
-    displayName: 'Verify Environment Path'
-    inputs:
-      targetType: 'inline'
-      script: |
-        Write-Host "Environment path set to: $env:WASI_SDK_PATH"
-
-
-  - task: NuGetToolInstaller@1
-
-  - task: DotNetCoreCLI@2
-    displayName: Install wasi-wasm
-    inputs:
-      command: 'custom'
-      custom: 'workload'
-      arguments: 'install wasi-experimental'
-
-  - task: DotNetCoreCLI@2
-    displayName: Install wasm-tools
-    inputs:
-      command: 'custom'
-      custom: 'workload'
-      arguments: 'install wasm-tools'
-
-  - task: NuGetCommand@2
-    displayName: 'Restore solution'
-    inputs:
-      restoreSolution: '$(solution)'
-
-  - task: DotNetCoreCLI@2
-    displayName: Build
-    inputs:
-      command: 'build'
-      projects: '**/*.csproj'
-      arguments: '--configuration Release -r $(buildPlatform)'
-
-  - task: DotNetCoreCLI@2
-    displayName: Tests
-    inputs:
-      command: test
-      projects: '**/*Tests/*.csproj'
-      arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 0 - 85
pipelines/MacOS/tests-macos.yml

@@ -1,85 +0,0 @@
-trigger:
-  - development
-  - master
-  - 2.0-cicd
-
-pool:
-  vmImage: 'macos-latest'
-
-variables:
-  solution: '**/*.sln'
-  buildPlatform: 'osx-$(arch)'
-  buildConfiguration: 'Release'
-  wasiVer: 'wasi-sdk-24.0-$(wasi-arch)-macos'
-  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
-
-steps:
-  - task: UseDotNet@2
-    displayName: 'Install .NET SDK'
-    inputs:
-      packageType: 'sdk'
-      version: '$(dotnetVersion)'
-  - task: CmdLine@2
-    displayName: 'Download WASI SDK'
-    inputs:
-      script: |
-        curl -L -o $(wasiVer).tar.gz $(wasiUrl)
-
-  - task: CmdLine@2
-    displayName: 'Unpack WASI SDK'
-    inputs:
-      script: |
-        tar -xzf $(wasiVer).tar.gz
-        echo "Contents of directory after extraction:"
-        dir $(wasiVer)
-
-  - task: PowerShell@2
-    displayName: 'Set Environment Path for WASI SDK'
-    inputs:
-      targetType: 'inline'
-      script: |
-        $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
-        Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
-
-  - task: PowerShell@2
-    displayName: 'Verify Environment Path'
-    inputs:
-      targetType: 'inline'
-      script: |
-        Write-Host "Environment path set to: $env:WASI_SDK_PATH"
-
-
-  - task: NuGetToolInstaller@1
-
-  - task: DotNetCoreCLI@2
-    displayName: Install wasi-wasm
-    inputs:
-      command: 'custom'
-      custom: 'workload'
-      arguments: 'install wasi-experimental'
-
-  - task: DotNetCoreCLI@2
-    displayName: Install wasm-tools
-    inputs:
-      command: 'custom'
-      custom: 'workload'
-      arguments: 'install wasm-tools'
-
-  - task: NuGetCommand@2
-    displayName: 'Restore solution'
-    inputs:
-      restoreSolution: '$(solution)'
-
-  - task: DotNetCoreCLI@2
-    displayName: Build
-    inputs:
-      command: 'build'
-      projects: '**/*.csproj'
-      arguments: '--configuration Release -r $(buildPlatform)'
-
-  - task: DotNetCoreCLI@2
-    displayName: Tests
-    inputs:
-      command: test
-      projects: '**/*Tests/*.csproj'
-      arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 0 - 85
pipelines/Windows/tests-windows.yml

@@ -1,85 +0,0 @@
-trigger:
-  - development
-  - master
-  - 2.0-cicd
-
-pool:
-  vmImage: 'windows-latest'
-
-variables:
-  solution: '**/*.sln'
-  buildPlatform: 'win-$(arch)'
-  buildConfiguration: 'Release'
-  wasiVer: 'wasi-sdk-24.0-$(wasi-arch)-windows'
-  wasiUrl: 'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/$(wasiVer).tar.gz'
-
-steps:
-  - task: UseDotNet@2
-    displayName: 'Install .NET SDK'
-    inputs:
-      packageType: 'sdk'
-      version: '$(dotnetVersion)'
-
-  - task: CmdLine@2
-    displayName: 'Download WASI SDK'
-    inputs:
-      script: |
-        curl -L -o $(wasiVer).tar.gz $(wasiUrl)
-
-  - task: CmdLine@2
-    displayName: 'Unpack WASI SDK'
-    inputs:
-      script: |
-        tar -xzf $(wasiVer).tar.gz
-        echo "Contents of directory after extraction:"
-        dir $(wasiVer)
-
-  - task: PowerShell@2
-    displayName: 'Set Environment Path for WASI SDK'
-    inputs:
-      targetType: 'inline'
-      script: |
-        $env:WASI_SDK_PATH = "$(Get-Location)\$(wasiVer)"
-        Write-Host "##vso[task.setvariable variable=WASI_SDK_PATH]$env:WASI_SDK_PATH"
-
-  - task: PowerShell@2
-    displayName: 'Verify Environment Path'
-    inputs:
-      targetType: 'inline'
-      script: |
-        Write-Host "Environment path set to: $env:WASI_SDK_PATH"
-
-  - task: NuGetToolInstaller@1
-
-  - task: DotNetCoreCLI@2
-    displayName: Install wasi-wasm
-    inputs:
-      command: 'custom'
-      custom: 'workload'
-      arguments: 'install wasi-experimental'
-
-  - task: DotNetCoreCLI@2
-    displayName: Install wasm-tools
-    inputs:
-      command: 'custom'
-      custom: 'workload'
-      arguments: 'install wasm-tools'
-
-  - task: NuGetCommand@2
-    displayName: 'Restore solution'
-    inputs:
-      restoreSolution: '$(solution)'
-
-  - task: DotNetCoreCLI@2
-    displayName: Build
-    inputs:
-      command: 'build'
-      projects: '**/*.csproj'
-      arguments: '--configuration Release -r $(buildPlatform)'
-
-  - task: DotNetCoreCLI@2
-    displayName: Tests
-    inputs:
-      command: test
-      projects: '**/*Tests/*.csproj'
-      arguments: '--configuration $(buildConfiguration) -r $(buildPlatform)'

+ 13 - 0
src/.config/dotnet-tools.json

@@ -0,0 +1,13 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {
+    "dotnet-dump": {
+      "version": "9.0.607501",
+      "commands": [
+        "dotnet-dump"
+      ],
+      "rollForward": false
+    }
+  }
+}

+ 6 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -191,7 +191,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     /// Finds the precise bounds in <paramref name="suggestedResolution"/>. If there are no chunks rendered for that resolution, full res chunks are used instead.
     /// </summary>
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
-    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full)
+    public RectI? FindTightCommittedBounds(ChunkResolution suggestedResolution = ChunkResolution.Full, bool fallbackToChunkAligned = false)
     {
         lock (lockObject)
         {
@@ -225,6 +225,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
                 }
                 else
                 {
+                    if (fallbackToChunkAligned)
+                    {
+                        return FindChunkAlignedCommittedBounds();
+                    }
+
                     RectI visibleArea = new RectI(chunkPos * FullChunkSize, new VecI(FullChunkSize))
                         .Intersect(new RectI(VecI.Zero, CommittedSize)).Translate(-chunkPos * FullChunkSize);
 

+ 1 - 1
src/ChunkyImageLib/IReadOnlyChunkyImage.cs

@@ -14,7 +14,7 @@ public interface IReadOnlyChunkyImage
     bool DrawCommittedChunkOn(VecI chunkPos, ChunkResolution resolution, DrawingSurface surface, VecD pos, Paint? paint = null);
     RectI? FindChunkAlignedMostUpToDateBounds();
     RectI? FindChunkAlignedCommittedBounds();
-    RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full);
+    RectI? FindTightCommittedBounds(ChunkResolution precision = ChunkResolution.Full, bool fallbackToChunkAligned = false);
     Color GetCommittedPixel(VecI posOnImage);
     Color GetMostUpToDatePixel(VecI posOnImage);
     bool LatestOrCommittedChunkExists(VecI chunkPos);

+ 1 - 1
src/ColorPicker

@@ -1 +1 @@
-Subproject commit 78237e9c5d70ebd878b34f4dd63f54b25bafea23
+Subproject commit 56faff5afcc6fc96b44b5caa6f15d0b3a902cd0a

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 06d7adbd41c53f0319c07b07d99faab57b376560
+Subproject commit 6a22e1b5affd0906a949f97b755ae588eda7980a

+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 8c9a1f874ec8d1f7cd773a1f6b044deb0343ad1a
+Subproject commit c6778bd7a33f799263a64d4b39404e1aa05e9dd0

+ 3 - 2
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -173,11 +173,12 @@ public class FFMpegRenderer : IAnimationRenderer
                 return;
             }
 
-            IOperatingSystem.Current.ProcessUtility.Execute("chmod", $"+x {filePath}");
+            IOperatingSystem.Current.ProcessUtility.Execute("chmod", $"+x {filePath}").WaitForExit(500);
         }
         catch (Exception e)
         {
-            IOperatingSystem.Current.ProcessUtility.Execute("chmod", $"+x {filePath}");
+            IOperatingSystem.Current.ProcessUtility.Execute("chmod", $"+x {filePath}")
+                .WaitForExit(500);
         }
     }
 

+ 27 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -18,6 +18,8 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     public const string ImageFramesKey = "Frames";
     public const string ImageLayerKey = "LayerImage";
 
+    public const int AccuratePreviewMaxSize = 2048;
+
     public override VecD GetScenePosition(KeyFrameTime time) => layerImage.CommittedSize / 2f;
     public override VecD GetSceneSize(KeyFrameTime time) => layerImage.CommittedSize;
 
@@ -49,7 +51,29 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override RectD? GetApproxBounds(KeyFrameTime frameTime)
     {
-        var chunkAlignedBounds = GetLayerImageAtFrame(frameTime.Frame).FindChunkAlignedCommittedBounds();
+        var layerImage = GetLayerImageAtFrame(frameTime.Frame);
+        return GetApproxBounds(layerImage);
+    }
+
+    private static RectD? GetApproxBounds(ChunkyImage layerImage)
+    {
+        if (layerImage.CommittedSize.LongestAxis <= AccuratePreviewMaxSize)
+        {
+            ChunkResolution resolution = layerImage.CommittedSize.LongestAxis switch
+            {
+                <= 256 => ChunkResolution.Full,
+                <= 512 => ChunkResolution.Half,
+                <= 1024 => ChunkResolution.Quarter,
+                _ => ChunkResolution.Eighth
+            };
+
+            // Half is efficient enough to be used even for full res chunks
+            bool fallbackToChunkAligned = (int)resolution > 2;
+
+            return (RectD?)layerImage.FindTightCommittedBounds(resolution, fallbackToChunkAligned);
+        }
+
+        var chunkAlignedBounds = layerImage.FindChunkAlignedCommittedBounds();
         if (chunkAlignedBounds == null)
         {
             return null;
@@ -145,7 +169,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                     return null;
                 }
 
-                RectI? bounds = kf.FindChunkAlignedCommittedBounds(); // Don't use tight bounds, very expensive
+                RectI? bounds = (RectI?)GetApproxBounds(kf);
                 if (bounds.HasValue)
                 {
                     return new RectD(bounds.Value.X, bounds.Value.Y,
@@ -163,7 +187,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
                 return null;
             }
 
-            var bounds = kf.FindChunkAlignedCommittedBounds(); // Don't use tight bounds, very expensive
+            var bounds = GetApproxBounds(kf);
             if (bounds.HasValue)
             {
                 return new RectD(bounds.Value.X, bounds.Value.Y,

+ 3 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/StructureNode.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 using PixiEditor.ChangeableDocument.Helpers;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
@@ -246,6 +247,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
 
         VecI targetSize = img.LatestSize;
 
+        var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         renderSurface = RequestTexture(textureId, targetSize, processingColorSpace, false);
 
         int saved = renderSurface.DrawingSurface.Canvas.Save();
@@ -263,6 +265,7 @@ public abstract class StructureNode : RenderNode, IReadOnlyStructureNode, IRende
         }
 
         renderSurface.DrawingSurface.Canvas.RestoreToCount(saved);
+        ctx?.Dispose();
     }
 
     protected void ApplyRasterClip(DrawingSurface toClip, DrawingSurface clipSource)

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

@@ -164,10 +164,11 @@ public class VectorLayerNode : LayerNode, ITransformableObject, IReadOnlyVectorN
         IReadOnlyDictionary<string, object> data, List<IChangeInfo> infos)
     {
         base.DeserializeAdditionalData(target, data, infos);
-        EmbeddedShapeData = (ShapeVectorData)data["ShapeData"];
+        EmbeddedShapeData = data["ShapeData"] as ShapeVectorData;
 
         if (EmbeddedShapeData == null)
         {
+            Console.WriteLine("Failed to deserialize shape data");
             return;
         }
 

+ 4 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -59,6 +59,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         else if (erasing)
         {
             srcPaint.BlendMode = BlendMode.DstOut;
+            if (this.color.A == 0)
+            {
+                this.color = color.WithAlpha(255);
+            }
         }
     }
 

+ 37 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/PreviewShiftLayers_UpdateableChange.cs

@@ -14,15 +14,18 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
     private List<Guid> layerGuids;
     private VecD delta;
     private Dictionary<Guid, ShapeVectorData> originalShapes;
+    private RectD? clipRect;
+    private VectorPath? clipPath;
 
     private int frame;
 
     [GenerateUpdateableChangeActions]
-    public PreviewShiftLayers_UpdateableChange(List<Guid> layerGuids, VecD delta, int frame)
+    public PreviewShiftLayers_UpdateableChange(List<Guid> layerGuids, RectD clipRect, VecD delta, int frame)
     {
         this.delta = delta;
         this.layerGuids = layerGuids;
         this.frame = frame;
+        this.clipRect = clipRect.IsZeroOrNegativeArea ? null : clipRect;
     }
 
     public override bool InitializeAndValidate(Document target)
@@ -50,6 +53,14 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                 originalShapes[layerGuid] = transformableObject.EmbeddedShapeData;
                 transformableObject.EmbeddedShapeData = null;
             }
+            else if (layer is ImageLayerNode imgLayer)
+            {
+                var image = imgLayer.GetLayerImageAtFrame(frame);
+                if (image is null)
+                {
+                    return false;
+                }
+            }
         }
 
         return true;
@@ -68,16 +79,25 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
         {
             var layer = target.FindMemberOrThrow<LayerNode>(layerGuid);
 
-            if (layer is ImageLayerNode)
+            if (layer is ImageLayerNode imgNode)
             {
-                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, true, (VecI)delta, frame);
+                if (clipRect.HasValue)
+                {
+                    clipPath?.Dispose();
+                    clipPath = new VectorPath();
+                    clipPath.AddRect(clipRect.Value);
+                    clipPath.Offset((VecI)delta);
+                }
+
+                var area = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, true, (VecI)delta, frame, clipPath);
+
                 changes.Add(new LayerImageArea_ChangeInfo(layerGuid, area));
             }
             else if (layer is VectorLayerNode vectorLayer)
             {
                 StrokeJoin join = StrokeJoin.Miter;
                 StrokeCap cap = StrokeCap.Butt;
-                
+
                 (vectorLayer.EmbeddedShapeData as PathVectorData)?.Path.Dispose();
 
                 var originalShape = originalShapes[layerGuid];
@@ -90,8 +110,9 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                     cap = shape.StrokeLineCap;
                 }
 
-                VecD mappedDelta = originalShape.TransformationMatrix.Invert().MapVector((float)delta.X, (float)delta.Y);
-                
+                VecD mappedDelta = originalShape.TransformationMatrix.Invert()
+                    .MapVector((float)delta.X, (float)delta.Y);
+
                 var finalMatrix = Matrix3X3.CreateTranslation((float)mappedDelta.X, (float)mappedDelta.Y);
 
                 path.AddPath(path, finalMatrix, AddPathMode.Append);
@@ -106,9 +127,10 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                     StrokeLineJoin = join,
                     StrokeLineCap = cap
                 };
-                
+
                 vectorLayer.EmbeddedShapeData = newShapeData;
-                changes.Add(new VectorShape_ChangeInfo(layerGuid, ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
+                changes.Add(new VectorShape_ChangeInfo(layerGuid,
+                    ShiftLayer_UpdateableChange.AffectedAreaFromBounds(target, layerGuid, frame)));
             }
         }
 
@@ -139,6 +161,7 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
                 var image = imgLayer.GetLayerImageAtFrame(frame);
                 var affected = image.FindAffectedArea();
                 image.CancelChanges();
+                image.SetClippingPath(null);
                 changes.Add(new LayerImageArea_ChangeInfo(layerGuid, affected));
             }
             else if (layer is VectorLayerNode transformableObject)
@@ -150,4 +173,10 @@ internal class PreviewShiftLayers_UpdateableChange : InterruptableUpdateableChan
 
         return changes;
     }
+
+    public override void Dispose()
+    {
+        base.Dispose();
+        clipPath?.Dispose();
+    }
 }

+ 9 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/ShiftLayerHelper.cs

@@ -1,18 +1,26 @@
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changes.Drawing;
 
 internal static class ShiftLayerHelper
 {
-    public static AffectedArea DrawShiftedLayer(Document target, Guid layerGuid, bool keepOriginal, VecI delta, int frame)
+    public static AffectedArea DrawShiftedLayer(Document target, Guid layerGuid, bool keepOriginal, VecI delta,
+        int frame, VectorPath? clipPath = null)
     {
         var targetImage = target.FindMemberOrThrow<ImageLayerNode>(layerGuid).GetLayerImageAtFrame(frame);
         var prevArea = targetImage.FindAffectedArea();
         targetImage.CancelChanges();
         if (!keepOriginal)
             targetImage.EnqueueClear();
+
+        if (clipPath != null)
+        {
+            targetImage.SetClippingPath(clipPath);
+        }
+
         targetImage.EnqueueDrawCommitedChunkyImage(delta, targetImage, false, false);
         var curArea = targetImage.FindAffectedArea();
 

+ 3 - 0
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
@@ -211,6 +212,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize,
         string? customOutput = null)
     {
+        var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         IsBusy = true;
 
         if (renderTexture == null || renderTexture.Size != renderSize)
@@ -262,6 +264,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         renderTexture.DrawingSurface.Canvas.Restore();
         toRenderOn.Canvas.Restore();
 
+        ctx.Dispose();
         IsBusy = false;
     }
 

+ 12 - 6
src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.Impl.cs

@@ -17,33 +17,33 @@ public partial class PaletteColor : IEquatable<PaletteColor>
         get => (byte)GValue;
         set => GValue = value;
     }
-    
+
     public byte B
     {
         get => (byte)BValue;
         set => BValue = value;
     }
-    
+
     public string Hex => $"#{R:X2}{G:X2}{B:X2}";
-    
+
     public PaletteColor(byte r, byte g, byte b)
     {
         RValue = r;
         GValue = g;
         BValue = b;
     }
-    
+
     public PaletteColor(uint r, uint g, uint b)
     {
         RValue = (byte)r;
         GValue = (byte)g;
         BValue = (byte)b;
     }
-    
+
     public PaletteColor()
     {
     }
-    
+
     public override string ToString()
     {
         return Hex;
@@ -51,6 +51,12 @@ public partial class PaletteColor : IEquatable<PaletteColor>
 
     public static bool operator ==(PaletteColor left, PaletteColor right)
     {
+        if (left is null && right is null)
+            return true;
+
+        if (left is null || right is null)
+            return false;
+
         return left.R == right.R && left.G == right.G && left.B == right.B;
     }
 

+ 3 - 1
src/PixiEditor.Extensions.CommonApi/UserPreferences/Settings/PixiEditor/RightClickMode.cs

@@ -9,5 +9,7 @@ public enum RightClickMode
     [Description("SHOW_CONTEXT_MENU")]
     ContextMenu,
     [Description("ERASE")]
-    Erase
+    Erase,
+    [Description("COLOR_PICKER")]
+    ColorPicker,
 }

+ 3 - 1
src/PixiEditor.Extensions.CommonApi/Utilities/ByteWriter.cs

@@ -1,3 +1,5 @@
+using System.Text;
+
 namespace PixiEditor.Extensions.CommonApi.Utilities;
 
 public class ByteWriter
@@ -19,8 +21,8 @@ public class ByteWriter
 
     public void WriteString(string value)
     {
+        WriteInt(Encoding.UTF8.GetByteCount(value));
         byte[] stringBytes = System.Text.Encoding.UTF8.GetBytes(value);
-        WriteInt(stringBytes.Length);
         _buffer.AddRange(stringBytes);
     }
 

+ 101 - 45
src/PixiEditor.Extensions.Runtime/ExtensionLoader.cs

@@ -10,49 +10,49 @@ namespace PixiEditor.Extensions.Runtime;
 
 public class ExtensionLoader
 {
+    private readonly Dictionary<string, OfficialExtensionData> _officialExtensionsKeys =
+        new Dictionary<string, OfficialExtensionData>();
+
     public List<Extension> LoadedExtensions { get; } = new();
 
-    public string PackagesPath { get; }
+    public string[] PackagesPath { get; }
     public string UnpackedExtensionsPath { get; }
 
-    public ExtensionServices Services { get; set; }
-
     private WasmRuntime.WasmRuntime _wasmRuntime = new WasmRuntime.WasmRuntime();
 
-    public ExtensionLoader(string packagesPath, string unpackedExtensionsPath)
+    public ExtensionLoader(string[] packagesPaths, string unpackedExtensionsPath)
     {
-        PackagesPath = packagesPath;
+        PackagesPath = packagesPaths;
         UnpackedExtensionsPath = unpackedExtensionsPath;
         ValidateExtensionFolder();
     }
 
+    public void AddOfficialExtension(string uniqueName, OfficialExtensionData data)
+    {
+        _officialExtensionsKeys.Add(uniqueName, data);
+    }
+
     public void LoadExtensions()
     {
-        foreach (var updateFile in Directory.GetFiles(PackagesPath, "*.update"))
+        foreach (var packagesPath in PackagesPath)
         {
-            try
-            {
-                string newExtension = Path.ChangeExtension(updateFile, ".pixiext");
-                if (File.Exists(newExtension))
-                {
-                    File.Delete(newExtension);
-                }
+            LoadExtensionsFromPath(packagesPath);
+        }
+    }
 
-                File.Move(updateFile, newExtension);
-            }
-            catch (IOException)
-            {
-                // File is in use, ignore
-            }
-            catch (UnauthorizedAccessException)
-            {
-                // File is in use, ignore
-            }
+    private void LoadExtensionsFromPath(string path)
+    {
+        if (!Directory.Exists(path))
+        {
+            return;
         }
 
-        foreach (var file in Directory.GetFiles(PackagesPath, "*.pixiext"))
+        foreach (var file in Directory.GetFiles(path))
         {
-            LoadExtension(file);
+            if (file.EndsWith(".pixiext"))
+            {
+                LoadExtension(file);
+            }
         }
     }
 
@@ -99,26 +99,26 @@ public class ExtensionLoader
     public Extension? LoadExtension(string extension)
     {
         var extZip = ZipFile.OpenRead(extension);
-        ExtensionMetadata metadata = ExtractMetadata(extZip);
-        bool isLoaded = LoadedExtensions.Any(x => x.Metadata.UniqueName == metadata.UniqueName);
-        if (isLoaded)
+        try
         {
-            return null;
-        }
-
+            ExtensionMetadata metadata = ExtractMetadata(extZip);
             if (IsDifferentThanCached(metadata, extension))
             {
                 UnpackExtension(extZip, metadata);
             }
 
+            string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
+            if (!File.Exists(extensionJson))
+            {
+                return null;
+            }
 
-        string extensionJson = Path.Combine(UnpackedExtensionsPath, metadata.UniqueName, "extension.json");
-        if (!File.Exists(extensionJson))
+            return LoadExtensionFromCache(extensionJson);
+        }
+        catch (Exception ex)
         {
             return null;
         }
-
-        return LoadExtensionFromCache(extensionJson);
     }
 
     public void UnpackExtension(ZipArchive extZip, ExtensionMetadata metadata)
@@ -276,12 +276,12 @@ public class ExtensionLoader
                 throw new ForbiddenUniqueNameExtension();
             }
 
-            if (!IsAdditionalContentOwned(fixedUniqueName))
+            if (!IsAdditionalContentInstalled(fixedUniqueName))
             {
                 return false;
             }
         }
-        // TODO: Validate if unique name is in fact, unique
+        // TODO: Validate if unique name is unique
 
         if (string.IsNullOrEmpty(metadata.DisplayName))
         {
@@ -296,29 +296,68 @@ public class ExtensionLoader
         return true;
     }
 
-    private bool IsAdditionalContentOwned(string fixedUniqueName)
+    private bool IsAdditionalContentInstalled(string fixedUniqueName)
     {
-        return IPlatform.Current.AdditionalContentProvider?.IsContentOwned(fixedUniqueName) ?? false;
+        if (!_officialExtensionsKeys.ContainsKey(fixedUniqueName)) return false;
+        AdditionalContentProduct? product = _officialExtensionsKeys[fixedUniqueName].Product;
+
+        if (product == null) return true;
+
+        return IPlatform.Current.AdditionalContentProvider?.IsContentInstalled(product.Value) ?? false;
     }
 
     private bool IsOfficialAssemblyLegit(string metadataUniqueName, ExtensionEntry entry)
     {
         if (entry == null) return false; // All official extensions must have a valid assembly
+        if (!_officialExtensionsKeys.ContainsKey(metadataUniqueName)) return false;
 
         if (entry is DllExtensionEntry dllExtensionEntry)
         {
-            return false;
+            return VerifyAssemblySignature(metadataUniqueName, dllExtensionEntry.Assembly);
         }
 
         if (entry is WasmExtensionEntry wasmExtensionEntry)
         {
             return true;
             //TODO: Verify wasm signature somehow
+            //return VerifyAssemblySignature(metadataUniqueName, wasmExtensionEntry.Instance);
         }
 
         return false;
     }
 
+    private bool VerifyAssemblySignature(string metadataUniqueName, Assembly assembly)
+    {
+        bool wasVerified = false;
+        bool verified = StrongNameSignatureVerificationEx(assembly.Location, true, ref wasVerified);
+        if (!verified || !wasVerified) return false;
+
+        byte[]? assemblyPublicKey = assembly.GetName().GetPublicKey();
+        if (assemblyPublicKey == null) return false;
+
+        return PublicKeysMatch(assemblyPublicKey, _officialExtensionsKeys[metadataUniqueName].PublicKeyName);
+    }
+
+    private bool PublicKeysMatch(byte[] assemblyPublicKey, string pathToPublicKey)
+    {
+        Assembly currentAssembly = Assembly.GetExecutingAssembly();
+        using Stream? stream =
+            currentAssembly.GetManifestResourceStream(
+                $"{currentAssembly.GetName().Name}.OfficialExtensions.{pathToPublicKey}");
+        if (stream == null) return false;
+
+        using MemoryStream memoryStream = new MemoryStream();
+        stream.CopyTo(memoryStream);
+        byte[] publicKey = memoryStream.ToArray();
+
+        return assemblyPublicKey.SequenceEqual(publicKey);
+    }
+
+    //TODO: uhh, other platforms dumbass?
+    [DllImport("mscoree.dll", CharSet = CharSet.Unicode)]
+    static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification,
+        ref bool pfWasVerified);
+
     private Extension LoadExtensionEntry(ExtensionEntry entry, ExtensionMetadata metadata)
     {
         Extension extension = entry.CreateExtension();
@@ -371,14 +410,18 @@ public class ExtensionLoader
 
     private void ValidateExtensionFolder()
     {
-        if (!Directory.Exists(PackagesPath))
+        try
         {
-            Directory.CreateDirectory(PackagesPath);
+            if (!Directory.Exists(UnpackedExtensionsPath))
+            {
+                Directory.CreateDirectory(UnpackedExtensionsPath);
+            }
         }
-
-        if (!Directory.Exists(UnpackedExtensionsPath))
+        catch (Exception ex)
         {
-            Directory.CreateDirectory(UnpackedExtensionsPath);
+#if DEBUG
+            throw;
+#endif
         }
     }
 
@@ -401,3 +444,16 @@ public class ExtensionLoader
         return null;
     }
 }
+
+public struct OfficialExtensionData
+{
+    public string PublicKeyName { get; }
+    public AdditionalContentProduct? Product { get; }
+    public string? PurchaseLink { get; }
+
+    public OfficialExtensionData(string publicKeyName, AdditionalContentProduct product, string? purchaseLink = null)
+    {
+        PublicKeyName = publicKeyName;
+        Product = product;
+    }
+}

+ 5 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs

@@ -122,7 +122,11 @@ public class ControlDefinition
             result.Add(ByteMap.GetTypeByteId(property.type));
             if (property.type == typeof(string))
             {
-                result.AddRange(BitConverter.GetBytes(property.value is string s ? s.Length : 0));
+                if (property.value is string str)
+                {
+                    int stringLengthBytes = Encoding.UTF8.GetByteCount(str);
+                    result.AddRange(BitConverter.GetBytes(stringLengthBytes));
+                }
             }
 
             result.AddRange(property.value switch

+ 3 - 3
src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs

@@ -61,10 +61,10 @@ public class LayoutBuilder
             Type type = ByteMap.GetTypeFromByteId((byte)propertyType);
             if (type == typeof(string))
             {
-                int stringLength = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
+                int stringBytesLength = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
                 offset += int32Size;
-                string value = Encoding.UTF8.GetString(layoutSpan[offset..(offset + stringLength)]);
-                offset += stringLength;
+                string value = Encoding.UTF8.GetString(layoutSpan[offset..(offset + stringBytesLength)]);
+                offset += stringBytesLength;
                 properties.Add(value);
             }
             else if (type == typeof(byte[]))

+ 1 - 1
src/PixiEditor.Linux/LinuxProcessUtility.cs

@@ -9,7 +9,7 @@ public class LinuxProcessUtility : IProcessUtility
 {
     public Process RunAsAdmin(string path, string args)
     {
-        throw new NotImplementedException("Running as admin is not supported on Linux");
+        throw new NotSupportedException("Running as admin is not supported on Linux.");
     }
 
     public bool IsRunningAsAdministrator()

+ 3 - 39
src/PixiEditor.MacOs/MacOperatingSystem.cs

@@ -12,7 +12,7 @@ public sealed class MacOperatingSystem : IOperatingSystem
     public string Name { get; } = "MacOS";
 
     public string AnalyticsId => "macOS";
-    
+
     public IInputKeys InputKeys { get; } = new MacOsInputKeys();
     public IProcessUtility ProcessUtility { get; } = new MacOsProcessUtility();
 
@@ -34,45 +34,9 @@ public sealed class MacOperatingSystem : IOperatingSystem
         ProcessUtility.ShellExecute(Path.GetDirectoryName(path));
     }
 
-    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime)
+    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction,
+        IApplicationLifetime lifetime)
     {
-        StringBuilder args = new StringBuilder();
-        
-        if(activationUris != null)
-        {
-            foreach (var uri in activationUris)
-            {
-                args.Append('"');
-                args.Append(uri.AbsolutePath);
-                args.Append('"');
-                args.Append(' ');
-            }
-        }
-        
-        dispatcher.Invoke(() => openInExistingAction(args.ToString(), true));
         return true;
     }
-
-    public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs)
-    {
-        if(activationUris == null)
-        {
-            activationUris = [];
-        }
-        
-        foreach (var file in fileActivatedEventArgs.Files)
-        {
-           activationUris.Add(file.Path);
-        }
-    }
-
-    public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs)
-    {
-        if(activationUris == null)
-        {
-            activationUris = [];
-        }
-        
-        activationUris.Add(openUriEventArgs.Uri);
-    }
 }

+ 8 - 4
src/PixiEditor.MacOs/MacOsProcessUtility.cs

@@ -7,12 +7,16 @@ internal class MacOsProcessUtility : IProcessUtility
 {
     public Process RunAsAdmin(string path, string args)
     {
+        string script = $"""
+
+                                     do shell script "/bin/bash '{path}' {args}" with administrator privileges
+                         """;
         ProcessStartInfo startInfo = new ProcessStartInfo
         {
-            FileName = path,
-            Verb = "runas",
-            Arguments = args,
-            UseShellExecute = true
+            FileName = "osascript",
+            ArgumentList = { "-e", script },
+            UseShellExecute = true,
+            CreateNoWindow = true,
         };
 
         Process p = new Process();

+ 1 - 3
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -36,7 +36,5 @@ public interface IOperatingSystem
 
     public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction,
         IApplicationLifetime lifetime);
-
-    public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs);
-    public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs);
 }
+

+ 1 - 1
src/PixiEditor.OperatingSystem/IProcessUtility.cs

@@ -7,6 +7,6 @@ public interface IProcessUtility
     public Process RunAsAdmin(string path, string? args);
     public bool IsRunningAsAdministrator();
     public Process ShellExecute(string toExecute);
-    public Process ShellExecute(string toExecute, string args);
+    public Process ShellExecute(string toExecute, string? args);
     public Process Execute(string path, string args);
 }

+ 1 - 1
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -239,7 +239,7 @@
             <SolidColorBrush x:Key="DockThemeControlBackgroundBrush" Color="{DynamicResource ThemeBackground1Color}" />
             <system:Double x:Key="DockFontSizeNormal">12</system:Double>
 
-            <FontFamily x:Key="ContentControlThemeFontFamily">avares://PixiEditor.UI.Common/Fonts#FiraSans, $Default</FontFamily>
+            <FontFamily x:Key="ContentControlThemeFontFamily">Arial</FontFamily>
             <system:Double x:Key="FontSizeSmall">10</system:Double>
             <system:Double x:Key="FontSizeNormal">12</system:Double>
             <system:Double x:Key="FontSizeLarge">16</system:Double>

BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Black.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-BlackItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Bold.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-BoldItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraBold.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraBoldItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraLight.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-ExtraLightItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Italic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Light.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-LightItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Medium.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-MediumItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Regular.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-SemiBold.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-SemiBoldItalic.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-Thin.ttf


BIN
src/PixiEditor.UI.Common/Fonts/FiraSans-ThinItalic.ttf


+ 0 - 8
src/PixiEditor.UI.Common/Fonts/FiraSans.axaml

@@ -1,8 +0,0 @@
-<Styles xmlns="https://github.com/avaloniaui"
-                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    <Styles.Resources>
-        <ResourceDictionary>
-            <FontFamily x:Key="FiraSans">avares://PixiEditor.UI.Common/Fonts#FiraSans</FontFamily>
-        </ResourceDictionary>
-    </Styles.Resources>
-</Styles>

+ 2 - 13
src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj

@@ -7,12 +7,8 @@
     </PropertyGroup>
 
     <ItemGroup>
-      <AvaloniaResource Include="Assets\*"/>
-      <None Remove="Assets\Animations\LoadingIndicator.json" />
-      <AvaloniaResource Include="Assets\Animations\LoadingIndicator.json" />
-      <AvaloniaResource Include="Fonts\PixiPerfect.ttf" />
-      <None Remove="Assets\Animations\CheckAnimation.json" />
-      <AvaloniaResource Include="Assets\Animations\CheckAnimation.json" />
+      <AvaloniaResource Include="Assets/**"/>
+      <AvaloniaResource Include="Fonts/**"/>
     </ItemGroup>
 
     <ItemGroup>
@@ -21,11 +17,4 @@
       <PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.5" />
       <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
     </ItemGroup>
-
-    <ItemGroup>
-      <Compile Update="Controls\SizeInput.axaml.cs">
-        <DependentUpon>SizeInput.axaml</DependentUpon>
-        <SubType>Code</SubType>
-      </Compile>
-    </ItemGroup>
 </Project>

+ 2 - 2
src/PixiEditor.UpdateInstaller.Exe/PixiEditor.UpdateInstaller.Exe.csproj

@@ -20,12 +20,12 @@
     <ProjectReference Include="..\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller\PixiEditor.UpdateInstaller.csproj"/>
   </ItemGroup>
   
-  <Target Name="Rename" AfterTargets="AfterBuild">
+  <Target Name="RenameBuildWin" AfterTargets="AfterBuild" Condition="'$(RuntimeIdentifier)' =='win-x64' or '$(RuntimeIdentifier)'=='win-arm64'">
     <Move SourceFiles="$(OutDir)PixiEditor.UpdateInstaller.Exe.exe" DestinationFiles="$(OutDir)PixiEditor.UpdateInstaller.exe"/>
     <Message Text="Renamed build executable file." Importance="high"/>
   </Target>
 
-  <Target Name="Rename" AfterTargets="Publish">
+  <Target Name="RenamePublishWin" AfterTargets="Publish" Condition="'$(RuntimeIdentifier)'=='win-x64' or '$(RuntimeIdentifier)'=='win-arm64'">
     <Move SourceFiles="$(PublishDir)PixiEditor.UpdateInstaller.Exe.exe" DestinationFiles="$(PublishDir)PixiEditor.UpdateInstaller.exe"/>
     <Message Text="Renamed published executable file." Importance="high"/>
   </Target>

+ 63 - 8
src/PixiEditor.UpdateInstaller.Exe/Program.cs

@@ -5,6 +5,7 @@ using PixiEditor.UpdateInstaller.ViewModels;
 UpdateController controller = new UpdateController();
 StringBuilder log = new StringBuilder();
 bool startAfterUpdate = false;
+string logDirectory = Path.Combine(Path.GetTempPath(), "PixiEditor");
 
 foreach (string arg in args)
 {
@@ -20,31 +21,85 @@ try
 {
     log.AppendLine($"{DateTime.Now}: Starting update installation...");
     controller.InstallUpdate(log);
+    log.AppendLine($"{DateTime.Now}: Update installation completed successfully.");
 }
 catch (Exception ex)
 {
     log.AppendLine($"{DateTime.Now}: Error during update installation: {ex.Message}");
-    File.AppendAllText("ErrorLog.txt",
+    string errorLogPath = Path.Combine(logDirectory, "ErrorLog.txt");
+    File.AppendAllText(errorLogPath,
         $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
 }
 finally
 {
     try
     {
-        File.WriteAllText("UpdateLog.txt", log.ToString());
+        if (startAfterUpdate)
+        {
+            log.AppendLine($"{DateTime.Now}: Starting PixiEditor after update.");
+            string binaryName = "PixiEditor.exe";
+            string path = Path.Join(controller.UpdateDirectory, binaryName);
+            if (File.Exists(path))
+            {
+                log.AppendLine($"{DateTime.Now}: Starting PixiEditor from {path}");
+                StartPixiEditor(path);
+            }
+            else
+            {
+                binaryName = "PixiEditor.Desktop.exe";
+                path = Path.Join(controller.UpdateDirectory, binaryName);
+                if (File.Exists(path))
+                {
+                    StartPixiEditor(path);
+                }
+                else
+                {
+                    log.AppendLine("PixiEditor executable not found.");
+                }
+            }
+        }
+    }
+    catch (Exception ex)
+    {
+        log.AppendLine($"{DateTime.Now}: Error starting PixiEditor: {ex.Message}");
+        string errorLogPath = Path.Combine(logDirectory, "ErrorLog.txt");
+        File.AppendAllText(errorLogPath,
+            $"Error starting PixiEditor: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
+    }
+
+    try
+    {
+        string updateLogPath = Path.Combine(logDirectory, "UpdateLog.txt");
+        File.WriteAllText(updateLogPath, log.ToString());
     }
     catch
     {
         // probably permissions or disk full, the best we can do is to ignore this
     }
 
-    if (startAfterUpdate)
+    void StartPixiEditor(string pixiEditorExecutablePath)
     {
-        var files = Directory.GetFiles(controller.UpdateDirectory, "PixiEditor.exe");
-        if (files.Length > 0)
+        Process.Start(new ProcessStartInfo(pixiEditorExecutablePath) { UseShellExecute = true });
+    }
+}
+
+/*
+void StartPixiEditorOnMacOS(UpdateController controller)
+{
+    string pixiEditorExecutablePath = Path.Combine(controller.UpdateDirectory, "PixiEditor.app");
+    if (Directory.Exists(pixiEditorExecutablePath))
+    {
+        log.AppendLine($"{DateTime.Now}: Starting PixiEditor with open {pixiEditorExecutablePath}");
+        Process.Start(new ProcessStartInfo
         {
-            string pixiEditorExecutablePath = files[0];
-            Process.Start(pixiEditorExecutablePath);
-        }
+            FileName = "open",
+            Arguments = $"\"{pixiEditorExecutablePath}\"",
+            UseShellExecute = true
+        });
+    }
+    else
+    {
+        log.AppendLine($"{DateTime.Now}: PixiEditor.app not found at {pixiEditorExecutablePath}");
     }
 }
+*/

+ 19 - 5
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs

@@ -13,10 +13,25 @@ public class UpdateController
         Current = this;
 
         string updateDirectory = Path.GetDirectoryName(Extensions.GetExecutablePath());
+        if (OperatingSystem.IsMacOS())
+        {
+            updateDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+        }
+        else
+        {
+            string infoPath = Path.Join(Path.GetTempPath(), "PixiEditor", "update-location.txt");
+            if (File.Exists(infoPath))
+            {
+                try
+                {
+                    updateDirectory = File.ReadAllText(infoPath);
+                }
+                catch (Exception ex)
+                {
+                }
+            }
+        }
 
-#if DEBUG
-        updateDirectory = Path.GetDirectoryName(Environment.GetCommandLineArgs().FirstOrDefault());
-#endif
         UpdateDirectory = updateDirectory;
     }
 
@@ -29,14 +44,13 @@ public class UpdateController
 
     public void InstallUpdate(StringBuilder log)
     {
-        string[] files = Directory.GetFiles(UpdateDownloader.DownloadLocation, "update-*.zip");
+        string[] files = Directory.GetFiles(UpdateDownloader.DownloadLocation, $"update-*.zip");
         log.AppendLine($"Found {files.Length} update files.");
 
         if (files.Length > 0)
         {
             Installer = new UpdateModule.UpdateInstaller(files[0], UpdateDirectory);
             log.AppendLine($"Installing update from {files[0]} to {UpdateDirectory}");
-            Console.WriteLine("Installing update, DO NOT CLOSE THIS WINDOW");
             Installer.Install(log);
         }
     }

+ 5 - 0
src/PixiEditor.UpdateModule/UpdateChecker.cs

@@ -78,6 +78,11 @@ public class UpdateChecker
         LatestReleaseInfo = await GetLatestReleaseInfoAsync(Channel.ApiUrl);
         return CheckUpdateAvailable(LatestReleaseInfo);
     }
+    
+    public void SetLatestReleaseInfo(ReleaseInfo releaseInfo)
+    {
+        LatestReleaseInfo = releaseInfo;
+    }
 
     public bool CheckUpdateAvailable(ReleaseInfo latestRelease)
     {

+ 18 - 14
src/PixiEditor.UpdateModule/UpdateDownloader.cs

@@ -11,25 +11,28 @@ public static class UpdateDownloader
 {
     public static string DownloadLocation { get; } = Path.Join(Path.GetTempPath(), "PixiEditor");
 
-    public static async Task DownloadReleaseZip(ReleaseInfo release)
+    public static event Action<double> ProgressChanged;
+
+    public static async Task DownloadReleaseZip(ReleaseInfo release, string contentType, string extension)
     {
-        Asset? matchingAsset = GetMatchingAsset(release);
+        Asset? matchingAsset = GetMatchingAsset(release, contentType);
 
         if (matchingAsset == null)
         {
             throw new FileNotFoundException("No matching update for your system found.");
         }
 
-        using HttpClient client = new HttpClient();
-        client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
-        client.DefaultRequestHeaders.Add("Accept", "application/octet-stream");
-        var response = await client.GetAsync(matchingAsset.Url);
-        if (response.StatusCode == HttpStatusCode.OK)
+        using WebClient client = new WebClient();
+        client.Headers.Add("User-Agent", "PixiEditor");
+        client.Headers.Add("Accept", "application/octet-stream");
+        client.DownloadProgressChanged += (sender, args) =>
         {
-            byte[] bytes = await response.Content.ReadAsByteArrayAsync();
-            CreateTempDirectory();
-            await File.WriteAllBytesAsync(Path.Join(DownloadLocation, $"update-{release.TagName}.zip"), bytes);
-        }
+            ProgressChanged?.Invoke(args.ProgressPercentage);
+        };
+
+        var bytes = await client.DownloadDataTaskAsync(matchingAsset.Url);
+        CreateTempDirectory();
+        await File.WriteAllBytesAsync(Path.Join(DownloadLocation, $"update-{release.TagName}.{extension}"), bytes);
     }
 
     public static async Task DownloadInstaller(ReleaseInfo info)
@@ -61,7 +64,7 @@ public static class UpdateDownloader
         }
     }
 
-    private static Asset? GetMatchingAsset(ReleaseInfo release, string assetType = "zip")
+    private static Asset? GetMatchingAsset(ReleaseInfo release, string assetType)
     {
         if (release.TagName.StartsWith("1."))
         {
@@ -70,8 +73,9 @@ public static class UpdateDownloader
                                                       && x.Name.Contains(archOld));
         }
 
-        string arch = "x64";
-        string os = OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsLinux() ? "linux" : "mac";
+        string arch = OperatingSystem.IsWindows() ? "x64" :
+            OperatingSystem.IsLinux() ? "amd64" : "universal";
+        string os = OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsLinux() ? "linux" : "macos";
         return release.Assets.FirstOrDefault(x => x.ContentType.Contains(assetType)
                                                   && x.Name.Contains(arch) && x.Name.Contains(os));
     }

+ 75 - 11
src/PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -1,7 +1,9 @@
 using System;
 using System.Diagnostics;
+using System.Formats.Tar;
 using System.IO;
 using System.IO.Compression;
+using System.Linq;
 using System.Text;
 
 namespace PixiEditor.UpdateModule;
@@ -16,7 +18,8 @@ public class UpdateInstaller
         TargetDirectory = targetDirectory;
     }
 
-    public static string UpdateFilesPath { get; set; } = Path.Join(UpdateDownloader.DownloadLocation, TargetDirectoryName);
+    public static string UpdateFilesPath { get; set; } =
+        Path.Join(UpdateDownloader.DownloadLocation, TargetDirectoryName);
 
     public string ArchiveFileName { get; set; }
 
@@ -25,20 +28,51 @@ public class UpdateInstaller
     public void Install(StringBuilder log)
     {
         var processes = Process.GetProcessesByName("PixiEditor.Desktop");
+        processes = processes.Concat(Process.GetProcessesByName("PixiEditor")).ToArray();
         log.AppendLine($"Found {processes.Length} PixiEditor processes running.");
         if (processes.Length > 0)
         {
             log.AppendLine("Killing PixiEditor processes...");
-            processes[0].WaitForExit();
+            foreach (var process in processes)
+            {
+                try
+                {
+                    log.AppendLine($"Killing process {process.ProcessName} with ID {process.Id}");
+                    process.Kill();
+                    process.WaitForExit();
+                }
+                catch (Exception ex)
+                {
+                    log.AppendLine($"Failed to kill process {process.ProcessName} with ID {process.Id}: {ex.Message}");
+                }
+            }
+
             log.AppendLine("Processes killed.");
         }
-        
+
         log.AppendLine("Extracting files");
-        
+
+        if (Directory.Exists(UpdateFilesPath))
+        {
+            Directory.Delete(UpdateFilesPath, true);
+        }
+
+        Directory.CreateDirectory(UpdateFilesPath);
+
+        log.AppendLine($"Extracting {ArchiveFileName} to {UpdateFilesPath}");
         ZipFile.ExtractToDirectory(ArchiveFileName, UpdateFilesPath, true);
-        
+
+        string[] extractedFiles = Directory.GetFiles(UpdateFilesPath, "*", SearchOption.AllDirectories);
+        log.AppendLine($"Extracted {extractedFiles.Length} files to {UpdateFilesPath}");
         log.AppendLine("Files extracted");
-        string dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
+
+        string dirWithFiles = UpdateFilesPath;
+        string binName = "PixiEditor.exe";
+        if (!File.Exists(Path.Combine(UpdateFilesPath, binName)))
+        {
+            dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
+        }
+
         log.AppendLine($"Copying files from {dirWithFiles} to {TargetDirectory}");
 
         try
@@ -54,14 +88,44 @@ public class UpdateInstaller
 
         log.AppendLine("Files copied");
         log.AppendLine("Deleting archive and update files");
-        
-        DeleteArchive();
+
+        Cleanup(log);
     }
 
-    private void DeleteArchive()
+    private void Cleanup(StringBuilder logger)
     {
         File.Delete(ArchiveFileName);
         Directory.Delete(UpdateFilesPath, true);
+        string updateLocationFile = Path.Join(Path.GetTempPath(), "PixiEditor", "update-location.txt");
+        logger.AppendLine($"Looking for: {updateLocationFile}");
+        if (File.Exists(updateLocationFile))
+        {
+            try
+            {
+                logger.AppendLine($"Deleting update location file: {updateLocationFile}");
+                File.Delete(updateLocationFile);
+            }
+            catch (Exception ex)
+            {
+                logger.AppendLine($"Failed to delete update location file: {ex.Message}");
+            }
+        }
+
+        string updateInstallerFile = Path.Join(Path.GetTempPath(), "PixiEditor",
+            "PixiEditor.UpdateInstaller.exe");
+        logger.AppendLine($"Looking for: {updateInstallerFile}");
+        if (File.Exists(updateInstallerFile))
+        {
+            try
+            {
+                logger.AppendLine($"Deleting update installer file: {updateInstallerFile}");
+                File.Delete(updateInstallerFile);
+            }
+            catch (Exception ex)
+            {
+                logger.AppendLine($"Failed to delete update installer file: {ex.Message}");
+            }
+        }
     }
 
     private void CopyFilesToDestination(string sourceDirectory, StringBuilder log)
@@ -86,12 +150,12 @@ public class UpdateInstaller
     {
         string[] subDirs = Directory.GetDirectories(originDirectory);
         log.AppendLine($"Found {subDirs.Length} subdirectories to copy.");
-        if(subDirs.Length == 0) return;
+        if (subDirs.Length == 0) return;
 
         foreach (string subDir in subDirs)
         {
             string targetDirPath = Path.Join(targetDirectory, Path.GetFileName(subDir));
-            
+
             log.AppendLine($"Copying {subDir} to {targetDirPath}");
 
             CopySubDirectories(subDir, targetDirPath, log);

+ 2 - 1
src/PixiEditor/Data/Configs/ToolSetsConfig.json

@@ -60,7 +60,8 @@
           "ExposeHardness": true,
           "ExposeSpacing": true,
           "BrushShapeSetting": "CircleSmooth",
-          "PaintShape": "Circle"
+          "PaintShape": "Circle",
+          "PixelPerfectEnabled": false
         }
       },
       "Select",

+ 3 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1078,5 +1078,7 @@
   "IS_DEFAULT_EXPORT": "Is Default Export",
   "EXPORT_OUTPUT": "Export Output",
   "RENDER_OUTPUT_SIZE": "Render Output Size",
-  "RENDER_OUTPUT_CENTER": "Render Output Center"
+  "RENDER_OUTPUT_CENTER": "Render Output Center",
+  "COLOR_PICKER": "Color Picker",
+  "UNAUTHORIZED_ACCESS": "Unauthorized access"
 }

+ 24 - 77
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -1,5 +1,4 @@
 using System.Collections.Generic;
-using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
@@ -8,6 +7,7 @@ using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform.Storage;
 using Avalonia.Threading;
 using Avalonia.Xaml.Interactivity;
 using PixiEditor.Extensions.Runtime;
@@ -18,7 +18,6 @@ using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.ExceptionHandling;
 using PixiEditor.Models.IO;
 using PixiEditor.OperatingSystem;
-using PixiEditor.PixiAuth;
 using PixiEditor.Platform;
 using PixiEditor.UI.Common.Controls;
 using PixiEditor.UI.Common.Localization;
@@ -32,8 +31,6 @@ namespace PixiEditor.Initialization;
 
 internal class ClassicDesktopEntry
 {
-    public static ClassicDesktopEntry? Active { get; private set; }
-    private bool restartQueued;
     private IClassicDesktopStyleApplicationLifetime desktop;
 
     public ClassicDesktopEntry(IClassicDesktopStyleApplicationLifetime desktop)
@@ -41,8 +38,6 @@ internal class ClassicDesktopEntry
         this.desktop = desktop;
         IActivatableLifetime? activable =
             (IActivatableLifetime?)App.Current.TryGetFeature(typeof(IActivatableLifetime));
-
-        Active = this;
         if (activable != null)
         {
             activable.Activated += ActivableOnActivated;
@@ -54,13 +49,26 @@ internal class ClassicDesktopEntry
 
     private void ActivableOnActivated(object? sender, ActivatedEventArgs e)
     {
+        // TODO: Handle activation more generically. This only is handled by macos btw.
+        if (desktop.MainWindow is not MainWindow mainWindow) return;
         if (e.Kind == ActivationKind.File && e is FileActivatedEventArgs fileActivatedEventArgs)
         {
-            IOperatingSystem.Current.HandleActivatedWithFile(fileActivatedEventArgs);
+            foreach (var storageItem in fileActivatedEventArgs.Files)
+            {
+                string? file = storageItem.TryGetLocalPath();
+                if (file != null && File.Exists(file))
+                {
+                    mainWindow.DataContext.FileSubViewModel.OpenFromPath(file);
+                }
+            }
         }
         else if (e.Kind == ActivationKind.OpenUri && e is ProtocolActivatedEventArgs openUriEventArgs)
         {
-            IOperatingSystem.Current.HandleActivatedWithUri(openUriEventArgs);
+            var uri = openUriEventArgs.Uri;
+            if (uri.Scheme == "lospec-palette")
+            {
+                Dispatcher.UIThread.InvokeAsync(async () => await mainWindow.DataContext.ColorsSubViewModel.ImportLospecPalette(uri.AbsoluteUri));
+            }
         }
     }
 
@@ -125,7 +133,12 @@ internal class ClassicDesktopEntry
 
         NumberInput.AttachGlobalBehaviors += AttachGlobalShortcutBehavior;
 
-        ExtensionLoader extensionLoader = new ExtensionLoader(Paths.ExtensionPackagesPath, Paths.UserExtensionsPath);
+        ExtensionLoader extensionLoader = new ExtensionLoader(
+            [Paths.InstallDirExtensionPackagesPath, Paths.LocalExtensionPackagesPath], Paths.UserExtensionsPath);
+        //TODO: fetch from extension store
+        extensionLoader.AddOfficialExtension("pixieditor.founderspack",
+            new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
+        extensionLoader.AddOfficialExtension("pixieditor.beta", new OfficialExtensionData());
         if (!safeMode)
         {
             extensionLoader.LoadExtensions();
@@ -134,12 +147,6 @@ internal class ClassicDesktopEntry
         return extensionLoader;
     }
 
-    public void Restart()
-    {
-        restartQueued = true;
-        desktop.TryShutdown();
-    }
-
     private IPlatform GetActivePlatform()
     {
 #if STEAM || DEV_STEAM
@@ -147,7 +154,7 @@ internal class ClassicDesktopEntry
 #elif MSIX || MSIX_DEBUG
         return new PixiEditor.Platform.MSStore.MicrosoftStorePlatform();
 #else
-        return new PixiEditor.Platform.Standalone.StandalonePlatform(Paths.ExtensionPackagesPath, GetApiUrl());
+        return new PixiEditor.Platform.Standalone.StandalonePlatform();
 #endif
     }
 
@@ -218,68 +225,8 @@ internal class ClassicDesktopEntry
         var vm = ViewModels_ViewModelMain.Current;
         if (vm is null)
             return;
-        e.Cancel = true;
-        Dispatcher.UIThread.InvokeAsync(async () =>
-        {
-            await vm.CloseWindow();
-            if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
-            {
-                await Dispatcher.UIThread.InvokeAsync(async () =>
-                {
-                    ConfirmationType confirmation = await ConfirmationDialog.Show(
-                        new LocalizedString("SESSION_UNSAVED_DATA", "Shutdown"),
-                        $"Shutdown");
-
-                    if (confirmation == ConfirmationType.Yes)
-                    {
-                        if (restartQueued)
-                        {
-                            var process = Process.GetCurrentProcess().MainModule.FileName;
-                            desktop.Exit += (_, _) =>
-                            {
-                                Process.Start(process);
-                            };
-                        }
-
-                        desktop.Shutdown();
-                    }
-                    else
-                    {
-                        restartQueued = false;
-                    }
-                });
-            }
-            else
-            {
-                if (restartQueued)
-                {
-                    var process = Process.GetCurrentProcess().MainModule.FileName;
-                    desktop.Exit += (_, _) =>
-                    {
-                        Process.Start(process);
-                    };
-                }
-
-                desktop.Shutdown();
-            }
-        });
-    }
-
-    private string GetApiUrl()
-    {
-        string baseUrl = BuildConstants.PixiEditorApiUrl;
-#if DEBUG
-        if (baseUrl.Contains('{') && baseUrl.Contains('}'))
-        {
-            string? envUrl = Environment.GetEnvironmentVariable("PIXIAUTH_API_URL");
-            if (envUrl != null)
-            {
-                baseUrl = envUrl;
-            }
-        }
-#endif
 
-        return baseUrl;
+        vm.OnShutdown(e, () => desktop.Shutdown(0));
     }
 
     private void AttachGlobalShortcutBehavior(BehaviorCollection collection)

+ 2 - 2
src/PixiEditor/Models/Commands/CommandController.cs

@@ -80,7 +80,7 @@ internal class CommandController
             {
                 if (Commands.ContainsKey(command))
                 {
-                    ReplaceShortcut(Commands[command], shortcut.KeyCombination, false);
+                    ReplaceShortcut(Commands[command], AdjustForOS(shortcut.KeyCombination, null), false);
                 }
             }
         }
@@ -706,7 +706,7 @@ internal class CommandController
         if (IOperatingSystem.Current.IsMacOs)
         {
             KeyCombination newCombination = combination;
-            if (combination.Modifiers.HasFlag(KeyModifiers.Control))
+            if (combination.Modifiers.HasFlag(KeyModifiers.Control) && !combination.Modifiers.HasFlag(KeyModifiers.Meta))
             {
                 newCombination.Modifiers &= ~KeyModifiers.Control;
                 newCombination.Modifiers |= KeyModifiers.Meta;

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

@@ -63,6 +63,9 @@ public record HumanReadableKeyCombination(string key, string[] modifiers = null)
                 case "win":
                     modifiers |= KeyModifiers.Meta;
                     break;
+                case "cmd":
+                    modifiers |= KeyModifiers.Meta;
+                    break;
             }
         }
         

+ 6 - 2
src/PixiEditor/Models/DocumentModels/Autosave/AutosaverSaveUserFileJob.cs

@@ -46,8 +46,12 @@ internal class AutosaverSaveUserFileJob(DocumentViewModel document) : IAutosaver
 
                 File.Copy(document.AutosaveViewModel.LastAutosavedPath, path, true);
 
-                document.MarkAsSaved();
-                document.AutosaveViewModel.AddAutosaveHistoryEntry(AutosaveHistoryType.Periodic, AutosaveHistoryResult.SavedUserFile);
+                Dispatcher.UIThread.Invoke(() =>
+                {
+                    document.MarkAsSaved();
+                    document.AutosaveViewModel.AddAutosaveHistoryEntry(AutosaveHistoryType.Periodic,
+                        AutosaveHistoryResult.SavedUserFile);
+                });
                 return UserFileAutosaveResult.Success;
             }
             catch (Exception e) when (e is UnauthorizedAccessException or DirectoryNotFoundException)

+ 1 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/EraserToolExecutor.cs

@@ -50,7 +50,7 @@ internal class EraserToolExecutor : UpdateableChangeExecutor
         spacing = toolbar.Spacing;
 
         colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
-        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)toolSize, true,
+        IAction? action = new LineBasedPen_Action(guidValue, Colors.White, controller!.LastPixelPosition, (float)eraserTool.ToolSize, true,
             antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable);
         internals!.ActionAccumulator.AddActions(action);
 

+ 9 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/PenToolExecutor.cs

@@ -22,6 +22,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     private bool antiAliasing;
     private float hardness;
     private float spacing = 1;
+    private bool transparentErase;
 
     private IPenToolbar penToolbar;
 
@@ -47,10 +48,15 @@ internal class PenToolExecutor : UpdateableChangeExecutor
         hardness = toolbar.Hardness;
         spacing = toolbar.Spacing;
 
-        colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
+        if (color.A > 0)
+        {
+            colorsHandler.AddSwatch(new PaletteColor(color.R, color.G, color.B));
+        }
+
+        transparentErase = color.A == 0;
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize, false, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, controller!.LastPixelPosition, (float)ToolSize, transparentErase, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, controller!.LastPixelPosition, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);
@@ -62,7 +68,7 @@ internal class PenToolExecutor : UpdateableChangeExecutor
     {
         IAction? action = pixelPerfect switch
         {
-            false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, false, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
+            false => new LineBasedPen_Action(guidValue, color, pos, (float)ToolSize, transparentErase, antiAliasing, hardness, spacing, SquareBrush, drawOnMask, document!.AnimationHandler.ActiveFrameBindable),
             true => new PixelPerfectPen_Action(guidValue, pos, color, drawOnMask, document!.AnimationHandler.ActiveFrameBindable)
         };
         internals!.ActionAccumulator.AddActions(action);

+ 9 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/TransformSelectedExecutor.cs

@@ -294,7 +294,9 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
                 to.X - from.X,
                 to.Y - from.Y);
 
-            internals.ActionAccumulator.AddActions(new PreviewShiftLayers_Action(selectedMembers, delta,
+            RectD clipRect = lastCorners.AABBBounds;
+            internals.ActionAccumulator.AddActions(new PreviewShiftLayers_Action(
+                selectedMembers, clipRect, delta,
                 document!.AnimationHandler.ActiveFrameBindable));
         }
     }
@@ -395,6 +397,12 @@ internal class TransformSelectedExecutor : UpdateableChangeExecutor, ITransforma
 
         actions.Add(new ShiftLayer_Action(newLayerGuids, delta, document!.AnimationHandler.ActiveFrameBindable));
 
+        if (original is { IsEmpty: false })
+        {
+            original.Offset((VecI)delta);
+            actions.Add(new SetSelection_Action(original));
+        }
+
         internals!.ActionAccumulator.AddFinishedActions(actions.ToArray());
 
         actions.Clear();

+ 1 - 1
src/PixiEditor/Models/Handlers/Tools/IEraserToolHandler.cs

@@ -2,5 +2,5 @@
 
 internal interface IEraserToolHandler : IToolHandler
 {
-
+    public double ToolSize { get; }
 }

+ 6 - 2
src/PixiEditor/Models/IO/Paths.cs

@@ -10,16 +10,20 @@ public static class Paths
     public static string DataFullPath { get; } =
         Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Data");
 
-    public static string ExtensionPackagesPath { get; } =
+    public static string InstallDirExtensionPackagesPath { get; } =
         Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Extensions");
 
+    public static string LocalExtensionPackagesPath { get; } = Path.Combine(
+        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+        "PixiEditor", "Extensions", "Packages");
+
     public static string UserConfigPath { get; } = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
         "PixiEditor", "Configs");
 
     public static string UserExtensionsPath { get; } = Path.Combine(
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-        "PixiEditor", "Extensions");
+        "PixiEditor", "Extensions", "Unpacked");
 
     public static string PathToPalettesFolder { get; } = Path.Join(
         Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),

+ 12 - 13
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -4,10 +4,8 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
-using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
@@ -55,16 +53,15 @@ internal class SceneRenderer : IDisposable
 
             var rendered = RenderGraph(target, resolution, targetOutput, finalGraph);
             cachedTextures[adjustedTargetOutput] = rendered;
+            return;
         }
-        else
-        {
-            var cachedTexture = cachedTextures[adjustedTargetOutput];
-            Matrix3X3 matrixDiff = SolveMatrixDiff(target, cachedTexture);
-            int saved = target.Canvas.Save();
-            target.Canvas.SetMatrix(matrixDiff);
-            target.Canvas.DrawSurface(cachedTexture.DrawingSurface, 0, 0);
-            target.Canvas.RestoreToCount(saved);
-        }
+
+        var cachedTexture = cachedTextures[adjustedTargetOutput];
+        Matrix3X3 matrixDiff = SolveMatrixDiff(target, cachedTexture);
+        int saved = target.Canvas.Save();
+        target.Canvas.SetMatrix(matrixDiff);
+        target.Canvas.DrawSurface(cachedTexture.DrawingSurface, 0, 0);
+        target.Canvas.RestoreToCount(saved);
     }
 
     private Texture RenderGraph(DrawingSurface target, ChunkResolution resolution, string? targetOutput,
@@ -263,7 +260,8 @@ internal class SceneRenderer : IDisposable
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
 
-            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size, Document.ProcessingColorSpace,
+            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size,
+                Document.ProcessingColorSpace,
                 finalOpacity);
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);
@@ -279,7 +277,8 @@ internal class SceneRenderer : IDisposable
             }
 
             double finalOpacity = onionOpacity * alphaFalloffMultiplier * (animationData.OnionFrames - i + 1);
-            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size, Document.ProcessingColorSpace,
+            RenderContext onionContext = new(target, frame, resolution, renderOutputSize, Document.Size,
+                Document.ProcessingColorSpace,
                 finalOpacity);
             onionContext.TargetOutput = targetOutput;
             finalGraph.Execute(onionContext);

+ 4 - 6
src/PixiEditor/Models/Serialization/Factories/ByteBuilder.cs

@@ -1,4 +1,5 @@
-using Drawie.Backend.Core.ColorsImpl;
+using System.Text;
+using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Numerics;
 
@@ -69,11 +70,8 @@ public class ByteBuilder
 
     public void AddString(string str)
     {
-        AddInt(str.Length);
-        foreach (var c in str)
-        {
-            AddInt(c);
-        }
+        AddInt(Encoding.UTF8.GetByteCount(str));
+        _data.AddRange(Encoding.UTF8.GetBytes(str));
     }
 
     public void AddFloat(float value)

+ 17 - 8
src/PixiEditor/Models/Serialization/Factories/ByteExtractor.cs

@@ -86,14 +86,10 @@ public class ByteExtractor
     public string GetString()
     {
         int length = GetInt();
-        StringBuilder builder = new StringBuilder();
-        
-        for (int i = 0; i < length; i++)
-        {
-            builder.Append((char)GetInt());
-        }
-        
-        return builder.ToString();
+        string value = Encoding.UTF8.GetString(_data, Position, length);
+
+        Position += length;
+        return value;
     }
 
     public float GetFloat()
@@ -113,4 +109,17 @@ public class ByteExtractor
         
         return value;
     }
+
+    internal string GetStringLegacyDontUse()
+    {
+        int length = GetInt();
+
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++)
+        {
+            sb.Append((char)GetInt());
+        }
+
+        return sb.ToString();
+    }
 }

+ 3 - 1
src/PixiEditor/Models/Serialization/Factories/FontFamilySerializationFactory.cs

@@ -36,7 +36,9 @@ public class FontFamilySerializationFactory : SerializationFactory<byte[], FontF
         }
 
         ByteExtractor extractor = new ByteExtractor(bytes);
-        string fontFamily = extractor.GetString();
+
+        string fontFamily = DeserializeStringCompatible(extractor, serializerData);
+
         bool isFontFromFile = extractor.GetBool();
         string fontPath = null;
         if (isFontFromFile && ResourceLocator != null)

+ 17 - 0
src/PixiEditor/Models/Serialization/Factories/SerializationFactory.cs

@@ -65,6 +65,23 @@ public abstract class SerializationFactory<TSerializable, TOriginal> : Serializa
     {
         return TryDeserialize(rawData, out TOriginal original, serializerData) ? original : default;
     }
+
+    protected string DeserializeStringCompatible(
+        ByteExtractor extractor,
+        (string serializerName, string serializerVersion) serializerData)
+    {
+        if (serializerData.serializerName != "PixiEditor")
+        {
+            return extractor.GetString();
+        }
+
+        if (IsFilePreVersion(serializerData, new Version(2, 0, 0, 87)))
+        {
+            return extractor.GetStringLegacyDontUse();
+        }
+
+        return extractor.GetString();
+    }
     
     public override Type OriginalType => typeof(TOriginal);
 }

+ 4 - 3
src/PixiEditor/Models/Serialization/Factories/TextSerializationFactory.cs

@@ -46,10 +46,11 @@ internal class TextSerializationFactory : VectorShapeSerializationFactory<TextVe
         float strokeWidth, (string serializerName, string serializerVersion) serializerData,
         out TextVectorData original)
     {
-        string text = extractor.GetString();
+        string text = DeserializeStringCompatible(extractor, serializerData);
+
         VecD position = extractor.GetVecD();
         bool antiAlias = extractor.GetBool();
-        string fontFamily = extractor.GetString();
+        string fontFamily = DeserializeStringCompatible(extractor, serializerData);
         bool isFontFromFile = extractor.GetBool();
         string fontPath = null;
         if (isFontFromFile && ResourceLocator != null)
@@ -67,7 +68,7 @@ internal class TextSerializationFactory : VectorShapeSerializationFactory<TextVe
         VectorPath path = null;
         if (hasPath)
         {
-            path = VectorPath.FromSvgPath(extractor.GetString());
+            path = VectorPath.FromSvgPath(DeserializeStringCompatible(extractor, serializerData));
         }
 
         FontFamilyName family =

+ 1 - 1
src/PixiEditor/Models/Serialization/Factories/VectorPathSerializationFactory.cs

@@ -51,7 +51,7 @@ internal class VectorPathSerializationFactory : VectorShapeSerializationFactory<
         VectorPath path;
         if (IsOldSerializer(serializerData))
         {
-            string svgPath = extractor.GetString();
+            string svgPath = extractor.GetStringLegacyDontUse();
             path = VectorPath.FromSvgPath(svgPath);
         }
         else

+ 8 - 5
src/PixiEditor/Models/Serialization/Factories/VectorShapeSerializationFactory.cs

@@ -20,7 +20,8 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
 
         foreach (Type type in types)
         {
-            if (type.IsAssignableTo(typeof(IPaintableSerializationFactory)) && type is { IsAbstract: false, IsInterface: false })
+            if (type.IsAssignableTo(typeof(IPaintableSerializationFactory)) &&
+                type is { IsAbstract: false, IsInterface: false })
             {
                 factories.Add((SerializationFactory)Activator.CreateInstance(type));
             }
@@ -61,9 +62,9 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
         ByteExtractor extractor = new ByteExtractor(data);
 
         Matrix3X3 matrix = extractor.GetMatrix3X3();
-        Paintable strokeColor = TryGetPaintable(extractor, fileIsPrePaintables);
+        Paintable strokeColor = TryGetPaintable(extractor, fileIsPrePaintables, serializerData);
         bool fill = TryGetBool(extractor, serializerData);
-        Paintable fillColor = TryGetPaintable(extractor, fileIsPrePaintables);
+        Paintable fillColor = TryGetPaintable(extractor, fileIsPrePaintables, serializerData);
         float strokeWidth;
         // Previous versions of the serializer saved stroke as int, and serializer data didn't exist
         if (string.IsNullOrEmpty(serializerData.serializerVersion) &&
@@ -100,14 +101,16 @@ public abstract class VectorShapeSerializationFactory<T> : SerializationFactory<
         return extractor.GetBool();
     }
 
-    private Paintable TryGetPaintable(ByteExtractor extractor, bool fileIsPrePaintables)
+    private Paintable TryGetPaintable(ByteExtractor extractor, bool fileIsPrePaintables,
+        (string serializerName, string serializerVersion) serializerData)
     {
         if (fileIsPrePaintables)
         {
             return new ColorPaintable(extractor.GetColor());
         }
 
-        string paintableType = extractor.GetString();
+        string paintableType = DeserializeStringCompatible(extractor, serializerData);
+
         SerializationFactory factory = PaintableFactories.FirstOrDefault(f => f.DeserializationId == paintableType);
         if (factory == null)
         {

+ 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
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("2.0.0.86")]
-[assembly: AssemblyFileVersion("2.0.0.86")]
+[assembly: AssemblyVersion("2.0.0.90")]
+[assembly: AssemblyFileVersion("2.0.0.90")]

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

@@ -183,7 +183,14 @@ internal partial class DocumentViewModel
             transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
             primitive.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
 
-            primitive.Fill.Unit = new SvgPaintServerUnit(data.FillPaintable);
+            Paintable finalFill = data.Fill ? data.FillPaintable : new ColorPaintable(Colors.Transparent);
+            primitive.Fill.Unit = new SvgPaintServerUnit(finalFill);
+
+            if (finalFill is ColorPaintable colorPaintable)
+            {
+                primitive.FillOpacity.Unit = new SvgNumericUnit(colorPaintable.Color.A / 255f, "");
+            }
+
             primitive.Stroke.Unit = new SvgPaintServerUnit(data.Stroke);
 
             primitive.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);

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

@@ -971,10 +971,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
                 if (maybeMember is IRasterizable rasterizable)
                 {
-                    using Texture texture = Texture.ForDisplay(SizeBindable);
+                    using Surface texture = new Surface(SizeBindable);
                     using Paint paint = new Paint();
                     rasterizable.Rasterize(texture.DrawingSurface, paint);
-                    return texture.GetSRGBPixel(pos);
+                    return texture.GetSrgbPixel(pos);
                 }
             }
             else

+ 2 - 1
src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs

@@ -85,7 +85,7 @@ internal class MenuBarViewModel : PixiObservableObject
     {
         menuItemBuilders = serviceProvider.GetServices<MenuItemBuilder>().ToArray();
         commandController = controller;
-        BuildMenu(controller);
+        RebuildMenu();
         controller.Commands.CommandAdded += CommandsOnCommandAdded;
     }
 
@@ -143,6 +143,7 @@ internal class MenuBarViewModel : PixiObservableObject
     private void RebuildMenu()
     {
         MenuEntries?.Clear();
+        NativeMenu?.Items?.Clear();
         nativeMenuItems?.Clear();
         menuItems.Clear();
 

+ 10 - 8
src/PixiEditor/ViewModels/SubViewModels/ColorsViewModel.cs

@@ -166,7 +166,9 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
 
     private async void OwnerOnStartupEvent()
     {
-        await ImportLospecPalette();
+        var args = StartupArgs.Args;
+        var lospecPaletteArg = args.FirstOrDefault(x => x.StartsWith("lospec-palette://"));
+        await ImportLospecPalette(lospecPaletteArg);
     }
 
     [Commands_Command.Basic("PixiEditor.Colors.OpenPaletteBrowser", "OPEN_PALETTE_BROWSER", "OPEN_PALETTE_BROWSER",
@@ -177,17 +179,14 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
         PalettesBrowser.Open();
     }
 
-    private async Task ImportLospecPalette()
+    public async Task ImportLospecPalette(string? uri)
     {
-        var args = StartupArgs.Args;
-        var lospecPaletteArg = args.FirstOrDefault(x => x.StartsWith("lospec-palette://"));
-
-        if (lospecPaletteArg != null)
+        if (uri != null)
         {
             var browser = PalettesBrowser.Open();
 
             browser.IsFetching = true;
-            var palette = await LospecPaletteFetcher.FetchPalette(lospecPaletteArg.Split(@"://")[1].Replace("/", ""));
+            var palette = await LospecPaletteFetcher.FetchPalette(uri.Split(@"://")[1].Replace("/", ""));
             if (palette != null)
             {
                 if (LocalPalettesFetcher.PaletteExists(palette.Name))
@@ -404,6 +403,9 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
     [Commands_Command.Internal("PixiEditor.Colors.SelectColor")]
     public void SelectColor(PaletteColor color)
     {
+        if (color == null)
+            return;
+
         PrimaryColor = color.ToColor();
     }
 
@@ -423,7 +425,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
     [Commands_Command.Internal("PixiEditor.CloseContextMenu")]
     public void CloseContextMenu(XAML_ContextMenu menu)
     {
-        menu.Close();
+        menu?.Close();
     }
 
     public void SetupPaletteProviders(IServiceProvider services)

+ 12 - 2
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -383,7 +383,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
         AddRecentlyOpened(path);
 
-        var fileType = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(path));
+        var fileExtension = Path.GetExtension(path);
+        var fileType = SupportedFilesHelper.ParseImageFormat(fileExtension);
 
         if (fileType != null)
         {
@@ -393,7 +394,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         else
         {
             CrashHelper.SendExceptionInfo(new InvalidFileTypeException(default,
-                $"Invalid file type '{fileType}'"));
+                $"Invalid file type '{fileExtension}'"));
         }
 
         return doc;
@@ -685,6 +686,15 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         var history =
             preferences.GetLocalPreference<List<AutosaveHistorySession>>(PreferencesConstants.AutosaveHistory);
 
+        bool autosaveEnabled = preferences.GetPreference<bool>(
+            PreferencesConstants.AutosaveEnabled,
+            PreferencesConstants.AutosaveEnabledDefault);
+
+        if(!autosaveEnabled)
+        {
+            return;
+        }
+
         // There are no autosave attempts .. but what if the user has just launched pixieditor for the first time,
         // and it unexpectedly closed before auto saving anything. They could've still had some files open, and they won't be reopened in this session
         // I'll say this is by design

+ 23 - 4
src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs

@@ -123,6 +123,9 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
     private void OnKeyDown(object? sender, FilteredKeyEventArgs args)
     {
+        if (args.Key == Key.None)
+            return;
+
         ProcessShortcutDown(args.IsRepeat, args.Key, args.Modifiers);
         Owner.DocumentManagerSubViewModel.ActiveDocument?.EventInlet.OnKeyDown(args.Key);
     }
@@ -174,7 +177,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         }
 
         if (isRepeat && Owner.ShortcutController.LastCommands != null &&
-            Owner.ShortcutController.LastCommands.Any(x => x is Command.ToolCommand cmd && cmd.Shortcut == new KeyCombination(key, argsModifiers)))
+            Owner.ShortcutController.LastCommands.Any(x =>
+                x is Command.ToolCommand cmd && cmd.Shortcut == new KeyCombination(key, argsModifiers)))
         {
             Owner.ToolsSubViewModel.HandleToolRepeatShortcutDown();
         }
@@ -256,6 +260,9 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
                 Owner.ColorsSubViewModel.SwapColors(true);
                 return true;
+            case RightClickMode.ColorPicker when tools.ActiveTool is not ColorPickerToolViewModel:
+                HandleRightMouseColorPickerDown(tools);
+                return true;
             case RightClickMode.Erase when tools.ActiveTool is ColorPickerToolViewModel:
                 Owner.ColorsSubViewModel.SwapColors(true);
                 return true;
@@ -305,7 +312,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
             var toolSize = eraserTool.Toolbar.Settings.First(x => x.Name == "ToolSize");
             previousEraseSize = (double)toolSize.Value;
             toolSize.Value = tools.ActiveTool is PenToolViewModel { PixelPerfectEnabled: true }
-                ? 1
+                ? 1d
                 : currentToolSize.Value;
         }
         else
@@ -316,6 +323,17 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         tools.SetActiveTool<EraserToolViewModel>(true);
     }
 
+    private void HandleRightMouseColorPickerDown(IToolsHandler tools)
+    {
+        ColorPickerToolViewModel? colorPickerTool = tools.GetTool<ColorPickerToolViewModel>();
+        if (colorPickerTool == null)
+        {
+            return;
+        }
+
+        tools.SetActiveTool<ColorPickerToolViewModel>(true);
+    }
+
     private void OnMiddleMouseButton()
     {
         Owner.ToolsSubViewModel.SetActiveTool<MoveViewportToolViewModel>(true);
@@ -344,7 +362,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         var tools = Owner.ToolsSubViewModel;
 
         var rightCanUp = (button == MouseButton.Right) &&
-                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor;
+                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor
+                             or RightClickMode.ColorPicker;
 
         if (button == MouseButton.Left || rightCanUp)
         {
@@ -383,7 +402,7 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
                 }
 
                 break;
-            case MouseButton.Right when tools.RightClickMode == RightClickMode.Erase:
+            case MouseButton.Right when tools.RightClickMode is RightClickMode.Erase or RightClickMode.ColorPicker:
                 HandleRightMouseEraseUp(tools);
                 break;
         }

+ 165 - 30
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -3,12 +3,14 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.IO;
 using System.Linq;
+using System.Reflection;
 using System.Text;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers;
@@ -24,7 +26,7 @@ namespace PixiEditor.ViewModels.SubViewModels;
 
 internal class UpdateViewModel : SubViewModel<ViewModelMain>
 {
-    public const int MaxRetryCount = 3;
+    private double currentProgress;
     public UpdateChecker UpdateChecker { get; set; }
 
     public List<UpdateChannel> UpdateChannels { get; } = new List<UpdateChannel>();
@@ -107,6 +109,22 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         get => _updateState == UpdateState.UpToDate;
     }
 
+    public double CurrentProgress
+    {
+        get => currentProgress;
+        set
+        {
+            currentProgress = value;
+            OnPropertyChanged(nameof(CurrentProgress));
+        }
+    }
+
+    public string ZipExtension => IOperatingSystem.Current.IsLinux ? "tar.gz" : "zip";
+    public string ZipContentType => IOperatingSystem.Current.IsLinux ? "x-gzip" : "zip";
+    public string InstallerExtension => IOperatingSystem.Current.IsWindows ? "exe" : "dmg";
+
+    public string BinaryExtension => IOperatingSystem.Current.IsWindows ? ".exe" : string.Empty;
+
     public bool SelfUpdatingAvailable =>
 #if UPDATE
         PixiEditorSettings.Update.CheckUpdatesOnStartup.Value && OsSupported();
@@ -120,8 +138,24 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     public UpdateViewModel(ViewModelMain owner)
         : base(owner)
     {
+        if (IOperatingSystem.Current.IsLinux)
+        {
+            if (File.Exists("no-updates"))
+            {
+                UpdateState = UpdateState.UnableToCheck;
+                return;
+            }
+        }
+
         Owner.OnStartupEvent += Owner_OnStartupEvent;
         Owner.OnClose += Owner_OnClose;
+        UpdateDownloader.ProgressChanged += d =>
+        {
+            Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                CurrentProgress = d;
+            });
+        };
         PixiEditorSettings.Update.UpdateChannel.ValueChanged += (_, value) =>
         {
             string prevChannel = UpdateChecker.Channel.ApiUrl;
@@ -139,7 +173,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     public async Task CheckForUpdate()
     {
-        if (!IOperatingSystem.Current.IsWindows)
+        if (!OsSupported())
         {
             return;
         }
@@ -152,15 +186,27 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             return;
         }
 
+        if (updateAvailable)
+        {
+            if (!IOperatingSystem.Current.IsWindows && UpdateChecker.LatestReleaseInfo.TagName.StartsWith("1."))
+            {
+                // 1.0 is windows only
+                UpdateState = UpdateState.UpToDate;
+                return;
+            }
+        }
+
         UpdateState = updateAvailable ? UpdateState.UpdateAvailable : UpdateState.UpToDate;
     }
 
     private void Owner_OnClose()
     {
+#if RELEASE || DEVRELEASE
         if (UpdateState == UpdateState.ReadyToInstall)
         {
             Install(false);
         }
+#endif
     }
 
     public async Task Download()
@@ -180,11 +226,13 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             try
             {
                 UpdateState = UpdateState.Downloading;
-                if (updateCompatible)
+                CurrentProgress = 0;
+                if (updateCompatible || !IOperatingSystem.Current.IsWindows)
                 {
-                    await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo);
+                    await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo, ZipContentType,
+                        ZipExtension);
                 }
-                else
+                else if (IOperatingSystem.Current.IsWindows)
                 {
                     await UpdateDownloader.DownloadInstaller(UpdateChecker.LatestReleaseInfo);
                 }
@@ -209,14 +257,16 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     private bool AutoUpdateFileExists()
     {
         string path = Path.Join(UpdateDownloader.DownloadLocation,
-            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip");
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.{ZipExtension}");
         return File.Exists(path);
     }
 
     private bool UpdateInstallerFileExists()
     {
+        if (IOperatingSystem.Current.IsLinux) return false;
+
         string path = Path.Join(UpdateDownloader.DownloadLocation,
-            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.{InstallerExtension}");
         return File.Exists(path);
     }
 
@@ -226,17 +276,36 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         Install(true);
     }
 
+    [Command.Debug("PixiEditor.Update.DebugInstall", "Debug Install Update",
+        "(DEBUG) Install update zip file without checking for updates")]
+    public void DebugInstall()
+    {
+        UpdateChecker.SetLatestReleaseInfo(new ReleaseInfo(true) { TagName = "2.2.2.2" });
+        Install(true);
+    }
+    
+    [Command.Debug("PixiEditor.Update.DebugDownload", "Debug Download Update",
+        "(DEBUG) Download update file")]
+    public void DebugDownload()
+    {
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            await CheckForUpdate();
+            await Download();
+        });
+    }
+
     private void Install(bool startAfterUpdate)
     {
-#if RELEASE || DEVRELEASE
-        string dir = AppDomain.CurrentDomain.BaseDirectory;
+        string dir = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName) ??
+                     AppDomain.CurrentDomain.BaseDirectory;
 
         UpdateDownloader.CreateTempDirectory();
         if (UpdateChecker.LatestReleaseInfo == null ||
             string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
         bool updateFileExists = AutoUpdateFileExists();
         string exePath = Path.Join(UpdateDownloader.DownloadLocation,
-            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.{InstallerExtension}");
 
         bool updateExeExists = File.Exists(exePath);
 
@@ -247,7 +316,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             updateExeExists = false;
         }
 
-        string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
+        string updaterPath = Path.Join(dir, $"PixiEditor.UpdateInstaller{BinaryExtension}");
+
 
         if (!updateFileExists && !updateExeExists)
         {
@@ -257,6 +327,26 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             return;
         }
 
+        try
+        {
+            if (Path.Exists(updaterPath))
+            {
+                string updateLocation = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName);
+
+                File.Copy(updaterPath,
+                    Path.Join(UpdateDownloader.DownloadLocation, $"PixiEditor.UpdateInstaller" + BinaryExtension),
+                    true);
+                updaterPath = Path.Join(UpdateDownloader.DownloadLocation,
+                    $"PixiEditor.UpdateInstaller" + BinaryExtension);
+                File.WriteAllText(Path.Join(UpdateDownloader.DownloadLocation, "update-location.txt"), updateLocation);
+            }
+        }
+        catch (IOException)
+        {
+            NoticeDialog.Show("COULD_NOT_UPDATE_WITHOUT_ADMIN", "INSUFFICIENT_PERMISSIONS");
+            return;
+        }
+
         if (updateFileExists && File.Exists(updaterPath))
         {
             InstallHeadless(updaterPath, startAfterUpdate);
@@ -265,28 +355,17 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         {
             OpenExeInstaller(exePath);
         }
-#endif
     }
 
     private static void InstallHeadless(string updaterPath, bool startAfterUpdate)
     {
-        try
-        {
-            ProcessHelper.RunAsAdmin(updaterPath, startAfterUpdate ? "--startOnSuccess" : null);
-            Shutdown();
-        }
-        catch (Win32Exception)
-        {
-            NoticeDialog.Show(
-                "COULD_NOT_UPDATE_WITHOUT_ADMIN",
-                "INSUFFICIENT_PERMISSIONS");
-        }
+        TryRestartToUpdate(updaterPath, startAfterUpdate ? "--startOnSuccess" : null);
     }
 
-    private static void OpenExeInstaller(string updateExeFile)
+    private void OpenExeInstaller(string updateExeFile)
     {
         bool alreadyUpdated = VersionHelpers.GetCurrentAssemblyVersion().ToString() ==
-                              updateExeFile.Split('-')[1].Split(".exe")[0];
+                              updateExeFile.Split('-')[1].Split("." + InstallerExtension)[0];
 
         if (!alreadyUpdated)
         {
@@ -298,12 +377,61 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         }
     }
 
+    private static bool InstallDirReadOnly()
+    {
+        string installDir = AppDomain.CurrentDomain.BaseDirectory;
+        DirectoryInfo dirInfo = new DirectoryInfo(installDir);
+        return dirInfo.Attributes.HasFlag(FileAttributes.ReadOnly);
+    }
+
     private static void RestartToUpdate(string updateExeFile)
+    {
+        TryRestartToUpdate(updateExeFile, null);
+    }
+
+    private static void TryRestartToUpdate(string updateExeFile, string? args)
     {
         try
         {
-            IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updateExeFile, null);
-            Shutdown();
+            if (IOperatingSystem.Current.IsLinux)
+            {
+                bool hasWritePermissions = !InstallDirReadOnly();
+                if (hasWritePermissions)
+                {
+                    args = "bash " + updateExeFile + " " + args + " &";
+                    IOperatingSystem.Current.ProcessUtility.ShellExecute("nohup", args);
+                    Shutdown();
+                }
+                else
+                {
+                    NoticeDialog.Show("COULD_NOT_UPDATE_WITHOUT_ADMIN", "INSUFFICIENT_PERMISSIONS");
+                }
+            }
+            else
+            {
+                var proc = IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updateExeFile, args);
+                if (IOperatingSystem.Current.IsMacOs)
+                {
+                    proc.WaitForExitAsync().ContinueWith(t =>
+                    {
+                        Dispatcher.UIThread.Invoke(() =>
+                        {
+                            if (t.IsCompletedSuccessfully && proc.ExitCode == 0)
+                            {
+                                Shutdown();
+                            }
+                            else
+                            {
+                                NoticeDialog.Show("COULD_NOT_UPDATE_WITHOUT_ADMIN", "INSUFFICIENT_PERMISSIONS");
+                            }
+                        });
+                    });
+                }
+                else
+                {
+                    Shutdown();
+                }
+            }
         }
         catch (Win32Exception)
         {
@@ -322,8 +450,9 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     {
         try
         {
+            string binExtension = IOperatingSystem.Current.IsWindows ? ".exe" : string.Empty;
             ProcessHelper.RunAsAdmin(Path.Join(AppDomain.CurrentDomain.BaseDirectory,
-                "PixiEditor.UpdateInstaller.exe"));
+                $"PixiEditor.UpdateInstaller{binExtension}"));
             Shutdown();
         }
         catch (Win32Exception)
@@ -374,7 +503,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     private bool OsSupported()
     {
-        return IOperatingSystem.Current.IsWindows;
+        return IOperatingSystem.Current.IsWindows || IOperatingSystem.Current.IsLinux ||
+               IOperatingSystem.Current.IsMacOs;
     }
 
     private void EnsureUpdateFilesDeleted()
@@ -398,14 +528,19 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     private void InitUpdateChecker()
     {
+        string platformName = IPlatform.Current.Name;
 #if UPDATE
         UpdateChannels.Add(new UpdateChannel("Release", "PixiEditor", "PixiEditor"));
         UpdateChannels.Add(new UpdateChannel("Development", "PixiEditor", "PixiEditor-development-channel"));
 #else
-        string platformName = IPlatform.Current.Name;
         UpdateChannels.Add(new UpdateChannel(platformName, "", ""));
 #endif
 
+        if (IPreferences.Current.GetLocalPreference<bool>("UseTestingUpdateChannel", false))
+        {
+            UpdateChannels.Add(new UpdateChannel("Testing", "PixiEditor", "PixiEditor-testing-channel"));
+        }
+
         string updateChannel = PixiEditorSettings.Update.UpdateChannel.Value;
 
         string version = VersionHelpers.GetCurrentAssemblyVersionString();

+ 1 - 1
src/PixiEditor/ViewModels/Tools/Tools/EraserToolViewModel.cs

@@ -22,7 +22,7 @@ internal class EraserToolViewModel : ToolViewModel, IEraserToolHandler
         Toolbar = ToolbarFactory.Create<EraserToolViewModel, PenToolbar>(this);
     }
 
-    [Settings.Inherited] public double ToolSize => GetValue<int>();
+    [Settings.Inherited] public double ToolSize => GetValue<double>();
 
     public override bool IsErasable => true;
 

+ 4 - 4
src/PixiEditor/ViewModels/Tools/Tools/PenToolViewModel.cs

@@ -18,7 +18,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
     [Command.Tool(Key = Key.B)]
     internal class PenToolViewModel : ShapeTool, IPenToolHandler
     {
-        private double actualToolSize;
+        private double actualToolSize = 1;
 
         public override string ToolNameLocalizationKey => "PEN_TOOL";
 
@@ -31,7 +31,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
         {
             Cursor = Cursors.PreciseCursor;
             Toolbar = ToolbarFactory.Create<PenToolViewModel, PenToolbar>(this);
-            
+
             ViewModelMain.Current.ToolsSubViewModel.SelectedToolChanged += SelectedToolChanged;
         }
 
@@ -82,7 +82,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
                 var setting = toolbar.Settings.FirstOrDefault(x => x.Name == nameof(toolbar.ToolSize));
                 if (setting is SizeSettingViewModel sizeSetting)
                 {
-                    sizeSetting.Value = 1;
+                    sizeSetting.Value = 1d;
                 }
             }
             
@@ -139,7 +139,7 @@ namespace PixiEditor.ViewModels.Tools.Tools
                 if (PixelPerfectEnabled)
                 {
                     actualToolSize = ToolSize;
-                    sizeSettingViewModel.Value = 1;
+                    sizeSettingViewModel.Value = 1d;
                 }
                 else
                 {

+ 24 - 0
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
 using Microsoft.Extensions.DependencyInjection;
@@ -360,6 +361,29 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         return false;
     }
 
+
+    public void OnShutdown(ShutdownRequestedEventArgs shutdownRequestedEventArgs, Action shutdown)
+    {
+        shutdownRequestedEventArgs.Cancel = true;
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            ResetNextSessionFiles();
+            UserWantsToClose = await DisposeAllDocumentsWithSaveConfirmation();
+
+            if (UserWantsToClose)
+            {
+                var analytics = Services.GetService<AnalyticsPeriodicReporter>();
+                if (analytics != null)
+                {
+                    await analytics.StopAsync();
+                }
+
+                OnClose?.Invoke();
+                shutdown();
+            }
+        });
+    }
+
     private void OnActiveDocumentChanged(object sender, DocumentChangedEventArgs e)
     {
         NotifyToolActionDisplayChanged();

+ 49 - 44
src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml

@@ -12,25 +12,29 @@
              mc:Ignorable="d"
              Foreground="White"
              d:DesignHeight="450" d:DesignWidth="600"
-             Width="600"
              x:Name="uc">
-    <Grid x:Name="mainGrid">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="Auto" />
-            <RowDefinition Height="*" />
-            <RowDefinition Height="Auto" />
-        </Grid.RowDefinitions>
+    <Grid Background="Transparent" Tapped="InputElement_OnTapped">
+        <Grid Height="700" 
+              MinWidth="600"
+              Width="600"
+              MaxWidth="920" x:Name="mainGrid" Tapped="MainGrid_OnTapped">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="*" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
 
-        <TextBox Text="{Binding SearchTerm, Mode=TwoWay, ElementName=uc}"
-                 FontSize="17"
-                 Padding="5"
-                 CornerRadius="5,5,0,0"
-                 x:Name="textBox">
-            <Interaction.Behaviors>
-                <behaviors:TextBoxFocusBehavior SelectOnMouseClick="{Binding SelectAll, ElementName=uc, Mode=OneWay}" />
-                <behaviours:GlobalShortcutFocusBehavior />
-            </Interaction.Behaviors>
-            <!--<TextBox.Styles>
+            <TextBox Text="{Binding SearchTerm, Mode=TwoWay, ElementName=uc}"
+                     FontSize="17"
+                     Padding="5"
+                     CornerRadius="5,5,0,0"
+                     x:Name="textBox">
+                <Interaction.Behaviors>
+                    <behaviors:TextBoxFocusBehavior
+                        SelectOnMouseClick="{Binding SelectAll, ElementName=uc, Mode=OneWay}" />
+                    <behaviours:GlobalShortcutFocusBehavior />
+                </Interaction.Behaviors>
+                <!--<TextBox.Styles>
                 <Style Selector="TextBox">
                     <Style.Resources>
                         <Style Selector="Border">
@@ -39,32 +43,33 @@
                     </Style.Resources>
                 </Style>
             </TextBox.Styles>-->
-        </TextBox>
-        <Border Grid.Row="1" BorderThickness="1,0,1,0" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-                Background="{DynamicResource ThemeBackgroundBrush}">
-            <Grid>
-                <TextBlock Text="{Binding Warnings, ElementName=uc}" TextAlignment="Center" Foreground="Gray"
-                           TextWrapping="Wrap"
-                           Margin="5,5,5,0"
-                           IsVisible="{Binding HasWarnings, ElementName=uc}" />
-                <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
-                    <ItemsControl ItemsSource="{Binding Results, ElementName=uc}" x:Name="itemscontrol">
-                        <ItemsControl.ItemTemplate>
-                            <DataTemplate DataType="search:SearchResult">
-                                <commandSearch:SearchResultControl
-                                    Result="{Binding}"
-                                    ButtonClickedCommand="{Binding ButtonClickedCommand, ElementName=uc}"
-                                    PointerMoved="SearchResult_MouseMove"/>
-                            </DataTemplate>
-                        </ItemsControl.ItemTemplate>
-                    </ItemsControl>
-                </ScrollViewer>
-            </Grid>
-        </Border>
-        <Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-                CornerRadius="0,0,5,5" Background="{DynamicResource ThemeBackgroundBrush1}" Padding="3">
-            <ContentPresenter
-                Content="{Binding SelectedResult.Description, Mode=OneWay, ElementName=uc, FallbackValue={x:Null}}" />
-        </Border>
+            </TextBox>
+            <Border Grid.Row="1" BorderThickness="1,0,1,0" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    Background="{DynamicResource ThemeBackgroundBrush}">
+                <Grid>
+                    <TextBlock Text="{Binding Warnings, ElementName=uc}" TextAlignment="Center" Foreground="Gray"
+                               TextWrapping="Wrap"
+                               Margin="5,5,5,0"
+                               IsVisible="{Binding HasWarnings, ElementName=uc}" />
+                    <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
+                        <ItemsControl ItemsSource="{Binding Results, ElementName=uc}" x:Name="itemscontrol">
+                            <ItemsControl.ItemTemplate>
+                                <DataTemplate DataType="search:SearchResult">
+                                    <commandSearch:SearchResultControl
+                                        Result="{Binding}"
+                                        ButtonClickedCommand="{Binding ButtonClickedCommand, ElementName=uc}"
+                                        PointerMoved="SearchResult_MouseMove" />
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                        </ItemsControl>
+                    </ScrollViewer>
+                </Grid>
+            </Border>
+            <Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    CornerRadius="0,0,5,5" Background="{DynamicResource ThemeBackgroundBrush1}" Padding="3">
+                <ContentPresenter
+                    Content="{Binding SelectedResult.Description, Mode=OneWay, ElementName=uc, FallbackValue={x:Null}}" />
+            </Border>
+        </Grid>
     </Grid>
 </UserControl>

+ 12 - 2
src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml.cs

@@ -120,7 +120,7 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
         if (e.Sender is not CommandSearchControl control) return;
         if (e.NewValue.Value)
         {
-            Dispatcher.UIThread.Invoke(
+            Dispatcher.UIThread.Post(
                 () =>
                 {
                     control.textBox.Focus();
@@ -133,7 +133,7 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
                     {
                         control.textBox.CaretIndex = control.SearchTerm?.Length ?? 0;
                     }
-                }, DispatcherPriority.Render);
+                }, DispatcherPriority.Input);
         }
     }
 
@@ -298,4 +298,14 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
         CommandSearchControl control = ((CommandSearchControl)e.Sender);
         control.UpdateSearchResults();
     }
+
+    private void InputElement_OnTapped(object? sender, TappedEventArgs e)
+    {
+        Hide();
+    }
+
+    private void MainGrid_OnTapped(object? sender, TappedEventArgs e)
+    {
+        e.Handled = true;
+    }
 }

+ 3 - 0
src/PixiEditor/Views/Main/MainTitleBar.axaml

@@ -75,6 +75,9 @@
                                         Background="{DynamicResource ThemeAccentBrush}"
                                         Command="{Binding UpdateViewModel.DownloadCommand}"
                                         IsVisible="{Binding UpdateViewModel.IsUpdateAvailable}" />
+                                <ProgressBar IsVisible="{Binding UpdateViewModel.IsDownloading}" 
+                                             ShowProgressText="True" Value="{Binding UpdateViewModel.CurrentProgress}"
+                                              Maximum="100" Minimum="0"/>
                                 <Button ui:Translator.Key="SWITCH_TO_NEW_VERSION"
                                         Background="{DynamicResource ThemeAccentBrush}"
                                         Command="{Binding UpdateViewModel.InstallCommand}"

+ 21 - 1
src/PixiEditor/Views/Main/MainTitleBar.axaml.cs

@@ -42,7 +42,27 @@ public partial class MainTitleBar : UserControl
         base.OnAttachedToVisualTree(e);
         if (IOperatingSystem.Current.IsMacOs && DataContext is MenuBarViewModel menuBarViewModel)
         {
-            NativeMenu.SetMenu(MainWindow.Current, menuBarViewModel.NativeMenu);
+            if (menuBarViewModel.NativeMenu != null)
+            {
+                NativeMenu.SetMenu(MainWindow.Current, menuBarViewModel.NativeMenu);
+            }
+            else
+            {
+                menuBarViewModel.PropertyChanged += (sender, args) =>
+                {
+                    if (args.PropertyName == nameof(MenuBarViewModel.NativeMenu))
+                    {
+                        if (menuBarViewModel.NativeMenu != null)
+                        {
+                            NativeMenu.SetMenu(MainWindow.Current, menuBarViewModel.NativeMenu);
+                        }
+                        else
+                        {
+                            NativeMenu.SetMenu(MainWindow.Current, null);
+                        }
+                    }
+                };
+            }
         }
     }
 }

+ 4 - 3
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml.cs

@@ -663,10 +663,11 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
         }
 
         var useContextMenu = vm.Owner.Owner.ToolsSubViewModel.RightClickMode == RightClickMode.ContextMenu;
-        var usesErase = tools.RightClickMode == RightClickMode.Erase && tools.ActiveTool.IsErasable;
-        var usesSecondaryColor = tools.RightClickMode == RightClickMode.SecondaryColor && tools.ActiveTool.UsesColor;
+        var usesErase = tools is { RightClickMode: RightClickMode.Erase, ActiveTool.IsErasable: true };
+        bool usesColorPicker = vm.Owner.Owner.ToolsSubViewModel.RightClickMode == RightClickMode.ColorPicker;
+        var usesSecondaryColor = tools is { RightClickMode: RightClickMode.SecondaryColor, ActiveTool.UsesColor: true };
 
-        if (!useContextMenu && (usesErase || usesSecondaryColor))
+        if (!useContextMenu && (usesErase || usesSecondaryColor || usesColorPicker))
         {
             return;
         }

+ 19 - 1
src/PixiEditor/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -140,7 +140,7 @@ internal class ViewportOverlays
 
     private void BindSelectionOverlay()
     {
-        Binding showFillBinding = new()
+        Binding toolIsSelectionBinding = new()
         {
             Source = ViewModelMain.Current,
             Path = "ToolsSubViewModel.ActiveTool",
@@ -148,6 +148,24 @@ internal class ViewportOverlays
             Mode = BindingMode.OneWay
         };
 
+        Binding isTransformingBinding = new()
+        {
+            Source = Viewport,
+            Path = "!Document.TransformViewModel.TransformActive",
+            Mode = BindingMode.OneWay
+        };
+
+        MultiBinding showFillBinding = new()
+        {
+            Converter = new AllTrueConverter(),
+            Mode = BindingMode.OneWay,
+            Bindings = new List<IBinding>()
+            {
+                toolIsSelectionBinding,
+                isTransformingBinding
+            }
+        };
+
         Binding pathBinding = new()
         {
             Source = Viewport, Path = "Document.SelectionPathBindable", Mode = BindingMode.OneWay

+ 1 - 3
src/PixiEditor/Views/MainView.axaml

@@ -35,8 +35,6 @@
         <commandSearch:CommandSearchControl
             IsVisible="{Binding SearchSubViewModel.SearchWindowOpen, Mode=TwoWay}"
             SearchTerm="{Binding SearchSubViewModel.SearchTerm, Mode=TwoWay}"
-            HorizontalAlignment="Center"
-            Height="700"
-            MaxWidth="920" />
+            />
     </Grid>
 </UserControl>

+ 8 - 0
src/PixiEditor/Views/Palettes/PaletteViewer.axaml.cs

@@ -214,6 +214,14 @@ internal partial class PaletteViewer : UserControl
             {
                 NoticeDialog.Show("COULD_NOT_SAVE_PALETTE", "ERROR");
             }
+            catch (UnauthorizedAccessException unauthorizedAccessException)
+            {
+                NoticeDialog.Show(new LocalizedString("UNAUTHORIZED_ACCESS", file.Path.LocalPath), "ERROR");
+            }
+            catch (Exception ex)
+            {
+                NoticeDialog.Show(new LocalizedString("ERROR_SAVING_PALETTE", ex.Message), "ERROR");
+            }
         });
     }
 

+ 2 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -465,7 +465,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                 }
             }
 
-            Cursor = finalCursor;
+            if(Cursor.ToString() != finalCursor.ToString())
+                Cursor = finalCursor;
             e.Handled = args.Handled;
         }
     }

+ 9 - 2
src/PixiEditor/Views/Visuals/PixiFilePreviewImage.cs

@@ -76,8 +76,15 @@ internal class PixiFilePreviewImage : TextureControl
 
         Dispatcher.UIThread.Post(() =>
         {
-            var surface = LoadTexture(imageBytes);
-            SetImage(surface);
+            try
+            {
+                var surface = LoadTexture(imageBytes);
+                SetImage(surface);
+            }
+            catch (Exception e)
+            {
+                SetCorrupt();
+            }
         });
     }
 

+ 1 - 0
tests/PixiEditor.Tests/PixiEditor.Tests.csproj

@@ -31,6 +31,7 @@
       <ProjectReference Include="..\..\src\PixiEditor.Desktop\PixiEditor.Desktop.csproj" />
       <ProjectReference Include="..\..\src\PixiEditor.Linux\PixiEditor.Linux.csproj" />
       <ProjectReference Include="..\..\src\PixiEditor.MacOs\PixiEditor.MacOs.csproj" />
+        <ProjectReference Include="..\..\src\PixiEditor.Windows\PixiEditor.Windows.csproj" />
     </ItemGroup>
     
     <ItemGroup>

+ 1 - 3
tests/PixiEditor.Tests/PixiEditorTest.cs

@@ -1,7 +1,5 @@
 using Drawie.Backend.Core.Bridge;
 using Drawie.Numerics;
-using Drawie.RenderApi.Vulkan;
-using Drawie.Silk;
 using Drawie.Skia;
 using Drawie.Windowing;
 using DrawiEngine;
@@ -57,7 +55,7 @@ public class FullPixiEditorTest : PixiEditorTest
 {
     public FullPixiEditorTest()
     {
-        ExtensionLoader loader = new ExtensionLoader("TestExtensions", "TestExtensions/Unpacked");
+        ExtensionLoader loader = new ExtensionLoader(["TestExtensions"], "TestExtensions/Unpacked");
 
         if (IOperatingSystem.Current == null)
         {

+ 19 - 0
tests/PixiEditor.Tests/SerializationTests.cs

@@ -1,3 +1,4 @@
+using Avalonia.Headless.XUnit;
 using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Surfaces.ImageData;
@@ -5,6 +6,7 @@ using Drawie.Skia;
 using DrawiEngine;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Changes.NodeGraph;
+using PixiEditor.Models.IO;
 using PixiEditor.Models.Serialization;
 using PixiEditor.Models.Serialization.Factories;
 using PixiEditor.Models.Serialization.Factories.Paintables;
@@ -47,4 +49,21 @@ public class SerializationTests : PixiEditorTest
             Assert.NotNull(factory);
         }
     }
+
+    [AvaloniaTheory]
+    [InlineData("Fibi")]
+    [InlineData("Pond")]
+    [InlineData("SmlPxlCircShadWithMask")]
+    [InlineData("SmallPixelArtCircleShadow")]
+    [InlineData("SmlPxlCircShadWithMaskClipped")]
+    [InlineData("SmlPxlCircShadWithMaskClippedInFolder")]
+    [InlineData("VectorRectangleClippedToCircle")]
+    [InlineData("VectorRectangleClippedToCircleShadowFilter")]
+    [InlineData("VectorRectangleClippedToCircleMasked")]
+    public void TestThatDeserializationOfSampleFilesDoesntThrow(string fileName)
+    {
+        string pixiFile = Path.Combine("TestFiles", "RenderTests", fileName + ".pixi");
+        var document = Importer.ImportDocument(pixiFile);
+        Assert.NotNull(document);
+    }
 }

+ 7 - 7
tests/PixiEditorTests.sln

@@ -67,8 +67,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.AnimationRendere
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.UI.Common", "..\src\PixiEditor.UI.Common\PixiEditor.UI.Common.csproj", "{13484484-682F-4D7A-AEF2-ECAF3A601311}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Windows", "..\src\PixiEditor.Windows\PixiEditor.Windows.csproj", "{D5A9B447-642A-4655-A8BE-CC59AD6BE275}"
-EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Platform", "..\src\PixiEditor.Platform\PixiEditor.Platform.csproj", "{61696194-C130-4388-9607-DA2D5295DC1F}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiParser", "..\src\PixiParser\src\PixiParser\PixiParser.csproj", "{2AFF4096-A539-4A01-B4F4-60389C11C039}"
@@ -125,6 +123,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Windowing", "..\src\
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drawie.Windowing.Glfw", "..\src\Drawie\src\Drawie.Windowing.Glfw\Drawie.Windowing.Glfw.csproj", "{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Windows", "..\src\PixiEditor.Windows\PixiEditor.Windows.csproj", "{2D1F681A-9C3F-4253-954E-E5A24285AD5D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -242,10 +242,6 @@ Global
 		{13484484-682F-4D7A-AEF2-ECAF3A601311}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{13484484-682F-4D7A-AEF2-ECAF3A601311}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{13484484-682F-4D7A-AEF2-ECAF3A601311}.Release|Any CPU.Build.0 = Release|Any CPU
-		{D5A9B447-642A-4655-A8BE-CC59AD6BE275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{D5A9B447-642A-4655-A8BE-CC59AD6BE275}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{D5A9B447-642A-4655-A8BE-CC59AD6BE275}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{D5A9B447-642A-4655-A8BE-CC59AD6BE275}.Release|Any CPU.Build.0 = Release|Any CPU
 		{61696194-C130-4388-9607-DA2D5295DC1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{61696194-C130-4388-9607-DA2D5295DC1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{61696194-C130-4388-9607-DA2D5295DC1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -358,6 +354,10 @@ Global
 		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59}.Release|Any CPU.Build.0 = Release|Any CPU
+		{2D1F681A-9C3F-4253-954E-E5A24285AD5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{2D1F681A-9C3F-4253-954E-E5A24285AD5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{2D1F681A-9C3F-4253-954E-E5A24285AD5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{2D1F681A-9C3F-4253-954E-E5A24285AD5D}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{0EF3CAB9-7361-472C-8789-D17D4EA2DEBB} = {D914C08C-5F1A-4E13-AAA6-F25E8C9748E2}
@@ -389,7 +389,6 @@ Global
 		{6245051F-835D-41EB-89E0-95652B51153C} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{2A54A13E-6D54-4BC6-836D-740F12B62D02} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{13484484-682F-4D7A-AEF2-ECAF3A601311} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
-		{D5A9B447-642A-4655-A8BE-CC59AD6BE275} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{61696194-C130-4388-9607-DA2D5295DC1F} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{2AFF4096-A539-4A01-B4F4-60389C11C039} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{7E65B1D3-507E-4C89-92A9-D25D0097888B} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
@@ -418,5 +417,6 @@ Global
 		{F021BE50-BDFB-427C-9495-888347C4E3B3} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{37662F23-90F7-4B41-8E45-5D1B09A96803} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 		{B13E1D5E-EF6B-4193-8BE6-4C85F8C1EE59} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
+		{2D1F681A-9C3F-4253-954E-E5A24285AD5D} = {E118E6FE-67E7-4472-A8D7-E7F470E66131}
 	EndGlobalSection
 EndGlobal