Skip to content

Commit 95fc881

Browse files
Merge pull request #912 from erikdarlingdata/feature/dashboard-file-splits
Split Lite + Dashboard god-class files into partial classes
2 parents 0cad812 + 2fb8cd3 commit 95fc881

47 files changed

Lines changed: 22629 additions & 21855 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright (c) 2026 Erik Darling, Darling Data LLC
3+
*
4+
* This file is part of the SQL Server Performance Monitor.
5+
*
6+
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
7+
*/
8+
9+
using System;
10+
using System.Windows;
11+
using System.Windows.Controls;
12+
using System.Windows.Input;
13+
using System.Windows.Media;
14+
using PerformanceMonitorDashboard.Models;
15+
16+
namespace PerformanceMonitorDashboard.Controls;
17+
18+
public partial class PlanViewerControl
19+
{
20+
#region Node Selection & Context Menu
21+
22+
private void Node_Click(object sender, MouseButtonEventArgs e)
23+
{
24+
if (sender is Border border && border.Tag is PlanNode node)
25+
{
26+
SelectNode(border, node);
27+
e.Handled = true;
28+
}
29+
}
30+
31+
private void SelectNode(Border border, PlanNode node)
32+
{
33+
// Deselect previous
34+
if (_selectedNodeBorder != null)
35+
{
36+
_selectedNodeBorder.BorderBrush = _selectedNodeOriginalBorder;
37+
_selectedNodeBorder.BorderThickness = _selectedNodeOriginalThickness;
38+
}
39+
40+
// Select new
41+
_selectedNodeOriginalBorder = border.BorderBrush;
42+
_selectedNodeOriginalThickness = border.BorderThickness;
43+
_selectedNodeBorder = border;
44+
_selectedNode = node;
45+
border.BorderBrush = SelectionBrush;
46+
border.BorderThickness = new Thickness(2);
47+
48+
ShowPropertiesPanel(node);
49+
}
50+
51+
private ContextMenu BuildNodeContextMenu(PlanNode node)
52+
{
53+
var menu = new ContextMenu();
54+
55+
var propsItem = new MenuItem { Header = "Properties" };
56+
propsItem.Click += (_, _) =>
57+
{
58+
// Find the border for this node by checking Tags
59+
foreach (var child in PlanCanvas.Children)
60+
{
61+
if (child is Border b && b.Tag == node)
62+
{
63+
SelectNode(b, node);
64+
break;
65+
}
66+
}
67+
};
68+
menu.Items.Add(propsItem);
69+
70+
menu.Items.Add(new Separator());
71+
72+
var copyOpItem = new MenuItem { Header = "Copy Operator Name" };
73+
copyOpItem.Click += (_, _) => Clipboard.SetDataObject(node.PhysicalOp, false);
74+
menu.Items.Add(copyOpItem);
75+
76+
if (!string.IsNullOrEmpty(node.FullObjectName))
77+
{
78+
var copyObjItem = new MenuItem { Header = "Copy Object Name" };
79+
copyObjItem.Click += (_, _) => Clipboard.SetDataObject(node.FullObjectName, false);
80+
menu.Items.Add(copyObjItem);
81+
}
82+
83+
if (!string.IsNullOrEmpty(node.Predicate))
84+
{
85+
var copyPredItem = new MenuItem { Header = "Copy Predicate" };
86+
copyPredItem.Click += (_, _) => Clipboard.SetDataObject(node.Predicate, false);
87+
menu.Items.Add(copyPredItem);
88+
}
89+
90+
if (!string.IsNullOrEmpty(node.SeekPredicates))
91+
{
92+
var copySeekItem = new MenuItem { Header = "Copy Seek Predicate" };
93+
copySeekItem.Click += (_, _) => Clipboard.SetDataObject(node.SeekPredicates, false);
94+
menu.Items.Add(copySeekItem);
95+
}
96+
97+
return menu;
98+
}
99+
100+
#endregion
101+
102+
#region Zoom
103+
104+
private void ZoomIn_Click(object sender, RoutedEventArgs e) => SetZoom(_zoomLevel + ZoomStep);
105+
private void ZoomOut_Click(object sender, RoutedEventArgs e) => SetZoom(_zoomLevel - ZoomStep);
106+
107+
private void ZoomFit_Click(object sender, RoutedEventArgs e)
108+
{
109+
if (PlanCanvas.Width <= 0 || PlanCanvas.Height <= 0) return;
110+
111+
var viewWidth = PlanScrollViewer.ActualWidth;
112+
var viewHeight = PlanScrollViewer.ActualHeight;
113+
if (viewWidth <= 0 || viewHeight <= 0) return;
114+
115+
var fitZoom = Math.Min(viewWidth / PlanCanvas.Width, viewHeight / PlanCanvas.Height);
116+
SetZoom(Math.Min(fitZoom, 1.0));
117+
}
118+
119+
private void SetZoom(double level)
120+
{
121+
_zoomLevel = Math.Max(MinZoom, Math.Min(MaxZoom, level));
122+
ZoomTransform.ScaleX = _zoomLevel;
123+
ZoomTransform.ScaleY = _zoomLevel;
124+
ZoomLevelText.Text = $"{(int)(_zoomLevel * 100)}%";
125+
}
126+
127+
private void PlanScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
128+
{
129+
if (Keyboard.Modifiers == ModifierKeys.Control)
130+
{
131+
e.Handled = true;
132+
SetZoom(_zoomLevel + (e.Delta > 0 ? ZoomStep : -ZoomStep));
133+
}
134+
}
135+
136+
private void PlanViewerControl_PreviewMouseDown(object sender, MouseButtonEventArgs e)
137+
{
138+
// Don't steal focus from interactive controls (ComboBox, DataGrid, TextBox, etc.)
139+
// ComboBox dropdown items live in a separate visual tree (Popup), so also check
140+
// for ComboBoxItem to avoid stealing focus when selecting dropdown items.
141+
if (e.OriginalSource is System.Windows.Controls.Primitives.TextBoxBase
142+
|| e.OriginalSource is ComboBox
143+
|| e.OriginalSource is ComboBoxItem
144+
|| FindVisualParent<ComboBox>(e.OriginalSource as DependencyObject) != null
145+
|| FindVisualParent<ComboBoxItem>(e.OriginalSource as DependencyObject) != null
146+
|| FindVisualParent<DataGrid>(e.OriginalSource as DependencyObject) != null)
147+
return;
148+
149+
Focus();
150+
}
151+
152+
private static T? FindVisualParent<T>(DependencyObject? child) where T : DependencyObject
153+
{
154+
while (child != null)
155+
{
156+
if (child is T parent) return parent;
157+
child = VisualTreeHelper.GetParent(child);
158+
}
159+
return null;
160+
}
161+
162+
private void PlanViewerControl_PreviewKeyDown(object sender, KeyEventArgs e)
163+
{
164+
if (e.Key == Key.V && Keyboard.Modifiers == ModifierKeys.Control
165+
&& e.OriginalSource is not TextBox)
166+
{
167+
var text = Clipboard.GetText();
168+
if (!string.IsNullOrWhiteSpace(text))
169+
{
170+
e.Handled = true;
171+
try
172+
{
173+
System.Xml.Linq.XDocument.Parse(text);
174+
}
175+
catch (System.Xml.XmlException ex)
176+
{
177+
MessageBox.Show(
178+
$"The plan XML is not valid:\n\n{ex.Message}",
179+
"Invalid Plan XML",
180+
MessageBoxButton.OK,
181+
MessageBoxImage.Warning);
182+
return;
183+
}
184+
LoadPlan(text, "Pasted Plan");
185+
}
186+
}
187+
}
188+
189+
#endregion
190+
191+
#region Canvas Panning
192+
193+
private void PlanScrollViewer_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
194+
{
195+
// Don't intercept scrollbar interactions
196+
if (IsScrollBarAtPoint(e))
197+
return;
198+
199+
// Don't pan if clicking on a node
200+
if (IsNodeAtPoint(e))
201+
return;
202+
203+
_isPanning = true;
204+
_panStart = e.GetPosition(PlanScrollViewer);
205+
_panStartOffsetX = PlanScrollViewer.HorizontalOffset;
206+
_panStartOffsetY = PlanScrollViewer.VerticalOffset;
207+
PlanScrollViewer.Cursor = Cursors.SizeAll;
208+
PlanScrollViewer.CaptureMouse();
209+
e.Handled = true;
210+
}
211+
212+
private void PlanScrollViewer_PreviewMouseMove(object sender, MouseEventArgs e)
213+
{
214+
if (!_isPanning) return;
215+
216+
var current = e.GetPosition(PlanScrollViewer);
217+
var dx = current.X - _panStart.X;
218+
var dy = current.Y - _panStart.Y;
219+
220+
PlanScrollViewer.ScrollToHorizontalOffset(Math.Max(0, _panStartOffsetX - dx));
221+
PlanScrollViewer.ScrollToVerticalOffset(Math.Max(0, _panStartOffsetY - dy));
222+
e.Handled = true;
223+
}
224+
225+
private void PlanScrollViewer_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
226+
{
227+
if (!_isPanning) return;
228+
_isPanning = false;
229+
PlanScrollViewer.Cursor = Cursors.Arrow;
230+
PlanScrollViewer.ReleaseMouseCapture();
231+
e.Handled = true;
232+
}
233+
234+
/// <summary>Check if the mouse event originated from a ScrollBar.</summary>
235+
private static bool IsScrollBarAtPoint(MouseButtonEventArgs e)
236+
{
237+
var source = e.OriginalSource as DependencyObject;
238+
while (source != null)
239+
{
240+
if (source is System.Windows.Controls.Primitives.ScrollBar)
241+
return true;
242+
source = VisualTreeHelper.GetParent(source);
243+
}
244+
return false;
245+
}
246+
247+
/// <summary>Check if the mouse event originated from a node Border (has PlanNode in Tag).</summary>
248+
private static bool IsNodeAtPoint(MouseButtonEventArgs e)
249+
{
250+
var source = e.OriginalSource as DependencyObject;
251+
while (source != null)
252+
{
253+
if (source is Border b && b.Tag is PlanNode)
254+
return true;
255+
source = VisualTreeHelper.GetParent(source);
256+
}
257+
return false;
258+
}
259+
260+
#endregion
261+
}

0 commit comments

Comments
 (0)