diff --git a/src/DynamoCore/Configuration/IPreferences.cs b/src/DynamoCore/Configuration/IPreferences.cs
index f33c84731e1..8146793a2e3 100644
--- a/src/DynamoCore/Configuration/IPreferences.cs
+++ b/src/DynamoCore/Configuration/IPreferences.cs
@@ -25,6 +25,21 @@ public interface IPreferences
///
public bool ShowDefaultGroupDescription { get; set; }
+ ///
+ /// Indicates if the optional input ports are hidden by default.
+ ///
+ public bool OptionalInPortsCollapsed { get; set; }
+
+ ///
+ /// Indicates if the unconnected output ports are hidden by default.
+ ///
+ public bool UnconnectedOutPortsCollapsed { get; set; }
+
+ ///
+ /// Indicates if the groups should be collapsed by default.
+ ///
+ public bool CollapseToMinSize { get; set; }
+
///
/// Returns height of console
///
diff --git a/src/DynamoCore/Configuration/PreferenceSettings.cs b/src/DynamoCore/Configuration/PreferenceSettings.cs
index 1b82ee0b1e0..5c2f6026b1f 100644
--- a/src/DynamoCore/Configuration/PreferenceSettings.cs
+++ b/src/DynamoCore/Configuration/PreferenceSettings.cs
@@ -80,6 +80,9 @@ private readonly static Lazy
private string backupLocation;
private string templateFilePath;
private bool isMLAutocompleteTOUApproved;
+ private bool optionalInputsCollapsed;
+ private bool unconnectedOutputsCollapsed;
+ private bool collapseToMinSize;
#region Constants
///
@@ -195,6 +198,48 @@ public bool IsADPAnalyticsReportingApproved
///
public bool ShowDefaultGroupDescription { get; set; }
+ ///
+ /// Indicates if the optional input ports are collapsed by default.
+ ///
+ public bool OptionalInPortsCollapsed
+ {
+ get => optionalInputsCollapsed;
+ set
+ {
+ if (optionalInputsCollapsed == value) return;
+ optionalInputsCollapsed = value;
+ RaisePropertyChanged(nameof(OptionalInPortsCollapsed));
+ }
+ }
+
+ ///
+ /// Indicates if the unconnected output ports are hidden by default.
+ ///
+ public bool UnconnectedOutPortsCollapsed
+ {
+ get => unconnectedOutputsCollapsed;
+ set
+ {
+ if (unconnectedOutputsCollapsed == value) return;
+ unconnectedOutputsCollapsed = value;
+ RaisePropertyChanged(nameof(UnconnectedOutPortsCollapsed));
+ }
+ }
+
+ ///
+ /// Indicates if the groups should be collapsed to minimal size by default.
+ ///
+ public bool CollapseToMinSize
+ {
+ get => collapseToMinSize;
+ set
+ {
+ if (collapseToMinSize == value) return;
+ collapseToMinSize = value;
+ RaisePropertyChanged(nameof(CollapseToMinSize));
+ }
+ }
+
///
/// Indicates if Host units should be used for graphic helpers for Dynamo Revit
///
@@ -1001,6 +1046,9 @@ public PreferenceSettings()
DefaultRunType = RunType.Automatic;
DefaultNodeAutocompleteSuggestion = NodeAutocompleteSuggestion.MLRecommendation;
ShowDefaultGroupDescription = true;
+ OptionalInPortsCollapsed = true;
+ UnconnectedOutPortsCollapsed = true;
+ CollapseToMinSize = true;
BackupInterval = DefaultBackupInterval;
BackupFilesCount = 1;
diff --git a/src/DynamoCore/Graph/Annotations/AnnotationModel.cs b/src/DynamoCore/Graph/Annotations/AnnotationModel.cs
index 90d1f95f8d0..2f98ee44348 100644
--- a/src/DynamoCore/Graph/Annotations/AnnotationModel.cs
+++ b/src/DynamoCore/Graph/Annotations/AnnotationModel.cs
@@ -24,8 +24,6 @@ public class AnnotationModel : ModelBase
private const double ExtendYHeight = 5.0;
private const double NoteYAdjustment = 8.0;
- double lastExpandedWidth = 0;
-
#region Properties
///
@@ -74,7 +72,7 @@ public override double Width
if (width == value) return;
width = value;
- RaisePropertyChanged("Width");
+ RaisePropertyChanged(nameof(Width));
}
}
@@ -94,7 +92,7 @@ public override double Height
if (height == value) return;
height = value;
- RaisePropertyChanged("Height");
+ RaisePropertyChanged(nameof(Height));
}
}
@@ -105,7 +103,7 @@ public override double Height
///
public double ModelAreaHeight
{
- get { return modelAreaHeight; }
+ get => modelAreaHeight;
set
{
modelAreaHeight = value;
@@ -124,7 +122,7 @@ public string Text
set
{
text = value;
- RaisePropertyChanged("Text");
+ RaisePropertyChanged(nameof(Text));
}
}
@@ -138,7 +136,7 @@ public string AnnotationText
set
{
annotationText = value;
- RaisePropertyChanged("AnnotationText");
+ RaisePropertyChanged(nameof(AnnotationText));
}
}
@@ -167,7 +165,7 @@ public string Background
set
{
background = value;
- RaisePropertyChanged("Background");
+ RaisePropertyChanged(nameof(Background));
}
}
@@ -443,6 +441,113 @@ internal set
}
}
}
+
+ private bool isOptionalInPortsCollapsed;
+ ///
+ /// Indicates whether optional input ports were manually expanded or collapsed when the graph was last saved.
+ /// Used only for serialization.
+ ///
+ public bool IsOptionalInPortsCollapsed
+ {
+ get => isOptionalInPortsCollapsed;
+ set
+ {
+ if (isOptionalInPortsCollapsed == value) return;
+ isOptionalInPortsCollapsed = value;
+ }
+ }
+
+ private bool isUnconnectedOutPortsCollapsed;
+ ///
+ /// Indicates whether unconnected output ports were manually expanded or collapsed when the graph was last saved.
+ /// Used only for serialization.
+ ///
+ public bool IsUnconnectedOutPortsCollapsed
+ {
+ get => isUnconnectedOutPortsCollapsed;
+ set
+ {
+ if (isUnconnectedOutPortsCollapsed == value) return;
+ isUnconnectedOutPortsCollapsed = value;
+ }
+
+ }
+
+ private bool hasToggledOptionalInPorts;
+ ///
+ /// Indicates whether the user manually toggled the visibility of optional input ports.
+ /// If true, this overrides the global preference setting.
+ ///
+ public bool HasToggledOptionalInPorts
+ {
+ get => hasToggledOptionalInPorts;
+ set
+ {
+ if (hasToggledOptionalInPorts == value) return;
+ hasToggledOptionalInPorts = value;
+ }
+ }
+
+ private bool hasToggledUnconnectedOutPorts;
+ ///
+ /// Indicates whether the user manually toggled the visibility of unconnected output ports.
+ /// If true, this overrides the global preference setting.
+ ///
+ public bool HasToggledUnconnectedOutPorts
+ {
+ get => hasToggledUnconnectedOutPorts;
+ set
+ {
+ if (hasToggledUnconnectedOutPorts == value) return;
+ hasToggledUnconnectedOutPorts = value;
+ }
+ }
+
+ private bool isCollapsedToMinSize;
+ ///
+ /// Gets or sets a value indicating whether the group was manually resized while collapsed
+ ///
+ public bool IsCollapsedToMinSize
+ {
+ get => isCollapsedToMinSize;
+ set
+ {
+ if (isCollapsedToMinSize == value) return;
+ isCollapsedToMinSize = value;
+ }
+ }
+
+ private bool suppressBoundaryUpdate;
+ ///
+ /// A temporary flag used to suppress boundary updates while internal operations,
+ /// such as connector redrawing, are in progress.
+ /// Should be set to true only during those operations to avoid redundant or recursive updates.
+ ///
+ internal bool SuppressBoundaryUpdate
+ {
+ get => suppressBoundaryUpdate;
+ set
+ {
+ if (value == suppressBoundaryUpdate) return;
+ suppressBoundaryUpdate = value;
+ }
+ }
+
+ private double minWidthOnCollapsed;
+ ///
+ /// Gets or sets the minimum width of the group when it is collapsed.
+ /// This value equals the combined width of the group's proxy input and output ports.
+ ///
+ public double MinWidthOnCollapsed
+ {
+ get => minWidthOnCollapsed;
+ set
+ {
+ if (minWidthOnCollapsed == value) return;
+ minWidthOnCollapsed = value;
+ }
+ }
+
#endregion
///
@@ -510,6 +615,10 @@ private void ClearRemovedPins()
private void model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
+ // Skip boundary updates caused by connector redraws if group is collapsed
+ if (!IsExpanded && SuppressBoundaryUpdate && e.PropertyName == nameof(Position))
+ return;
+
switch (e.PropertyName)
{
case nameof(Position):
@@ -574,71 +683,103 @@ internal void UpdateGroupFrozenStatus()
///
/// Updates the group boundary based on the nodes / notes selection.
- ///
+ ///
internal void UpdateBoundaryFromSelection()
- {
+ {
var selectedModelsList = nodes.ToList();
-
- if (selectedModelsList.Any())
+ if (!selectedModelsList.Any())
{
- var groupModels = selectedModelsList.OrderBy(x => x.X).ToList();
+ // No models in the group — set dimensions to zero
+ Width = 0;
+ Height = 0;
+ return;
+ }
- //Shifting x by 10 and y to the height of textblock
- var regionX = groupModels.Min(x => x.X) - ExtendSize;
- //Increase the Y value by 10. This provides the extra space between
- // a model and textbox. Otherwise there will be some overlap
- var regionY = groupModels.Min(y => (y as NoteModel) == null ? (y.Y) : (y.Y - NoteYAdjustment)) -
- ExtendSize - (TextBlockHeight == 0.0 ? MinTextHeight : TextBlockHeight);
+ // Sort models left to right for consistent calculations
+ var groupModels = selectedModelsList.OrderBy(x => x.X).ToList();
- //calculates the distance between the nodes
- var xDistance = groupModels.Max(x => (x.X + x.Width)) - regionX;
- var yDistance = groupModels.Max(y => (y as NoteModel) == null ? (y.Y + y.Height) : (y.Y + y.Height - NoteYAdjustment)) - regionY;
-
- // InitialTop is to store the Y value without the Textblock height
- this.InitialTop = groupModels.Min(y => (y as NoteModel) == null ? (y.Y) : (y.Y - NoteYAdjustment));
+ // Determine left boundary (smallest X), shifted left by padding
+ double regionX = groupModels.Min(x => x.X) - ExtendSize;
+ // Determine top boundary, adjusted for note offset and text block height
+ double regionY = groupModels.Min(y => (y as NoteModel) == null ? y.Y : y.Y - NoteYAdjustment)
+ - ExtendSize
+ - (TextBlockHeight == 0.0 ? MinTextHeight : TextBlockHeight);
- var region = new Rect2D
- {
- X = regionX,
- Y = regionY,
- Width = xDistance + ExtendSize + WidthAdjustment,
- Height = yDistance + ExtendSize + ExtendYHeight + HeightAdjustment - TextBlockHeight
- };
+ // Compute the horizontal span of all models
+ double xDistance = groupModels.Max(x => x.X + x.Width) - regionX;
- bool positionChanged = region.X != X || region.Y != Y;
+ // Save the actual top-most Y value (before subtracting text block height)
+ this.InitialTop = groupModels.Min(y => (y as NoteModel) == null ? y.Y : y.Y - NoteYAdjustment);
- this.X = region.X;
- this.Y = region.Y;
- this.ModelAreaHeight = IsExpanded ? region.Height : ModelAreaHeight;
- Height = this.ModelAreaHeight + TextBlockHeight;
+ // Track whether position has changed
+ bool positionChanged = regionX != X || regionY != Y;
+ X = regionX;
+ Y = regionY;
- if (IsExpanded)
- {
- Width = Math.Max(region.Width, TextMaxWidth + ExtendSize);
- lastExpandedWidth = Width;
- }
- else
- {
- //If the annotation is not expanded, then it will remain the same width of the last time it was expanded
- Width = lastExpandedWidth;
- }
+ // Use different logic for expanded vs. collapsed state
+ if (IsExpanded)
+ {
+ UpdateExpandedLayout(groupModels, regionX, regionY, xDistance);
+ }
+ else
+ {
+ UpdateCollapsedLayout(xDistance);
+ }
- //Initial Height is to store the Actual height of the group.
- //that is the height should be the initial height without the textblock height.
- if (this.InitialHeight <= 0.0)
- this.InitialHeight = region.Height;
+ // Notify UI if position changed
+ if (positionChanged)
+ RaisePropertyChanged(nameof(Position));
+ }
- if (positionChanged)
- {
- RaisePropertyChanged(nameof(Position));
- }
+ ///
+ /// Calculates and sets the group size and bounds when the group is expanded.
+ /// Includes full height of contained models and padding.
+ ///
+ private void UpdateExpandedLayout(List groupModels, double regionX, double regionY, double xDistance)
+ {
+ // Compute total vertical height of models in group
+ double yDistance = groupModels.Max(y => (y as NoteModel) == null ? y.Y + y.Height : y.Y + y.Height - NoteYAdjustment)
+ - regionY;
+
+ // Define the full rectangular area of the group (excluding text block)
+ var region = new Rect2D
+ {
+ X = regionX,
+ Y = regionY,
+ Width = xDistance + ExtendSize + Math.Max(WidthAdjustment, 0),
+ Height = yDistance + ExtendSize + ExtendYHeight + HeightAdjustment - TextBlockHeight
+ };
+
+ // Store layout size and apply dimensions
+ ModelAreaHeight = region.Height;
+ Height = ModelAreaHeight + TextBlockHeight;
+ Width = Math.Max(region.Width, TextMaxWidth + ExtendSize);
+
+ // Only store the first calculated initial height
+ if (InitialHeight <= 0.0)
+ InitialHeight = region.Height;
+ }
+
+ ///
+ /// Calculates and sets the group size when collapsed.
+ /// Supports two modes: full-width collapse and minimum-size collapse.
+ ///
+ private void UpdateCollapsedLayout(double xDistance)
+ {
+ // Choose width based on collapse preference
+ if (!IsCollapsedToMinSize)
+ {
+ // Collapse vertically, keep full width
+ Width = Math.Max(xDistance + ExtendSize + WidthAdjustment, TextMaxWidth + ExtendSize);
}
else
{
- this.Width = 0;
- this.Height = 0;
+ // Fully minimize the group
+ Width = Math.Max(MinWidthOnCollapsed + ExtendSize, TextMaxWidth + ExtendSize);
}
+
+ Height = TextBlockHeight + ModelAreaHeight;
}
///
@@ -756,6 +897,10 @@ void SerializeCore(XmlElement element, SaveContext context)
helper.SetAttribute("backgrouund", (this.Background == null ? "" : this.Background.ToString()));
helper.SetAttribute(nameof(IsSelected), IsSelected);
helper.SetAttribute(nameof(IsExpanded), this.IsExpanded);
+ helper.SetAttribute(nameof(IsOptionalInPortsCollapsed), this.IsOptionalInPortsCollapsed);
+ helper.SetAttribute(nameof(IsUnconnectedOutPortsCollapsed), this.IsUnconnectedOutPortsCollapsed);
+ helper.SetAttribute(nameof(HasToggledOptionalInPorts), this.HasToggledOptionalInPorts);
+ helper.SetAttribute(nameof(HasToggledUnconnectedOutPorts), this.HasToggledUnconnectedOutPorts);
//Serialize Selected models
XmlDocument xmlDoc = element.OwnerDocument;
@@ -789,6 +934,10 @@ protected override void DeserializeCore(XmlElement element, SaveContext context)
this.InitialHeight = helper.ReadDouble("InitialHeight", DoubleValue);
this.IsSelected = helper.ReadBoolean(nameof(IsSelected), false);
this.IsExpanded = helper.ReadBoolean(nameof(IsExpanded), true);
+ this.IsOptionalInPortsCollapsed = helper.ReadBoolean(nameof(IsOptionalInPortsCollapsed), true);
+ this.IsUnconnectedOutPortsCollapsed = helper.ReadBoolean(nameof(IsUnconnectedOutPortsCollapsed), true);
+ this.HasToggledOptionalInPorts = helper.ReadBoolean(nameof(HasToggledOptionalInPorts), false);
+ this.HasToggledUnconnectedOutPorts = helper.ReadBoolean(nameof(HasToggledUnconnectedOutPorts), false);
if (IsSelected)
DynamoSelection.Instance.Selection.Add(this);
@@ -834,6 +983,8 @@ protected override void DeserializeCore(XmlElement element, SaveContext context)
RaisePropertyChanged(nameof(AnnotationText));
RaisePropertyChanged(nameof(Nodes));
RaisePropertyChanged(nameof(IsExpanded));
+ RaisePropertyChanged(nameof(IsOptionalInPortsCollapsed));
+ RaisePropertyChanged(nameof(IsUnconnectedOutPortsCollapsed));
this.ReportPosition();
}
diff --git a/src/DynamoCore/Graph/Notes/NoteModel.cs b/src/DynamoCore/Graph/Notes/NoteModel.cs
index 7a25a818b59..df482eeb4a8 100644
--- a/src/DynamoCore/Graph/Notes/NoteModel.cs
+++ b/src/DynamoCore/Graph/Notes/NoteModel.cs
@@ -146,15 +146,7 @@ protected override void DeserializeCore(XmlElement nodeElement, SaveContext cont
Text = helper.ReadString("text", "New Note");
X = helper.ReadDouble("x", 0.0);
Y = helper.ReadDouble("y", 0.0);
-
- try
- {
- PinnedNodeGuid = helper.ReadGuid("pinnedNode");
- }
- catch (Exception) { }
-
- if (pinnedNode != null && helper.ReadGuid("pinnedNode") != Guid.Empty)
- pinnedNode.GUID = helper.ReadGuid("pinnedNode");
+ PinnedNodeGuid = helper.ReadGuid("pinnedNode", Guid.Empty);
// Notify listeners that the position of the note has changed,
// then parent group will also redraw itself.
diff --git a/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs b/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs
index ba4cd6b3f14..006322d8797 100644
--- a/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs
+++ b/src/DynamoCore/Graph/Workspaces/WorkspaceModel.cs
@@ -143,6 +143,10 @@ public class ExtraAnnotationViewInfo
public string PinnedNode;
public double WidthAdjustment;
public double HeightAdjustment;
+ public bool IsOptionalInPortsCollapsed;
+ public bool IsUnconnectedOutPortsCollapsed;
+ public bool hasToggledOptionalInPorts;
+ public bool HasToggledUnconnectedOutPorts;
// TODO, Determine if these are required
public double Left;
@@ -171,7 +175,11 @@ public override bool Equals(object obj)
this.GroupStyleId == other.GroupStyleId &&
this.Background == other.Background &&
this.WidthAdjustment == other.WidthAdjustment &&
- this.HeightAdjustment == other.HeightAdjustment;
+ this.HeightAdjustment == other.HeightAdjustment &&
+ this.IsOptionalInPortsCollapsed == other.IsOptionalInPortsCollapsed &&
+ this.IsUnconnectedOutPortsCollapsed == other.IsUnconnectedOutPortsCollapsed &&
+ this.hasToggledOptionalInPorts == other.hasToggledOptionalInPorts &&
+ this.HasToggledUnconnectedOutPorts == other.HasToggledUnconnectedOutPorts;
//TODO try to get rid of these if possible
//needs investigation if we are okay letting them get
@@ -2715,6 +2723,11 @@ private void LoadAnnotation(ExtraAnnotationViewInfo annotationViewInfo)
annotationModel.GUID = annotationGuidValue;
annotationModel.HeightAdjustment = annotationViewInfo.HeightAdjustment;
annotationModel.WidthAdjustment = annotationViewInfo.WidthAdjustment;
+ annotationModel.IsOptionalInPortsCollapsed = annotationViewInfo.IsOptionalInPortsCollapsed;
+ annotationModel.IsUnconnectedOutPortsCollapsed = annotationViewInfo.IsUnconnectedOutPortsCollapsed;
+ annotationModel.HasToggledOptionalInPorts = annotationViewInfo.hasToggledOptionalInPorts;
+ annotationModel.HasToggledUnconnectedOutPorts = annotationViewInfo.HasToggledUnconnectedOutPorts;
+
annotationModel.UpdateGroupFrozenStatus();
annotationModel.ModelBaseRequested += annotationModel_GetModelBase;
diff --git a/src/DynamoCore/Models/DynamoModel.cs b/src/DynamoCore/Models/DynamoModel.cs
index 3a06686cf03..b7a5e9e79dc 100644
--- a/src/DynamoCore/Models/DynamoModel.cs
+++ b/src/DynamoCore/Models/DynamoModel.cs
@@ -3352,6 +3352,10 @@ private AnnotationModel CreateAnnotationModel(
Background = model.Background,
FontSize = model.FontSize,
GroupStyleId = model.GroupStyleId,
+ IsOptionalInPortsCollapsed = model.IsOptionalInPortsCollapsed,
+ IsUnconnectedOutPortsCollapsed = model.IsUnconnectedOutPortsCollapsed,
+ HasToggledOptionalInPorts = model.HasToggledOptionalInPorts,
+ HasToggledUnconnectedOutPorts = model.HasToggledUnconnectedOutPorts,
};
modelLookup.Add(model.GUID, annotationModel);
diff --git a/src/DynamoCore/PublicAPI.Unshipped.txt b/src/DynamoCore/PublicAPI.Unshipped.txt
index 9952591cc6b..a6f2640aeb1 100644
--- a/src/DynamoCore/PublicAPI.Unshipped.txt
+++ b/src/DynamoCore/PublicAPI.Unshipped.txt
@@ -125,6 +125,8 @@ Dynamo.Configuration.PreferenceSettings.BackupInterval.get -> int
Dynamo.Configuration.PreferenceSettings.BackupInterval.set -> void
Dynamo.Configuration.PreferenceSettings.BackupLocation.get -> string
Dynamo.Configuration.PreferenceSettings.BackupLocation.set -> void
+Dynamo.Configuration.PreferenceSettings.CollapseToMinSize.get -> bool
+Dynamo.Configuration.PreferenceSettings.CollapseToMinSize.set -> void
Dynamo.Configuration.PreferenceSettings.ConnectorType.get -> Dynamo.Graph.Connectors.ConnectorType
Dynamo.Configuration.PreferenceSettings.ConnectorType.set -> void
Dynamo.Configuration.PreferenceSettings.ConsoleHeight.get -> int
@@ -217,6 +219,8 @@ Dynamo.Configuration.PreferenceSettings.NumberFormat.get -> string
Dynamo.Configuration.PreferenceSettings.NumberFormat.set -> void
Dynamo.Configuration.PreferenceSettings.OpenFileInManualExecutionMode.get -> bool
Dynamo.Configuration.PreferenceSettings.OpenFileInManualExecutionMode.set -> void
+Dynamo.Configuration.PreferenceSettings.OptionalInPortsCollapsed.get -> bool
+Dynamo.Configuration.PreferenceSettings.OptionalInPortsCollapsed.set -> void
Dynamo.Configuration.PreferenceSettings.PackageDirectoriesToUninstall.get -> System.Collections.Generic.List
Dynamo.Configuration.PreferenceSettings.PackageDirectoriesToUninstall.set -> void
Dynamo.Configuration.PreferenceSettings.PackageDownloadTouAccepted.get -> bool
@@ -260,6 +264,8 @@ Dynamo.Configuration.PreferenceSettings.StaticFields() -> System.Collections.Gen
Dynamo.Configuration.PreferenceSettings.TemplateFilePath.get -> string
Dynamo.Configuration.PreferenceSettings.TemplateFilePath.set -> void
Dynamo.Configuration.PreferenceSettings.TrustedLocations.get -> System.Collections.Generic.List
+Dynamo.Configuration.PreferenceSettings.UnconnectedOutPortsCollapsed.get -> bool
+Dynamo.Configuration.PreferenceSettings.UnconnectedOutPortsCollapsed.set -> void
Dynamo.Configuration.PreferenceSettings.UseHardwareAcceleration.get -> bool
Dynamo.Configuration.PreferenceSettings.UseHardwareAcceleration.set -> void
Dynamo.Configuration.PreferenceSettings.UseHostScaleUnits.get -> bool
@@ -702,18 +708,30 @@ Dynamo.Graph.Annotations.AnnotationModel.GroupState.get -> Dynamo.Graph.Nodes.El
Dynamo.Graph.Annotations.AnnotationModel.GroupStyleId.get -> System.Guid
Dynamo.Graph.Annotations.AnnotationModel.GroupStyleId.set -> void
Dynamo.Graph.Annotations.AnnotationModel.HasNestedGroups.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.HasToggledOptionalInPorts.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.HasToggledOptionalInPorts.set -> void
+Dynamo.Graph.Annotations.AnnotationModel.HasToggledUnconnectedOutPorts.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.HasToggledUnconnectedOutPorts.set -> void
Dynamo.Graph.Annotations.AnnotationModel.HeightAdjustment.get -> double
Dynamo.Graph.Annotations.AnnotationModel.HeightAdjustment.set -> void
Dynamo.Graph.Annotations.AnnotationModel.InitialHeight.get -> double
Dynamo.Graph.Annotations.AnnotationModel.InitialHeight.set -> void
Dynamo.Graph.Annotations.AnnotationModel.InitialTop.get -> double
Dynamo.Graph.Annotations.AnnotationModel.InitialTop.set -> void
+Dynamo.Graph.Annotations.AnnotationModel.IsCollapsedToMinSize.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.IsCollapsedToMinSize.set -> void
Dynamo.Graph.Annotations.AnnotationModel.IsExpanded.get -> bool
Dynamo.Graph.Annotations.AnnotationModel.IsExpanded.set -> void
Dynamo.Graph.Annotations.AnnotationModel.IsFrozen.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.IsOptionalInPortsCollapsed.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.IsOptionalInPortsCollapsed.set -> void
+Dynamo.Graph.Annotations.AnnotationModel.IsUnconnectedOutPortsCollapsed.get -> bool
+Dynamo.Graph.Annotations.AnnotationModel.IsUnconnectedOutPortsCollapsed.set -> void
Dynamo.Graph.Annotations.AnnotationModel.IsVisible.get -> bool
Dynamo.Graph.Annotations.AnnotationModel.loadFromXML.get -> bool
Dynamo.Graph.Annotations.AnnotationModel.loadFromXML.set -> void
+Dynamo.Graph.Annotations.AnnotationModel.MinWidthOnCollapsed.get -> double
+Dynamo.Graph.Annotations.AnnotationModel.MinWidthOnCollapsed.set -> void
Dynamo.Graph.Annotations.AnnotationModel.ModelAreaHeight.get -> double
Dynamo.Graph.Annotations.AnnotationModel.ModelAreaHeight.set -> void
Dynamo.Graph.Annotations.AnnotationModel.ModelBaseRequested -> System.Func
@@ -1284,12 +1302,16 @@ Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.ExtraAnnotationViewInfo() -> voi
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.FontSize -> double
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.GroupStyleId -> System.Guid
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.HasNestedGroups -> bool
+Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.hasToggledOptionalInPorts -> bool
+Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.HasToggledUnconnectedOutPorts -> bool
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.Height -> double
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.HeightAdjustment -> double
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.Id -> string
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.InitialHeight -> double
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.InitialTop -> double
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.IsExpanded -> bool
+Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.IsOptionalInPortsCollapsed -> bool
+Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.IsUnconnectedOutPortsCollapsed -> bool
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.Left -> double
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.Nodes -> System.Collections.Generic.IEnumerable
Dynamo.Graph.Workspaces.ExtraAnnotationViewInfo.PinnedNode -> string
@@ -1564,6 +1586,8 @@ Dynamo.Interfaces.IPreferences.BackgroundPreviews.get -> System.Collections.Gene
Dynamo.Interfaces.IPreferences.BackgroundPreviews.set -> void
Dynamo.Interfaces.IPreferences.BackupFiles.get -> System.Collections.Generic.List
Dynamo.Interfaces.IPreferences.BackupFiles.set -> void
+Dynamo.Interfaces.IPreferences.CollapseToMinSize.get -> bool
+Dynamo.Interfaces.IPreferences.CollapseToMinSize.set -> void
Dynamo.Interfaces.IPreferences.ConnectorType.get -> Dynamo.Graph.Connectors.ConnectorType
Dynamo.Interfaces.IPreferences.ConnectorType.set -> void
Dynamo.Interfaces.IPreferences.ConsoleHeight.get -> int
@@ -1587,6 +1611,8 @@ Dynamo.Interfaces.IPreferences.MaxNumRecentFiles.get -> int
Dynamo.Interfaces.IPreferences.MaxNumRecentFiles.set -> void
Dynamo.Interfaces.IPreferences.NumberFormat.get -> string
Dynamo.Interfaces.IPreferences.NumberFormat.set -> void
+Dynamo.Interfaces.IPreferences.OptionalInPortsCollapsed.get -> bool
+Dynamo.Interfaces.IPreferences.OptionalInPortsCollapsed.set -> void
Dynamo.Interfaces.IPreferences.PackageDirectoriesToUninstall.get -> System.Collections.Generic.List
Dynamo.Interfaces.IPreferences.PackageDirectoriesToUninstall.set -> void
Dynamo.Interfaces.IPreferences.PythonTemplateFilePath.get -> string
@@ -1605,6 +1631,8 @@ Dynamo.Interfaces.IPreferences.ShowPreviewBubbles.get -> bool
Dynamo.Interfaces.IPreferences.ShowPreviewBubbles.set -> void
Dynamo.Interfaces.IPreferences.TemplateFilePath.get -> string
Dynamo.Interfaces.IPreferences.TemplateFilePath.set -> void
+Dynamo.Interfaces.IPreferences.UnconnectedOutPortsCollapsed.get -> bool
+Dynamo.Interfaces.IPreferences.UnconnectedOutPortsCollapsed.set -> void
Dynamo.Interfaces.IPreferences.WindowH.get -> double
Dynamo.Interfaces.IPreferences.WindowH.set -> void
Dynamo.Interfaces.IPreferences.WindowW.get -> double
diff --git a/src/DynamoCoreWpf/Properties/Resources.Designer.cs b/src/DynamoCoreWpf/Properties/Resources.Designer.cs
index b1da786f996..a99c354141f 100644
--- a/src/DynamoCoreWpf/Properties/Resources.Designer.cs
+++ b/src/DynamoCoreWpf/Properties/Resources.Designer.cs
@@ -3758,6 +3758,15 @@ public static string GroupNameDefaultText {
}
}
+ ///
+ /// Looks up a localized string similar to Optional.
+ ///
+ public static string GroupOptionalInportsText {
+ get {
+ return ResourceManager.GetString("GroupOptionalInportsText", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Group Style.
///
@@ -3794,6 +3803,15 @@ public static string GroupStylesSaveButtonText {
}
}
+ ///
+ /// Looks up a localized string similar to Unconnected.
+ ///
+ public static string GroupUnconnectedOutportsText {
+ get {
+ return ResourceManager.GetString("GroupUnconnectedOutportsText", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Hide Classic Node Library.
///
@@ -8005,6 +8023,24 @@ public static string PreferencesViewAlreadyExistingStyleWarning {
}
}
+ ///
+ /// Looks up a localized string similar to Collapsed Group.
+ ///
+ public static string PreferencesViewCollapsedGroupHeader {
+ get {
+ return ResourceManager.GetString("PreferencesViewCollapsedGroupHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Collapse to minimal size by default.
+ ///
+ public static string PreferencesViewCollapseToMinSizeDescription {
+ get {
+ return ResourceManager.GetString("PreferencesViewCollapseToMinSizeDescription", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Default Python Engine.
///
@@ -8216,6 +8252,24 @@ public static string PreferencesViewGroupStylesHeader {
}
}
+ ///
+ /// Looks up a localized string similar to Hide optional input ports by default.
+ ///
+ public static string PreferencesViewHideInportsDescription {
+ get {
+ return ResourceManager.GetString("PreferencesViewHideInportsDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Hide unconnected output ports by default.
+ ///
+ public static string PreferencesViewHideOutportsDescription {
+ get {
+ return ResourceManager.GetString("PreferencesViewHideOutportsDescription", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to When toggled on, file names of exported images include date and time of export..
///
diff --git a/src/DynamoCoreWpf/Properties/Resources.en-US.resx b/src/DynamoCoreWpf/Properties/Resources.en-US.resx
index 21873a1f2fb..cb3825e4480 100644
--- a/src/DynamoCoreWpf/Properties/Resources.en-US.resx
+++ b/src/DynamoCoreWpf/Properties/Resources.en-US.resx
@@ -4176,4 +4176,22 @@ To make this file into a new template, save it to a different folder, then move
Node Icon Data is dumped to \"{0}\".
Debug menu | Dump all node icons
+
+ Optional
+
+
+ Unconnected
+
+
+ Collapsed Group
+
+
+ Collapse to minimal size by default
+
+
+ Hide optional input ports by default
+
+
+ Hide unconnected output ports by default
+
diff --git a/src/DynamoCoreWpf/Properties/Resources.resx b/src/DynamoCoreWpf/Properties/Resources.resx
index 61b60a37d3a..2f4a4436efc 100644
--- a/src/DynamoCoreWpf/Properties/Resources.resx
+++ b/src/DynamoCoreWpf/Properties/Resources.resx
@@ -4160,4 +4160,22 @@ To make this file into a new template, save it to a different folder, then move
Node Icon Data is dumped to \"{0}\".
Debug menu | Dump all node icons
+
+ Optional
+
+
+ Unconnected
+
+
+ Collapsed Group
+
+
+ Collapse to minimal size by default
+
+
+ Hide optional input ports by default
+
+
+ Hide unconnected output ports by default
+
diff --git a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt
index 121fb9df7c9..68495daea94 100644
--- a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt
+++ b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt
@@ -53,6 +53,10 @@ Dynamo.Controls.BooleanNegationConverter
Dynamo.Controls.BooleanNegationConverter.BooleanNegationConverter() -> void
Dynamo.Controls.BooleanNegationConverter.Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) -> object
Dynamo.Controls.BooleanNegationConverter.ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) -> object
+Dynamo.Controls.BooleanToAngleConverter
+Dynamo.Controls.BooleanToAngleConverter.BooleanToAngleConverter() -> void
+Dynamo.Controls.BooleanToAngleConverter.Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) -> object
+Dynamo.Controls.BooleanToAngleConverter.ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) -> object
Dynamo.Controls.BooleanToBrushConverter
Dynamo.Controls.BooleanToBrushConverter.BooleanToBrushConverter() -> void
Dynamo.Controls.BooleanToBrushConverter.Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture) -> object
@@ -1775,6 +1779,10 @@ Dynamo.ViewModels.AnnotationViewModel.Height.set -> void
Dynamo.ViewModels.AnnotationViewModel.InPorts.get -> System.Collections.ObjectModel.ObservableCollection
Dynamo.ViewModels.AnnotationViewModel.IsExpanded.get -> bool
Dynamo.ViewModels.AnnotationViewModel.IsExpanded.set -> void
+Dynamo.ViewModels.AnnotationViewModel.IsOptionalInPortsCollapsed.get -> bool
+Dynamo.ViewModels.AnnotationViewModel.IsOptionalInPortsCollapsed.set -> void
+Dynamo.ViewModels.AnnotationViewModel.IsUnconnectedOutPortsCollapsed.get -> bool
+Dynamo.ViewModels.AnnotationViewModel.IsUnconnectedOutPortsCollapsed.set -> void
Dynamo.ViewModels.AnnotationViewModel.Left.get -> double
Dynamo.ViewModels.AnnotationViewModel.Left.set -> void
Dynamo.ViewModels.AnnotationViewModel.ModelAreaHeight.get -> double
@@ -1787,6 +1795,7 @@ Dynamo.ViewModels.AnnotationViewModel.NodeContentCount.get -> int
Dynamo.ViewModels.AnnotationViewModel.NodeHoveringState.get -> bool
Dynamo.ViewModels.AnnotationViewModel.NodeHoveringState.set -> void
Dynamo.ViewModels.AnnotationViewModel.Nodes.get -> System.Collections.Generic.IEnumerable
+Dynamo.ViewModels.AnnotationViewModel.OptionalInPorts.get -> System.Collections.ObjectModel.ObservableCollection
Dynamo.ViewModels.AnnotationViewModel.OutPorts.get -> System.Collections.ObjectModel.ObservableCollection
Dynamo.ViewModels.AnnotationViewModel.PreviewState.get -> Dynamo.ViewModels.PreviewState
Dynamo.ViewModels.AnnotationViewModel.RemoveGroupFromGroupCommand.get -> Dynamo.UI.Commands.DelegateCommand
@@ -1796,6 +1805,7 @@ Dynamo.ViewModels.AnnotationViewModel.ToggleIsFrozenGroupCommand.get -> Dynamo.U
Dynamo.ViewModels.AnnotationViewModel.ToggleIsVisibleGroupCommand.get -> Dynamo.UI.Commands.DelegateCommand
Dynamo.ViewModels.AnnotationViewModel.Top.get -> double
Dynamo.ViewModels.AnnotationViewModel.Top.set -> void
+Dynamo.ViewModels.AnnotationViewModel.UnconnectedOutPorts.get -> System.Collections.ObjectModel.ObservableCollection
Dynamo.ViewModels.AnnotationViewModel.Width.get -> double
Dynamo.ViewModels.AnnotationViewModel.Width.set -> void
Dynamo.ViewModels.AnnotationViewModel.ZIndex.get -> double
@@ -2826,6 +2836,8 @@ Dynamo.ViewModels.PreferencesViewModel.BackupLocation.get -> string
Dynamo.ViewModels.PreferencesViewModel.BackupLocation.set -> void
Dynamo.ViewModels.PreferencesViewModel.CanResetBackupLocation.get -> bool
Dynamo.ViewModels.PreferencesViewModel.CanResetTemplateLocation.get -> bool
+Dynamo.ViewModels.PreferencesViewModel.CollapseToMinSize.get -> bool
+Dynamo.ViewModels.PreferencesViewModel.CollapseToMinSize.set -> void
Dynamo.ViewModels.PreferencesViewModel.CopyToClipboardCommand.get -> Dynamo.UI.Commands.DelegateCommand
Dynamo.ViewModels.PreferencesViewModel.CopyToClipboardCommand.set -> void
Dynamo.ViewModels.PreferencesViewModel.CurrentWarningMessage.get -> string
@@ -2888,6 +2900,8 @@ Dynamo.ViewModels.PreferencesViewModel.NotificationCenterIsChecked.get -> bool
Dynamo.ViewModels.PreferencesViewModel.NotificationCenterIsChecked.set -> void
Dynamo.ViewModels.PreferencesViewModel.NumberFormatList.get -> System.Collections.ObjectModel.ObservableCollection
Dynamo.ViewModels.PreferencesViewModel.NumberFormatList.set -> void
+Dynamo.ViewModels.PreferencesViewModel.OptionalInputsCollapsed.get -> bool
+Dynamo.ViewModels.PreferencesViewModel.OptionalInputsCollapsed.set -> void
Dynamo.ViewModels.PreferencesViewModel.OptionsGeometryScale.get -> Dynamo.ViewModels.GeometryScalingOptions
Dynamo.ViewModels.PreferencesViewModel.OptionsGeometryScale.set -> void
Dynamo.ViewModels.PreferencesViewModel.PackagePathsForInstall.get -> System.Collections.ObjectModel.ObservableCollection
@@ -2932,6 +2946,8 @@ Dynamo.ViewModels.PreferencesViewModel.ShowDefaultGroupDescription.get -> bool
Dynamo.ViewModels.PreferencesViewModel.ShowDefaultGroupDescription.set -> void
Dynamo.ViewModels.PreferencesViewModel.ShowEdges.get -> bool
Dynamo.ViewModels.PreferencesViewModel.ShowEdges.set -> void
+Dynamo.ViewModels.PreferencesViewModel.UnconnectedOutputsCollapsed.get -> bool
+Dynamo.ViewModels.PreferencesViewModel.UnconnectedOutputsCollapsed.set -> void
Dynamo.ViewModels.PreferencesViewModel.UseRenderInstancing.get -> bool
Dynamo.ViewModels.PreferencesViewModel.UseRenderInstancing.set -> void
Dynamo.ViewModels.PreferencesViewModel.ShowPreviewBubbles.get -> bool
@@ -4934,10 +4950,12 @@ static Dynamo.Wpf.Properties.Resources.GroupContextMenuUngroup.get -> string
static Dynamo.Wpf.Properties.Resources.GroupDefaultText.get -> string
static Dynamo.Wpf.Properties.Resources.GroupFrozenButtonToolTip.get -> string
static Dynamo.Wpf.Properties.Resources.GroupNameDefaultText.get -> string
+static Dynamo.Wpf.Properties.Resources.GroupOptionalInportsText.get -> string
static Dynamo.Wpf.Properties.Resources.GroupStyleContextAnnotation.get -> string
static Dynamo.Wpf.Properties.Resources.GroupStyleFontSizeToolTip.get -> string
static Dynamo.Wpf.Properties.Resources.GroupStylesCancelButtonText.get -> string
static Dynamo.Wpf.Properties.Resources.GroupStylesSaveButtonText.get -> string
+static Dynamo.Wpf.Properties.Resources.GroupUnconnectedOutportsText.get -> string
static Dynamo.Wpf.Properties.Resources.HideClassicNodeLibrary.get -> string
static Dynamo.Wpf.Properties.Resources.HideWiresPopupMenuItem.get -> string
static Dynamo.Wpf.Properties.Resources.IDSDKErrorMessage.get -> string
@@ -5400,6 +5418,8 @@ static Dynamo.Wpf.Properties.Resources.PreferencesSettingUpdateTemplateLocationT
static Dynamo.Wpf.Properties.Resources.PreferencesUseHostScaleUnits.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesUseHostScaleUnitsToolTip.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewAlreadyExistingStyleWarning.get -> string
+static Dynamo.Wpf.Properties.Resources.PreferencesViewCollapsedGroupHeader.get -> string
+static Dynamo.Wpf.Properties.Resources.PreferencesViewCollapseToMinSizeDescription.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewDefaultPythonEngine.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewDefaultRunSettingsInfoTooltip.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewDisableBuiltInPackages.get -> string
@@ -5421,6 +5441,8 @@ static Dynamo.Wpf.Properties.Resources.PreferencesViewGeneralSettingsRun.get ->
static Dynamo.Wpf.Properties.Resources.PreferencesViewGeneralSettingsTemplate.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewGeneralTab.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewGroupStylesHeader.get -> string
+static Dynamo.Wpf.Properties.Resources.PreferencesViewHideInportsDescription.get -> string
+static Dynamo.Wpf.Properties.Resources.PreferencesViewHideOutportsDescription.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewIncludeTimestampExportPathTooltip.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewLanguageLabel.get -> string
static Dynamo.Wpf.Properties.Resources.PreferencesViewLanguageSwitchHelp.get -> string
diff --git a/src/DynamoCoreWpf/UI/Converters.cs b/src/DynamoCoreWpf/UI/Converters.cs
index 8b67604d49c..13c8dc946e8 100644
--- a/src/DynamoCoreWpf/UI/Converters.cs
+++ b/src/DynamoCoreWpf/UI/Converters.cs
@@ -3917,21 +3917,37 @@ public object ConvertBack(object value, Type targetType, object parameter, Syste
}
///
- /// Returns a dark or light color depending on the contrast ration of the color with the background color
- /// Contrast ration should be larger than 4.5:1
+ /// Returns a dark or light color depending on the contrast ratio of the color with the background color
+ /// Contrast ratio should be larger than 4.5:1
/// Contrast calculation algorithm from https://stackoverflow.com/questions/70187918/adapt-given-color-pairs-to-adhere-to-w3c-accessibility-standard-for-epubs/70192373#70192373
+ ///
+ /// Expected values for controlType:
+ /// - "groupPortToggle" : applies alternate dark color for optional/unconnected port toggle controls
+ /// - null : applies default dark color for annotation text and description
///
public class TextForegroundSaturationColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
+ var controlType = parameter as string;
+
+ // Text and description colors
var lightColor = (System.Windows.Media.Color)SharedDictionaryManager.DynamoColorsAndBrushesDictionary["WhiteColor"];
var darkColor = (System.Windows.Media.Color)SharedDictionaryManager.DynamoColorsAndBrushesDictionary["DarkerGrey"];
+ // Port toggle colors
+ var lightColorToggle = (System.Windows.Media.Color)SharedDictionaryManager.DynamoColorsAndBrushesDictionary["Blue350"];
+ var darkColorToggle = (System.Windows.Media.Color)SharedDictionaryManager.DynamoColorsAndBrushesDictionary["Blue450"];
var backgroundColor = (System.Windows.Media.Color)value;
-
var contrastRatio = GetContrastRatio(darkColor, backgroundColor);
+ // Custom scheme override
+ if (controlType == "groupPortToggle")
+ {
+ lightColor = lightColorToggle;
+ darkColor = darkColorToggle;
+ }
+
return contrastRatio < 4.5 ? new SolidColorBrush(lightColor) : new SolidColorBrush(darkColor);
}
@@ -4336,4 +4352,21 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu
throw new NotImplementedException();
}
}
+
+ ///
+ /// Converts a boolean value to an angle for rotating an arrow icon.
+ /// Returns 0° when true (expanded), and 180° when false (collapsed).
+ ///
+ public class BooleanToAngleConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+ {
+ return (value is bool isChecked && isChecked) ? 0.0 : 180.0;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+ {
+ return Binding.DoNothing;
+ }
+ }
}
diff --git a/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs
index 1b054f611d5..02006c00f0d 100644
--- a/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs
+++ b/src/DynamoCoreWpf/ViewModels/Core/AnnotationViewModel.cs
@@ -2,13 +2,18 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
+using System.ComponentModel;
using System.Linq;
+using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
+using System.Windows.Threading;
using Dynamo.Configuration;
using Dynamo.Graph;
using Dynamo.Graph.Annotations;
using Dynamo.Graph.Nodes;
+using Dynamo.Graph.Notes;
+using Dynamo.Interfaces;
using Dynamo.Logging;
using Dynamo.Models;
using Dynamo.Selection;
@@ -32,9 +37,21 @@ public class AnnotationViewModel : ViewModelBase
// vertical offset accounts for the port margins
private const int verticalOffset = 17;
private const int portVerticalMidPoint = 17;
+ private const int portToggleOffset = 30;
private ObservableCollection groupStyleList;
private IEnumerable preferencesStyleItemsList;
private PreferenceSettings preferenceSettings;
+ private double heightBeforeToggle;
+ private double widthBeforeToggle;
+
+ // Collapsed proxy ports for Code Block Nodes appear visually misaligned - 0.655px
+ // taller compared to their actual ports. This is due to the fixed height - 16.345px
+ // used inside CBNs for code lines, while proxy ports use 14px height + 3px top margin.
+ // To compensate for this visual mismatch and keep connector alignment consistent,
+ // we apply this adjusted proxy height.
+ private const double CBNProxyPortVisualHeight = 17;
+ private const double MinSpacing = 50;
+ private const double MinChangeThreshold = 1;
public readonly WorkspaceViewModel WorkspaceViewModel;
@@ -236,6 +253,21 @@ private set
}
}
+ private ObservableCollection optionalInPorts;
+ ///
+ /// Collection of optional input ports.
+ /// These are inputs using default values or unconnected.
+ ///
+ [JsonIgnore]
+ public ObservableCollection OptionalInPorts
+ {
+ get => optionalInPorts;
+ private set
+ {
+ optionalInPorts = value;
+ }
+ }
+
private ObservableCollection outPorts;
///
/// Collection of all output ports on this group.
@@ -253,6 +285,74 @@ private set
}
}
+ private ObservableCollection unconnectedOutPorts;
+ ///
+ /// Collection of unconnected output ports in the group.
+ ///
+ [JsonIgnore]
+ public ObservableCollection UnconnectedOutPorts
+ {
+ get => unconnectedOutPorts;
+ private set
+ {
+ unconnectedOutPorts = value;
+ }
+ }
+
+ private bool isOptionalInPortsCollapsed;
+ ///
+ /// Controls visibility of optional input ports in the group.
+ ///
+ [JsonIgnore]
+ public bool IsOptionalInPortsCollapsed
+ {
+ get => isOptionalInPortsCollapsed;
+ set
+ {
+ if (isOptionalInPortsCollapsed == value) return;
+
+ // Record for undo
+ var undoRecorder = WorkspaceViewModel.Model.UndoRecorder;
+ using (undoRecorder.BeginActionGroup())
+ undoRecorder.RecordModificationForUndo(annotationModel);
+
+ isOptionalInPortsCollapsed = value;
+ annotationModel.IsOptionalInPortsCollapsed = value;
+
+ RaisePropertyChanged(nameof(IsOptionalInPortsCollapsed));
+ WorkspaceViewModel.HasUnsavedChanges = true;
+
+ HandlePrePortToggleLayout();
+ }
+ }
+
+ private bool isUnconnectedOutPortsCollapsed;
+ ///
+ /// Controls visibility of unconnected output ports in the group.
+ ///
+ public bool IsUnconnectedOutPortsCollapsed
+ {
+ get => isUnconnectedOutPortsCollapsed;
+ set
+ {
+ if (isUnconnectedOutPortsCollapsed == value) return;
+
+ // Record for undo
+ var undoRecorder = WorkspaceViewModel.Model.UndoRecorder;
+ using (undoRecorder.BeginActionGroup())
+ undoRecorder.RecordModificationForUndo(annotationModel);
+
+
+ isUnconnectedOutPortsCollapsed = value;
+ annotationModel.IsUnconnectedOutPortsCollapsed = value;
+
+ RaisePropertyChanged(nameof(IsUnconnectedOutPortsCollapsed));
+ WorkspaceViewModel.HasUnsavedChanges = true;
+
+ HandlePrePortToggleLayout();
+ }
+ }
+
///
/// Gets or sets the models IsExpanded property.
/// When set it will either show all of the groups node
@@ -277,6 +377,7 @@ public bool IsExpanded
// Methods to collapse or expand the group based on the new value of IsExpanded.
ManageAnnotationMVExpansionAndCollapse();
+ HandlePrePortToggleLayout();
}
}
@@ -625,6 +726,16 @@ public AnnotationViewModel(WorkspaceViewModel workspaceViewModel, AnnotationMode
this.WorkspaceViewModel = workspaceViewModel;
this.preferenceSettings = WorkspaceViewModel.DynamoViewModel.PreferenceSettings;
+ preferenceSettings.PropertyChanged += OnPreferenceChanged;
+
+ isOptionalInPortsCollapsed = annotationModel.HasToggledOptionalInPorts
+ ? annotationModel.IsOptionalInPortsCollapsed
+ : preferenceSettings.OptionalInPortsCollapsed;
+
+ isUnconnectedOutPortsCollapsed = annotationModel.HasToggledUnconnectedOutPorts
+ ? annotationModel.IsUnconnectedOutPortsCollapsed
+ : preferenceSettings.UnconnectedOutPortsCollapsed;
+
model.PropertyChanged += model_PropertyChanged;
model.RemovedFromGroup += OnModelRemovedFromGroup;
model.AddedToGroup += OnModelAddedToGroup;
@@ -645,6 +756,8 @@ public AnnotationViewModel(WorkspaceViewModel workspaceViewModel, AnnotationMode
InPorts = new ObservableCollection();
OutPorts = new ObservableCollection();
+ OptionalInPorts = new ObservableCollection();
+ UnconnectedOutPorts = new ObservableCollection();
ViewModelBases = this.WorkspaceViewModel.GetViewModelsInternal(annotationModel.Nodes.Select(x => x.GUID));
@@ -669,8 +782,40 @@ public AnnotationViewModel(WorkspaceViewModel workspaceViewModel, AnnotationMode
groupStyleList = new ObservableCollection();
//This will add the GroupStyles created in Preferences panel to the Group Style Context menu.
LoadGroupStylesFromPreferences(preferenceSettings.GroupStyleItemsList);
+
+ // Passes the CollapseToMinSize from PreferenceSettings to the model
+ if (preferenceSettings.CollapseToMinSize)
+ {
+ annotationModel.IsCollapsedToMinSize = true;
+ }
}
+ private void OnPreferenceChanged(object sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(IPreferences.OptionalInPortsCollapsed):
+ if (!annotationModel.HasToggledOptionalInPorts)
+ {
+ IsOptionalInPortsCollapsed = preferenceSettings.OptionalInPortsCollapsed;
+ }
+ break;
+ case nameof(IPreferences.UnconnectedOutPortsCollapsed):
+ if (!annotationModel.HasToggledUnconnectedOutPorts)
+ {
+ IsUnconnectedOutPortsCollapsed = preferenceSettings.UnconnectedOutPortsCollapsed;
+ }
+ break;
+ case nameof(IPreferences.CollapseToMinSize):
+ annotationModel.IsCollapsedToMinSize = preferenceSettings.CollapseToMinSize;
+ // Update the boundary only if the group is collapsed
+ if (!IsExpanded)
+ {
+ annotationModel.UpdateBoundaryFromSelection();
+ }
+ break;
+ }
+ }
///
/// Creates input ports for the group based on its Nodes.
@@ -680,7 +825,8 @@ public AnnotationViewModel(WorkspaceViewModel workspaceViewModel, AnnotationMode
///
private void SetGroupInputPorts()
{
- List newPortViewModels;
+ List mainPortViewModels;
+ List optionalPortViewModels;
// we need to store the original ports here
// as we need those later for when we
@@ -703,11 +849,15 @@ private void SetGroupInputPorts()
// visually add them to the group but they
// should still reference their NodeModel
// owner
- newPortViewModels = CreateProxyInPorts(originalInPorts);
+ var newPortViewModels = CreateProxyInPorts(originalInPorts);
+ mainPortViewModels = newPortViewModels.Main;
+ optionalPortViewModels = newPortViewModels.Optional;
- if (newPortViewModels == null) return;
- InPorts.AddRange(newPortViewModels);
- return;
+ if (mainPortViewModels != null)
+ InPorts.AddRange(mainPortViewModels);
+
+ if (optionalPortViewModels != null)
+ OptionalInPorts.AddRange(optionalPortViewModels);
}
///
@@ -717,10 +867,11 @@ private void SetGroupInputPorts()
///
private void SetGroupOutPorts()
{
- List newPortViewModels;
+ List mainPortViewModels;
+ List unconnectedPortViewModels;
// we need to store the original ports here
- // as we need thoese later for when we
+ // as we need those later for when we
// need to collapse the groups content
if (this.AnnotationModel.HasNestedGroups)
{
@@ -740,11 +891,15 @@ private void SetGroupOutPorts()
// visually add them to the group but they
// should still reference their NodeModel
// owner
- newPortViewModels = CreateProxyOutPorts(originalOutPorts);
+ var newPortViewModels = CreateProxyOutPorts(originalOutPorts);
+ mainPortViewModels = newPortViewModels.Main;
+ unconnectedPortViewModels = newPortViewModels.Unconnected;
- if (newPortViewModels == null) return;
- OutPorts.AddRange(newPortViewModels);
- return;
+ if (mainPortViewModels != null)
+ OutPorts.AddRange(mainPortViewModels);
+
+ if (unconnectedPortViewModels != null)
+ UnconnectedOutPorts.AddRange(unconnectedPortViewModels);
}
internal IEnumerable GetGroupInPorts(IEnumerable ownerNodes = null)
@@ -819,57 +974,192 @@ private Point2D CalculatePortPosition(PortModel portModel, double verticalPositi
return new Point2D();
}
- private List CreateProxyInPorts(IEnumerable groupPortModels)
+ private (List Main, List Optional) CreateProxyInPorts(IEnumerable groupPortModels)
{
var originalPortViewModels = WorkspaceViewModel.Nodes
.SelectMany(x => x.InPorts)
.Where(x => groupPortModels.Contains(x.PortModel))
.ToList();
- var newPortViewModels = new List();
+ var mainPortViewModels = new List();
+ var optionalPortViewModels = new List();
+
double verticalPosition = 0;
+
foreach (var groupPort in groupPortModels)
{
+ // Track proxy connection changes while group is collapsed
+ groupPort.PropertyChanged += OnPortConnectionChanged;
+
var originalPort = originalPortViewModels.FirstOrDefault(x => x.PortModel.GUID == groupPort.GUID);
if (originalPort != null)
{
var portViewModel = originalPort.CreateProxyPortViewModel(groupPort);
- newPortViewModels.Add(portViewModel);
- // calculate new position for the proxy outports
- groupPort.Center = CalculatePortPosition(groupPort, verticalPosition);
- verticalPosition += originalPort.Height;
- AttachProxyPortEventHandlers(portViewModel);
+ if (!originalPort.UsingDefaultValue || groupPort.Connectors.Any())
+ {
+ mainPortViewModels.Add(portViewModel);
+
+ // Calculate new position for the proxy inports
+ groupPort.Center = CalculatePortPosition(groupPort, verticalPosition);
+ verticalPosition += originalPort.Height;
+ }
+ else
+ {
+ // Defer position setting for optional (unconnected) ports
+ optionalPortViewModels.Add(portViewModel);
+ }
}
}
- return newPortViewModels;
+ // Leave space for toggle button
+ verticalPosition += portToggleOffset;
+
+ // Position optional input ports
+ foreach (var portViewModel in optionalPortViewModels)
+ {
+ var groupPort = portViewModel.PortModel;
+ groupPort.Center = CalculatePortPosition(groupPort, verticalPosition);
+ verticalPosition += groupPort.Height;
+ }
+
+ return (mainPortViewModels, optionalPortViewModels);
}
- private List CreateProxyOutPorts(IEnumerable groupPortModels)
+ private (List Main, List Unconnected) CreateProxyOutPorts(IEnumerable groupPortModels)
{
var originalPortViewModels = WorkspaceViewModel.Nodes
.SelectMany(x => x.OutPorts)
.Where(x => groupPortModels.Contains(x.PortModel))
.ToList();
- var newPortViewModels = new List();
+ var mainPortViewModels = new List();
+ var unconnectedPortViewModels = new List();
+
double verticalPosition = 0;
- foreach (var group in groupPortModels)
+
+ foreach (var groupPort in groupPortModels)
{
- var originalPort = originalPortViewModels.FirstOrDefault(x => x.PortModel.GUID == group.GUID);
+ // Track proxy connection changes while group is collapsed
+ groupPort.PropertyChanged += OnPortConnectionChanged;
+
+ var originalPort = originalPortViewModels.FirstOrDefault(x => x.PortModel.GUID == groupPort.GUID);
if (originalPort != null)
{
- var portViewModel = originalPort.CreateProxyPortViewModel(group);
- newPortViewModels.Add(portViewModel);
- // calculate new position for the proxy outports
- group.Center = CalculatePortPosition(group, verticalPosition);
- verticalPosition += originalPort.Height;
+ var portViewModel = originalPort.CreateProxyPortViewModel(groupPort);
+
+ if (originalPort.IsConnected)
+ {
+ mainPortViewModels.Add(portViewModel);
- AttachProxyPortEventHandlers(portViewModel);
+ // Calculate new position for the proxy inports
+ groupPort.Center = CalculatePortPosition(groupPort, verticalPosition);
+ verticalPosition += originalPort.Height;
+ }
+ else
+ {
+ // Defer position setting for unconnected ports
+ unconnectedPortViewModels.Add(portViewModel);
+ }
}
}
+ // Leave space for toggle button
+ verticalPosition += portToggleOffset;
- return newPortViewModels;
+ // Position unconnected output ports
+ foreach (var portViewModel in unconnectedPortViewModels)
+ {
+ var groupPort = portViewModel.PortModel;
+ groupPort.Center = CalculatePortPosition(groupPort, verticalPosition);
+ verticalPosition += groupPort.Height;
+ }
+
+ return (mainPortViewModels, unconnectedPortViewModels);
+ }
+
+ private void OnPortConnectionChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(PortModel.IsConnected)) return;
+ if (sender is not PortModel port) return;
+
+ Application.Current.Dispatcher.BeginInvoke(() =>
+ {
+ var proxyPortVM = FindPortViewModel(port);
+ if (proxyPortVM == null) return;
+
+ bool updatedInputs = false;
+ bool updatedOutputs = false;
+
+ if (port.PortType == PortType.Input)
+ {
+ // Connected input ports should be in the InPorts collection.
+ if (port.Connectors.Any() && OptionalInPorts.Contains(proxyPortVM))
+ {
+ OptionalInPorts.Remove(proxyPortVM);
+ InPorts.Add(proxyPortVM);
+ updatedInputs = true;
+ }
+ // Disconnected optional ports using default value go to OptionalInPorts.
+ else if (!port.Connectors.Any() && port.UsingDefaultValue)
+ {
+ InPorts.Remove(proxyPortVM);
+ OptionalInPorts.Add(proxyPortVM);
+ updatedInputs = true;
+ }
+ }
+
+ if (port.PortType == PortType.Output)
+ {
+ if (port.IsConnected && UnconnectedOutPorts.Contains(proxyPortVM))
+ {
+ UnconnectedOutPorts.Remove(proxyPortVM);
+ OutPorts.Add(proxyPortVM);
+ updatedOutputs = true;
+ }
+ else if (!port.IsConnected && !UnconnectedOutPorts.Contains(proxyPortVM))
+ {
+ OutPorts.Remove(proxyPortVM);
+ UnconnectedOutPorts.Add(proxyPortVM);
+ updatedOutputs = true;
+ }
+ }
+
+ if (updatedInputs)
+ {
+ UpdateProxyPortsPosition();
+ RaisePropertyChanged(nameof(InPorts));
+ RaisePropertyChanged(nameof(OptionalInPorts));
+ }
+ if (updatedOutputs)
+ {
+
+ UpdateProxyPortsPosition();
+ RaisePropertyChanged(nameof(OutPorts));
+ RaisePropertyChanged(nameof(UnconnectedOutPorts));
+ }
+ });
+ }
+
+ private PortViewModel FindPortViewModel(PortModel model)
+ {
+ return OutPorts.Concat(UnconnectedOutPorts)
+ .Concat(InPorts)
+ .Concat(OptionalInPorts)
+ .FirstOrDefault(pvm => pvm.PortModel == model);
+ }
+
+ private void UnsubscribeFromProxyPortEvents()
+ {
+ if (originalInPorts != null)
+ {
+ foreach (var port in originalInPorts)
+ port.PropertyChanged -= OnPortConnectionChanged;
+ }
+
+ if (originalOutPorts != null)
+ {
+ foreach (var port in originalOutPorts)
+ port.PropertyChanged -= OnPortConnectionChanged;
+ }
}
#region Proxy Port Snapping Events
@@ -948,16 +1238,43 @@ internal void UpdateProxyPortsPosition()
}
verticalPosition = 0;
- for (int i = 0; i < outPorts.Count(); i++)
+ // Update all input ports
+ verticalPosition = PositionPorts(inPorts, verticalPosition);
+ verticalPosition += portToggleOffset;
+ verticalPosition = PositionPorts(optionalInPorts, verticalPosition);
+
+ // Reset vertical position for output ports
+ verticalPosition = 0;
+
+ verticalPosition = PositionPorts(outPorts, verticalPosition);
+ verticalPosition += portToggleOffset;
+ PositionPorts(unconnectedOutPorts, verticalPosition);
+ }
+
+ private double PositionPorts(IEnumerable portViewModels, double startY)
+ {
+ double y = startY;
+
+ foreach (var portVM in portViewModels)
{
- var model = outPorts[i]?.PortModel;
- if (model != null && model.IsProxyPort)
+ var model = portVM?.PortModel;
+ if (model?.IsProxyPort == true)
{
- // calculate new position for the proxy outports.
- model.Center = CalculatePortPosition(model, verticalPosition);
- verticalPosition += model.Height;
+ model.Center = CalculatePortPosition(model, y);
+
+ bool isCondensedCBN = model.Owner is CodeBlockNodeModel &&
+ !InPorts.Contains(portVM) &&
+ !OptionalInPorts.Contains(portVM);
+
+ double height = isCondensedCBN ?
+ CBNProxyPortVisualHeight :
+ model.Height;
+
+ y += height;
}
}
+
+ return y;
}
internal void ClearSelection()
@@ -1038,8 +1355,15 @@ private void CollapseConnectors()
}
}
- private void RedrawConnectors()
+ private void RedrawConnectors(bool isCollapsedCheck = false)
{
+ if (isCollapsedCheck && IsExpanded)
+ return;
+
+ // Suppress boundary updates while redrawing connectors to avoid
+ // redundant UpdateBoundaryFromSelection calls caused by connector repositioning
+ annotationModel.SuppressBoundaryUpdate = true;
+
var allNodes = this.Nodes
.OfType()
.SelectMany(x => x.Nodes.OfType())
@@ -1055,6 +1379,9 @@ private void RedrawConnectors()
connectorViewModel.Redraw();
connector.Start.Owner.ReportPosition();
}
+
+ // Re-enable boundary updates once internal redraw is complete
+ annotationModel.SuppressBoundaryUpdate = false;
}
///
@@ -1122,15 +1449,18 @@ private void UpdateConnectorsAndPortsOnShowContents(IEnumerable nodes
///
private void ManageAnnotationMVExpansionAndCollapse()
{
- if (InPorts.Any() || OutPorts.Any())
+ if (InPorts.Any() || OutPorts.Any() || OptionalInPorts.Any() || UnconnectedOutPorts.Any())
{
DetachProxyPortEventHandlers();
InPorts.Clear();
OutPorts.Clear();
+ OptionalInPorts.Clear();
+ UnconnectedOutPorts.Clear();
}
if (annotationModel.IsExpanded)
{
+ UnsubscribeFromProxyPortEvents();
this.ShowGroupContents();
}
else
@@ -1138,6 +1468,7 @@ private void ManageAnnotationMVExpansionAndCollapse()
this.SetGroupInputPorts();
this.SetGroupOutPorts();
this.CollapseGroupContents(true);
+ UpdateProxyPortsPosition();
RaisePropertyChanged(nameof(NodeContentCount));
}
WorkspaceViewModel.HasUnsavedChanges = true;
@@ -1147,6 +1478,314 @@ private void ManageAnnotationMVExpansionAndCollapse()
ReportNodesPosition();
}
+ ///
+ /// Adjusts layout by moving nearby elements to prevent overlap when the group expands.
+ ///
+ internal void UpdateLayoutForGroupExpansion()
+ {
+ var model = annotationModel;
+
+ double deltaY = ModelAreaHeight - heightBeforeToggle;
+ double deltaX = Width - widthBeforeToggle;
+
+ // Log the current size
+ heightBeforeToggle = ModelAreaHeight;
+ widthBeforeToggle = Width;
+
+ // Skip layout update if changes are negligible
+ if (deltaX < MinChangeThreshold && deltaY < MinChangeThreshold)
+ return;
+
+ var alreadyMoved = new HashSet();
+ var undoRecorder = WorkspaceViewModel.Model.UndoRecorder;
+
+ using (undoRecorder.BeginActionGroup())
+ {
+ if (deltaY > MinChangeThreshold)
+ ApplySpacing(model, isHorizontal: false, alreadyMoved);
+
+ if (deltaX > MinChangeThreshold)
+ ApplySpacing(model, isHorizontal: true, alreadyMoved);
+ }
+ }
+
+ ///
+ /// Applies spacing to reposition nearby models when a group expands, avoiding overlaps and updating boundaries.
+ ///
+ private void ApplySpacing(AnnotationModel expandingGroup, bool isHorizontal, HashSet alreadyMoved)
+ {
+ var offsets = GetAffectedModels(expandingGroup, isHorizontal, alreadyMoved);
+ if (offsets.Count == 0) return;
+
+ // Ensure changes to all affected models are tracked for Undo
+ WorkspaceViewModel.Model.RecordModelsForModification(offsets.Keys.ToList());
+
+ foreach (var (model, offset) in offsets)
+ {
+ // Skip moving groups directly, just update pinned notes to ensure boundaries update
+ if (model is AnnotationModel) continue;
+
+ if (isHorizontal)
+ model.X += offset;
+ else
+ model.Y += offset;
+
+ model.ReportPosition();
+ alreadyMoved.Add(model);
+ }
+
+ // To ensure group boundaries are updated correctly
+ foreach (var note in WorkspaceViewModel.Model.Annotations
+ .SelectMany(group => group.Nodes.OfType())
+ .Where(note => note.PinnedNode != null))
+ {
+ note.ReportPosition();
+ }
+ }
+
+ ///
+ /// Calculates all models affected by a group's expansion and the offset needed to reposition them.
+ ///
+ private Dictionary GetAffectedModels(
+ AnnotationModel expandingGroup,
+ bool isHorizontal,
+ HashSet skip)
+ {
+ // Track already processed items from prior horizontal/vertical pass
+ var visited = new HashSet(skip);
+
+ // Ensure expanding group and all its content (including nested groups) are ignored
+ if (!visited.Any())
+ {
+ visited.Add(expandingGroup);
+
+ foreach (var node in expandingGroup.Nodes)
+ {
+ visited.Add(node);
+
+ if (node is AnnotationModel nestedGroup)
+ {
+ foreach (var nestedNode in nestedGroup.Nodes)
+ visited.Add(nestedNode);
+ }
+ }
+ }
+
+ var toProcess = new List();
+ var directlyAffected = new List();
+ var otherGroups = WorkspaceViewModel.Model.Annotations.Where(g => !visited.Contains(g));
+ var allGroupedItems = WorkspaceViewModel.Model.Annotations.SelectMany(g => g.Nodes);
+ double smallestSpacing = double.MaxValue;
+
+ var expandedBounds = GetExpandingGroupBounds(expandingGroup);
+ visited.Add(expandingGroup);
+ foreach (var node in expandingGroup.Nodes) visited.Add(node);
+
+ // Pick direction-specific helpers
+ Func overlaps = isHorizontal ? IsRightAndVerticallyOverlapping : IsBelowAndHorizontallyOverlapping;
+ Func getSpacing = isHorizontal ? (a, b) => b.Left - a.Right : (a, b) => b.Top - a.Bottom;
+
+ // --- Step 1: Find directly affected items ---
+ // Groups
+ foreach (var group in otherGroups)
+ {
+ if (overlaps(expandedBounds, group.Rect))
+ {
+ var spacing = getSpacing(expandedBounds, group.Rect);
+ if (spacing < MinSpacing)
+ {
+ smallestSpacing = Math.Min(smallestSpacing, spacing);
+ directlyAffected.Add(group);
+ foreach (var node in group.Nodes)
+ visited.Add(node);
+ }
+ }
+ }
+
+ // Free-standing items
+ var freeItems = WorkspaceViewModel.Model.Notes.Cast()
+ .Concat(WorkspaceViewModel.Model.Nodes.Cast())
+ .Where(item => !allGroupedItems.Contains(item))
+ .ToList();
+
+ foreach (var item in freeItems)
+ {
+ if (overlaps(expandedBounds, item.Rect))
+ {
+ var spacing = getSpacing(expandedBounds, item.Rect);
+ if (spacing < MinSpacing)
+ {
+ smallestSpacing = Math.Min(smallestSpacing, spacing);
+ directlyAffected.Add(item is NoteModel { PinnedNode: NodeModel pinned } ? pinned : item);
+ }
+ }
+ }
+
+ // --- Step 2: Initialize movement ---
+ var moveBy = MinSpacing - smallestSpacing;
+ if (moveBy <= 0) return new();
+
+ var allToMove = new Dictionary();
+ foreach (var model in directlyAffected)
+ {
+ toProcess.Add(model);
+ allToMove[model] = moveBy;
+
+ if (model is AnnotationModel group)
+ {
+ foreach (var node in group.Nodes)
+ {
+ if (node is not NoteModel { PinnedNode: not null })
+ allToMove[node] = moveBy;
+ }
+ }
+ }
+
+ // --- Step 3: Recursively propagate movement downstream ---
+ for (int i = 0; i < toProcess.Count; i++)
+ {
+ var current = toProcess[i];
+ var offset = allToMove.GetValueOrDefault(current, moveBy);
+ var currentBounds = current.Rect;
+
+ // Simulate the moved position
+ currentBounds = isHorizontal
+ ? new Rect2D(currentBounds.X, currentBounds.Y, currentBounds.Width + offset, currentBounds.Height)
+ : new Rect2D(currentBounds.X, currentBounds.Y, currentBounds.Width, currentBounds.Height + offset);
+
+ // Groups
+ foreach (var group in otherGroups)
+ {
+ if (!overlaps(currentBounds, group.Rect)) continue;
+
+ var requiredOffset = MinSpacing - getSpacing(currentBounds, group.Rect);
+ if (requiredOffset <= 0) continue;
+
+ allToMove[group] = Math.Max(requiredOffset, allToMove.GetValueOrDefault(group, 0));
+ toProcess.Add(group);
+
+ foreach (var node in group.Nodes)
+ {
+ if (node is NoteModel note && note.PinnedNode != null) continue;
+
+ allToMove[node] = Math.Max(requiredOffset, allToMove.GetValueOrDefault(node, 0));
+ visited.Add(node);
+ }
+ }
+
+ // Free-standing items
+ foreach (var item in freeItems)
+ {
+ if (!overlaps(currentBounds, item.Rect)) continue;
+
+ var requiredOffset = MinSpacing - getSpacing(currentBounds, item.Rect);
+ if (requiredOffset <= 0) continue;
+
+ if (item is NoteModel note && note.PinnedNode is NodeModel pinned)
+ {
+ if (!visited.Contains(pinned))
+ {
+ allToMove[pinned] = Math.Max(requiredOffset, allToMove.GetValueOrDefault(pinned, 0));
+ toProcess.Add(pinned);
+ }
+ }
+ else
+ {
+ allToMove[item] = Math.Max(requiredOffset, allToMove.GetValueOrDefault(item, 0));
+ toProcess.Add(item);
+ }
+ }
+ }
+
+ AddExternalConnectorPinsToMove(allToMove);
+ return allToMove;
+ }
+
+ ///
+ /// Adds external connector pins to the movement list if they connect nodes from different groups.
+ ///
+ private void AddExternalConnectorPinsToMove(Dictionary allToMove)
+ {
+ // Get all moved nodes
+ var movedNodes = allToMove.Keys.OfType().ToList();
+
+ // Collect only moved groups
+ var movedGroups = allToMove.Keys.OfType();
+
+ // Map each node to its group
+ var nodeToGroup = new Dictionary();
+
+ foreach (var group in movedGroups)
+ {
+ foreach (var node in group.Nodes.OfType())
+ {
+ nodeToGroup[node] = group;
+ }
+ }
+
+ foreach (var node in movedNodes)
+ {
+ if (!allToMove.TryGetValue(node, out var nodeOffset)) continue;
+
+ foreach (var connector in node.AllConnectors)
+ {
+ var startNode = connector.Start.Owner;
+ var endNode = connector.End.Owner;
+
+ bool sameGroup =
+ nodeToGroup.TryGetValue(startNode, out var groupA) &&
+ nodeToGroup.TryGetValue(endNode, out var groupB) &&
+ groupA == groupB;
+
+ if (sameGroup) continue;
+
+ // Add each pin with the largest offset from its connected node (if multiple nodes affect it)
+ foreach (var pin in connector.ConnectorPinModels)
+ {
+ if (!allToMove.TryGetValue(pin, out var existingOffset))
+ {
+ allToMove[pin] = nodeOffset;
+ }
+ else
+ {
+ allToMove[pin] = Math.Max(existingOffset, nodeOffset);
+ }
+ }
+ }
+ }
+ }
+
+ private static Rect2D GetExpandingGroupBounds(AnnotationModel group)
+ {
+ var width = group.Width;
+ var height = group.ModelAreaHeight + group.TextBlockHeight;
+
+ return new Rect2D(group.X, group.Y, width, height);
+ }
+
+ private bool IsBelowAndHorizontallyOverlapping(Rect2D thisGroup, Rect2D other)
+ {
+ return other.Top > thisGroup.Top &&
+ other.Left < thisGroup.Right &&
+ other.Right > thisGroup.Left;
+ }
+
+ private bool IsRightAndVerticallyOverlapping(Rect2D thisGroup, Rect2D other)
+ {
+ return other.Left > thisGroup.Left &&
+ other.Top < thisGroup.Bottom &&
+ other.Bottom > thisGroup.Top;
+ }
+
+ private void HandlePrePortToggleLayout()
+ {
+ Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
+ {
+ UpdateLayoutForGroupExpansion();
+ }),
+ DispatcherPriority.ApplicationIdle);
+ }
+
private void UpdateFontSize(object parameter)
{
if (parameter == null) return;
@@ -1302,7 +1941,14 @@ private void model_PropertyChanged(object sender, System.ComponentModel.Property
case nameof(IsExpanded):
ManageAnnotationMVExpansionAndCollapse();
break;
-
+ case nameof(AnnotationModel.IsOptionalInPortsCollapsed):
+ isOptionalInPortsCollapsed = annotationModel.IsOptionalInPortsCollapsed;
+ RaisePropertyChanged(nameof(IsOptionalInPortsCollapsed));
+ break;
+ case nameof(AnnotationModel.IsUnconnectedOutPortsCollapsed):
+ IsUnconnectedOutPortsCollapsed = annotationModel.IsUnconnectedOutPortsCollapsed;
+ RaisePropertyChanged(nameof(IsUnconnectedOutPortsCollapsed));
+ break;
}
}
diff --git a/src/DynamoCoreWpf/ViewModels/Core/Converters/SerializationConverters.cs b/src/DynamoCoreWpf/ViewModels/Core/Converters/SerializationConverters.cs
index 0a44f399147..ecfd941bd5c 100644
--- a/src/DynamoCoreWpf/ViewModels/Core/Converters/SerializationConverters.cs
+++ b/src/DynamoCoreWpf/ViewModels/Core/Converters/SerializationConverters.cs
@@ -158,6 +158,14 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
writer.WriteValue(anno.InitialHeight);
writer.WritePropertyName("TextblockHeight");
writer.WriteValue(anno.TextBlockHeight);
+ writer.WritePropertyName(nameof(anno.IsOptionalInPortsCollapsed));
+ writer.WriteValue(anno.IsOptionalInPortsCollapsed);
+ writer.WritePropertyName(nameof(anno.IsUnconnectedOutPortsCollapsed));
+ writer.WriteValue(anno.IsUnconnectedOutPortsCollapsed);
+ writer.WritePropertyName(nameof(anno.HasToggledOptionalInPorts));
+ writer.WriteValue(anno.HasToggledOptionalInPorts);
+ writer.WritePropertyName(nameof(anno.HasToggledUnconnectedOutPorts));
+ writer.WriteValue(anno.HasToggledUnconnectedOutPorts);
writer.WritePropertyName("Background");
writer.WriteValue(anno.Background != null ? anno.Background : "");
if (anno.PinnedNode != null)
diff --git a/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs b/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs
index 6d01b442c10..ff487ebdff6 100644
--- a/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs
+++ b/src/DynamoCoreWpf/ViewModels/Menu/PreferencesViewModel.cs
@@ -829,6 +829,45 @@ public bool ShowDefaultGroupDescription
}
}
+ ///
+ /// Indicates if the optional input ports are collapsed by default.
+ ///
+ public bool OptionalInputsCollapsed
+ {
+ get => preferenceSettings.OptionalInPortsCollapsed;
+ set
+ {
+ preferenceSettings.OptionalInPortsCollapsed = value;
+ RaisePropertyChanged(nameof(OptionalInputsCollapsed));
+ }
+ }
+
+ ///
+ /// Indicates if the unconnected output ports are hidden by default.
+ ///
+ public bool UnconnectedOutputsCollapsed
+ {
+ get => preferenceSettings.UnconnectedOutPortsCollapsed;
+ set
+ {
+ preferenceSettings.UnconnectedOutPortsCollapsed = value;
+ RaisePropertyChanged(nameof(UnconnectedOutputsCollapsed));
+ }
+ }
+
+ ///
+ /// Indicates if the groups should be collapsed to minimal size by default.
+ ///
+ public bool CollapseToMinSize
+ {
+ get => preferenceSettings.CollapseToMinSize;
+ set
+ {
+ preferenceSettings.CollapseToMinSize = value;
+ RaisePropertyChanged(nameof(CollapseToMinSize));
+ }
+ }
+
///
/// Indicates if Host units should be used for graphic helpers for Dynamo Revit
/// Also toggles between Host and Dynamo units
@@ -1890,6 +1929,15 @@ private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
case nameof(ShowDefaultGroupDescription):
description = Res.ResourceManager.GetString(nameof(Res.PreferencesViewShowDefaultGroupDescription), System.Globalization.CultureInfo.InvariantCulture);
goto default;
+ case nameof(OptionalInputsCollapsed):
+ description = Res.ResourceManager.GetString(nameof(Res.PreferencesViewHideInportsDescription), System.Globalization.CultureInfo.InvariantCulture);
+ goto default;
+ case nameof(UnconnectedOutputsCollapsed):
+ description = Res.ResourceManager.GetString(nameof(Res.PreferencesViewHideOutportsDescription), System.Globalization.CultureInfo.InvariantCulture);
+ goto default;
+ case nameof(CollapseToMinSize):
+ description = Res.ResourceManager.GetString(nameof(Res.PreferencesViewCollapseToMinSizeDescription), System.Globalization.CultureInfo.InvariantCulture);
+ goto default;
case nameof(ShowCodeBlockLineNumber):
description = Res.ResourceManager.GetString(nameof(Res.PreferencesViewShowCodeBlockNodeLineNumber), System.Globalization.CultureInfo.InvariantCulture);
goto default;
diff --git a/src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs b/src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs
index 5a800d0733b..71dcb151284 100644
--- a/src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs
+++ b/src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
@@ -27,8 +29,6 @@
using TextBox = System.Windows.Controls.TextBox;
using Thickness = System.Windows.Thickness;
using ModifierKeys = System.Windows.Input.ModifierKeys;
-using System.ComponentModel;
-using System.Collections.Specialized;
namespace Dynamo.Nodes
{
@@ -51,10 +51,19 @@ public partial class AnnotationView : IViewModelView
private Border collapsedAnnotationRectangle;
private Expander groupExpander;
private ItemsControl inputPortControl;
+ private ItemsControl optionalInputPortControl;
+ private ToggleButton inputToggleControl;
private ItemsControl outputPortControl;
+ private ItemsControl unconnectedOutputPortControl;
+ private ToggleButton outputToggleControl;
+ private Grid inputPortsGrid;
+ private Grid outputPortsGrid;
+ private Grid groupContent;
+ private Border nodeCountBorder;
+ private InCanvasSearchControl searchBar;
+
private Thumb mainGroupThumb;
private StackPanel groupPopupPanel;
- private InCanvasSearchControl searchBar;
private bool _isUpdatingLayout = false;
private bool isSearchFromGroupContext;
@@ -301,19 +310,8 @@ private void AnnotationView_Loaded(object sender, RoutedEventArgs e)
}
ViewModel.UpdateProxyPortsPosition();
- // Create and attach the popup menu for group annotations
- CreateAndAttachAnnotationPopup();
-
- // Subscribe to search box activity to close the other popup items when typing
- var searchVM = ViewModel.WorkspaceViewModel.InCanvasSearchViewModel;
- searchVM.PropertyChanged += OnSearchViewModelPropertyChanged;
-
// Add new nodes to group when search is triggered from group
ViewModel.WorkspaceViewModel.Nodes.CollectionChanged += OnWorkspaceNodesChanged;
-
- // Reset group context flag when popup closes
- _groupContextMenuClosedHandler = (s, e) => isSearchFromGroupContext = false;
- GroupContextMenuPopup.Closed += _groupContextMenuClosedHandler;
}
}
@@ -358,7 +356,7 @@ private void OnUngroupAnnotation(object sender, RoutedEventArgs e)
{
this.ViewModel.IsExpanded = true;
}
-
+
ViewModel.WorkspaceViewModel.DynamoViewModel.ExecuteCommand(
new DynCmd.SelectModelCommand(annotationGuid, Keyboard.Modifiers.AsDynamoType()));
ViewModel.WorkspaceViewModel.DynamoViewModel.DeleteCommand.Execute(null);
@@ -417,7 +415,7 @@ private void AnnotationView_OnMouseLeftButtonDown(object sender, MouseButtonEven
private void AnnotationView_OnMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
- ViewModel.SelectAll();
+ ViewModel.SelectAll();
}
///
@@ -425,11 +423,8 @@ private void AnnotationView_OnMouseRightButtonDown(object sender, MouseButtonEve
///
private void AnnotationView_OnMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
- if (!GroupContextMenuPopup.IsOpen)
- {
- OpenContextMenuAtMouse();
- e.Handled = true;
- }
+ OpenContextMenuAtMouse();
+ e.Handled = true;
}
///
@@ -475,8 +470,7 @@ private void GroupTextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
if (ViewModel != null && (e.HeightChanged || e.WidthChanged) && !_isUpdatingLayout)
{
_isUpdatingLayout = true;
-
- // Use Dispatcher.BeginInvoke to batch layout updates
+ // Use Dispatcher.BeginInvoke to schedule layout updates on the UI thread
Dispatcher.BeginInvoke(new Action(() =>
{
try
@@ -600,6 +594,7 @@ private void GroupDescriptionTextBox_TextChanged(object sender, TextChangedEvent
private void CollapsedAnnotationRectangle_SizeChanged(object sender, SizeChangedEventArgs e)
{
+ GetMinWidthOnCollapsed();
SetModelAreaHeight();
}
@@ -657,6 +652,9 @@ private void GroupDescriptionControls_SizeChanged(object sender, SizeChangedEven
private void OpenContextMenuAtMouse()
{
+ // Create and attach the popup menu for group annotations
+ CreateAndAttachAnnotationPopup();
+
var workspaceView = WpfUtilities.FindParent(this);
if (workspaceView == null) return;
@@ -1525,7 +1523,6 @@ private Thumb CreateResizeThumb()
var thumb = new Thumb();
thumb.Name = "ResizeThumb";
thumb.DragDelta += AnnotationRectangleThumb_DragDelta;
-
thumb.Style = _groupResizeThumbStyle;
thumb.MouseEnter += Thumb_MouseEnter;
@@ -1551,7 +1548,6 @@ private static Style CreateGroupResizeThumbStyle()
// Create the Polygon
var polygonFactory = new FrameworkElementFactory(typeof(Polygon));
-
// Set Points collection
var points = new PointCollection
{
@@ -1628,53 +1624,17 @@ private Grid CreateCollapsedMainGrid()
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
// Add children
- inputPortControl = CreateInputPortControl();
- outputPortControl = CreateOutputPortControl();
- var groupContent = CreateGroupContent();
+ inputPortsGrid = CreateInputPortsGrid();
+ outputPortsGrid = CreateOutputPortsGrid();
+ groupContent = CreateGroupContent();
- grid.Children.Add(inputPortControl);
- grid.Children.Add(outputPortControl);
+ grid.Children.Add(inputPortsGrid);
+ grid.Children.Add(outputPortsGrid);
grid.Children.Add(groupContent);
return grid;
}
- private ItemsControl CreateInputPortControl()
- {
- var itemsControl = new ItemsControl
- {
- Name = "inputPortControl",
- HorizontalContentAlignment = HorizontalAlignment.Left,
- Margin = new Thickness(-25, 10, -25, 10)
- };
-
- Panel.SetZIndex(itemsControl, 20);
- Grid.SetColumn(itemsControl, 0);
- Grid.SetRow(itemsControl, 0);
-
- itemsControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("InPorts"));
-
- return itemsControl;
- }
-
- private ItemsControl CreateOutputPortControl()
- {
- var itemsControl = new ItemsControl
- {
- Name = "outputPortControl",
- HorizontalContentAlignment = HorizontalAlignment.Right,
- Margin = new Thickness(-25, 10, -25, 10)
- };
-
- Panel.SetZIndex(itemsControl, 20);
- Grid.SetColumn(itemsControl, 2);
- Grid.SetRow(itemsControl, 0);
-
- itemsControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("OutPorts"));
-
- return itemsControl;
- }
-
private Grid CreateGroupContent()
{
var grid = new Grid
@@ -1773,7 +1733,7 @@ private DynamoToolTip CreateNestedGroupsTooltip()
private Border CreateNodeCountBorder()
{
- var border = new Border
+ nodeCountBorder = new Border
{
Name = "NodeCount",
BorderThickness = new Thickness(1),
@@ -1787,7 +1747,7 @@ private Border CreateNodeCountBorder()
VerticalAlignment = VerticalAlignment.Bottom,
};
- Grid.SetColumn(border, 1);
+ Grid.SetColumn(nodeCountBorder, 1);
// Create and add TextBlock
var textBlock = new TextBlock
@@ -1803,17 +1763,20 @@ private Border CreateNodeCountBorder()
StringFormat = "+{0}"
});
- border.Child = textBlock;
+ nodeCountBorder.Child = textBlock;
- return border;
+ return nodeCountBorder;
}
#endregion
#region Context Menu
- private void CreateAndAttachAnnotationPopup()
+ internal void CreateAndAttachAnnotationPopup()
{
+ // If the context menu is already created, no need to recreate or re-subscribe
+ if (GroupContextMenuPopup != null) return;
+
GroupContextMenuPopup = new Popup
{
Name = "AnnotationContextPopup",
@@ -1831,6 +1794,12 @@ private void CreateAndAttachAnnotationPopup()
};
GroupContextMenuPopup.Child = border;
+
+ // Subscribe to search box activity to close the other popup items when typing
+ var searchVM = ViewModel.WorkspaceViewModel.InCanvasSearchViewModel;
+ searchVM.PropertyChanged += OnSearchViewModelPropertyChanged;
+ _groupContextMenuClosedHandler = (s, e) => isSearchFromGroupContext = false;
+ GroupContextMenuPopup.Closed += _groupContextMenuClosedHandler;
}
private StackPanel CreatePopupPanel()
@@ -2803,5 +2772,279 @@ private void SetTextHeight()
this.groupDescriptionControls.DesiredSize.Height +
this.groupNameControl.DesiredSize.Height;
}
+
+ private ControlTemplate CreateCollapsedPortToggleButtonTemplate()
+ {
+ var template = new ControlTemplate(typeof(ToggleButton));
+
+ var border = new FrameworkElementFactory(typeof(Border));
+ border.SetValue(Border.BackgroundProperty, Brushes.Transparent);
+
+ var stackPanel = new FrameworkElementFactory(typeof(StackPanel));
+ stackPanel.SetValue(StackPanel.MarginProperty, new TemplateBindingExtension(MarginProperty));
+ stackPanel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
+
+ // TextBlock with MultiBinding text
+ var label = new FrameworkElementFactory(typeof(TextBlock));
+ label.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center);
+ label.SetBinding(TextBlock.ForegroundProperty, new Binding("Background")
+ {
+ Converter = _textForegroundSaturationColorConverter,
+ ConverterParameter = "groupPortToggle"
+ });
+
+ var multiBinding = new MultiBinding { StringFormat = "{0} {1}" };
+ multiBinding.Bindings.Add(new Binding("Tag") { RelativeSource = RelativeSource.TemplatedParent });
+ multiBinding.Bindings.Add(new Binding("Content") { RelativeSource = RelativeSource.TemplatedParent });
+ label.SetBinding(TextBlock.TextProperty, multiBinding);
+
+ // Arrow Path
+ var path = new FrameworkElementFactory(typeof(Path));
+ path.SetValue(FrameworkElement.WidthProperty, 8.0);
+ path.SetValue(FrameworkElement.HeightProperty, 6.0);
+ path.SetValue(FrameworkElement.MarginProperty, new Thickness(6, 0, 0, 0));
+ path.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
+ path.SetValue(Path.DataProperty, Geometry.Parse("M0,0 L0,2 L4,6 L8,2 L8,0 L4,4 z"));
+ path.SetBinding(Shape.FillProperty, new Binding("Background")
+ {
+ Converter = _textForegroundSaturationColorConverter,
+ ConverterParameter = "groupPortToggle"
+ });
+
+ // RotateTransform bound to IsChecked via converter
+ var rotateTransform = new RotateTransform();
+ var rotateBinding = new Binding("IsChecked")
+ {
+ RelativeSource = RelativeSource.TemplatedParent,
+ Converter = new BooleanToAngleConverter()
+ };
+ BindingOperations.SetBinding(rotateTransform, RotateTransform.AngleProperty, rotateBinding);
+ path.SetValue(Path.RenderTransformProperty, rotateTransform);
+ path.SetValue(Path.RenderTransformOriginProperty, new Point(0.5, 0.5));
+
+ // Compose visual tree
+ stackPanel.AppendChild(label);
+ stackPanel.AppendChild(path);
+ border.AppendChild(stackPanel);
+ template.VisualTree = border;
+
+ return template;
+ }
+
+ private Style CreateBaseCollapsedPortToggleStyle()
+ {
+ var style = new Style(typeof(ToggleButton));
+ style.Setters.Add(new Setter(Control.BackgroundProperty, Brushes.Transparent));
+ style.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0)));
+ style.Setters.Add(new Setter(Control.CursorProperty, Cursors.Hand));
+ style.Setters.Add(new Setter(Control.TemplateProperty, CreateCollapsedPortToggleButtonTemplate()));
+ style.Setters.Add(new Setter(Control.HeightProperty, 30.0));
+
+ return style;
+ }
+
+ private Grid CreateInputPortsGrid()
+ {
+ var grid = new Grid
+ {
+ Name = "inputPortsGrid",
+ VerticalAlignment = VerticalAlignment.Top,
+ };
+ Grid.SetRow(grid, 0);
+ Grid.SetColumn(grid, 0);
+
+ // Define rows
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ // Main Input Ports
+ inputPortControl = new ItemsControl
+ {
+ Name = "inputPortControl",
+ HorizontalContentAlignment = HorizontalAlignment.Left,
+ Margin = new Thickness(-25, 10, 0, 0)
+ };
+ Panel.SetZIndex(inputPortControl, 20);
+ Grid.SetRow(inputPortControl, 0);
+ inputPortControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("InPorts"));
+ grid.Children.Add(inputPortControl);
+
+ // Toggle Button for Collapsing Optional Ports
+ inputToggleControl = new ToggleButton
+ {
+ Name = "inputToggleControl",
+ Margin = new Thickness(5, 0, 0, 0),
+ HorizontalAlignment = HorizontalAlignment.Left,
+ Content = Wpf.Properties.Resources.GroupOptionalInportsText
+ };
+ inputToggleControl.Click += OptionalPortsToggle_Click;
+ inputToggleControl.SetBinding(ToggleButton.IsCheckedProperty, new Binding("IsOptionalInPortsCollapsed") { Mode = BindingMode.TwoWay });
+ inputToggleControl.SetBinding(ToggleButton.TagProperty, new Binding("OptionalInPorts.Count"));
+ Grid.SetRow(inputToggleControl, 1);
+
+ var toggleStyle = CreateBaseCollapsedPortToggleStyle();
+
+ // Add DataTrigger to hide toggle if no optional ports exist
+ var trigger = new DataTrigger
+ {
+ Binding = new Binding("OptionalInPorts.Count"),
+ Value = 0
+ };
+ trigger.Setters.Add(new Setter(VisibilityProperty, Visibility.Collapsed));
+ toggleStyle.Triggers.Add(trigger);
+
+ inputToggleControl.Style = toggleStyle;
+ grid.Children.Add(inputToggleControl);
+
+ // Optional Input Ports
+ optionalInputPortControl = new ItemsControl
+ {
+ Name = "optionalInputPortControl",
+ HorizontalContentAlignment = HorizontalAlignment.Left,
+ Margin = new Thickness(-25, 0, 0, 0)
+ };
+ Panel.SetZIndex(optionalInputPortControl, 20);
+ Grid.SetRow(optionalInputPortControl, 2);
+ optionalInputPortControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("OptionalInPorts"));
+
+ // Style to collapse/expand based on IsOptionalInPortsCollapsed
+ var optionalStyle = new Style(typeof(ItemsControl));
+ optionalStyle.Setters.Add(new Setter(VisibilityProperty, Visibility.Collapsed));
+
+ var optionalTrigger = new DataTrigger
+ {
+ Binding = new Binding("IsOptionalInPortsCollapsed"),
+ Value = false
+ };
+ optionalTrigger.Setters.Add(new Setter(VisibilityProperty, Visibility.Visible));
+ optionalStyle.Triggers.Add(optionalTrigger);
+
+ optionalInputPortControl.Style = optionalStyle;
+ grid.Children.Add(optionalInputPortControl);
+
+ return grid;
+ }
+
+ private Grid CreateOutputPortsGrid()
+ {
+ var grid = new Grid
+ {
+ Name = "outputPortsGrid",
+ VerticalAlignment = VerticalAlignment.Top
+ };
+ Grid.SetRow(grid, 0);
+ Grid.SetColumn(grid, 2);
+
+ // Define rows
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ // Main Output Ports
+ outputPortControl = new ItemsControl
+ {
+ Name = "outputPortControl",
+ HorizontalContentAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 10, -25, 0)
+ };
+ Panel.SetZIndex(outputPortControl, 20);
+ Grid.SetRow(outputPortControl, 0);
+ outputPortControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("OutPorts"));
+ grid.Children.Add(outputPortControl);
+
+ // Toggle Button for Collapsing Unconnected Ports
+ outputToggleControl = new ToggleButton
+ {
+ Name = "outputToggleControl",
+ Margin = new Thickness(0, 0, 5, 0),
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Content = Wpf.Properties.Resources.GroupUnconnectedOutportsText
+ };
+ outputToggleControl.Click += UnconnectedPortsToggle_Click;
+ outputToggleControl.SetBinding(ToggleButton.IsCheckedProperty, new Binding("IsUnconnectedOutPortsCollapsed") { Mode = BindingMode.TwoWay });
+ outputToggleControl.SetBinding(ToggleButton.TagProperty, new Binding("UnconnectedOutPorts.Count"));
+ Grid.SetRow(outputToggleControl, 1);
+
+ var toggleStyle = CreateBaseCollapsedPortToggleStyle();
+
+ // Add DataTrigger to hide toggle if no unconnected ports exist
+ var trigger = new DataTrigger
+ {
+ Binding = new Binding("UnconnectedOutPorts.Count"),
+ Value = 0
+ };
+ trigger.Setters.Add(new Setter(VisibilityProperty, Visibility.Collapsed));
+ toggleStyle.Triggers.Add(trigger);
+
+ outputToggleControl.Style = toggleStyle;
+ grid.Children.Add(outputToggleControl);
+
+ // Unconnected Output Ports
+ unconnectedOutputPortControl = new ItemsControl
+ {
+ Name = "unconnectedOutputPortControl",
+ HorizontalContentAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 0, -25, 0)
+ };
+ Panel.SetZIndex(unconnectedOutputPortControl, 20);
+ Grid.SetRow(unconnectedOutputPortControl, 2);
+ unconnectedOutputPortControl.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("UnconnectedOutPorts"));
+
+ var optionalStyle = new Style(typeof(ItemsControl));
+ optionalStyle.Setters.Add(new Setter(VisibilityProperty, Visibility.Collapsed));
+ var optionalTrigger = new DataTrigger
+ {
+ Binding = new Binding("IsUnconnectedOutPortsCollapsed"),
+ Value = false
+ };
+ optionalTrigger.Setters.Add(new Setter(VisibilityProperty, Visibility.Visible));
+ optionalStyle.Triggers.Add(optionalTrigger);
+ unconnectedOutputPortControl.Style = optionalStyle;
+
+ grid.Children.Add(unconnectedOutputPortControl);
+
+ return grid;
+ }
+
+ ///
+ /// Calculates the minimum width required for a collapsed group,
+ /// considering input/output ports, toggles, and their visibility states.
+ ///
+ private void GetMinWidthOnCollapsed()
+ {
+ if (ViewModel != null)
+ {
+ ViewModel.AnnotationModel.MinWidthOnCollapsed =
+ inputPortsGrid.ActualWidth +
+ outputPortsGrid.ActualWidth;
+ }
+ }
+
+ ///
+ /// Handles the click event for the optional input ports toggle button.
+ /// Sets a flag indicating the user has manually toggled this state.
+ ///
+ private void OptionalPortsToggle_Click(object sender, RoutedEventArgs e)
+ {
+ // Mark it as manually changed by user
+ if (!ViewModel.AnnotationModel.HasToggledOptionalInPorts)
+ {
+ ViewModel.AnnotationModel.HasToggledOptionalInPorts = true;
+ }
+ }
+
+ ///
+ /// Handles the click event for the unconnected output ports toggle button.
+ /// Sets a flag indicating the user has manually toggled this state.
+ ///
+ private void UnconnectedPortsToggle_Click(object sender, RoutedEventArgs e)
+ {
+ // Mark it as manually changed by user
+ if (!ViewModel.AnnotationModel.HasToggledUnconnectedOutPorts)
+ {
+ ViewModel.AnnotationModel.HasToggledUnconnectedOutPorts = true;
+ }
+ }
}
}
diff --git a/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml b/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml
index 689b930b4be..a36f5e67689 100644
--- a/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml
+++ b/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml
@@ -1181,6 +1181,10 @@
+
+
+
+