浏览代码

Merge branch 'master' into ux-improvements

Equbuxu 2 年之前
父节点
当前提交
d3e6b58827
共有 38 个文件被更改,包括 683 次插入134 次删除
  1. 1 7
      azure-pipelines.yml
  2. 1 1
      incompatible.json
  3. 1 1
      src/ChunkyImageLib/ChunkyImage.cs
  4. 15 15
      src/Installer/installer-setup-x64-light.iss
  5. 15 15
      src/Installer/installer-setup-x86-light.iss
  6. 2 3
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  7. 8 8
      src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs
  8. 117 0
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  10. 260 0
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  11. 17 0
      src/PixiEditor.ChangeableDocument/Enums/FlipType.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  13. 1 1
      src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs
  14. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  15. 1 1
      src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj
  16. 29 1
      src/PixiEditor.UpdateModule/UpdateChecker.cs
  17. 1 1
      src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj
  18. 4 2
      src/PixiEditor/Models/Commands/XAML/Command.cs
  19. 21 0
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  20. 0 7
      src/PixiEditor/Models/Enums/FlipType.cs
  21. 2 2
      src/PixiEditor/PixiEditor.csproj
  22. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  23. 65 4
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs
  24. 7 0
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  25. 2 9
      src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs
  26. 8 2
      src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs
  27. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs
  28. 51 16
      src/PixiEditor/Views/Dialogs/AboutPopup.xaml
  29. 1 0
      src/PixiEditor/Views/Dialogs/PalettesBrowser.xaml.cs
  30. 30 21
      src/PixiEditor/Views/MainWindow.xaml
  31. 4 3
      src/PixiEditor/Views/MainWindow.xaml.cs
  32. 1 1
      src/PixiEditor/Views/UserControls/Viewport.xaml
  33. 1 3
      src/PixiEditorTests/PixiEditorTests.csproj
  34. 7 0
      src/global.json
  35. 1 1
      windows-x64-release-dev.yml
  36. 1 1
      windows-x64-release.yml
  37. 1 1
      windows-x86-release-dev.yml
  38. 1 1
      windows-x86-release.yml

+ 1 - 7
azure-pipelines.yml

@@ -17,12 +17,6 @@ steps:
   inputs:
     restoreSolution: '$(solution)'
 
-- task: DotNetCoreCLI@2
-  inputs:
-    command: 'custom'
-    custom: 'tool'
-    arguments: 'install --global Codecov.Tool'
-
 - task: DotNetCoreCLI@2
   displayName: Build
   inputs:
@@ -35,7 +29,7 @@ steps:
   inputs:
     command: test
     projects: '**/*Tests/*.csproj'
-    arguments: '--configuration $(buildConfiguration) --collect "Code coverage"'
+    arguments: '--configuration $(buildConfiguration)'
 
 #- task: PowerShell@2
 #  inputs:

+ 1 - 1
incompatible.json

@@ -1 +1 @@
-["0.1.3.0", "0.1.3.1", "0.1.3.2", "0.1.3.3", "0.1.3.4", "0.1.3.5", "0.1.3.6", "0.1.4.0", "0.1.5.0", "0.1.6.0"]
+["0.1.3.0", "0.1.3.1", "0.1.3.2", "0.1.3.3", "0.1.3.4", "0.1.3.5", "0.1.3.6", "0.1.4.0", "0.1.5.0", "0.1.6.0", "0.1.7.0", "0.1.8.0", "0.1.9.0", "0.1.9.1", "0.1.9.2"]

+ 1 - 1
src/ChunkyImageLib/ChunkyImage.cs

@@ -156,7 +156,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
                 preciseBounds ??= globalChunkBounds;
                 preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
             }
-            preciseBounds = preciseBounds?.Intersect(new RectI(VecI.Zero, CommittedSize));
+            preciseBounds = preciseBounds?.Intersect(new RectI(preciseBounds.Value.Pos, CommittedSize));
 
             return preciseBounds;
         }

+ 15 - 15
src/Installer/installer-setup-x64-light.iss

@@ -4,13 +4,13 @@
 // requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
 #define UseNetCoreCheck
 #ifdef UseNetCoreCheck
-  ;#define UseDotNet60
-  #define UseDotNet60Desktop
+  ;#define UseDotNet70
+  #define UseDotNet70Desktop
 #endif
 
 // custom setup info
 #define MyAppName "PixiEditor"
-#define MyAppVersion GetFileVersion("..\Builds\PixiEditor-x64-light\PixiEditor\PixiEditor.exe")     ;Not perfect solution, it's enviroment dependend
+#define MyAppVersion GetFileVersion("..\..\Builds\PixiEditor-x64-light\PixiEditor\PixiEditor.exe")     ;Not perfect solution, it's enviroment dependend
 #define MyAppPublisher "PixiEditor"
 #define MyAppURL "https://github.com/PixiEditor/PixiEditor"
 #define MyAppExeName "PixiEditor.exe"
@@ -401,24 +401,24 @@ var
   Version: String;
 begin
 
-#ifdef UseDotNet60
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 6.0.0') then begin
-    AddDependency('dotnet60' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.NETCore.App 7.0.0') then begin
+    AddDependency('dotnet70' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/34df41d5-c813-4e30-8aa3-3603ce6600c0/976e801af82c7108abbcb736a8bc5c14/dotnet-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b9cfdb9e-d5cd-4024-b318-00390b729d2f/65690f2440f40654898020cdfffa1050/dotnet-runtime-6.0.0-win-x64.exe'),
+      '.NET Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/75c0d7c7-9f30-46fd-9675-a301f0e051f4/ec04d5cc40aa6537a4af21fad6bf8ba9/dotnet-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/87bc5966-97cc-498c-8381-bff4c43aafc6/baca88b989e7d2871e989d33a667d8e9/dotnet-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif
 
-#ifdef UseDotNet60Desktop
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.0') then begin
-    AddDependency('dotnet60desktop' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70Desktop
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 7.0.0') then begin
+    AddDependency('dotnet70desktop' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/a1ca7d0d-ce01-4878-b952-3fa1e6d9a7c6/e386db367490b631b8c013a9fb0f3794/windowsdesktop-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a865ccae-2219-4184-bcd6-0178dc580589/ba452d37e8396b7a49a9adc0e1a07e87/windowsdesktop-runtime-6.0.0-win-x64.exe'),
+      '.NET Desktop Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/d05a833c-2cf9-4d06-89ae-a0f3e10c5c91/c668ff42e23c2f67aa3d80227860585f/windowsdesktop-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5b2fbe00-507e-450e-8b52-43ab052aadf2/79d54c3a19ce3fce314f2367cf4e3b21/windowsdesktop-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif

+ 15 - 15
src/Installer/installer-setup-x86-light.iss

@@ -4,13 +4,13 @@
 // requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
 #define UseNetCoreCheck
 #ifdef UseNetCoreCheck
-  ;#define UseDotNet60
-  #define UseDotNet60Desktop
+  ;#define UseDotNet70
+  #define UseDotNet70Desktop
 #endif
 
 // custom setup info
 #define MyAppName "PixiEditor"
-#define MyAppVersion GetFileVersion("..\Builds\PixiEditor-x86-light\PixiEditor\PixiEditor.exe")     ;Not perfect solution, it's enviroment dependend
+#define MyAppVersion GetFileVersion("..\..\Builds\PixiEditor-x86-light\PixiEditor\PixiEditor.exe")     ;Not perfect solution, it's enviroment dependend
 #define MyAppPublisher "PixiEditor"
 #define MyAppURL "https://github.com/PixiEditor/PixiEditor"
 #define MyAppExeName "PixiEditor.exe"
@@ -400,24 +400,24 @@ var
   Version: String;
 begin
 
-#ifdef UseDotNet60
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 6.0.0') then begin
-    AddDependency('dotnet60' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.NETCore.App 7.0.0') then begin
+    AddDependency('dotnet70' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/34df41d5-c813-4e30-8aa3-3603ce6600c0/976e801af82c7108abbcb736a8bc5c14/dotnet-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b9cfdb9e-d5cd-4024-b318-00390b729d2f/65690f2440f40654898020cdfffa1050/dotnet-runtime-6.0.0-win-x64.exe'),
+      '.NET Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/75c0d7c7-9f30-46fd-9675-a301f0e051f4/ec04d5cc40aa6537a4af21fad6bf8ba9/dotnet-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/87bc5966-97cc-498c-8381-bff4c43aafc6/baca88b989e7d2871e989d33a667d8e9/dotnet-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif
 
-#ifdef UseDotNet60Desktop
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.0') then begin
-    AddDependency('dotnet60desktop' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70Desktop
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 7.0.0') then begin
+    AddDependency('dotnet70desktop' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/a1ca7d0d-ce01-4878-b952-3fa1e6d9a7c6/e386db367490b631b8c013a9fb0f3794/windowsdesktop-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a865ccae-2219-4184-bcd6-0178dc580589/ba452d37e8396b7a49a9adc0e1a07e87/windowsdesktop-runtime-6.0.0-win-x64.exe'),
+      '.NET Desktop Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/d05a833c-2cf9-4d06-89ae-a0f3e10c5c91/c668ff42e23c2f67aa3d80227860585f/windowsdesktop-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5b2fbe00-507e-450e-8b52-43ab052aadf2/79d54c3a19ce3fce314f2367cf4e3b21/windowsdesktop-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif

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

@@ -262,7 +262,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return false;
     }
     
-    public List<Guid> ExtractLayers(List<Guid> members)
+    public List<Guid> ExtractLayers(IList<Guid> members)
     {
         var result = new List<Guid>();
         foreach (var member in members)
@@ -282,7 +282,7 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
         return result;
     }
 
-    private List<Guid> ExtractLayers(Folder folder, List<Guid> list)
+    private void ExtractLayers(Folder folder, List<Guid> list)
     {
         foreach (var member in folder.Children)
         {
@@ -295,6 +295,5 @@ internal class Document : IChangeable, IReadOnlyDocument, IDisposable
                 ExtractLayers(childFolder, list);
             }
         }
-        return list;
     }
 }

+ 8 - 8
src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs

@@ -6,25 +6,25 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 internal class CenterContent_Change : Change
 {
     private VecI _oldOffset;
-    private List<Guid> _affectedLayers;
+    private List<Guid> affectedLayers;
     private Dictionary<Guid, CommittedChunkStorage>? originalLayerChunks;
 
     [GenerateMakeChangeAction]
     public CenterContent_Change(List<Guid> layers)
     {
-        _affectedLayers = layers;
+        affectedLayers = layers;
     }
     
     public override bool InitializeAndValidate(Document target)
     {
-        if (_affectedLayers.Count == 0)
+        if (affectedLayers.Count == 0)
         {
             return false;
         }
 
-        _affectedLayers = target.ExtractLayers(_affectedLayers);
+        affectedLayers = target.ExtractLayers(affectedLayers);
 
-        foreach (var layer in _affectedLayers)
+        foreach (var layer in affectedLayers)
         {
             if (!target.HasMember(layer)) return false;
         }
@@ -38,7 +38,7 @@ internal class CenterContent_Change : Change
     {
         VecI currentCenter = new VecI(0, 0);
         RectI? currentBounds = null;
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
             Layer layer = document.FindMemberOrThrow<Layer>(layerGuid);
             RectI? tightBounds = layer.LayerImage.FindPreciseCommittedBounds();
@@ -64,7 +64,7 @@ internal class CenterContent_Change : Change
         List<IChangeInfo> changes = new List<IChangeInfo>();
         originalLayerChunks = new Dictionary<Guid, CommittedChunkStorage>();
         
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
             Layer layer = target.FindMemberOrThrow<Layer>(layerGuid);
             var chunks = ShiftLayerHelper.DrawShiftedLayer(target, layerGuid, false, shift);
@@ -81,7 +81,7 @@ internal class CenterContent_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         List<IChangeInfo> changes = new List<IChangeInfo>();
-        foreach (var layerGuid in _affectedLayers)
+        foreach (var layerGuid in affectedLayers)
         {
             var image = target.FindMemberOrThrow<Layer>(layerGuid).LayerImage;
             CommittedChunkStorage? originalChunks = originalLayerChunks?[layerGuid];

+ 117 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -0,0 +1,117 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+
+internal sealed class FlipImage_Change : Change
+{
+    private readonly FlipType flipType;
+    private List<Guid> membersToFlip;
+
+    [GenerateMakeChangeAction]
+    public FlipImage_Change(FlipType flipType, List<Guid>? membersToFlip = null)
+    {
+        this.flipType = flipType;
+        membersToFlip ??= new List<Guid>();
+        this.membersToFlip = membersToFlip;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (membersToFlip.Count > 0)
+        {
+            membersToFlip = target.ExtractLayers(membersToFlip);
+            
+            foreach (var layer in membersToFlip)
+            {
+                if (!target.HasMember(layer)) return false;
+            }  
+        }
+        
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var changes = Flip(target);
+        
+        ignoreInUndo = false;
+        return changes;
+    }
+
+    private void FlipImage(ChunkyImage img)
+    {
+        using Paint paint = new()
+        {
+            BlendMode = DrawingApi.Core.Surface.BlendMode.Src
+        };
+
+        RectI bounds = new RectI(VecI.Zero, img.LatestSize);
+        if (membersToFlip.Count > 0)
+        {
+            var preciseBounds = img.FindPreciseCommittedBounds();
+            if (preciseBounds.HasValue)
+            {
+                bounds = preciseBounds.Value;
+            }
+        }
+
+        using Surface originalSurface = new(img.LatestSize);
+        img.DrawMostUpToDateRegionOn(
+            new RectI(VecI.Zero, img.LatestSize), 
+            ChunkResolution.Full,
+            originalSurface.DrawingSurface,
+            VecI.Zero);
+
+        using Surface flipped = new Surface(img.LatestSize);
+
+        bool flipX = flipType == FlipType.Horizontal;
+        bool flipY = flipType == FlipType.Vertical;
+        
+        flipped.DrawingSurface.Canvas.Save();
+                flipped.DrawingSurface.Canvas.Scale(flipX ? -1 : 1, flipY ? -1 : 1, flipX ? bounds.X + (bounds.Width / 2f) : 0,
+            flipY ? bounds.Y + (bounds.Height / 2f) : 0f);
+        flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
+        flipped.DrawingSurface.Canvas.Restore();
+        
+        img.EnqueueClear();
+        img.EnqueueDrawImage(VecI.Zero, flipped);
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        return Flip(target);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> Flip(Document target)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+
+        target.ForEveryMember(member =>
+        {
+            if (membersToFlip.Count == 0 || membersToFlip.Contains(member.GuidValue))
+            {
+                if (member is Layer layer)
+                {
+                    FlipImage(layer.LayerImage);
+                    changes.Add(
+                        new LayerImageChunks_ChangeInfo(member.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                    layer.LayerImage.CommitChanges();
+                }
+
+                if (member.Mask is not null)
+                {
+                    FlipImage(member.Mask);
+                    member.Mask.CommitChanges();
+                }
+            }
+        });
+
+        return changes;
+    }
+}

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

@@ -19,7 +19,7 @@ internal abstract class ResizeBasedChangeBase : Change
         return true;
     }
     
-    protected void Resize(ChunkyImage img, Guid memberGuid, VecI size, VecI offset, Dictionary<Guid, CommittedChunkStorage> deletedChunksDict)
+    protected virtual void Resize(ChunkyImage img, Guid memberGuid, VecI size, VecI offset, Dictionary<Guid, CommittedChunkStorage> deletedChunksDict)
     {
         img.EnqueueResize(size);
         img.EnqueueClear();

+ 260 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -0,0 +1,260 @@
+using ChunkyImageLib.Operations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Root;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.DrawingApi.Core.Surface;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
+using BlendMode = PixiEditor.ChangeableDocument.Enums.BlendMode;
+
+namespace PixiEditor.ChangeableDocument.Changes.Root;
+
+internal sealed class RotateImage_Change : Change
+{
+    private readonly RotationAngle rotation;
+    private List<Guid> membersToRotate;
+    
+    private VecI originalSize;
+    private int originalHorAxisY;
+    private int originalVerAxisX;
+    private Dictionary<Guid, CommittedChunkStorage> deletedChunks = new();
+    private Dictionary<Guid, CommittedChunkStorage> deletedMaskChunks = new();
+
+    [GenerateMakeChangeAction]
+    public RotateImage_Change(RotationAngle rotation, List<Guid>? membersToRotate)
+    {
+        this.rotation = rotation;
+        membersToRotate ??= new List<Guid>();
+        this.membersToRotate = membersToRotate;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (membersToRotate.Count > 0)
+        {
+            membersToRotate = target.ExtractLayers(membersToRotate);
+            
+            foreach (var layer in membersToRotate)
+            {
+                if (!target.HasMember(layer)) return false;
+            }  
+        }
+        
+        originalSize = target.Size;
+        originalHorAxisY = target.HorizontalSymmetryAxisY;
+        originalVerAxisX = target.VerticalSymmetryAxisX;
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        var changes = Rotate(target);
+        
+        ignoreInUndo = false;
+        return changes;
+    }
+
+    private void Resize(ChunkyImage img, Guid memberGuid,
+        Dictionary<Guid, CommittedChunkStorage> deletedChunksDict, List<IChangeInfo>? changes)
+    {
+        RectI bounds = new RectI(VecI.Zero, img.CommittedSize);
+        if (membersToRotate.Count > 0)
+        {
+            var preciseBounds = img.FindPreciseCommittedBounds();
+            if (preciseBounds.HasValue)
+            {
+                bounds = preciseBounds.Value;
+            }
+        }
+
+        int originalWidth = bounds.Width;
+        int originalHeight = bounds.Height;
+        
+        int newWidth = rotation == RotationAngle.D180 ? originalWidth : originalHeight;
+        int newHeight = rotation == RotationAngle.D180 ? originalHeight : originalWidth;
+
+        VecI oldSize = new VecI(originalWidth, originalHeight);
+        VecI newSize = new VecI(newWidth, newHeight);
+        
+        using Paint paint = new()
+        {
+            BlendMode = DrawingApi.Core.Surface.BlendMode.Src
+        };
+        
+        using Surface originalSurface = new(oldSize);
+        img.DrawMostUpToDateRegionOn(
+            bounds, 
+            ChunkResolution.Full,
+            originalSurface.DrawingSurface,
+            VecI.Zero);
+
+        using Surface flipped = new Surface(newSize);
+
+        float translationX = newSize.X;
+        float translationY = newSize.Y;
+        switch (rotation)
+        {
+            case RotationAngle.D90:
+                translationY = 0;
+                break;
+            case RotationAngle.D270:
+                translationX = 0;
+                break;
+        }
+        
+        flipped.DrawingSurface.Canvas.Save();
+        flipped.DrawingSurface.Canvas.Translate(translationX, translationY);
+        flipped.DrawingSurface.Canvas.RotateRadians(RotationAngleToRadians(rotation), 0, 0);
+        flipped.DrawingSurface.Canvas.DrawSurface(originalSurface.DrawingSurface, 0, 0, paint);
+        flipped.DrawingSurface.Canvas.Restore();
+
+        if (membersToRotate.Count == 0) 
+        {
+            img.EnqueueResize(newSize);
+        }
+
+        img.EnqueueClear();
+        img.EnqueueDrawImage(bounds.Pos, flipped);
+
+        var affectedChunks = img.FindAffectedChunks();
+        deletedChunksDict.Add(memberGuid, new CommittedChunkStorage(img, affectedChunks));
+        changes?.Add(new LayerImageChunks_ChangeInfo(memberGuid, affectedChunks));
+        img.CommitChanges();
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> Rotate(Document target)
+    {
+        if (membersToRotate.Count == 0)
+        {
+            return RotateWholeImage(target);
+        }
+
+        return RotateMembers(target, membersToRotate);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RotateMembers(Document target, List<Guid> guids)
+    {
+        List<IChangeInfo> changes = new List<IChangeInfo>();
+
+        target.ForEveryMember((member) =>
+        {
+            if (guids.Contains(member.GuidValue))
+            {
+                if (member is Layer layer)
+                {
+                    Resize(layer.LayerImage, layer.GuidValue, deletedChunks, changes);
+                }
+
+                if (member.Mask is null)
+                    return;
+
+                Resize(member.Mask, member.GuidValue, deletedMaskChunks, null);
+            }
+        });
+
+        return changes;
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RotateWholeImage(Document target)
+    {
+        int newWidth = rotation == RotationAngle.D180 ? target.Size.X : target.Size.Y;
+        int newHeight = rotation == RotationAngle.D180 ? target.Size.Y : target.Size.X;
+
+        VecI newSize = new VecI(newWidth, newHeight);
+
+        float normalizedSymmX = originalVerAxisX / Math.Max(target.Size.X, 0.1f);
+        float normalizedSymmY = originalHorAxisY / Math.Max(target.Size.Y, 0.1f);
+
+        target.Size = newSize;
+        target.VerticalSymmetryAxisX = (int)(newSize.X * normalizedSymmX);
+        target.HorizontalSymmetryAxisY = (int)(newSize.Y * normalizedSymmY);
+
+        target.ForEveryMember((member) =>
+        {
+            if (member is Layer layer)
+            {
+                Resize(layer.LayerImage, layer.GuidValue, deletedChunks, null);
+            }
+
+            if (member.Mask is null)
+                return;
+
+            Resize(member.Mask, member.GuidValue, deletedMaskChunks, null);
+        });
+
+        return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
+    }
+    
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        if (membersToRotate.Count == 0)
+        {
+            return RevertRotateWholeImage(target);
+        }
+
+        return RevertRotateMembers(target);
+    }
+
+    private OneOf<None, IChangeInfo, List<IChangeInfo>> RevertRotateWholeImage(Document target)
+    {
+        target.Size = originalSize;
+        RevertRotateMembers(target);
+
+        target.HorizontalSymmetryAxisY = originalHorAxisY;
+        target.VerticalSymmetryAxisX = originalVerAxisX;
+
+        return new Size_ChangeInfo(originalSize, originalVerAxisX, originalHorAxisY);
+    }
+
+    private List<IChangeInfo> RevertRotateMembers(Document target)
+    {
+        List<IChangeInfo> revertChanges = new List<IChangeInfo>();
+        target.ForEveryMember((member) =>
+        {
+            if(membersToRotate.Count > 0 && !membersToRotate.Contains(member.GuidValue)) return;
+            if (member is Layer layer)
+            {
+                layer.LayerImage.EnqueueResize(originalSize);
+                deletedChunks[layer.GuidValue].ApplyChunksToImage(layer.LayerImage);
+                revertChanges.Add(new LayerImageChunks_ChangeInfo(layer.GuidValue, layer.LayerImage.FindAffectedChunks()));
+                layer.LayerImage.CommitChanges();
+            }
+
+            if (member.Mask is null)
+                return;
+            member.Mask.EnqueueResize(originalSize);
+            deletedMaskChunks[member.GuidValue].ApplyChunksToImage(member.Mask);
+            revertChanges.Add(new LayerImageChunks_ChangeInfo(member.GuidValue, member.Mask.FindAffectedChunks()));
+            member.Mask.CommitChanges();
+        });
+
+        DisposeDeletedChunks();
+        return revertChanges;
+    }
+
+    private void DisposeDeletedChunks()
+    {
+        foreach (var stored in deletedChunks)
+            stored.Value.Dispose();
+        deletedChunks = new();
+
+        foreach (var stored in deletedMaskChunks)
+            stored.Value.Dispose();
+        deletedMaskChunks = new();
+    }
+
+    public override void Dispose()
+    {
+        DisposeDeletedChunks();
+    }
+
+    private float RotationAngleToRadians(RotationAngle rotationAngle)
+    {
+        return rotationAngle switch
+        {
+            RotationAngle.D90 => 90f * Matrix3X3.DegreesToRadians,
+            RotationAngle.D180 => 180f * Matrix3X3.DegreesToRadians,
+            RotationAngle.D270 => 270f * Matrix3X3.DegreesToRadians,
+            _ => throw new ArgumentOutOfRangeException(nameof(rotationAngle), rotationAngle, null)
+        };
+    }
+}

+ 17 - 0
src/PixiEditor.ChangeableDocument/Enums/FlipType.cs

@@ -0,0 +1,17 @@
+namespace PixiEditor.ChangeableDocument.Enums;
+
+public enum FlipType
+{
+    Horizontal,
+    Vertical
+}
+
+/// <summary>
+///     Rotation specified in degrees
+/// </summary>
+public enum RotationAngle
+{
+    D90,
+    D180,
+    D270
+}

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <WarningsAsErrors>Nullable</WarningsAsErrors>

+ 1 - 1
src/PixiEditor.DrawingApi.Core/Numerics/Matrix3X3.cs

@@ -12,7 +12,7 @@ namespace PixiEditor.DrawingApi.Core.Numerics;
 /// </remarks>
 public struct Matrix3X3 : IEquatable<Matrix3X3>
 {
-    internal const float DegreesToRadians = 0.017453292f;
+    public const float DegreesToRadians = 0.017453292f;
 
     public static readonly Matrix3X3 Identity = new() { ScaleX = 1f, ScaleY = 1f, Persp2 = 1f };
     

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

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
-    <TargetFramework>net6.0-windows</TargetFramework>
+    <TargetFramework>net7.0-windows</TargetFramework>
     <UseWPF>true</UseWPF>
     <ApplicationManifest>app.manifest</ApplicationManifest>
     <Platforms>AnyCPU;x64;x86</Platforms>

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <Platforms>AnyCPU;x64;x86</Platforms>
   </PropertyGroup>
 

+ 29 - 1
src/PixiEditor.UpdateModule/UpdateChecker.cs

@@ -45,6 +45,33 @@ public class UpdateChecker
     {
         return NormalizeVersionString(originalVer) != NormalizeVersionString(newVer);
     }
+    
+    /// <summary>
+    ///     Checks if originalVer is smaller than newVer
+    /// </summary>
+    /// <param name="originalVer">Version on the left side of the equation</param>
+    /// <param name="newVer">Version to compare to</param>
+    /// <returns>True if originalVer is smaller than newVer.</returns>
+    public static bool VersionSmaller(string originalVer, string newVer)
+    {
+        string normalizedOriginal = NormalizeVersionString(originalVer);
+        string normalizedNew = NormalizeVersionString(newVer);
+
+        if (normalizedOriginal == normalizedNew) return false;
+
+        bool parsed = TryParseToFloatVersion(normalizedOriginal, out float orgFloat);
+        if (!parsed) throw new Exception($"Couldn't parse version {originalVer} to float.");
+
+        parsed = TryParseToFloatVersion(normalizedNew, out float newFloat);
+        if (!parsed) throw new Exception($"Couldn't parse version {newVer} to float.");
+
+        return orgFloat < newFloat;
+    }
+
+    private static bool TryParseToFloatVersion(string normalizedString, out float ver)
+    {
+        return float.TryParse(normalizedString.Replace(".", string.Empty).Insert(1, "."), NumberStyles.Any, CultureInfo.InvariantCulture, out ver);
+    }
 
     public async Task<bool> CheckUpdateAvailable()
     {
@@ -65,7 +92,8 @@ public class UpdateChecker
     public async Task<bool> IsUpdateCompatible()
     {
         string[] incompatibleVersions = await GetUpdateIncompatibleVersionsAsync(LatestReleaseInfo.TagName);
-        return IsUpdateCompatible(incompatibleVersions);
+        bool isDowngrading = VersionSmaller(LatestReleaseInfo.TagName, CurrentVersionTag);
+        return IsUpdateCompatible(incompatibleVersions) && !isDowngrading; // Incompatible.json doesn't support backwards compatibility, thus downgrading always means update is not compatble
     }
 
     public async Task<string[]> GetUpdateIncompatibleVersionsAsync(string tag)

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

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0-windows</TargetFramework>
+    <TargetFramework>net7.0-windows</TargetFramework>
     <Nullable>enable</Nullable>
     <UseWPF>true</UseWPF>
     <WarningsAsErrors>Nullable</WarningsAsErrors>

+ 4 - 2
src/PixiEditor/Models/Commands/XAML/Command.cs

@@ -1,4 +1,6 @@
-using System.Windows.Input;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Input;
 using System.Windows.Markup;
 using PixiEditor.Helpers;
 
@@ -20,7 +22,7 @@ internal class Command : MarkupExtension
 
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
-        if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
+        if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
         {
             var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
             return GetICommand(

+ 21 - 0
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -3,6 +3,7 @@ using System.Windows.Interop;
 using System.Windows.Shapes;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
+using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Actions.Generated;
 using PixiEditor.ChangeableDocument.Actions.Undo;
 using PixiEditor.ChangeableDocument.ChangeInfos.Root.ReferenceLayerChangeInfos;
@@ -289,6 +290,26 @@ internal class DocumentOperationsModule
         Internals.ActionAccumulator.AddFinishedActions(new ClipCanvas_Action());
     }
 
+    public void FlipImage(FlipType flipType) => FlipImage(flipType, null);
+
+    public void FlipImage(FlipType flipType, List<Guid> membersToFlip)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return;
+        
+        Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, membersToFlip));
+    }
+
+    public void RotateImage(RotationAngle rotation) => RotateImage(rotation, null);
+
+    public void RotateImage(RotationAngle rotation, List<Guid> membersToRotate)
+    {
+        if (Internals.ChangeController.IsChangeActive)
+            return;
+        
+        Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate));
+    }
+    
     public void CenterContent(IReadOnlyList<Guid> structureMembers)
     {
         if (Internals.ChangeController.IsChangeActive)

+ 0 - 7
src/PixiEditor/Models/Enums/FlipType.cs

@@ -1,7 +0,0 @@
-namespace PixiEditor.Models.Enums;
-
-public enum FlipType
-{
-    Horizontal,
-    Vertical
-}

+ 2 - 2
src/PixiEditor/PixiEditor.csproj

@@ -2,7 +2,7 @@
 
 	<PropertyGroup>
 		<OutputType>WinExe</OutputType>
-		<TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>
+		<TargetFramework>net7.0-windows</TargetFramework>
 		<UseWPF>true</UseWPF>
 		<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 		<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
@@ -16,9 +16,9 @@
 		<Authors>Krzysztof Krysiński, Egor Mozgovoy, CPK</Authors>
 		<Configurations>Debug;Release;MSIX;MSIX Debug;Dev Release</Configurations>
 		<Platforms>AnyCPU;x64;x86</Platforms>
-		<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
         <ImplicitUsings>true</ImplicitUsings>
         <AssemblyVersion></AssemblyVersion>
+        <LangVersion>11</LangVersion>
 	</PropertyGroup>
 
 	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='MSIX|AnyCPU'">

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

@@ -50,5 +50,5 @@ using System.Windows;
 // 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("0.1.9.2")]
-[assembly: AssemblyFileVersion("0.1.9.2")]
+[assembly: AssemblyVersion("0.1.9.4")]
+[assembly: AssemblyFileVersion("0.1.9.4")]

+ 65 - 4
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentManagerViewModel.cs

@@ -1,8 +1,10 @@
 using System.Collections.ObjectModel;
 using System.Windows.Input;
+using ChunkyImageLib.Operations;
 using PixiEditor.ChangeableDocument.Enums;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Enums;
 using PixiEditor.Models.Events;
 using PixiEditor.ViewModels.SubViewModels.Tools.Tools;
 using PixiEditor.Views.UserControls.SymmetryOverlay;
@@ -55,6 +57,67 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
         
         ActiveDocument?.Operations.ClipCanvas();
     }
+    
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", "Flip Image Horizontally", "Flip Image Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipImageHorizontally()
+    {
+        if (ActiveDocument is null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal);
+    }
+    
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", "Flip Selected Layers Horizontally", "Flip Selected Layers Horizontally", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipLayersHorizontally()
+    {
+        if (ActiveDocument?.SelectedStructureMember == null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Horizontal, ActiveDocument.GetSelectedMembers());
+    }
+    
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", "Flip Image Vertically", "Flip Image Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipImageVertically()
+    {
+        if (ActiveDocument is null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Vertical);
+    }
+    
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", "Flip Selected Layers Vertically", "Flip Selected Layers Vertically", CanExecute = "PixiEditor.HasDocument")]
+    public void FlipLayersVertically()
+    {
+        if (ActiveDocument?.SelectedStructureMember == null)
+            return;
+        
+        ActiveDocument?.Operations.FlipImage(FlipType.Vertical, ActiveDocument.GetSelectedMembers());
+    }
+    
+    [Command.Basic("PixiEditor.Document.Rotate90Deg", "Rotate Image 90 degrees", 
+        "Rotate Image 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
+    [Command.Basic("PixiEditor.Document.Rotate180Deg", "Rotate Image 180 degrees", 
+        "Rotate Image 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
+    [Command.Basic("PixiEditor.Document.Rotate270Deg", "Rotate Image -90 degrees", 
+        "Rotate Image -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
+    public void RotateImage(RotationAngle angle)
+    {
+        ActiveDocument?.Operations.RotateImage(angle);
+    }
+
+    [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "Rotate Selected Layers 90 degrees", 
+        "Rotate Selected Layers 90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90)]
+    [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "Rotate Selected Layers 180 degrees", 
+        "Rotate Selected Layers 180 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180)]
+    [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "Rotate Selected Layers -90 degrees", 
+        "Rotate Selected Layers -90 degrees", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270)]
+    public void RotateLayers(RotationAngle angle)
+    {
+        if (ActiveDocument?.SelectedStructureMember == null)
+            return;
+        
+        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers());
+    }
 
     [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "Toggle vertical symmetry axis", "Toggle vertical symmetry axis", CanExecute = "PixiEditor.HasDocument", IconPath = "SymmetryVertical.png")]
     public void ToggleVerticalSymmetryAxis()
@@ -132,11 +195,9 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.Document.CenterContent", "Center Content", "Center Content", CanExecute = "PixiEditor.HasDocument")]
     public void CenterContent()
     {
-        if(ActiveDocument is null || ActiveDocument.SelectedStructureMember == null)
+        if(ActiveDocument?.SelectedStructureMember == null)
             return;
         
-        List<Guid> layerGuids = new List<Guid>() { ActiveDocument.SelectedStructureMember.GuidValue };
-        layerGuids.AddRange(ActiveDocument.SoftSelectedStructureMembers.Select(x => x.GuidValue));
-        ActiveDocument.Operations.CenterContent(layerGuids);
+        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers());
     }
 }

+ 7 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -484,4 +484,11 @@ internal partial class DocumentViewModel : NotifyableObject
     public void InternalAddSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Add(member);
     public void InternalRemoveSoftSelectedMember(StructureMemberViewModel member) => softSelectedStructureMembers.Remove(member);
     #endregion
+
+    public List<Guid> GetSelectedMembers()
+    {
+        List<Guid> layerGuids = new List<Guid>() { SelectedStructureMember.GuidValue };
+        layerGuids.AddRange( SoftSelectedStructureMembers.Select(x => x.GuidValue));
+        return layerGuids;
+    }
 }

+ 2 - 9
src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs

@@ -56,14 +56,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
 
     private void UpdateUseTouchGesture()
     {
-        if (Owner.ToolsSubViewModel.ActiveTool is not (MoveViewportToolViewModel or ZoomToolViewModel))
-        {
-            UseTouchGestures = IsPenModeEnabled;
-        }
-        else
-        {
-            UseTouchGestures = true;
-        }
+        UseTouchGestures = Owner.ToolsSubViewModel.ActiveTool is MoveViewportToolViewModel or ZoomToolViewModel || IsPenModeEnabled;
     }
 
     [Command.Internal("PixiEditor.Stylus.StylusOutOfRange")]
@@ -75,7 +68,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
     [Command.Internal("PixiEditor.Stylus.StylusSystemGesture")]
     public void StylusSystemGesture(StylusSystemGestureEventArgs e)
     {
-        if (e.SystemGesture == SystemGesture.Drag || e.SystemGesture == SystemGesture.Tap)
+        if (e.SystemGesture is SystemGesture.Drag or SystemGesture.Tap)
         {
             return;
         }

+ 8 - 2
src/PixiEditor/ViewModels/SubViewModels/Main/UpdateViewModel.cs

@@ -43,7 +43,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             RaisePropertyChanged(nameof(UpdateReadyToInstall));
             if (value)
             {
-                VersionText = $"to install update (current {VersionHelpers.GetCurrentAssemblyVersionString()})"; // Button shows "Restart" before this text
+                VersionText = $"to install update ({UpdateChecker.LatestReleaseInfo.TagName})"; // Button shows "Restart" before this text
             }
         }
     }
@@ -105,7 +105,13 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                     $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
 
                 bool updateExeExists = File.Exists(exePath);
-                
+
+                if (updateExeExists && !UpdateChecker.VersionDifferent(UpdateChecker.LatestReleaseInfo.TagName, UpdateChecker.CurrentVersionTag))
+                {
+                    File.Delete(exePath);
+                    updateExeExists = false;
+                }
+
                 string updaterPath = Path.Join(dir, "PixiEditor.UpdateInstaller.exe");
 
                 if (updateFileExists || updateExeExists)

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -17,7 +17,7 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
 {
     private CommandController commandController;
     private ShortcutPopup? shortcutPopup;
-    private ShortcutPopup ShortcutPopup => shortcutPopup ?? (shortcutPopup = new(commandController));
+    private ShortcutPopup ShortcutPopup => shortcutPopup ??= new(commandController);
 
     private AboutPopup? _aboutPopup;
     private AboutPopup AboutPopup => _aboutPopup ??= new();

+ 51 - 16
src/PixiEditor/Views/Dialogs/AboutPopup.xaml

@@ -10,7 +10,7 @@
         xmlns:userControls="clr-namespace:PixiEditor.Views.UserControls"
         mc:Ignorable="d" WindowStyle="None"
         Title="About" WindowStartupLocation="CenterScreen"
-        Height="490" Width="400" Name="aboutPopup" MinWidth="100" MinHeight="100">
+        Height="510" Width="400" Name="aboutPopup" MinWidth="100" MinHeight="100">
     <Window.CommandBindings>
         <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
                         Executed="CommandBinding_Executed_Close" />
@@ -42,9 +42,12 @@
                         <ImageBrush ImageSource="/Images/SocialMedia/Avatars/flabbet.png"/>
                     </Ellipse.Fill>
                 </Ellipse>
-                <Button Content="Krzysztof Krysiński (flabbet)" VerticalAlignment="Center" Style="{StaticResource HyperlinkTextButton}"
-                        Margin="10 0 0 0" FontSize="14"
-                        Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/flabbet"/>
+                <Label Style="{StaticResource SettingsText}" Margin="10 0 0 0" FontSize="14">
+                    <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/flabbet" Style="{StaticResource SettingsLink}">
+                        <Run Text="Krzysztof Krysiński (flabbet)"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </Hyperlink>
+                </Label>
             </StackPanel>
             <StackPanel Orientation="Horizontal" Margin="20 5">
                 <Ellipse Width="32" Height="32">
@@ -52,8 +55,12 @@
                         <ImageBrush ImageSource="/Images/SocialMedia/Avatars/Equbuxu.png"/>
                     </Ellipse.Fill>
                 </Ellipse>
-                <Button Content="Egor Mozgovoy (Equbuxu)" Style="{StaticResource HyperlinkTextButton}" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                        Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/equbuxu"/>
+                <Label Style="{StaticResource SettingsText}"  Margin="10 0 0 0" FontSize="14">
+                    <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/equbuxu" Style="{StaticResource SettingsLink}">
+                        <Run Text="Egor Mozgovoy (Equbuxu)"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </Hyperlink>
+                </Label>
             </StackPanel>
             <StackPanel Orientation="Horizontal" Margin="20 0">
                 <Ellipse Width="32" Height="32">
@@ -61,19 +68,47 @@
                         <ImageBrush ImageSource="/Images/SocialMedia/Avatars/CPK.png"/>
                     </Ellipse.Fill>
                 </Ellipse>
-                <Button Style="{StaticResource HyperlinkTextButton}" Content="Philip Kreuz (CPK)" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                        Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/CPKreuz"/>
+                <Label Style="{StaticResource SettingsText}" Margin="10 0 0 0" FontSize="14">
+                    <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/CPKreuz" Style="{StaticResource SettingsLink}">
+                        <Run Text="Phillip Kreuz (CPK)"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </Hyperlink>
+                </Label>
             </StackPanel>
             
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="And other awesome contributors" VerticalAlignment="Center" Margin="20 10 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/PixiEditor/PixiEditor/graphs/contributors"/>
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/PixiEditor/PixiEditor/graphs/contributors"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="And other awesome contributors"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
             
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="License" VerticalAlignment="Center" Margin="10 20 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenLicense}"/>
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="Third Party Licenses" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenOtherLicenses}"/>
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="Documentation" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenDocumentation}"/>
+            <Separator Margin="0 10 0 0"/>
+            
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenLicense}"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="License"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
+            
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenOtherLicenses}"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="Third Party Licenses"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
+            
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenDocumentation}"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="Documentation"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
             
             <userControls:AlignableWrapPanel DockPanel.Dock="Bottom" HorizontalContentAlignment="Center" HorizontalAlignment="Center" Margin="0,20,0,15">
                     <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://pixieditor.net"

+ 1 - 0
src/PixiEditor/Views/Dialogs/PalettesBrowser.xaml.cs

@@ -185,6 +185,7 @@ internal partial class PalettesBrowser : Window
     {
         await Dispatcher.InvokeAsync(async () =>
         {
+            SortedResults ??= new WpfObservableRangeCollection<Palette>();
             switch (refreshType)
             {
                 case RefreshType.All:

+ 30 - 21
src/PixiEditor/Views/MainWindow.xaml

@@ -256,21 +256,23 @@
                             IsEnabled="{Binding DocumentManagerSubViewModel.ActiveDocument, Source={vm:MainVM}, Converter={converters:NotNullToBoolConverter}}"
                             IsChecked="{Binding DocumentManagerSubViewModel.ActiveDocument.VerticalSymmetryAxisEnabledBindable}"
                             Header="_Vertical Line Symmetry"/>
-                        <!--<Separator/>
-                    <MenuItem Header="_Rotate to right 90&#186;" Command="{Binding DocumentSubViewModel.RotateToRightCommand}">
-                        <MenuItem.CommandParameter>
-                            <sys:Double>90</sys:Double>
-                        </MenuItem.CommandParameter>
-                    </MenuItem>
-                    <MenuItem Header="_Rotate to left 90&#186;" Command="{Binding DocumentSubViewModel.RotateToRightCommand}">
-                        <MenuItem.CommandParameter>
-                            <sys:Double>-90</sys:Double>
-                        </MenuItem.CommandParameter>
-                    </MenuItem>
-                    <Separator/>
-                    <MenuItem Header="_Flip Horizontal" Command="{Binding DocumentSubViewModel.FlipCommand}" CommandParameter="Horizontal"/>
-                    <MenuItem Header="_Flip Vertical" Command="{Binding DocumentSubViewModel.FlipCommand}" CommandParameter="Vertical"/>
-                -->
+                        <Separator/>
+                        <MenuItem Header="_Rotation">
+                            <MenuItem Header="Rotate Image 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90Deg"/>
+                            <MenuItem Header="Rotate Image 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180Deg"/>
+                            <MenuItem Header="Rotate Image -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270Deg"/>
+                            
+                            <Separator/>
+                            <MenuItem Header="Rotate Selected Layers 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
+                            <MenuItem Header="Rotate Selected Layers 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
+                            <MenuItem Header="Rotate Selected Layers -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
+                        </MenuItem>
+                        <MenuItem Header="_Flip">
+                            <MenuItem Header="Flip Image _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipImageHorizontal"/>
+                            <MenuItem Header="Flip Image _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipImageVertical"/>
+                            <MenuItem Header="Flip Selected Layers _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                            <MenuItem Header="Flip Selected Layers _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                        </MenuItem>
                     </MenuItem>
                     <MenuItem
                         Header="_View">
@@ -548,16 +550,16 @@
                                             Stylus.IsTouchFeedbackEnabled="False"
                                             Document="{Binding Document}">
                                             <usercontrols:Viewport.ContextMenu>
-                                                <ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
+                                                <ContextMenu DataContext="{Binding PlacementTarget.Document, RelativeSource={RelativeSource Self}}">
                                                     <ContextMenu.Template>
                                                         <ControlTemplate>
-                                                            <Border Height="120" Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
-                                                                <Grid>
+                                                            <Border Background="{StaticResource AccentColor}" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
+                                                                <Grid Height="235">
                                                                     <Grid.ColumnDefinitions>
-                                                                        <ColumnDefinition Width="100"/>
                                                                         <ColumnDefinition Width="{Binding Palette, Converter={converters:PaletteItemsToWidthConverter}}"/>
+                                                                        <ColumnDefinition />
                                                                     </Grid.ColumnDefinitions>
-                                                                    <Border BorderThickness="0 0 1 0" BorderBrush="Black">
+                                                                    <Border Grid.Column="1" BorderThickness="0 0 1 0" BorderBrush="Black">
                                                                         <StackPanel Orientation="Vertical" Grid.Column="0">
                                                                             <MenuItem
 																		Header="_Select All"
@@ -575,9 +577,16 @@
                                                                             <MenuItem
 																		Header="_Paste"
 																		cmds:ContextMenu.Command="PixiEditor.Clipboard.Paste" />
+                                                                            <Separator />
+                                                                            <MenuItem Header="Flip _Horizontally" cmds:Menu.Command="PixiEditor.Document.FlipLayersHorizontal"/>
+                                                                            <MenuItem Header="Flip _Vertically" cmds:Menu.Command="PixiEditor.Document.FlipLayersVertical"/>
+                                                                            <Separator />
+                                                                            <MenuItem Header="Rotate 90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate90DegLayers"/>
+                                                                            <MenuItem Header="Rotate 180&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate180DegLayers"/>
+                                                                            <MenuItem Header="Rotate -90&#186;" cmds:Menu.Command="PixiEditor.Document.Rotate270DegLayers"/>
                                                                         </StackPanel>
                                                                     </Border>
-                                                                    <ScrollViewer Margin="5" Grid.Column="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+                                                                    <ScrollViewer Margin="5" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                                                                         <ItemsControl ItemsSource="{Binding Palette}" AlternationCount="9999">
                                                                             <ItemsControl.ItemsPanel>
                                                                                 <ItemsPanelTemplate>

+ 4 - 3
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -56,6 +56,8 @@ internal partial class MainWindow : Window
         {
             UpdateTaskbarIcon(x ? DataContext?.DocumentManagerSubViewModel.ActiveDocument : null);
         });
+
+        DataContext.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
     }
 
     public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
@@ -98,14 +100,13 @@ internal partial class MainWindow : Window
         ((HwndSource)PresentationSource.FromVisual(this)).AddHook(Helpers.WindowSizeHelper.SetMaxSizeHook);
     }
 
-    /*
-    private void BitmapManager_DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
+    private void DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
     {
         if (preferences.GetPreference("ImagePreviewInTaskbar", false))
         {
             UpdateTaskbarIcon(e.NewDocument);
         }
-    }*/
+    }
 
     private void UpdateTaskbarIcon(DocumentViewModel document)
     {

+ 1 - 1
src/PixiEditor/Views/UserControls/Viewport.xaml

@@ -120,7 +120,7 @@
         <zoombox:Zoombox
             Tag="{Binding ElementName=vpUc}"
             x:Name="zoombox"
-            UseTouchGestures="{Binding UseTouchGestures, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            UseTouchGestures="{Binding UseTouchGestures, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWay}"
             Scale="{Binding ZoomboxScale, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
             Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
             Angle="{Binding Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"

+ 1 - 3
src/PixiEditorTests/PixiEditorTests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>
+    <TargetFramework>net7.0-windows</TargetFramework>
 
     <IsPackable>false</IsPackable>
 
@@ -30,7 +30,6 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Codecov" Version="1.13.0" />
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.2">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -38,7 +37,6 @@
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
-    <PackageReference Include="Moq" Version="4.17.2" />
     <PackageReference Include="OpenCover" Version="4.7.1221" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

+ 7 - 0
src/global.json

@@ -0,0 +1,7 @@
+{
+  "sdk": {
+    "version": "7.0.0",
+    "rollForward": "latestMajor",
+    "allowPrerelease": false
+  }
+}

+ 1 - 1
windows-x64-release-dev.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 - task: NuGetToolInstaller@1
 
 - task: NuGetCommand@2

+ 1 - 1
windows-x64-release.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 - task: NuGetToolInstaller@1
 
 - task: NuGetCommand@2

+ 1 - 1
windows-x86-release-dev.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 
 - task: NuGetToolInstaller@1
 

+ 1 - 1
windows-x86-release.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 
 - task: NuGetToolInstaller@1