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 @@ + + + +