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 8a993aa8991..3a1704241d4 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 /// @@ -190,6 +193,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 /// @@ -996,6 +1041,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 eed05a32232..03155cde30b 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)); } } @@ -444,6 +442,130 @@ 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; + } + } + + private double minCollapsedPortAreaHeight; + /// + /// Gets or sets the minimum height of the port area when the group is collapsed. + /// Used to calculate the total height of the collapsed group based on proxy ports + /// + public double MinCollapsedPortAreaHeight + { + get => minCollapsedPortAreaHeight; + set + { + if (minCollapsedPortAreaHeight != value) + { + minCollapsedPortAreaHeight = value; + RaisePropertyChanged(nameof(MinCollapsedPortAreaHeight)); + } + } + } + #endregion /// @@ -511,6 +633,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): @@ -575,71 +701,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; } /// @@ -757,6 +915,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; @@ -790,6 +952,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); @@ -835,6 +1001,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 5f31a284177..33cd83926bd 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 @@ -2707,6 +2715,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 2ff460b5cee..33bc89646e2 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 09837e29546..c5cd3c8cc77 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 @@ -700,18 +706,32 @@ 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.MinCollapsedPortAreaHeight.get -> double +Dynamo.Graph.Annotations.AnnotationModel.MinCollapsedPortAreaHeight.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 @@ -1282,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 @@ -1562,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 @@ -1585,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 @@ -1603,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 14e85faee87..d2aad326dd4 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 37c27f08686..dd2ad2d52fe 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 806cda56dea..4835903e64b 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 + + \ No newline at end of file diff --git a/src/DynamoCoreWpf/PublicAPI.Unshipped.txt b/src/DynamoCoreWpf/PublicAPI.Unshipped.txt index 416d2a90bbb..393068d8df8 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 @@ -1794,6 +1803,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 @@ -2824,6 +2834,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 @@ -2886,6 +2898,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 @@ -2930,6 +2944,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 @@ -4931,10 +4947,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 @@ -5397,6 +5415,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 @@ -5418,6 +5438,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 396585bad2c..f873da18890 100644 --- a/src/DynamoCoreWpf/UI/Converters.cs +++ b/src/DynamoCoreWpf/UI/Converters.cs @@ -3916,21 +3916,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); } @@ -4335,4 +4351,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 15fa3e87d0b..44b9849eb48 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; @@ -29,9 +34,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; // 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 @@ -240,6 +257,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. @@ -257,6 +289,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 @@ -281,6 +381,7 @@ public bool IsExpanded // Methods to collapse or expand the group based on the new value of IsExpanded. ManageAnnotationMVExpansionAndCollapse(); + HandlePrePortToggleLayout(); } } @@ -629,6 +730,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; @@ -649,6 +760,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)); @@ -673,8 +786,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. @@ -684,7 +829,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 @@ -707,11 +853,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 (mainPortViewModels != null) + InPorts.AddRange(mainPortViewModels); - if (newPortViewModels == null) return; - InPorts.AddRange(newPortViewModels); - return; + if (optionalPortViewModels != null) + OptionalInPorts.AddRange(optionalPortViewModels); } /// @@ -721,10 +871,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) { @@ -744,11 +895,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 (mainPortViewModels != null) + OutPorts.AddRange(mainPortViewModels); - if (newPortViewModels == null) return; - OutPorts.AddRange(newPortViewModels); - return; + if (unconnectedPortViewModels != null) + UnconnectedOutPorts.AddRange(unconnectedPortViewModels); } internal IEnumerable GetGroupInPorts(IEnumerable ownerNodes = null) @@ -823,53 +978,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; + + 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); + + // 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; + + // 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)); + } + }); + } - return newPortViewModels; + 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; + } } internal void UpdateProxyPortsPosition() @@ -893,16 +1187,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() @@ -983,8 +1304,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()) @@ -1000,6 +1328,9 @@ private void RedrawConnectors() connectorViewModel.Redraw(); connector.Start.Owner.ReportPosition(); } + + // Re-enable boundary updates once internal redraw is complete + annotationModel.SuppressBoundaryUpdate = false; } /// @@ -1067,14 +1398,17 @@ private void UpdateConnectorsAndPortsOnShowContents(IEnumerable nodes /// private void ManageAnnotationMVExpansionAndCollapse() { - if (InPorts.Any() || OutPorts.Any()) + if (InPorts.Any() || OutPorts.Any() || OptionalInPorts.Any() || UnconnectedOutPorts.Any()) { InPorts.Clear(); OutPorts.Clear(); + OptionalInPorts.Clear(); + UnconnectedOutPorts.Clear(); } if (annotationModel.IsExpanded) { + UnsubscribeFromProxyPortEvents(); this.ShowGroupContents(); } else @@ -1091,6 +1425,298 @@ 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) + { + var visited = new HashSet(skip); + foreach (var node in expandingGroup.Nodes) + visited.Add(node); + + var toProcess = new List(); + var directlyAffected = new List(); + var otherGroups = WorkspaceViewModel.Model.Annotations.Where(g => g != expandingGroup); + 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; @@ -1206,6 +1832,7 @@ private void model_PropertyChanged(object sender, System.ComponentModel.Property RaisePropertyChanged("Width"); RaisePropertyChanged(nameof(ModelAreaRect)); UpdateAllGroupedGroups(); + RedrawConnectors(false); break; case "Height": RaisePropertyChanged("Height"); @@ -1246,7 +1873,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 5f96ce82fc3..a811fe9aaec 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 @@ -1885,6 +1924,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 72c8f016730..5602827b26f 100644 --- a/src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs +++ b/src/DynamoCoreWpf/Views/Core/AnnotationView.xaml.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; @@ -48,7 +49,16 @@ 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 Thumb mainGroupThumb; private bool _isUpdatingLayout = false; @@ -368,7 +378,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); @@ -427,7 +437,7 @@ private void AnnotationView_OnMouseLeftButtonDown(object sender, MouseButtonEven private void AnnotationView_OnMouseRightButtonDown(object sender, MouseButtonEventArgs e) { - ViewModel.SelectAll(); + ViewModel.SelectAll(); } /// @@ -474,7 +484,6 @@ 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 Dispatcher.BeginInvoke(new Action(() => { @@ -599,6 +608,7 @@ private void GroupDescriptionTextBox_TextChanged(object sender, TextChangedEvent private void CollapsedAnnotationRectangle_SizeChanged(object sender, SizeChangedEventArgs e) { + GetMinWidthOnCollapsed(); SetModelAreaHeight(); } @@ -1440,7 +1450,6 @@ private Thumb CreateResizeThumb() var thumb = new Thumb(); thumb.Name = "ResizeThumb"; thumb.DragDelta += AnnotationRectangleThumb_DragDelta; - thumb.Style = _groupResizeThumbStyle; thumb.MouseEnter += Thumb_MouseEnter; @@ -1466,7 +1475,6 @@ private static Style CreateGroupResizeThumbStyle() // Create the Polygon var polygonFactory = new FrameworkElementFactory(typeof(Polygon)); - // Set Points collection var points = new PointCollection { @@ -1505,8 +1513,6 @@ private Border CreateCollapsedAnnotationRectangle() }; // Set bindings - border.SetBinding(FrameworkElement.WidthProperty, new Binding("Width")); - border.SetBinding(FrameworkElement.HeightProperty, new Binding("ModelAreaHeight")); border.SetBinding(UIElement.VisibilityProperty, new Binding("IsExpanded") { ElementName = "GroupExpander", @@ -1545,53 +1551,18 @@ 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(inputPortsGrid); + grid.Children.Add(outputPortsGrid); - grid.Children.Add(inputPortControl); - grid.Children.Add(outputPortControl); 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 @@ -1690,7 +1661,7 @@ private DynamoToolTip CreateNestedGroupsTooltip() private Border CreateNodeCountBorder() { - var border = new Border + nodeCountBorder = new Border { Name = "NodeCount", BorderThickness = new Thickness(1), @@ -1704,7 +1675,7 @@ private Border CreateNodeCountBorder() VerticalAlignment = VerticalAlignment.Bottom, }; - Grid.SetColumn(border, 1); + Grid.SetColumn(nodeCountBorder, 1); // Create and add TextBlock var textBlock = new TextBlock @@ -1720,9 +1691,9 @@ private Border CreateNodeCountBorder() StringFormat = "+{0}" }); - border.Child = textBlock; + nodeCountBorder.Child = textBlock; - return border; + return nodeCountBorder; } #endregion @@ -2885,25 +2856,277 @@ private void SetTextHeight() this.groupDescriptionControls.DesiredSize.Height + this.groupNameControl.DesiredSize.Height; } - - private (double maxWidth, double totalHeight) MeasurePortBounds(ItemsControl portControl) + + private ControlTemplate CreateCollapsedPortToggleButtonTemplate() { - double maxWidth = 0; - double totalHeight = 0; + var template = new ControlTemplate(typeof(ToggleButton)); - foreach (var item in portControl.Items) + 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") { - if (portControl.ItemContainerGenerator.ContainerFromItem(item) is FrameworkElement container && container.IsVisible) - { - container.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); - var size = container.DesiredSize; + Converter = _textForegroundSaturationColorConverter, + ConverterParameter = "groupPortToggle" + }); - maxWidth = Math.Max(maxWidth, size.Width); - totalHeight += size.Height; - } + 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" + }; + Grid.SetRow(grid, 0); + Grid.SetColumn(grid, 0); + + // Define rows + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(34) }); + 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, -20, 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" + }; + 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; } + } - return (maxWidth, totalHeight); + /// + /// 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 ad19be04c63..7dc6882d383 100644 --- a/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml +++ b/src/DynamoCoreWpf/Views/Menu/PreferencesView.xaml @@ -1181,6 +1181,10 @@ + + + +