Skip to content

Commit ed026f7

Browse files
johnpiersonclaude
andauthored
DYN-10313 - Python Port Copy Paste + Undo Redo Fixes (#17021)
Co-authored-by: Claude <claude@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 949dff9 commit ed026f7

6 files changed

Lines changed: 237 additions & 106 deletions

File tree

src/DynamoCore/Graph/Nodes/PortModel.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class PortModel : ModelBase, IEquatable<PortModel>
3838
private bool keepListStructure = false;
3939
private int level = 1;
4040
private string toolTip;
41+
private string name;
4142

4243
#endregion
4344

@@ -74,8 +75,15 @@ public ObservableCollection<ConnectorModel> Connectors
7475
/// </summary>
7576
public string Name
7677
{
77-
get;
78-
internal set;
78+
get => name;
79+
internal set
80+
{
81+
if (name != value)
82+
{
83+
name = value;
84+
RaisePropertyChanged(nameof(Name));
85+
}
86+
}
7987
}
8088

8189
/// <summary>

src/DynamoCoreWpf/ViewModels/Core/InPortViewModel.cs

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Collections.ObjectModel;
42
using System.Linq;
53
using System.Windows;
64
using System.Windows.Media;
@@ -9,7 +7,6 @@
97
using Dynamo.UI;
108
using Dynamo.UI.Commands;
119
using ProtoCore.Utils;
12-
using UI.Prompts;
1310

1411
namespace Dynamo.ViewModels
1512
{
@@ -20,7 +17,7 @@ public partial class InPortViewModel : PortViewModel
2017
private DelegateCommand useLevelsCommand;
2118
private DelegateCommand keepListStructureCommand;
2219
private DelegateCommand portMouseLeftButtonOnLevelCommand;
23-
private DelegateCommand editPortPropertiesCommand;
20+
2421

2522
private bool showUseLevelMenu;
2623
private bool isPythonNodePort;
@@ -306,53 +303,6 @@ private void ChangeLevel(int level)
306303
}
307304
}
308305

309-
/// <summary>
310-
/// Used by the 'Edit Port Properties' button in the node output context menu.
311-
/// Triggers the Port Properties Panel
312-
/// </summary>
313-
public DelegateCommand EditPortPropertiesCommand
314-
{
315-
get
316-
{
317-
return editPortPropertiesCommand ??
318-
(editPortPropertiesCommand = new DelegateCommand(EditPortProperties));
319-
}
320-
}
321-
/// <summary>
322-
/// Used by the 'Edit Port Properties' button in the node output context menu.
323-
/// Triggers the Port Properties Panel
324-
/// </summary>
325-
private void EditPortProperties(object parameter)
326-
{
327-
var wsViewModel = node.WorkspaceViewModel;
328-
329-
// Hide the popup, we no longer need it
330-
wsViewModel.OnRequestPortContextMenu(ShowHideFlags.Show, this);
331-
332-
var dialog = new PortPropertiesEditPrompt()
333-
{
334-
DescriptionInput = { Text = port.ToolTip },
335-
nameBox = { Text = port.Name },
336-
PortType = PortType.Output,
337-
OutPortNames = ListOutportNames(this.NodeViewModel.InPorts),
338-
};
339-
340-
if (dialog.ShowDialog() != true)
341-
{
342-
return;
343-
}
344-
345-
port.Name = dialog.PortName;
346-
port.ToolTip = dialog.Description;
347-
348-
RaisePropertyChanged(nameof(PortName));
349-
}
350-
351-
private List<string> ListOutportNames(ObservableCollection<PortViewModel> outPorts)
352-
{
353-
return outPorts.Where(x => !x.PortName.Equals(this.PortName)).Select(x => x.PortName).ToList();
354-
}
355-
356306
/// <summary>
357307
/// Handles the Mouse left button down on the level.
358308
/// </summary>

src/DynamoCoreWpf/ViewModels/Core/OutPortViewModel.cs

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Collections.Generic;
2-
using System.Collections.ObjectModel;
31
using System.ComponentModel;
42
using System.Linq;
53
using System.Windows;
@@ -11,7 +9,6 @@
119
using Dynamo.UI;
1210
using Dynamo.UI.Commands;
1311
using ProtoCore.Utils;
14-
using UI.Prompts;
1512
using Color = System.Windows.Media.Color;
1613

1714
namespace Dynamo.ViewModels
@@ -22,7 +19,7 @@ public partial class OutPortViewModel : PortViewModel
2219

2320
private DelegateCommand breakConnectionsCommand;
2421
private DelegateCommand hideConnectionsCommand;
25-
private DelegateCommand editPortPropertiesCommand;
22+
2623
private DelegateCommand portMouseLeftButtonOnContextCommand;
2724

2825
private SolidColorBrush portValueMarkerColor = new SolidColorBrush(Color.FromArgb(255, 204, 204, 204));
@@ -278,19 +275,6 @@ public DelegateCommand HideConnectionsCommand
278275
get { return hideConnectionsCommand ?? (hideConnectionsCommand = new DelegateCommand(HideConnections)); }
279276
}
280277

281-
/// <summary>
282-
/// Used by the 'Edit Port Properties' button in the node output context menu.
283-
/// Triggers the Port Properties Panel
284-
/// </summary>
285-
public DelegateCommand EditPortPropertiesCommand
286-
{
287-
get
288-
{
289-
return editPortPropertiesCommand ??
290-
(editPortPropertiesCommand = new DelegateCommand(EditPortProperties));
291-
}
292-
}
293-
294278
/// <summary>
295279
/// Used by the 'Break Connections' button in the node output context menu.
296280
/// Removes any current connections this port has.
@@ -340,41 +324,6 @@ private void HideConnections(object parameter)
340324
RefreshHideWiresState();
341325
}
342326

343-
/// <summary>
344-
/// Used by the 'Edit Port Properties' button in the node output context menu.
345-
/// Triggers the Port Properties Panel
346-
/// </summary>
347-
private void EditPortProperties(object parameter)
348-
{
349-
var wsViewModel = node.WorkspaceViewModel;
350-
351-
// Hide the popup, we no longer need it
352-
wsViewModel.OnRequestPortContextMenu(ShowHideFlags.Show, this);
353-
354-
var dialog = new PortPropertiesEditPrompt()
355-
{
356-
DescriptionInput = { Text = port.ToolTip },
357-
nameBox = { Text = port.Name },
358-
PortType = PortType.Output,
359-
OutPortNames = ListOutportNames(this.NodeViewModel.OutPorts),
360-
};
361-
362-
if (dialog.ShowDialog() != true)
363-
{
364-
return;
365-
}
366-
367-
port.Name = dialog.PortName;
368-
port.ToolTip = dialog.Description;
369-
370-
RaisePropertyChanged(nameof(PortName));
371-
}
372-
373-
private List<string> ListOutportNames(ObservableCollection<PortViewModel> outPorts)
374-
{
375-
return outPorts.Where(x => !x.PortName.Equals(this.PortName)).Select(x => x.PortName).ToList();
376-
}
377-
378327
/// <summary>
379328
/// Returns true if they are hidden.
380329
/// </summary>

src/DynamoCoreWpf/ViewModels/Core/PortViewModel.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Windows.Threading;
1010
using Dynamo.Graph.Nodes;
1111
using Dynamo.Graph.Workspaces;
12+
using UI.Prompts;
1213
using Dynamo.Models;
1314
using Dynamo.Search.SearchElements;
1415
using Dynamo.UI.Commands;
@@ -27,6 +28,7 @@ public partial class PortViewModel : ViewModelBase
2728
protected readonly NodeViewModel node;
2829
private DelegateCommand useLevelsCommand;
2930
private DelegateCommand keepListStructureCommand;
31+
private DelegateCommand editPortPropertiesCommand;
3032
private bool showUseLevelMenu;
3133
private const double autocompletePopupSpacing = 2.5;
3234
private const double proxyPortContextMenuOffset = 20;
@@ -500,7 +502,7 @@ private void PortPropertyChanged(object sender, System.ComponentModel.PropertyCh
500502
case nameof(PortType):
501503
RaisePropertyChanged(nameof(PortType));
502504
break;
503-
case nameof(PortName):
505+
case nameof(PortModel.Name):
504506
RaisePropertyChanged(nameof(PortName));
505507
break;
506508
case nameof(IsConnected):
@@ -740,5 +742,59 @@ private string GetPortDisplayName(string value)
740742
}
741743
return ">";
742744
}
745+
746+
/// <summary>
747+
/// Used by the 'Edit Port Properties' button in the node port context menu.
748+
/// Opens the Port Properties dialog to rename the port and edit its description.
749+
/// </summary>
750+
public DelegateCommand EditPortPropertiesCommand
751+
{
752+
get
753+
{
754+
return editPortPropertiesCommand ??
755+
(editPortPropertiesCommand = new DelegateCommand(EditPortProperties));
756+
}
757+
}
758+
759+
/// <summary>
760+
/// Opens the Port Properties dialog and, on confirmation, records the owning node
761+
/// for undo before applying the new name and description.
762+
/// </summary>
763+
private void EditPortProperties(object parameter)
764+
{
765+
var wsViewModel = node.WorkspaceViewModel;
766+
767+
// Hide the popup, we no longer need it
768+
wsViewModel.OnRequestPortContextMenu(ShowHideFlags.Hide, this);
769+
770+
// Collect sibling port names (same port type) to prevent duplicates.
771+
var siblingPorts = port.PortType == PortType.Input ? node.InPorts : node.OutPorts;
772+
773+
var dialog = new PortPropertiesEditPrompt()
774+
{
775+
DescriptionInput = { Text = port.ToolTip },
776+
nameBox = { Text = port.Name },
777+
PortType = port.PortType,
778+
OutPortNames = siblingPorts.Where(p => !p.PortName.Equals(PortName))
779+
.Select(p => p.PortName)
780+
.ToList(),
781+
};
782+
783+
if (dialog.ShowDialog() != true)
784+
{
785+
return;
786+
}
787+
788+
var workspaceModel = node.WorkspaceViewModel.Model;
789+
using (workspaceModel.UndoRecorder.BeginActionGroup())
790+
{
791+
WorkspaceModel.RecordModelForModification(port.Owner, workspaceModel.UndoRecorder);
792+
port.Name = dialog.PortName;
793+
port.ToolTip = dialog.Description;
794+
}
795+
workspaceModel.HasUnsavedChanges = true;
796+
797+
RaisePropertyChanged(nameof(PortName));
798+
}
743799
}
744800
}

src/Libraries/PythonNodeModels/PythonNode.cs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Collections.ObjectModel;
44
using System.ComponentModel;
5+
using System.Globalization;
56
using System.IO;
67
using System.Linq;
78
using System.Reflection;
@@ -173,6 +174,88 @@ protected override bool UpdateValueCore(UpdateValueParams updateValueParams)
173174
return base.UpdateValueCore(updateValueParams);
174175
}
175176

177+
#region SerializeCore/DeserializeCore
178+
179+
/// <summary>
180+
/// Preserves custom input and output port names and tooltips during XML serialization.
181+
/// The base NodeModel serialization writes PortInfo elements for input ports;
182+
/// this override adds "portName" and "portToolTip" attributes to each of those
183+
/// elements and appends OutPortInfo elements for output ports.
184+
/// These fields are emitted and consumed for all SaveContext values (Copy, Undo, Save, etc.).
185+
/// </summary>
186+
protected override void SerializeCore(XmlElement element, SaveContext context)
187+
{
188+
base.SerializeCore(element, context);
189+
190+
// Add portName and portToolTip attributes to each PortInfo element written by NodeModel.SerializeCore.
191+
foreach (XmlNode child in element.ChildNodes)
192+
{
193+
if (child.Name == "PortInfo" && child is XmlElement portInfo)
194+
{
195+
var indexAttr = portInfo.GetAttribute("index");
196+
if (int.TryParse(indexAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out int index)
197+
&& index >= 0 && index < InPorts.Count)
198+
{
199+
portInfo.SetAttribute("portName", InPorts[index].Name);
200+
portInfo.SetAttribute("portToolTip", InPorts[index].ToolTip);
201+
}
202+
}
203+
}
204+
205+
// Write output port names and tooltips as separate elements (NodeModel does not serialize output PortInfo).
206+
for (int i = 0; i < OutPorts.Count; i++)
207+
{
208+
XmlElement outPortInfo = element.OwnerDocument.CreateElement("OutPortInfo");
209+
outPortInfo.SetAttribute("index", i.ToString(CultureInfo.InvariantCulture));
210+
outPortInfo.SetAttribute("portName", OutPorts[i].Name);
211+
outPortInfo.SetAttribute("portToolTip", OutPorts[i].ToolTip);
212+
element.AppendChild(outPortInfo);
213+
}
214+
}
215+
216+
/// <summary>
217+
/// Restores custom input and output port names after base deserialization has
218+
/// recreated the ports with their default names.
219+
/// </summary>
220+
protected override void DeserializeCore(XmlElement nodeElement, SaveContext context)
221+
{
222+
base.DeserializeCore(nodeElement, context);
223+
224+
// After base.DeserializeCore, all ports exist with default names and tooltips.
225+
// Re-apply any custom values that were serialized.
226+
foreach (XmlNode child in nodeElement.ChildNodes)
227+
{
228+
if (child.Name == "PortInfo" && child is XmlElement portInfo)
229+
{
230+
var indexAttr = portInfo.GetAttribute("index");
231+
if (int.TryParse(indexAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out int index)
232+
&& index >= 0 && index < InPorts.Count)
233+
{
234+
if (portInfo.HasAttribute("portName"))
235+
InPorts[index].Name = portInfo.GetAttribute("portName");
236+
237+
if (portInfo.HasAttribute("portToolTip"))
238+
InPorts[index].ToolTip = portInfo.GetAttribute("portToolTip");
239+
}
240+
}
241+
else if (child.Name == "OutPortInfo" && child is XmlElement outPortInfo)
242+
{
243+
var indexAttr = outPortInfo.GetAttribute("index");
244+
if (int.TryParse(indexAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outIndex)
245+
&& outIndex >= 0 && outIndex < OutPorts.Count)
246+
{
247+
if (outPortInfo.HasAttribute("portName"))
248+
OutPorts[outIndex].Name = outPortInfo.GetAttribute("portName");
249+
250+
if (outPortInfo.HasAttribute("portToolTip"))
251+
OutPorts[outIndex].ToolTip = outPortInfo.GetAttribute("portToolTip");
252+
}
253+
}
254+
}
255+
}
256+
257+
#endregion
258+
176259
}
177260

178261
[NodeName("Python Script")]

0 commit comments

Comments
 (0)