diff --git a/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs b/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
index 9ac95ce..3d3bd41 100644
--- a/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/IClipTypeStrategy.cs
@@ -29,4 +29,13 @@ public interface IClipTypeStrategy
/// "new clip" flows in the host and by plugins.
///
string DefaultXml(string clipName);
+
+ ///
+ /// Pull a clip-display-name hint out of when the
+ /// wire format carries one (e.g. <Script name="…"> for
+ /// Mac-XMSC, <BaseTable name="…"> for Mac-XMTB).
+ /// Returns null for formats with no embedded name or when the
+ /// element is absent. Must not throw on malformed input.
+ ///
+ string? TryGetSourceName(string xml);
}
diff --git a/src/SharpFM.Model/ClipTypes/LayoutClipStrategy.cs b/src/SharpFM.Model/ClipTypes/LayoutClipStrategy.cs
index 8fe3c4b..71723a5 100644
--- a/src/SharpFM.Model/ClipTypes/LayoutClipStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/LayoutClipStrategy.cs
@@ -29,4 +29,6 @@ public ClipParseResult Parse(string xml)
public string DefaultXml(string clipName) =>
"";
+
+ public string? TryGetSourceName(string xml) => null;
}
diff --git a/src/SharpFM.Model/ClipTypes/OpaqueClipStrategy.cs b/src/SharpFM.Model/ClipTypes/OpaqueClipStrategy.cs
index 46c6043..c21153f 100644
--- a/src/SharpFM.Model/ClipTypes/OpaqueClipStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/OpaqueClipStrategy.cs
@@ -44,4 +44,6 @@ public ClipParseResult Parse(string xml)
public string DefaultXml(string clipName) =>
"";
+
+ public string? TryGetSourceName(string xml) => null;
}
diff --git a/src/SharpFM.Model/ClipTypes/ScriptClipStrategy.cs b/src/SharpFM.Model/ClipTypes/ScriptClipStrategy.cs
index 0c88d82..dd4224e 100644
--- a/src/SharpFM.Model/ClipTypes/ScriptClipStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/ScriptClipStrategy.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
using SharpFM.Model.Parsing;
using SharpFM.Model.Scripting;
@@ -63,4 +66,17 @@ public ClipParseResult Parse(string xml)
public string DefaultXml(string clipName) =>
"";
+
+ public string? TryGetSourceName(string xml)
+ {
+ try
+ {
+ var name = XDocument.Parse(xml).Descendants("Script").FirstOrDefault()?.Attribute("name")?.Value;
+ return string.IsNullOrEmpty(name) ? null : name;
+ }
+ catch (XmlException)
+ {
+ return null;
+ }
+ }
}
diff --git a/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs b/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
index 2e25f52..6d4f103 100644
--- a/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
+++ b/src/SharpFM.Model/ClipTypes/TableClipStrategy.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
using SharpFM.Model.Parsing;
using SharpFM.Model.Schema;
using SharpFM.Model.Scripting;
@@ -66,4 +69,17 @@ public string DefaultXml(string clipName) =>
_wrapsBaseTable
? $""
: "";
+
+ public string? TryGetSourceName(string xml)
+ {
+ try
+ {
+ var name = XDocument.Parse(xml).Descendants("BaseTable").FirstOrDefault()?.Attribute("name")?.Value;
+ return string.IsNullOrEmpty(name) ? null : name;
+ }
+ catch (XmlException)
+ {
+ return null;
+ }
+ }
}
diff --git a/src/SharpFM/ViewModels/MainWindowViewModel.cs b/src/SharpFM/ViewModels/MainWindowViewModel.cs
index 59fb5c5..bde6459 100644
--- a/src/SharpFM/ViewModels/MainWindowViewModel.cs
+++ b/src/SharpFM/ViewModels/MainWindowViewModel.cs
@@ -334,6 +334,19 @@ public async Task CopyAsClass()
}
}
+ // Pick a clip name that doesn't collide with anything already loaded.
+ // Returns the input when it's free, otherwise appends "(2)", "(3)", … until
+ // a free slot is found.
+ private string UniqueClipName(string desired)
+ {
+ if (FileMakerClips.All(c => c.Clip.Name != desired)) return desired;
+ for (var n = 2; ; n++)
+ {
+ var candidate = $"{desired} ({n})";
+ if (FileMakerClips.All(c => c.Clip.Name != candidate)) return candidate;
+ }
+ }
+
public async Task PasteFileMakerClipData()
{
try
@@ -355,6 +368,10 @@ public async Task PasteFileMakerClipData()
// don't add duplicates
if (FileMakerClips.Any(k => k.Clip.Xml == clip.Xml)) continue;
+ var sourceName = ClipTypeRegistry.For(format).TryGetSourceName(clip.Xml);
+ var desired = string.IsNullOrWhiteSpace(sourceName) ? "new-clip" : sourceName;
+ clip = clip.Rename(UniqueClipName(desired));
+
lastAdded = new ClipViewModel(clip);
FileMakerClips.Add(lastAdded);
count++;
diff --git a/tests/SharpFM.Tests/ClipTypes/LayoutClipStrategyTests.cs b/tests/SharpFM.Tests/ClipTypes/LayoutClipStrategyTests.cs
index b3e1de5..de06923 100644
--- a/tests/SharpFM.Tests/ClipTypes/LayoutClipStrategyTests.cs
+++ b/tests/SharpFM.Tests/ClipTypes/LayoutClipStrategyTests.cs
@@ -12,6 +12,15 @@ public void Identity_IsLayout()
Assert.Equal("Layout", LayoutClipStrategy.Instance.DisplayName);
}
+ [Fact]
+ public void TryGetSourceName_ReturnsNull()
+ {
+ var name = LayoutClipStrategy.Instance.TryGetSourceName(
+ "");
+
+ Assert.Null(name);
+ }
+
[Fact]
public void Parse_ValidLayoutSnippet_ReturnsSuccess()
{
diff --git a/tests/SharpFM.Tests/ClipTypes/OpaqueClipStrategyTests.cs b/tests/SharpFM.Tests/ClipTypes/OpaqueClipStrategyTests.cs
index 43a8c35..a294dc8 100644
--- a/tests/SharpFM.Tests/ClipTypes/OpaqueClipStrategyTests.cs
+++ b/tests/SharpFM.Tests/ClipTypes/OpaqueClipStrategyTests.cs
@@ -53,4 +53,12 @@ public void DefaultXml_ProducesParseableSnippet()
var result = OpaqueClipStrategy.Instance.Parse(seed);
Assert.IsType(result);
}
+
+ [Fact]
+ public void TryGetSourceName_ReturnsNull()
+ {
+ var name = OpaqueClipStrategy.Instance.TryGetSourceName("");
+
+ Assert.Null(name);
+ }
}
diff --git a/tests/SharpFM.Tests/ClipTypes/ScriptClipStrategyTests.cs b/tests/SharpFM.Tests/ClipTypes/ScriptClipStrategyTests.cs
index b111bf6..60a067c 100644
--- a/tests/SharpFM.Tests/ClipTypes/ScriptClipStrategyTests.cs
+++ b/tests/SharpFM.Tests/ClipTypes/ScriptClipStrategyTests.cs
@@ -14,6 +14,41 @@ public void StepsAndScript_HaveDistinctIdentities()
Assert.Equal("Script", ScriptClipStrategy.Script.DisplayName);
}
+ [Fact]
+ public void TryGetSourceName_ReturnsScriptNameAttribute()
+ {
+ var name = ScriptClipStrategy.Script.TryGetSourceName(
+ "");
+
+ Assert.Equal("FooBar", name);
+ }
+
+ [Fact]
+ public void TryGetSourceName_StepsClipWithoutScriptWrapper_ReturnsNull()
+ {
+ var name = ScriptClipStrategy.Steps.TryGetSourceName(
+ "");
+
+ Assert.Null(name);
+ }
+
+ [Fact]
+ public void TryGetSourceName_MalformedXml_ReturnsNull()
+ {
+ var name = ScriptClipStrategy.Script.TryGetSourceName("");
+
+ Assert.Null(name);
+ }
+
+ [Fact]
+ public void TryGetSourceName_PreservesPunctuationInName()
+ {
+ var name = ScriptClipStrategy.Script.TryGetSourceName(
+ "");
+
+ Assert.Equal("My \"favorite\" script", name);
+ }
+
[Fact]
public void Parse_EmptyScriptSnippet_ReturnsLosslessSuccess()
{
diff --git a/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs b/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
index 73e5ca7..d299d54 100644
--- a/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
+++ b/tests/SharpFM.Tests/ClipTypes/TableClipStrategyTests.cs
@@ -12,6 +12,32 @@ public void TableAndField_HaveDistinctIdentities()
Assert.Equal("Mac-XMFD", TableClipStrategy.Field.FormatId);
}
+ [Fact]
+ public void TryGetSourceName_ReturnsBaseTableNameAttribute()
+ {
+ var name = TableClipStrategy.Table.TryGetSourceName(
+ "");
+
+ Assert.Equal("Customers", name);
+ }
+
+ [Fact]
+ public void TryGetSourceName_FieldClipWithoutBaseTable_ReturnsNull()
+ {
+ var name = TableClipStrategy.Field.TryGetSourceName(
+ "");
+
+ Assert.Null(name);
+ }
+
+ [Fact]
+ public void TryGetSourceName_MalformedXml_ReturnsNull()
+ {
+ var name = TableClipStrategy.Table.TryGetSourceName("");
+
+ Assert.Null(name);
+ }
+
[Fact]
public void Parse_ValidTable_ReturnsSuccess()
{
diff --git a/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs b/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs
index 6e84eb2..958d82c 100644
--- a/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs
+++ b/tests/SharpFM.Tests/ViewModels/MainWindowViewModelTests.cs
@@ -7,6 +7,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using SharpFM.Dialogs;
+using SharpFM.Model;
using SharpFM.Plugin;
using SharpFM.Plugin.UI;
using SharpFM.Services;
@@ -199,6 +200,77 @@ public async Task PasteFileMakerClipData_SelectsLastPastedClip()
Assert.Contains("Pasted 2 clip(s)", vm.StatusMessage);
}
+ [Fact]
+ public async Task PasteFileMakerClipData_ScriptWithMetadataName_UsesMetadataAsClipName()
+ {
+ var clipboard = new MockClipboardService();
+ clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(
+ "");
+ var vm = CreateVm(clipboard);
+ vm.FileMakerClips.Clear();
+
+ await vm.PasteFileMakerClipData();
+
+ Assert.Equal("OrderTotal", vm.SelectedClip!.Clip.Name);
+ }
+
+ [Fact]
+ public async Task PasteFileMakerClipData_TableWithBaseTableName_UsesItAsClipName()
+ {
+ var clipboard = new MockClipboardService();
+ clipboard.ClipboardData["Mac-XMTB"] = BuildClipBytes(
+ "");
+ var vm = CreateVm(clipboard);
+ vm.FileMakerClips.Clear();
+
+ await vm.PasteFileMakerClipData();
+
+ Assert.Equal("Customers", vm.SelectedClip!.Clip.Name);
+ }
+
+ [Fact]
+ public async Task PasteFileMakerClipData_NoMetadataName_FallsBackToNewClip()
+ {
+ var clipboard = new MockClipboardService();
+ clipboard.ClipboardData["Mac-XMSS"] = BuildClipBytes(
+ "");
+ var vm = CreateVm(clipboard);
+ vm.FileMakerClips.Clear();
+
+ await vm.PasteFileMakerClipData();
+
+ Assert.Equal("new-clip", vm.SelectedClip!.Clip.Name);
+ }
+
+ [Fact]
+ public async Task PasteFileMakerClipData_CollidingName_AppendsNumericSuffix()
+ {
+ var clipboard = new MockClipboardService();
+ clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(
+ "");
+ var vm = CreateVm(clipboard);
+ vm.FileMakerClips.Clear();
+ vm.FileMakerClips.Add(new ClipViewModel(Clip.FromXml("OrderTotal", "Mac-XMSC", "")));
+
+ await vm.PasteFileMakerClipData();
+
+ Assert.Equal("OrderTotal (2)", vm.SelectedClip!.Clip.Name);
+ }
+
+ [Fact]
+ public async Task PasteFileMakerClipData_PreservesPunctuationInName()
+ {
+ var clipboard = new MockClipboardService();
+ clipboard.ClipboardData["Mac-XMSC"] = BuildClipBytes(
+ "");
+ var vm = CreateVm(clipboard);
+ vm.FileMakerClips.Clear();
+
+ await vm.PasteFileMakerClipData();
+
+ Assert.Equal("My \"favorite\" script", vm.SelectedClip!.Clip.Name);
+ }
+
private static byte[] BuildClipBytes(string xml)
{
var payload = Encoding.UTF8.GetBytes(xml);