Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,416 changes: 1,416 additions & 0 deletions ModernFormsNext.Designer.Tests/DesignerClipboardTests.cs

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions ModernFormsNext.Designer.Tests/DesignerRuntimeLayoutParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,41 @@ public void ChildReorderRemoveAndAddStayEquivalentToRuntimeOrder()
DesignerRuntimeLayoutParityHarness.AssertParity("order-added", document, document);
}

[Theory]
[InlineData(1d)]
[InlineData(1.25d)]
[InlineData(1.5d)]
[InlineData(2d)]
public void ClipboardPasteAndDuplicateUseProductionLayoutSemanticsAtDpiScale(double dpiScale)
{
var document = BasicDocument();
var source = Node("source", "Panel", 12, 18, 110, 80);
source.Properties["Padding"] = PaddingValue(new Padding(3, 5, 7, 9));
source.Children.Add(Node("header", "Panel", 0, 0, 30, 24, DockStyle.Top));
source.Children.Add(Node("content", "Panel", 0, 0, 30, 30, DockStyle.Fill));
var target = Node("target", "Panel", 140, 10, 140, 170);
target.Properties["Padding"] = PaddingValue(new Padding(6));
document.Controls.Add(source);
document.Controls.Add(target);
using var session = new DesignerSession();
session.LoadDocument(document);

session.SelectNode(source);
Assert.True(session.CopySelectedNode());
session.SelectNode(target);
Assert.True(session.PasteCopiedNode());
Assert.True(session.DuplicateSelectedNode());

Assert.Equal(2, target.Children.Count);
Assert.Equal("source1", target.Children[0].Name);
Assert.Equal("source2", target.Children[1].Name);
Assert.Equal(new DesignBounds(28, 34, 110, 80), target.Children[0].Bounds);
Assert.Equal(new DesignBounds(44, 50, 110, 80), target.Children[1].Bounds);
DesignerRuntimeLayoutParityHarness.AssertDpiParity(
new ParityScenario($"clipboard-paste-duplicate-{dpiScale:0.##}x", () => document),
dpiScale);
}

public static IEnumerable<object[]> CoreParityScenarios()
{
yield return Scenario("ordinary-child", () => WithChildren(Node("child", "Panel", 17, 23, 91, 47)));
Expand Down
49 changes: 49 additions & 0 deletions ModernFormsNext.Designer/Clipboard/DesignerClipboard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
namespace ModernFormsNext.Designer.Clipboard;

/// <summary>
/// Stores Designer clipboard content without depending on the runtime or operating-system clipboard.
/// </summary>
/// <remarks>
/// Content is an immutable string payload. Keeping the abstraction data-only allows one Designer
/// session to copy between open documents without retaining controls, selections, histories, or
/// host services from the source document.
/// </remarks>
internal interface IDesignerClipboard
{
event EventHandler? Changed;

string? Content { get; }

void SetContent(string content);

void Clear();
}

internal sealed class DesignerClipboard : IDesignerClipboard
{
private string? content;

public event EventHandler? Changed;

public string? Content => content;

public void SetContent(string content)
{
ArgumentException.ThrowIfNullOrWhiteSpace(content);

if (string.Equals(this.content, content, StringComparison.Ordinal))
return;

this.content = content;
Changed?.Invoke(this, EventArgs.Empty);
}

public void Clear()
{
if (content is null)
return;

content = null;
Changed?.Invoke(this, EventArgs.Empty);
}
}
69 changes: 69 additions & 0 deletions ModernFormsNext.Designer/Clipboard/DesignerClipboardPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Text.Json.Serialization;
using ModernFormsNext.Designing;

namespace ModernFormsNext.Designer.Clipboard;

/// <summary>
/// Defines the private, versioned data contract stored by the Designer clipboard.
/// </summary>
/// <remarks>
/// These DTOs deliberately mirror only persisted design data. They do not contain runtime
/// controls, CLR type instances, handles, delegates, dispatchers, histories, or selections.
/// </remarks>
internal sealed class DesignerClipboardPayload
{
public const string CurrentFormat = "ModernFormsNext.Designer";
public const int CurrentVersion = 1;

public string? Format { get; set; }

public int? Version { get; set; }

public DesignerClipboardNode? Root { get; set; }
}

internal sealed class DesignerClipboardNode
{
public string? TypeName { get; set; }

public string? Name { get; set; }

public int? X { get; set; }

public int? Y { get; set; }

public int? Width { get; set; }

public int? Height { get; set; }

public DesignerMemberVisibility? MemberVisibility { get; set; }

public SortedDictionary<string, DesignerClipboardValue>? Properties { get; set; }

public SortedDictionary<string, string?>? Events { get; set; }

public List<DesignerClipboardNode>? Children { get; set; }
}

internal sealed class DesignerClipboardValue
{
public DesignPropertyValueKind? Kind { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? StringValue { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public bool? BooleanValue { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Int32Value { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public double? DoubleValue { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? TypeName { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public SortedDictionary<string, DesignerClipboardValue>? Properties { get; set; }
}
Loading
Loading