Przeglądaj źródła

Merge branch 'master' into fix/clipboard-cut-fix

Krzysztof Krysiński 3 dni temu
rodzic
commit
421ea2452c
21 zmienionych plików z 264 dodań i 71 usunięć
  1. 21 23
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  2. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameData.cs
  3. 4 4
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  4. 73 0
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromFolder_Change.cs
  5. 62 14
      src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs
  6. 3 1
      src/PixiEditor/Helpers/SupportedFilesHelper.cs
  7. 2 0
      src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs
  8. 2 1
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  9. 12 5
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  10. 9 0
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  11. 8 8
      src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs
  12. 22 2
      src/PixiEditor/ViewModels/SettingsWindowViewModel.cs
  13. 3 1
      src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs
  14. 36 1
      src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs
  15. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterEllipseToolViewModel.cs
  16. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterLineToolViewModel.cs
  17. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  18. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorEllipseToolViewModel.cs
  19. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorLineToolViewModel.cs
  20. 1 1
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  21. 0 4
      src/PixiEditor/Views/Nodes/NodeGraphView.cs

+ 21 - 23
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -44,7 +44,7 @@ internal class RectangleOperation : IMirroredDrawOperation
 
         if (Data.AntiAliasing)
         {
-            DrawAntiAliased(surf, rect, radiusInPx);
+            DrawAntiAliased(surf, rect, innerRect, radiusInPx);
         }
         else
         {
@@ -90,8 +90,24 @@ internal class RectangleOperation : IMirroredDrawOperation
         surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode);
     }
 
-    private void DrawAntiAliased(DrawingSurface surf, RectD rect, double radius)
+    private void DrawAntiAliased(DrawingSurface surf, RectD rect, RectD innerRect, double radius)
     {
+        surf.Canvas.Save();
+        paint.StrokeWidth = Data.StrokeWidth > 0 ? Data.StrokeWidth : 1;
+        paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
+        paint.Style = PaintStyle.Fill;
+
+        if (radius == 0)
+        {
+            surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
+                (float)rect.Height, paint);
+        }
+        else
+        {
+            surf.Canvas.DrawRoundRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
+                (float)rect.Height, (float)radius, (float)radius, paint);
+        }
+
         // draw fill
         if (Data.FillPaintable.AnythingVisible)
         {
@@ -102,34 +118,16 @@ internal class RectangleOperation : IMirroredDrawOperation
             paint.Style = PaintStyle.Fill;
             if (radius == 0)
             {
-                surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width, (float)rect.Height, paint);
+                surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width, (float)innerRect.Height, paint);
             }
             else
             {
-                surf.Canvas.DrawRoundRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
-                    (float)rect.Height, (float)radius, (float)radius, paint);
+                surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
+                    (float)innerRect.Height, (float)radius, (float)radius, paint);
             }
 
             surf.Canvas.RestoreToCount(saved);
         }
-
-        // draw stroke
-        surf.Canvas.Save();
-        paint.StrokeWidth = Data.StrokeWidth > 0 ? Data.StrokeWidth : 1;
-        paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
-        paint.Style = PaintStyle.Stroke;
-        RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
-
-        if (radius == 0)
-        {
-            surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
-                (float)innerRect.Height, paint);
-        }
-        else
-        {
-            surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
-                (float)innerRect.Height, (float)radius, (float)radius, paint);
-        }
     }
 
     public AffectedArea FindAffectedArea(VecI imageSize)

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrameData.cs

@@ -8,7 +8,7 @@ public class KeyFrameData : IDisposable, IReadOnlyKeyFrameData
 {
     public int StartFrame { get; set; }
     public int Duration { get; set; }
-    public Guid KeyFrameGuid { get; }
+    public Guid KeyFrameGuid { get; internal set; }
     public string AffectedElement { get; set; }
     public object Data { get; set; }
     public bool IsVisible { get; set; } = true;

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

@@ -444,12 +444,12 @@ internal class Document : IChangeable, IReadOnlyDocument
 
     private void ExtractLayers(FolderNode folder, List<Guid> list)
     {
-        List<Guid> result = new();
-        folder.TraverseBackwards(node =>
+        if(folder.Content.Connection == null) return;
+        folder.Content.Connection.Node.TraverseBackwards(node =>
         {
-            if (node is LayerNode layer && !result.Contains(layer.Id))
+            if (node is LayerNode layer && !list.Contains(layer.Id))
             {
-                result.Add(layer.Id);
+                list.Add(layer.Id);
             }
 
             return true;

+ 73 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateAnimationDataFromFolder_Change.cs

@@ -0,0 +1,73 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class CreateAnimationDataFromFolder_Change : Change
+{
+    private readonly Guid folderGuid;
+    private Guid[] layerGuids;
+
+    [GenerateMakeChangeAction]
+    public CreateAnimationDataFromFolder_Change(Guid folderGuid)
+    {
+        this.folderGuid = folderGuid;
+    }
+
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (!target.TryFindMember<FolderNode>(folderGuid, out FolderNode? layer))
+        {
+            return false;
+        }
+
+        var layers = target.ExtractLayers([layer.Id]);
+        if (layers.Count == 0) return false;
+
+        return true;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
+    {
+        FolderNode folder = target.FindNode(folderGuid) as FolderNode;
+        List<IChangeInfo> infos = new List<IChangeInfo>();
+        layerGuids = target.ExtractLayers([folder.Id]).ToArray();
+
+        foreach (var layer in layerGuids)
+        {
+            var node = target.FindNode(layer);
+            if(node is not LayerNode) continue;
+            foreach (var frame in node.KeyFrames)
+            {
+                Guid keyFrameId = frame.KeyFrameGuid;
+                target.AnimationData.AddKeyFrame(new RasterKeyFrame(keyFrameId, folder.Id, frame.StartFrame, target)
+                {
+                    Duration = frame.Duration
+                });
+                infos.Add(new CreateRasterKeyFrame_ChangeInfo(folder.Id, frame.StartFrame, keyFrameId, true));
+            }
+        }
+
+        ignoreInUndo = false;
+        return infos;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        var layer = target.FindNode(folderGuid) as FolderNode;
+        List<IChangeInfo> infos = new List<IChangeInfo>();
+
+        var keyFrame = target.AnimationData.KeyFrames;
+        var ids = keyFrame.Where(x => x.NodeId == layer.Id || layerGuids.Contains(x.NodeId)).Select(x => x.Id).ToList();
+
+        foreach (var id in ids)
+        {
+            target.AnimationData.RemoveKeyFrame(id);
+            infos.Add(new DeleteKeyFrame_ChangeInfo(id));
+        }
+
+        return infos;
+    }
+}

+ 62 - 14
src/PixiEditor.ChangeableDocument/Changes/Structure/DuplicateFolder_Change.cs

@@ -1,6 +1,7 @@
 using System.Collections.Immutable;
 using System.Collections.ObjectModel;
 using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Animations;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.NodeGraph;
@@ -17,6 +18,8 @@ internal class DuplicateFolder_Change : Change
     private Guid[] contentDuplicateGuids;
 
     private Guid[]? childGuidsToUse;
+    private Dictionary<Guid, List<Guid>> keyFramesMap = new();
+    private Dictionary<Guid, Guid> nodeMap = new();
 
     private ConnectionsData? connectionsData;
     private Dictionary<Guid, ConnectionsData> contentConnectionsData = new();
@@ -64,14 +67,15 @@ internal class DuplicateFolder_Change : Change
         List<IChangeInfo> operations = new();
 
         target.NodeGraph.AddNode(clone);
-        
+
         var previousConnection = targetInput.Connection;
 
         operations.Add(CreateNode_ChangeInfo.CreateFromNode(clone));
         operations.AddRange(NodeOperations.AppendMember(targetInput, clone.Output, clone.Background, clone.Id));
-        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node, previousConnection?.Node as Node, out originalPositions));
+        operations.AddRange(NodeOperations.AdjustPositionsAfterAppend(clone, targetInput.Node,
+            previousConnection?.Node as Node, out originalPositions));
 
-        DuplicateContent(target, clone, existingLayer, operations);
+        DuplicateContent(target, clone, existingLayer, operations, firstApply);
 
         ignoreInUndo = false;
 
@@ -116,29 +120,70 @@ internal class DuplicateFolder_Change : Change
     }
 
     private void DuplicateContent(Document target, FolderNode clone, FolderNode existingLayer,
-        List<IChangeInfo> operations)
+        List<IChangeInfo> operations, bool firstApply)
     {
-        Dictionary<Guid, Guid> nodeMap = new Dictionary<Guid, Guid>();
+        if (firstApply)
+        {
+            nodeMap = new Dictionary<Guid, Guid>();
+            nodeMap[existingLayer.Id] = clone.Id;
+        }
 
-        nodeMap[existingLayer.Id] = clone.Id;
         int counter = 0;
         List<Guid> contentGuidList = new();
 
+        if (firstApply)
+        {
+            keyFramesMap = new Dictionary<Guid, List<Guid>>();
+        }
+
+        int childCounter = 0;
+
         existingLayer.Content.Connection?.Node.TraverseBackwards(x =>
         {
             if (x is not Node targetNode)
                 return false;
 
             Node? node = targetNode.Clone();
-            
-            if(node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+
+            if (contentDuplicateGuids != null && contentDuplicateGuids.Length > 0)
+            {
+                node.Id = contentDuplicateGuids[childCounter];
+                childCounter++;
+            }
+            else
             {
-                node.Id = childGuidsToUse[counter];
-                counter++;
+                if (node is not FolderNode && childGuidsToUse is not null && counter < childGuidsToUse.Length)
+                {
+                    node.Id = childGuidsToUse[counter];
+                    counter++;
+                }
+            }
+
+            if (firstApply)
+            {
+                keyFramesMap[node.Id] = new List<Guid>();
+                keyFramesMap[node.Id].AddRange(x.KeyFrames.Select(kf => kf.KeyFrameGuid));
+            }
+            else
+            {
+                if (keyFramesMap.TryGetValue(node.Id, out List<Guid>? keyFrameGuids))
+                {
+                    for (int i = 0; i < x.KeyFrames.Count; i++)
+                    {
+                        if (i < keyFrameGuids.Count)
+                        {
+                            var kf = x.KeyFrames[i] as KeyFrameData;
+                            kf.KeyFrameGuid = keyFrameGuids[i];
+                        }
+                    }
+                }
+            }
+
+            if (firstApply)
+            {
+                nodeMap[x.Id] = node.Id;
+                contentGuidList.Add(node.Id);
             }
-            
-            nodeMap[x.Id] = node.Id;
-            contentGuidList.Add(node.Id);
 
             target.NodeGraph.AddNode(node);
 
@@ -154,6 +199,9 @@ internal class DuplicateFolder_Change : Change
                 target.FindNodeOrThrow<Node>(targetNodeId), target.NodeGraph));
         }
 
-        contentDuplicateGuids = contentGuidList.ToArray();
+        if (firstApply)
+        {
+            contentDuplicateGuids = contentGuidList.ToArray();
+        }
     }
 }

+ 3 - 1
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -83,7 +83,9 @@ internal class SupportedFilesHelper
         if (file is null)
             return null;
 
-        string extension = Path.GetExtension(file.Path.LocalPath);
+        string? localPath = file.TryGetLocalPath();
+
+        string extension = Path.GetExtension(localPath ?? file.Name);
         return allSupportedExtensions.Single(i => i.CanSave && i.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
 

+ 2 - 0
src/PixiEditor/Models/Commands/Attributes/Commands/ToolAttribute.cs

@@ -10,6 +10,8 @@ internal partial class Command
         public Key Transient { get; set; }
         public bool TransientImmediate { get; set; } = false;
 
+        public string? CommonToolType { get; set; }
+
         public ToolAttribute() : base(null, null, null)
         {
         }

+ 2 - 1
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -235,7 +235,8 @@ internal class DocumentOperationsModule : IDocumentOperations
         {
             Internals.ActionAccumulator.AddFinishedActions(
                 new DuplicateFolder_Action(guidValue, newGuid, null),
-                new SetSelectedMember_PassthroughAction(newGuid));
+                new SetSelectedMember_PassthroughAction(newGuid),
+                new CreateAnimationDataFromFolder_Action(newGuid));
         }
 
         return newGuid;

+ 12 - 5
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -84,11 +84,18 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
             toolbar.FillBrush = textData.FillPaintable.ToBrush();
             toolbar.StrokeBrush = textData.Stroke.ToBrush();
             toolbar.ToolSize = textData.StrokeWidth;
-            toolbar.FontFamily = textData.Font.Family;
-            toolbar.FontSize = textData.Font.Size;
-            toolbar.Spacing = textData.Spacing ?? textData.Font.Size;
-            toolbar.Bold = textData.Font.Bold;
-            toolbar.Italic = textData.Font.Italic;
+            try
+            {
+                toolbar.FontFamily = textData.Font.Family;
+                toolbar.FontSize = textData.Font.Size;
+                toolbar.Spacing = textData.Spacing ?? textData.Font.Size;
+                toolbar.Bold = textData.Font.Bold;
+                toolbar.Italic = textData.Font.Italic;
+            }
+            catch (InvalidOperationException) // Native font likely disposed
+            {
+                
+            }
 
             onPath = textData.Path;
             lastText = textData.Text;

+ 9 - 0
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -299,6 +299,12 @@ internal partial class DocumentViewModel
         float centerY = (float)rectangleData.Center.Y;
         float halfWidth = (float)rectangleData.Size.X / 2f;
         float halfHeight = (float)rectangleData.Size.Y / 2f;
+        float minHalf = Math.Min(halfWidth, halfHeight);
+        float clampedCorner = Math.Clamp((float)rectangleData.CornerRadius, 0f, 1f);
+        float radius = minHalf * clampedCorner;
+        float radiusX = Math.Min(radius, halfWidth);
+        float radiusY = Math.Min(radius, halfHeight);
+
 
         rect.X.Unit = SvgNumericUnit.FromUserUnits(centerX - halfWidth);
         rect.Y.Unit = SvgNumericUnit.FromUserUnits(centerY - halfHeight);
@@ -306,6 +312,9 @@ internal partial class DocumentViewModel
         rect.Width.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.X);
         rect.Height.Unit = SvgNumericUnit.FromUserUnits(rectangleData.Size.Y);
 
+        rect.Rx.Unit = SvgNumericUnit.FromUserUnits(radiusX);
+        rect.Ry.Unit = SvgNumericUnit.FromUserUnits(radiusY);
+        
         return rect;
     }
 

+ 8 - 8
src/PixiEditor/ViewModels/Document/NodeGraphViewModel.cs

@@ -20,7 +20,7 @@ namespace PixiEditor.ViewModels.Document;
 internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposable
 {
     private bool isFullyCreated;
-    
+
     public DocumentViewModel DocumentViewModel { get; }
     public ObservableCollection<INodeHandler> AllNodes { get; } = new();
     public ObservableCollection<NodeConnectionViewModel> Connections { get; } = new();
@@ -110,7 +110,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
         connection.OutputProperty.ConnectedInputs.Add(connection.InputProperty);
 
         Connections.Add(connection);
-        
+
         UpdatesFramesPartOf(connection.InputNode);
         UpdatesFramesPartOf(connection.OutputNode);
 
@@ -126,11 +126,11 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
             connection.InputProperty.ConnectedOutput = null;
             connection.OutputProperty.ConnectedInputs.Remove(connection.InputProperty);
             Connections.Remove(connection);
+
+            UpdatesFramesPartOf(connection.InputNode);
+            UpdatesFramesPartOf(connection.OutputNode);
         }
 
-        UpdatesFramesPartOf(connection.InputNode);
-        UpdatesFramesPartOf(connection.OutputNode);
-        
         var node = AllNodes.FirstOrDefault(x => x.Id == nodeId);
         if (node != null)
         {
@@ -152,7 +152,7 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
         var lastKnownFramesPartOf = node.Frames.OfType<NodeZoneViewModel>().ToHashSet();
         var startLookup = Frames.OfType<NodeZoneViewModel>().ToDictionary(x => x.Start);
         var currentlyPartOf = new HashSet<NodeZoneViewModel>();
-        
+
         node.TraverseBackwards(x =>
         {
             if (x is IPairNodeEndViewModel)
@@ -185,9 +185,9 @@ internal class NodeGraphViewModel : ViewModelBase, INodeGraphHandler, IDisposabl
     {
         if (isFullyCreated)
             return;
-        
+
         isFullyCreated = true;
-        
+
         foreach (var nodeZoneViewModel in Frames.OfType<NodeZoneViewModel>())
         {
             UpdateNodesPartOf(nodeZoneViewModel);

+ 22 - 2
src/PixiEditor/ViewModels/SettingsWindowViewModel.cs

@@ -95,9 +95,21 @@ internal partial class SettingsWindowViewModel : ViewModelBase
     [Command.Internal("PixiEditor.Shortcuts.Export")]
     public static async Task ExportShortcuts()
     {
+        IStorageFolder? suggestedStartLocation = null;
+        try
+        {
+            suggestedStartLocation =
+                await MainWindow.Current!.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
+        }
+        catch (Exception)
+        {
+            // If we can't get the documents folder, we will just use the default location
+            // This is not a critical error, so we can ignore it
+        }
+
         var file = await MainWindow.Current!.StorageProvider.SaveFilePickerAsync(new()
         {
-            SuggestedStartLocation = await MainWindow.Current!.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents),
+            SuggestedStartLocation = suggestedStartLocation,
             FileTypeChoices = new List<FilePickerFileType>()
             {
                 new FilePickerFileType("PixiShorts (*.pixisc)")
@@ -127,7 +139,15 @@ internal partial class SettingsWindowViewModel : ViewModelBase
         
         if (file is not null)
         {
-            File.Copy(CommandController.ShortcutsPath, file.Path.LocalPath, true);
+            try
+            {
+                File.Copy(CommandController.ShortcutsPath, file.Path.LocalPath, true);
+            }
+            catch (Exception ex)
+            {
+                string errMessageTrimmed = ex.Message.Length > 100 ? ex.Message[..100] + "..." : ex.Message;
+                NoticeDialog.Show(title: "ERROR", message: new LocalizedString("UNKNOWN_ERROR_SAVING").Value + $" {errMessageTrimmed}");
+            }
         }
         
         // Sometimes, focus was brought back to the last edited shortcut

+ 3 - 1
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -347,13 +347,15 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     private void MoveSelectedMember(bool upwards)
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
-        var member = Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember;
+        var member = doc?.SelectedStructureMember;
         if (member is null)
             return;
         var path = doc!.StructureHelper.FindPath(member.Id);
         if (path.Count < 2 || path[1] is not FolderNodeViewModel folderVm)
             return;
         var parent = folderVm;
+        if(parent.Children.Count == 0)
+            return;
         int curIndex = parent.Children.IndexOf(path[0]);
         if (upwards)
         {

+ 36 - 1
src/PixiEditor/ViewModels/SubViewModels/ToolsViewModel.cs

@@ -2,6 +2,7 @@
 using System.Collections.ObjectModel;
 using System.ComponentModel;
 using System.Linq;
+using System.Reflection;
 using Avalonia.Input;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.ChangeableDocument;
@@ -180,14 +181,36 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
     {
         ActiveTool?.OnToolDeselected(false);
         ActiveToolSet = toolSetHandler;
+        if (ActiveTool != null && !ActiveToolSet.Tools.Contains(ActiveTool))
+        {
+            TrySelectCommonToolInNewToolSet();
+        }
         ActiveToolSet.ApplyToolSetSettings();
         UpdateEnabledState();
 
+
         ActiveTool?.OnToolSelected(false);
 
         OnPropertyChanged(nameof(NonSelectedToolSets));
     }
 
+    private void TrySelectCommonToolInNewToolSet()
+    {
+        var commonTool = ActiveToolSet.Tools.FirstOrDefault(tool =>
+        {
+            var attr = tool.GetType().GetCustomAttribute<Command.ToolAttribute>();
+            if (attr is null) return false;
+
+            return ActiveTool?.GetType().GetCustomAttribute<Command.ToolAttribute>()?.CommonToolType ==
+                   attr.CommonToolType;
+        });
+
+        if (commonTool is not null)
+        {
+            SetActiveTool(commonTool, false);
+        }
+    }
+
     [Command.Basic("PixiEditor.Tools.ToggleSelectionTinting", "TOGGLE_TINTING_SELECTION", "TOGGLE_TINTING_SELECTION_DESCRIPTIVE", AnalyticsTrack = true)]
     public void ToggleTintSelection() => SelectionTintingEnabled = !SelectionTintingEnabled;
 
@@ -389,7 +412,7 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         if (foundTool == null)
         {
             foundTool = allTools.FirstOrDefault(x => x.GetType().IsAssignableFrom(toolType));
-            if(foundTool == null)
+            if(foundTool == null || SimilarToolInActiveToolSetExists(toolType))
                 return;
 
             var toolset = AllToolSets.FirstOrDefault(x => x.Tools.Contains(foundTool));
@@ -426,6 +449,18 @@ internal class ToolsViewModel : SubViewModel<ViewModelMain>, IToolsHandler
         }
     }
 
+    private bool SimilarToolInActiveToolSetExists(Type toolType)
+    {
+        Command.ToolAttribute attr = toolType.GetCustomAttribute<Command.ToolAttribute>();
+        if (attr is null) return false;
+
+        return ActiveToolSet.Tools.Any(tool =>
+        {
+            var toolAttr = tool.GetType().GetCustomAttribute<Command.ToolAttribute>();
+            return toolAttr is not null && toolAttr.CommonToolType == attr.CommonToolType;
+        });
+    }
+
     public void HandleToolRepeatShortcutDown()
     {
         if (ActiveTool == null) return;

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

@@ -10,7 +10,7 @@ using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.C)]
+[Command.Tool(Key = Key.C, CommonToolType = "Ellipse")]
 internal class RasterEllipseToolViewModel : ShapeTool, IRasterEllipseToolHandler
 {
     private string defaultActionDisplay = "ELLIPSE_TOOL_ACTION_DISPLAY_DEFAULT";

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

@@ -12,7 +12,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.L)]
+[Command.Tool(Key = Key.L, CommonToolType = "Line")]
 internal class RasterLineToolViewModel : ShapeTool, ILineToolHandler
 {
     private string defaultActionDisplay = "LINE_TOOL_ACTION_DISPLAY_DEFAULT";

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

@@ -11,7 +11,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.R)]
+[Command.Tool(Key = Key.R, CommonToolType = "Rectangle")]
 internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHandler
 {
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";

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

@@ -12,7 +12,7 @@ using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.C)]
+[Command.Tool(Key = Key.C, CommonToolType = "Ellipse")]
 internal class VectorEllipseToolViewModel : ShapeTool, IVectorEllipseToolHandler
 {
     public const string NewLayerKey = "NEW_ELLIPSE_LAYER_NAME";

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

@@ -16,7 +16,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.L)]
+[Command.Tool(Key = Key.L, CommonToolType = "Line")]
 internal class VectorLineToolViewModel : ShapeTool, IVectorLineToolHandler
 {
     public const string NewLayerKey = "NEW_LINE_LAYER_NAME";

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

@@ -13,7 +13,7 @@ using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 
-[Command.Tool(Key = Key.R)]
+[Command.Tool(Key = Key.R, CommonToolType = "Rectangle")]
 internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHandler
 {
     public const string NewLayerKey = "NEW_RECTANGLE_LAYER_NAME";

+ 0 - 4
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -773,10 +773,6 @@ internal class NodeGraphView : Zoombox.Zoombox
                 {
                     connection = (endConnectionProperty, startConnectionProperty, null);
                 }
-                else
-                {
-                    return;
-                }
             }
         }