diff --git a/src/StructuredLogViewer/Controls/GraphControl.cs b/src/StructuredLogViewer/Controls/GraphControl.cs index 0aa229b92..8aa0f9a58 100644 --- a/src/StructuredLogViewer/Controls/GraphControl.cs +++ b/src/StructuredLogViewer/Controls/GraphControl.cs @@ -116,6 +116,34 @@ public bool HideTransitiveEdges } } + private bool directReferencesOnly; + private Vertex focusVertex; + public bool DirectReferencesOnly + { + get => directReferencesOnly; + set + { + if (directReferencesOnly == value) + { + return; + } + + directReferencesOnly = value; + + // If enabling direct references only mode, set focus to first selected vertex + if (value && selectedVertices.Any()) + { + focusVertex = selectedVertices.First(); + } + else if (!value) + { + focusVertex = null; + } + + Redraw(); + } + } + private bool layerByDepth; public bool LayerByDepth { @@ -171,8 +199,16 @@ private void Populate() return; } - var maxHeight = graph.Vertices.Max(g => g.Height); - var maxDepth = graph.Vertices.Max(g => g.Depth); + // Get vertices to display based on filtering mode + var verticesToDisplay = GetVerticesToDisplay(); + + if (!verticesToDisplay.Any()) + { + return; + } + + var maxHeight = verticesToDisplay.Max(g => g.Height); + var maxDepth = verticesToDisplay.Max(g => g.Depth); var primaryOrientation = Orientation.Vertical; var secondaryOrientation = Orientation.Horizontal; if (Horizontal) @@ -189,7 +225,7 @@ private void Populate() groupBy = v => maxDepth - v.Depth; } - var groups = graph.Vertices.GroupBy(groupBy).OrderBy(g => g.Key).ToArray(); + var groups = verticesToDisplay.GroupBy(groupBy).OrderBy(g => g.Key).ToArray(); if (Inverted) { groups = groups.Reverse().ToArray(); @@ -211,6 +247,14 @@ private void Populate() var paddingHeight = Math.Pow(vertex.InDegree, 0.6); var opacity = vertex.InDegree > 1 ? 0.9 : 0.5; + + // Highlight the focus vertex if in direct references mode + if (DirectReferencesOnly && vertex == focusVertex) + { + opacity = 1.0; + background = DarkTheme ? Color.FromRgb(255, 140, 0) : Color.FromRgb(255, 255, 0); // Orange/Yellow highlight + } + var vertexControl = new TextBlock() { Text = vertex.Title.TrimQuotes(), @@ -241,6 +285,13 @@ private void Populate() SelectVertices([vertex]); } } + + // In direct references mode, clicking a vertex recenters the graph on that vertex + if (DirectReferencesOnly && vertex != focusVertex) + { + focusVertex = vertex; + Redraw(); + } }; layerPanel.Children.Add(vertexControl); } @@ -251,6 +302,49 @@ private void Populate() SelectVertices(selectedVertices.ToArray()); } + private IEnumerable GetVerticesToDisplay() + { + if (!DirectReferencesOnly) + { + return graph.Vertices; + } + + // If no focus vertex is set, use the first vertex with the highest in-degree (most connected) + // or fall back to the first vertex + if (focusVertex == null) + { + focusVertex = graph.Vertices + .OrderByDescending(v => v.InDegree + v.Outgoing.Count()) + .ThenBy(v => v.Title) + .FirstOrDefault(); + + if (focusVertex == null) + { + return Enumerable.Empty(); + } + } + + // In direct references mode, show only the focus vertex and its directly connected vertices + var directlyConnected = new HashSet(); + + // Add the focus vertex itself + directlyConnected.Add(focusVertex); + + // Add vertices that the focus vertex depends on (outgoing connections) + foreach (var outgoing in focusVertex.Outgoing) + { + directlyConnected.Add(outgoing); + } + + // Add vertices that depend on the focus vertex (incoming connections) + foreach (var incoming in focusVertex.Incoming) + { + directlyConnected.Add(incoming); + } + + return directlyConnected; + } + private Color ComputeBackground(int depth) { byte ratio, halfratio; @@ -378,7 +472,7 @@ void SelectVertices(IEnumerable vertices) selectedVertices.UnionWith(vertices); - var controls = vertices.Select(GetControl).ToArray(); + var controls = vertices.Select(GetControl).Where(c => c != null).ToArray(); SelectControls(controls); } @@ -481,6 +575,8 @@ void SelectControl(FrameworkElement vertexControl) private void AddIncomingEdges(Rect destinationRect, Vertex destinationVertex) { + var visibleVertices = DirectReferencesOnly ? GetVerticesToDisplay().ToHashSet() : null; + foreach (var incoming in destinationVertex.Incoming) { if (HideTransitiveEdges && @@ -490,6 +586,12 @@ private void AddIncomingEdges(Rect destinationRect, Vertex destinationVertex) continue; } + // In direct references mode, only show edges to visible vertices + if (DirectReferencesOnly && visibleVertices != null && !visibleVertices.Contains(incoming)) + { + continue; + } + if (GetControl(incoming) is { } sourceControl) { var sourceRect = GetRectOnCanvas(sourceControl); @@ -502,9 +604,16 @@ private void AddIncomingEdges(Rect destinationRect, Vertex destinationVertex) private void AddOutgoingEdges(Rect sourceRect, Vertex node) { IEnumerable list = HideTransitiveEdges ? node.NonRedundantOutgoing : node.Outgoing; + var visibleVertices = DirectReferencesOnly ? GetVerticesToDisplay().ToHashSet() : null; foreach (var outgoing in list) { + // In direct references mode, only show edges to visible vertices + if (DirectReferencesOnly && visibleVertices != null && !visibleVertices.Contains(outgoing)) + { + continue; + } + if (GetControl(outgoing) is { } destinationControl) { var destinationRect = GetRectOnCanvas(destinationControl); @@ -553,7 +662,15 @@ public void Locate(string text) foundControl.BringIntoView(); } - SelectVertices(found.Select(GetVertex).ToArray()); + var foundVertices = found.Select(GetVertex).ToArray(); + SelectVertices(foundVertices); + + // In direct references mode, set focus to the first found vertex + if (DirectReferencesOnly && foundVertices.Any()) + { + focusVertex = foundVertices.First(); + Redraw(); + } } private FrameworkElement FindControlByText(string text) diff --git a/src/StructuredLogViewer/Controls/GraphHostControl.cs b/src/StructuredLogViewer/Controls/GraphHostControl.cs index e2336f905..b6a257fde 100644 --- a/src/StructuredLogViewer/Controls/GraphHostControl.cs +++ b/src/StructuredLogViewer/Controls/GraphHostControl.cs @@ -160,6 +160,13 @@ private void Initialize() Margin = new Thickness(0, 0, 8, 0) }; + var directReferencesOnlyCheck = new CheckBox + { + Content = "Direct references only", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0) + }; + var helpButton = new Button { Content = "Help", @@ -242,6 +249,7 @@ private void Initialize() toolbar.Children.Add(showTextButton); toolbar.Children.Add(transitiveReduceCheck); + toolbar.Children.Add(directReferencesOnlyCheck); toolbar.Children.Add(depthCheckbox); toolbar.Children.Add(horizontalCheckbox); toolbar.Children.Add(invertedCheckbox); @@ -298,6 +306,9 @@ private void Initialize() transitiveReduceCheck.Checked += (s, e) => graphControl.HideTransitiveEdges = true; transitiveReduceCheck.Unchecked += (s, e) => graphControl.HideTransitiveEdges = false; + directReferencesOnlyCheck.Checked += (s, e) => graphControl.DirectReferencesOnly = true; + directReferencesOnlyCheck.Unchecked += (s, e) => graphControl.DirectReferencesOnly = false; + void Locate() { var text = searchTextBox.Text;