Browse Source

Extract selected text command

Krzysztof Krysiński 2 months ago
parent
commit
32916fa5f8

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 45f50107675b9212a28d0e5a8d28bf15765f3eeb
+Subproject commit 13d7c363f5bee90cbde15f1c98cf1744b2bcd493

+ 62 - 44
src/PixiEditor.ChangeableDocument/Changes/Text/ExtractSelectedText_Change.cs

@@ -48,6 +48,13 @@ internal class ExtractSelectedText_Change : Change
 
 
         subdividions = GetSubdivisions(selectionStart, selectionEnd, textData.Text);
         subdividions = GetSubdivisions(selectionStart, selectionEnd, textData.Text);
 
 
+        subdividions?.RemoveAll(x => x.text == "\n");
+
+        if (subdividions?.Count == 0)
+        {
+            subdividions = null;
+        }
+
         if (subdividions != null)
         if (subdividions != null)
         {
         {
             newLayerIds = new Guid[subdividions.Count - 1];
             newLayerIds = new Guid[subdividions.Count - 1];
@@ -81,7 +88,10 @@ internal class ExtractSelectedText_Change : Change
 
 
             if (index == 0)
             if (index == 0)
             {
             {
-                textData.Text = subdividion.text.ReplaceLineEndings("");
+                textData.Text = subdividion.text.EndsWith("\n")
+                    ? subdividion.text[..^1]
+                    : subdividion.text;
+
                 var aabb = textData.TransformedVisualAABB.RoundOutwards();
                 var aabb = textData.TransformedVisualAABB.RoundOutwards();
                 var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
                 var affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
                     (RectI)aabb, ChunkyImage.FullChunkSize));
                     (RectI)aabb, ChunkyImage.FullChunkSize));
@@ -89,7 +99,6 @@ internal class ExtractSelectedText_Change : Change
                 continue;
                 continue;
             }
             }
 
 
-
             if (node.EmbeddedShapeData.Clone() is not TextVectorData data)
             if (node.EmbeddedShapeData.Clone() is not TextVectorData data)
             {
             {
                 throw new InvalidOperationException("Failed to clone TextVectorData.");
                 throw new InvalidOperationException("Failed to clone TextVectorData.");
@@ -101,11 +110,14 @@ internal class ExtractSelectedText_Change : Change
                 throw new InvalidOperationException("Failed to clone VectorLayerNode.");
                 throw new InvalidOperationException("Failed to clone VectorLayerNode.");
             }
             }
 
 
-            string text = subdividion.text.ReplaceLineEndings("");
+            string text = subdividion.text.EndsWith("\n")
+                ? subdividion.text[..^1]
+                : subdividion.text;
+
             newNode.Id = newLayerIds[index - 1];
             newNode.Id = newLayerIds[index - 1];
             newNode.DisplayName = text.Length > 20
             newNode.DisplayName = text.Length > 20
-                ? text[..20] + "..."
-                : text;
+                ? text[..20].ReplaceLineEndings("") + "..."
+                : text.ReplaceLineEndings("");
 
 
             data.Text = text;
             data.Text = text;
             newNode.EmbeddedShapeData = data;
             newNode.EmbeddedShapeData = data;
@@ -129,8 +141,7 @@ internal class ExtractSelectedText_Change : Change
 
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
     {
-        return new None();
-        /*var node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
+        var node = target.FindNodeOrThrow<VectorLayerNode>(memberId);
         if (node.EmbeddedShapeData is not TextVectorData textData)
         if (node.EmbeddedShapeData is not TextVectorData textData)
         {
         {
             throw new InvalidOperationException("Node does not contain TextVectorData.");
             throw new InvalidOperationException("Node does not contain TextVectorData.");
@@ -140,32 +151,27 @@ internal class ExtractSelectedText_Change : Change
 
 
         List<IChangeInfo> changes = new List<IChangeInfo>();
         List<IChangeInfo> changes = new List<IChangeInfo>();
 
 
-        if (nestedActions != null)
-        {
-            foreach (var action in nestedActions)
-            {
-                changes.AddRange(action.Revert(target).AsT2);
-            }
-        }
-
         AffectedArea affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
         AffectedArea affected = new AffectedArea(OperationHelper.FindChunksTouchingRectangle(
             (RectI)textData.TransformedVisualAABB.RoundOutwards(), ChunkyImage.FullChunkSize));
             (RectI)textData.TransformedVisualAABB.RoundOutwards(), ChunkyImage.FullChunkSize));
 
 
         changes.Add(new VectorShape_ChangeInfo(node.Id, affected));
         changes.Add(new VectorShape_ChangeInfo(node.Id, affected));
 
 
-        var newNode = target.FindNode<VectorLayerNode>(newLayerId);
-        if (newNode != null)
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+        foreach (var newLayerId in newLayerIds)
         {
         {
-            changes.AddRange(NodeOperations.DetachStructureNode(newNode));
-            changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
-            changes.Add(new DeleteNode_ChangeInfo(newLayerId));
+            var newNode = target.FindNode<VectorLayerNode>(newLayerId);
+            if (newNode != null)
+            {
+                changes.AddRange(NodeOperations.DetachStructureNode(newNode));
+                changes.Add(new DeleteStructureMember_ChangeInfo(newLayerId));
 
 
-            target.NodeGraph.RemoveNode(newNode);
+                target.NodeGraph.RemoveNode(newNode);
+            }
         }
         }
 
 
         originalPositions.Clear();
         originalPositions.Clear();
 
 
-        return changes;*/
+        return changes;
     }
     }
 
 
     private VecD GetPositionForNewText(string text, int startIndex, TextVectorData textData)
     private VecD GetPositionForNewText(string text, int startIndex, TextVectorData textData)
@@ -192,41 +198,53 @@ internal class ExtractSelectedText_Change : Change
         if (start == 0 && end == text.Length)
         if (start == 0 && end == text.Length)
             return null;
             return null;
 
 
+        if (end - start == 1 && start < text.Length && text[start] == '\n')
+            return null;
+
         var result = new List<(int start, int end, string text)>();
         var result = new List<(int start, int end, string text)>();
         var richText = new RichText(text);
         var richText = new RichText(text);
 
 
-        if (start > 0)
-            result.Add((0, start, text.Substring(0, start)));
+        richText.IndexOnLine(start, out int startLineIndex);
+        richText.IndexOnLine(end, out int endLineIndex);
+        bool spansMultipleLines = startLineIndex != endLineIndex;
 
 
         int cursor = start;
         int cursor = start;
-        int adjustedEnd = end;
 
 
-        while (cursor < adjustedEnd)
+        if (start > 0)
         {
         {
-            richText.IndexOnLine(cursor, out int lineIndex);
-            var (lineStart, lineEnd) = richText.GetLineStartEnd(lineIndex); // lineEnd is exclusive
-
-            int segmentStart = cursor;
-            int segmentEnd = Math.Min(adjustedEnd, lineEnd);
-
-            // If selection ends exactly before line break, include the \n
-            if (segmentEnd < lineEnd - 1 && text[segmentEnd] == '\n' && segmentEnd + 1 == lineEnd)
+            result.Add((0, start, text.Substring(0, start)));
+            var (startLineStart, startLineEnd) = richText.GetLineStartEnd(startLineIndex);
+            bool isMiddleOfLine = start > startLineStart && start < startLineEnd;
+            if (isMiddleOfLine && spansMultipleLines)
             {
             {
-                segmentEnd += 1;
-                adjustedEnd += 1; // shift selection forward so suffix doesn't get the newline
+                int substringLength = Math.Min(startLineEnd - start, text.Length - start);
+                result.Add((start, startLineEnd, text.Substring(start, substringLength)));
+                cursor = startLineEnd;
             }
             }
-
-            result.Add((segmentStart, segmentEnd, text.Substring(segmentStart, segmentEnd - segmentStart)));
-            cursor = segmentEnd;
         }
         }
 
 
-        if (adjustedEnd < text.Length)
-            result.Add((adjustedEnd, text.Length, text.Substring(adjustedEnd)));
+        if (cursor < end)
+        {
+            result.Add((cursor, end, text.Substring(cursor, end - cursor)));
+            cursor = end;
 
 
+            if (cursor >= text.Length)
+                return result;
 
 
-        result.RemoveAll(x => x.text == "\n");
-        if (result.Count == 0)
-            return null;
+            var (endLineStart, endLineEnd) = richText.GetLineStartEnd(endLineIndex);
+            bool endsMiddleOfLine = end > endLineStart && end < endLineEnd;
+            if (endsMiddleOfLine)
+            {
+                int substringLength = Math.Min(endLineEnd - end, text.Length - end);
+                result.Add((end, endLineEnd, text.Substring(end, substringLength)));
+                cursor = endLineEnd;
+            }
+        }
+
+        if (cursor < text.Length)
+        {
+            result.Add((cursor, text.Length, text.Substring(cursor)));
+        }
 
 
         return result;
         return result;
     }
     }

+ 3 - 3
src/PixiEditor.ChangeableDocument/Changes/Vectors/SeparateShapes_Change.cs

@@ -118,6 +118,8 @@ internal class SeparateShapes_Change : Change
 
 
         changes.Add(new VectorShape_ChangeInfo(memberId, affected));
         changes.Add(new VectorShape_ChangeInfo(memberId, affected));
 
 
+        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
+
         // Remove the newly created nodes
         // Remove the newly created nodes
         foreach (var newMemberId in newMemberIds)
         foreach (var newMemberId in newMemberIds)
         {
         {
@@ -125,15 +127,13 @@ internal class SeparateShapes_Change : Change
             if (createdNode != null)
             if (createdNode != null)
             {
             {
                 changes.AddRange(NodeOperations.DetachStructureNode(createdNode));
                 changes.AddRange(NodeOperations.DetachStructureNode(createdNode));
-                changes.Add(new DeleteNode_ChangeInfo(newMemberId));
+                changes.Add(new DeleteStructureMember_ChangeInfo(newMemberId));
 
 
                 target.NodeGraph.RemoveNode(createdNode);
                 target.NodeGraph.RemoveNode(createdNode);
                 createdNode?.Dispose();
                 createdNode?.Dispose();
             }
             }
         }
         }
 
 
-        changes.AddRange(NodeOperations.RevertPositions(originalPositions, target));
-
         originalPositions.Clear();
         originalPositions.Clear();
 
 
         return changes;
         return changes;

+ 5 - 0
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -452,6 +452,11 @@ internal class DocumentUpdater
             var closestId = doc.StructureHelper.FindClosestMember(new[] { info.Id });
             var closestId = doc.StructureHelper.FindClosestMember(new[] { info.Id });
             var closestMember = doc.StructureHelper.Find(closestId);
             var closestMember = doc.StructureHelper.Find(closestId);
 
 
+            if (closestMember == null)
+            {
+                closestMember = doc.NodeGraphHandler.StructureTree.Members.FirstOrDefault();
+            }
+
             if (closestMember != null)
             if (closestMember != null)
             {
             {
                 closestMember.Selection = StructureMemberSelectionType.Hard;
                 closestMember.Selection = StructureMemberSelectionType.Hard;

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

@@ -29,6 +29,7 @@ using PixiEditor.UI.Common.Localization;
 using PixiEditor.ViewModels.Dock;
 using PixiEditor.ViewModels.Dock;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.Document.Nodes;
 using PixiEditor.ViewModels.Document.Nodes;
+using PixiEditor.Views.Overlays.TextOverlay;
 
 
 namespace PixiEditor.ViewModels.SubViewModels;
 namespace PixiEditor.ViewModels.SubViewModels;
 #nullable enable
 #nullable enable
@@ -603,6 +604,8 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
 
 
     [Command.Basic("PixiEditor.Layer.ExtractSelectedText", "EXTRACT_SELECTED_TEXT", "EXTRACT_SELECTED_TEXT_DESCRIPTIVE",
     [Command.Basic("PixiEditor.Layer.ExtractSelectedText", "EXTRACT_SELECTED_TEXT", "EXTRACT_SELECTED_TEXT_DESCRIPTIVE",
         CanExecute = "PixiEditor.Layer.SelectedMemberIsSelectedText",
         CanExecute = "PixiEditor.Layer.SelectedMemberIsSelectedText",
+        Key = Key.X, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
+        ShortcutContexts = [ typeof(ViewportWindowViewModel), typeof(TextOverlay)],
         MenuItemPath = "LAYER/TEXT/EXTRACT_SELECTED_TEXT", AnalyticsTrack = true)]
         MenuItemPath = "LAYER/TEXT/EXTRACT_SELECTED_TEXT", AnalyticsTrack = true)]
     public void ExtractSelectedText()
     public void ExtractSelectedText()
     {
     {

+ 11 - 5
src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs

@@ -11,6 +11,7 @@ using Drawie.Numerics;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Extensions.UI.Overlays;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using PixiEditor.Helpers.UI;
 using PixiEditor.Helpers.UI;
+using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Input;
 using PixiEditor.Models.Input;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
@@ -452,7 +453,7 @@ internal class TextOverlay : Overlay
         var key = args.Key;
         var key = args.Key;
         var keyModifiers = args.KeyModifiers;
         var keyModifiers = args.KeyModifiers;
 
 
-        if (IsUndoRedoShortcut(key, keyModifiers))
+        if (IsRegisteredExternalShortcut(key, keyModifiers))
         {
         {
             ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
             ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
             return;
             return;
@@ -467,10 +468,14 @@ internal class TextOverlay : Overlay
         InsertChar(key, args.KeySymbol);
         InsertChar(key, args.KeySymbol);
     }
     }
 
 
-    private bool IsUndoRedoShortcut(Key key, KeyModifiers keyModifiers)
+    private bool IsRegisteredExternalShortcut(Key key, KeyModifiers keyModifiers)
     {
     {
-        return key == Key.Z && keyModifiers == KeyModifiers.Control ||
-               key == Key.Y && keyModifiers == KeyModifiers.Control;
+        var command = CommandController.Current.Commands[new KeyCombination(key, keyModifiers)];
+        var ctxCommand = command.FirstOrDefault(x =>
+            x.InternalName == "PixiEditor.Undo.Undo"
+            || x.InternalName == "PixiEditor.Undo.Redo"
+            || (x.ShortcutContexts != null && x.ShortcutContexts.Contains(typeof(TextOverlay))));
+        return ctxCommand != null;
     }
     }
 
 
     private void InsertChar(Key key, string symbol)
     private void InsertChar(Key key, string symbol)
@@ -618,7 +623,8 @@ internal class TextOverlay : Overlay
 
 
             int clampedDesiredLineIndex = Math.Clamp(lineIndex + direction.Y, 0, richText.Lines.Length - 1);
             int clampedDesiredLineIndex = Math.Clamp(lineIndex + direction.Y, 0, richText.Lines.Length - 1);
 
 
-            VecF position = glyphPositions[lastXMovementCursorIndex];
+
+            VecF position = glyphPositions[Math.Min(lastXMovementCursorIndex, glyphPositions.Length - 1)];
             (int lineStart, int lineEnd) = richText.GetLineStartEnd(clampedDesiredLineIndex);
             (int lineStart, int lineEnd) = richText.GetLineStartEnd(clampedDesiredLineIndex);
             VecF[] lineGlyphPositions = glyphPositions[lineStart..lineEnd];
             VecF[] lineGlyphPositions = glyphPositions[lineStart..lineEnd];
             int closestIndex = lineGlyphPositions.Select((pos, i) => (i, pos))
             int closestIndex = lineGlyphPositions.Select((pos, i) => (i, pos))