// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // Copyright (C) LibreHardwareMonitor and Contributors. using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Windows.WinUI.Controls; using LibreHardwareMonitor.Windows.WinUI.Services; using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; using LibreHardwareMonitor.Windows.WinUI.Utilities; using LibreHardwareMonitor.Windows.WinUI.ViewModels; using Microsoft.UI; using Microsoft.UI.Dispatching; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Media; using WinRT.Interop; using IOPath = System.IO.Path; namespace LibreHardwareMonitor.Windows.WinUI; public sealed partial class MainWindow : Window { private readonly AppWindow _appWindow; private readonly WindowChromeManager _chromeManager; private readonly WindowPlacementService _placementService; private readonly DispatcherQueueTimer _timer; private readonly IStartupTracer _startupTrace; private readonly TrayIconService _trayIconService; private readonly DialogService _dialogService; private readonly SensorColumnMeasurer _columnMeasurer; private readonly SecondaryWindowCoordinator _secondaryWindows; private bool _firstLayoutRecorded; private bool _isUpdating; private bool _initialWindowStateApplied; private bool _isClosingForExit; private bool _isShuttingDown; private bool _runtimeErrorLogged; private bool _isMonitoringStarted; private bool _startupCompletionRequested; private bool _startupCompleteRecorded; private bool _sensorTreeRebuildQueued; private bool _sensorColumnWidthUpdateQueued; public MainWindow() : this(NoOpStartupTracer.Instance) { } internal MainWindow(IStartupTracer startupTrace) { _startupTrace = startupTrace; _startupTrace.Mark("MainWindow.Constructor.Begin"); AppSettings settings = MeasureStartup("MainWindow.LoadSettings", AppSettings.LoadDefault); ViewModel = MeasureStartup("MainWindow.CreateViewModel", () => new MainWindowViewModel(settings, _startupTrace)); _appWindow = MeasureStartup("MainWindow.GetAppWindow", () => { IntPtr hwnd = WindowNative.GetWindowHandle(this); WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); AppWindow appWindow = AppWindow.GetFromWindowId(windowId); appWindow.Title = "Libre Hardware Monitor"; return appWindow; }); IntPtr mainHwnd = WindowNative.GetWindowHandle(this); _chromeManager = new WindowChromeManager(mainHwnd); _placementService = new WindowPlacementService(_appWindow, settings, mainHwnd); _trayIconService = MeasureStartup("MainWindow.CreateTrayIconService", () => new TrayIconService( WindowNative.GetWindowHandle(this), settings, () => ViewModel.TemperatureUnit)); _trayIconService.IsMainIconEnabled = ViewModel.MinimizeToTray; _dialogService = new DialogService(() => Content.XamlRoot, ViewModel, WindowNative.GetWindowHandle(this)); _secondaryWindows = new SecondaryWindowCoordinator(ViewModel, HideShowMainWindow); _columnMeasurer = new SensorColumnMeasurer(DispatcherQueue, ViewModel, _startupTrace); MeasureStartup("MainWindow.ApplySavedDeviceColumnWidth", _columnMeasurer.ApplySavedWidth, () => FormattableString.Invariant($"width={_columnMeasurer.DeviceColumnWidth:F0}")); _columnMeasurer.SettleTriggered += (_, _) => UpdateSensorColumnWidths(); MeasureStartup("MainWindow.InitializeComponent", InitializeComponent); TryApplyMicaBackdrop(); RootGrid.Loaded += RootGrid_Loaded; RootGrid.LayoutUpdated += RootGrid_LayoutUpdated; Bind(ContentGrid, UIElement.IsHitTestVisibleProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled)); MeasureStartup("MainWindow.PopulateMenuSubmenus", PopulateMenuSubmenus); MeasureStartup("MainWindow.PopulateSensorHeader", PopulateSensorHeader); MeasureStartup("MainWindow.AttachPlotView", () => PlotControl.AttachViewModel(ViewModel)); MeasureStartup("MainWindow.RestoreWindowBounds", _placementService.Restore); MeasureStartup("MainWindow.MaximizeWindow", _placementService.Maximize); MeasureStartup("MainWindow.ApplyTheme", ApplyTheme); MeasureStartup("MainWindow.UpdatePlotLayout", UpdatePlotLayout); _timer = MeasureStartup("MainWindow.CreateTimer", () => { DispatcherQueueTimer timer = DispatcherQueue.CreateTimer(); timer.Interval = ViewModel.UpdateInterval; timer.Tick += UpdateTimer_Tick; return timer; }); MeasureStartup("MainWindow.WireEvents", () => { ViewModel.PropertyChanged += (_, args) => { if (args.PropertyName is nameof(ViewModel.UpdateInterval) or nameof(ViewModel.UpdateIntervalIndex)) _timer.Interval = ViewModel.UpdateInterval; if (args.PropertyName is nameof(ViewModel.ShowPlot) or nameof(ViewModel.PlotLocation) or nameof(ViewModel.PlotGridColumn) or nameof(ViewModel.PlotGridRow)) UpdatePlotLayout(); if (args.PropertyName is nameof(ViewModel.PlotStackedAxes) or nameof(ViewModel.ShowPlotAxisLabels) or nameof(ViewModel.PlotTimeWindowIndex)) DrawPlot(); if (args.PropertyName == nameof(ViewModel.ShowGadget)) UpdateGadgetVisibility(); if (args.PropertyName == nameof(ViewModel.MinimizeToTray)) _trayIconService.IsMainIconEnabled = ViewModel.MinimizeToTray; if (args.PropertyName == nameof(ViewModel.TemperatureUnit)) { _trayIconService.Update(); SyncGadgetSensors(); ViewModel.RefreshPlotSeries(); } if (args.PropertyName == nameof(ViewModel.ShowHiddenSensors)) QueueSensorTreeRebuild(); if (args.PropertyName is nameof(ViewModel.ShowValueColumn) or nameof(ViewModel.ShowMinColumn) or nameof(ViewModel.ShowMaxColumn)) QueueSensorColumnWidthUpdate(); if (args.PropertyName == nameof(ViewModel.ThemeMode)) ApplyTheme(); }; ViewModel.RootItems.CollectionChanged += RootItems_CollectionChanged; ViewModel.PlotInvalidated += (_, _) => DrawPlot(); ViewModel.GadgetSensorsChanged += (_, _) => SyncGadgetSensors(); ViewModel.TraySensorsChanged += (_, _) => SyncTraySensors(); _trayIconService.HideShowRequested += (_, _) => HideShowMainWindow(); _trayIconService.ExitRequested += (_, _) => CloseApplication(); _appWindow.Changed += AppWindow_Changed; Closed += MainWindow_Closed; }); _startupTrace.Mark("MainWindow.Constructor.Complete"); } public MainWindowViewModel ViewModel { get; } public void StartMonitoringAfterActivation() { _startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Queued"); if (!DispatcherQueue.TryEnqueue(async () => { if (_isMonitoringStarted) return; _isMonitoringStarted = true; try { _startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Begin"); ApplyInitialWindowState(); await MeasureStartupAsync("MainWindowViewModel.StartAsync", ViewModel.StartAsync); SyncTraySensors(); SyncGadgetSensors(); UpdateGadgetVisibility(); MeasureStartup("MainWindow.StartTimer", _timer.Start); _startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Complete"); RequestStartupTraceComplete(); _startupTrace.Flush(); } catch (Exception ex) { _startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.Exception", $"{ex.GetType().FullName}: {ex.Message}"); _startupTrace.Flush(); RecordRuntimeError("Hardware initialization failed", ex); } })) { _startupTrace.Mark("MainWindow.StartMonitoringAfterActivation.EnqueueFailed"); _startupTrace.Flush(); } } private string GetRootSizeDetail() { return FormattableString.Invariant($"root={RootGrid.ActualWidth:F0}x{RootGrid.ActualHeight:F0}, rows={_columnMeasurer.RowCount}, rootNodes={SensorTree.RootNodes.Count}"); } private void RootGrid_Loaded(object sender, RoutedEventArgs e) { RootGrid.Loaded -= RootGrid_Loaded; _startupTrace.Mark("MainWindow.RootLoaded", GetRootSizeDetail()); _startupTrace.Flush(); } private void RootGrid_LayoutUpdated(object? sender, object e) { if (_firstLayoutRecorded) return; _firstLayoutRecorded = true; RootGrid.LayoutUpdated -= RootGrid_LayoutUpdated; _startupTrace.Mark("MainWindow.FirstLayoutUpdated", GetRootSizeDetail()); _startupTrace.Flush(); } private void MeasureStartup(string phase, Action action) { if (_startupTrace.IsComplete) { action(); return; } _startupTrace.Measure(phase, action); } private void MeasureStartup(string phase, Action action, Func getDetail) { if (_startupTrace.IsComplete) { action(); return; } _startupTrace.Measure(phase, action, getDetail); } private T MeasureStartup(string phase, Func action) { if (_startupTrace.IsComplete) return action(); return _startupTrace.Measure(phase, action); } private async Task MeasureStartupAsync(string phase, Func action) { if (_startupTrace.IsComplete) { await action(); return; } await _startupTrace.MeasureAsync(phase, action); } private void PopulateMenuSubmenus() { PopulateRadioSubMenu(TemperatureUnitMenu, [ ("Celsius", TemperatureUnit.Celsius), ("Fahrenheit", TemperatureUnit.Fahrenheit) ], () => ViewModel.TemperatureUnit, v => ViewModel.TemperatureUnit = v); PopulateRadioSubMenu(PlotLocationMenu, [ ("Window", PlotLocation.Window), ("Bottom", PlotLocation.Bottom), ("Right", PlotLocation.Right) ], () => ViewModel.PlotLocation, v => ViewModel.PlotLocation = v); PopulateRadioSubMenu(ThemeMenu, [ ("Auto", AppThemeMode.Auto), ("Light", AppThemeMode.Light), ("Dark", AppThemeMode.Dark), ("Black", AppThemeMode.Black) ], () => ViewModel.ThemeMode, v => ViewModel.ThemeMode = v); PopulateRadioSubMenu(StrokeThicknessMenu, [ ("1pt", 1), ("2pt", 2), ("3pt", 3), ("4pt", 4) ], () => (int)ViewModel.PlotStrokeThickness, v => ViewModel.PlotStrokeThickness = v); PopulateRadioSubMenu(FileRotationMenu, [ ("Per Session", LoggerFileRotation.PerSession), ("Daily", LoggerFileRotation.Daily) ], () => ViewModel.FileRotationMethod, v => ViewModel.FileRotationMethod = v); PopulateRadioSubMenu(UpdateIntervalMenu, [ ("250ms", 0), ("500ms", 1), ("1s", 2), ("2s", 3), ("5s", 4), ("10s", 5) ], () => ViewModel.UpdateIntervalIndex, v => ViewModel.UpdateIntervalIndex = v); PopulateRadioSubMenu(LoggingIntervalMenu, [ ("1s", 0), ("2s", 1), ("5s", 2), ("10s", 3), ("30s", 4), ("1min", 5), ("2min", 6), ("5min", 7), ("10min", 8), ("30min", 9), ("1h", 10), ("2h", 11), ("6h", 12) ], () => ViewModel.LoggingIntervalIndex, v => ViewModel.LoggingIntervalIndex = v); PopulateRadioSubMenu(SensorTimeWindowMenu, [ ("30s", 0), ("1min", 1), ("2min", 2), ("5min", 3), ("10min", 4), ("30min", 5), ("1h", 6), ("2h", 7), ("6h", 8), ("12h", 9), ("24h", 10) ], () => ViewModel.SensorValuesTimeWindowIndex, v => ViewModel.SensorValuesTimeWindowIndex = v); RunWebServerItem.IsEnabled = !ViewModel.IsWebServerUnavailable; WebServerInterfaceItem.IsEnabled = !ViewModel.IsWebServerUnavailable; WebServerAuthItem.IsEnabled = !ViewModel.IsWebServerUnavailable; } private static void PopulateRadioSubMenu(MenuFlyoutSubItem subItem, (string Label, T Value)[] items, Func getter, Action setter) { foreach ((string label, T value) in items) { ToggleMenuFlyoutItem item = new() { Text = label, IsChecked = Equals(getter(), value), Tag = value }; item.Click += (_, _) => { setter(value); foreach (ToggleMenuFlyoutItem sibling in subItem.Items.OfType()) sibling.IsChecked = Equals(sibling.Tag, value); }; subItem.Items.Add(item); } } private async void OnSaveReport(object sender, RoutedEventArgs e) => await _dialogService.SaveReportAsync(); private void OnResetHardware(object sender, RoutedEventArgs e) => ViewModel.ResetHardware(); private void OnExit(object sender, RoutedEventArgs e) => CloseApplication(); private void OnResetMinMax(object sender, RoutedEventArgs e) => ViewModel.ResetMinMax(); private void OnExpandAll(object sender, RoutedEventArgs e) { ViewModel.SetAllExpanded(true); RebuildSensorTree(); } private void OnCollapseAll(object sender, RoutedEventArgs e) { ViewModel.SetAllExpanded(false); RebuildSensorTree(); } private void OnResetPlot(object sender, RoutedEventArgs e) => ResetPlot(); private async void OnWebServerSettings(object sender, RoutedEventArgs e) => await _dialogService.ShowWebServerSettingsAsync(); private async void OnWebServerAuthentication(object sender, RoutedEventArgs e) => await _dialogService.ShowWebServerAuthenticationAsync(); private async void OnAbout(object sender, RoutedEventArgs e) => await _dialogService.ShowAboutAsync(); private void PopulateSensorHeader() { Grid header = _columnMeasurer.CreateHeaderGrid(); header.Padding = new Thickness(8, 5, 8, 5); header.Background = (Brush)Application.Current.Resources["LayerFillColorDefaultBrush"]; header.Children.Add(CreateHeaderText("Sensor", 0, Visibility.Visible)); header.Children.Add(CreateHeaderText("Value", 1, ViewModel.ValueColumnVisibility, nameof(ViewModel.ValueColumnVisibility))); header.Children.Add(CreateHeaderText("Min", 2, ViewModel.MinColumnVisibility, nameof(ViewModel.MinColumnVisibility))); header.Children.Add(CreateHeaderText("Max", 3, ViewModel.MaxColumnVisibility, nameof(ViewModel.MaxColumnVisibility))); SensorHeaderHost.Children.Add(header); } private void RootItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { QueueSensorTreeRebuild(); } private void QueueSensorTreeRebuild() { if (_sensorTreeRebuildQueued) return; _sensorTreeRebuildQueued = true; if (!DispatcherQueue.TryEnqueue(() => { _sensorTreeRebuildQueued = false; RebuildSensorTree(); })) { _sensorTreeRebuildQueued = false; } } private void QueueSensorColumnWidthUpdate() { if (_sensorColumnWidthUpdateQueued) return; _sensorColumnWidthUpdateQueued = true; if (!DispatcherQueue.TryEnqueue(() => { _sensorColumnWidthUpdateQueued = false; UpdateSensorColumnWidths(); })) { _sensorColumnWidthUpdateQueued = false; } } private async void UpdateTimer_Tick(DispatcherQueueTimer sender, object args) { if (_isUpdating || _isShuttingDown) return; _isUpdating = true; try { await ViewModel.UpdateAsync(); // The window may have been closed (and the view model/computer disposed) while we awaited the update. if (_isShuttingDown) return; UpdateSensorColumnWidths(); _trayIconService.Update(); _secondaryWindows.SyncGadgetSensors(); DrawPlot(); } catch (Exception ex) { // A transient update failure must not permanently freeze the UI: keep the timer running and just record it. RecordRuntimeError("Sensor update failed", ex); } finally { _isUpdating = false; } } private void RecordRuntimeError(string message, Exception exception) { _startupTrace.Mark("MainWindow.RuntimeError", $"{message}: {exception.GetType().FullName}: {exception.Message}"); _startupTrace.Flush(); // Use a dedicated runtime log (not the startup log that App.xaml.cs appends to) and write it only once, so a // recurring per-tick failure neither clobbers the startup diagnostics nor grows the file without bound. string logPath = IOPath.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.runtime.log"); if (!_runtimeErrorLogged) { try { File.WriteAllText(logPath, exception.ToString()); } catch { // Never let diagnostic logging throw out of the update loop. } _runtimeErrorLogged = true; } ViewModel.SetStatusText($"{message}. See {IOPath.GetFileName(logPath)}."); } private void MainWindow_Closed(object sender, WindowEventArgs args) { if (!_isClosingForExit && ViewModel.MinimizeOnClose) { args.Handled = true; MinimizeOrHideMainWindow(); return; } _isShuttingDown = true; _timer.Stop(); _columnMeasurer.StopSettleTimer(); _trayIconService.Dispose(); _secondaryWindows.CloseAll(); _placementService.Save(); ViewModel.Dispose(); _startupTrace.Dispose(); } private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args) { if (_isClosingForExit || !ViewModel.MinimizeToTray) return; if (sender.Presenter is OverlappedPresenter { State: OverlappedPresenterState.Minimized }) HideMainWindowToTray(); } private void ApplyInitialWindowState() { if (_initialWindowStateApplied) return; _initialWindowStateApplied = true; if (ViewModel.StartMinimized) MinimizeOrHideMainWindow(); } private void CloseApplication() { _isClosingForExit = true; Close(); } private void HideShowMainWindow() { if (_chromeManager.IsHiddenOrMinimizedOrInvisible) { _chromeManager.Restore(); Activate(); } else { HideMainWindowToTray(); } } private void MinimizeOrHideMainWindow() { if (ViewModel.MinimizeToTray) HideMainWindowToTray(); else _chromeManager.Minimize(); } private void HideMainWindowToTray() { _chromeManager.HideToTray(); } private void SyncTraySensors() { _trayIconService.RestoreSelectedSensors(ViewModel.GetTraySensorItems()); } private void SyncGadgetSensors() { _secondaryWindows.SyncGadgetSensors(); } private void UpdateGadgetVisibility() { _secondaryWindows.UpdateGadgetVisibility(); } private void UpdatePlotWindowVisibility() { _secondaryWindows.UpdatePlotWindowVisibility(); } private void RebuildSensorTree() { MeasureStartup("MainWindow.RebuildSensorTree", RebuildSensorTreeCore, GetRootSizeDetail); _columnMeasurer.ScheduleSettle(); SyncTraySensors(); SyncGadgetSensors(); if (_startupCompletionRequested) CompleteStartupTraceIfReady(); } private void RebuildSensorTreeCore() { _columnMeasurer.ResetRows(); SensorTree.RootNodes.Clear(); foreach (SensorTreeItemViewModel item in ViewModel.RootItems) { TreeViewNode? node = CreateTreeNode(item); if (node != null) SensorTree.RootNodes.Add(node); } UpdateSensorColumnWidths(); } private void CompleteStartupTraceIfReady() { if (_startupCompleteRecorded || _startupTrace.IsComplete || _columnMeasurer.RowCount == 0) return; _startupCompleteRecorded = true; _startupTrace.Complete("MainWindow.StartupComplete", GetRootSizeDetail()); } private void RequestStartupTraceComplete() { _startupCompletionRequested = true; if (!DispatcherQueue.TryEnqueue(CompleteStartupTraceIfReady)) CompleteStartupTraceIfReady(); } private TreeViewNode? CreateTreeNode(SensorTreeItemViewModel item) { if (item.RowVisibility == Visibility.Collapsed) return null; TreeViewNode node = new() { Content = CreateSensorRow(item), IsExpanded = item.IsExpanded }; foreach (SensorTreeItemViewModel child in item.Children) { TreeViewNode? childNode = CreateTreeNode(child); if (childNode != null) node.Children.Add(childNode); } return node; } private FrameworkElement CreateSensorRow(SensorTreeItemViewModel item) { Grid row = _columnMeasurer.CreateRowGrid(); row.DataContext = item; row.Padding = new Thickness(0, 3, 0, 3); StackPanel sensorCell = new() { Orientation = Orientation.Horizontal, Spacing = 6 }; CheckBox plotCheck = new() { MinWidth = 22 }; Bind(plotCheck, ToggleButton.IsCheckedProperty, item, nameof(SensorTreeItemViewModel.Plot), BindingMode.TwoWay); Bind(plotCheck, UIElement.VisibilityProperty, item, nameof(SensorTreeItemViewModel.PlotCheckVisibility)); plotCheck.Checked += (_, _) => QueuePlotSeriesRefresh(); plotCheck.Unchecked += (_, _) => QueuePlotSeriesRefresh(); sensorCell.Children.Add(plotCheck); FontIcon icon = new() { FontSize = 14 }; Bind(icon, FontIcon.GlyphProperty, item, nameof(SensorTreeItemViewModel.IconGlyph)); sensorCell.Children.Add(icon); TextBlock name = new() { TextTrimming = TextTrimming.CharacterEllipsis }; Bind(name, TextBlock.TextProperty, item, nameof(SensorTreeItemViewModel.Text)); ToolTip toolTip = new(); Bind(toolTip, ContentControl.ContentProperty, item, nameof(SensorTreeItemViewModel.ToolTip)); ToolTipService.SetToolTip(name, toolTip); sensorCell.Children.Add(name); row.Children.Add(sensorCell); row.Children.Add(CreateValueTextBlock(item, nameof(SensorTreeItemViewModel.Value), nameof(SensorTreeItemViewModel.ValueColumnVisibility), 1)); row.Children.Add(CreateValueTextBlock(item, nameof(SensorTreeItemViewModel.Min), nameof(SensorTreeItemViewModel.MinColumnVisibility), 2)); row.Children.Add(CreateValueTextBlock(item, nameof(SensorTreeItemViewModel.Max), nameof(SensorTreeItemViewModel.MaxColumnVisibility), 3)); row.ContextFlyout = BuildSensorContextMenu(item); row.DoubleTapped += async (_, _) => { if (item.Sensor?.Parameters.Count > 0) await _dialogService.ShowParametersAsync(item); }; return row; } private MenuFlyout BuildSensorContextMenu(SensorTreeItemViewModel item) { MenuFlyout flyout = new(); if (item.Sensor?.Parameters.Count > 0) flyout.Items.Add(CreateMenuItem("Parameters...", async (_, _) => await _dialogService.ShowParametersAsync(item))); MenuFlyoutItem rename = CreateMenuItem("Rename", async (_, _) => await _dialogService.RenameAsync(item)); rename.IsEnabled = item.CanRename; flyout.Items.Add(rename); ToggleMenuFlyoutItem plot = new() { Text = "Show in Plot", IsEnabled = item.Sensor != null }; Bind(plot, ToggleMenuFlyoutItem.IsCheckedProperty, item, nameof(SensorTreeItemViewModel.Plot), BindingMode.TwoWay); plot.Click += (_, _) => QueuePlotSeriesRefresh(); flyout.Items.Add(plot); ToggleMenuFlyoutItem tray = new() { Text = "Show in Tray", IsChecked = ViewModel.IsSensorInTray(item), IsEnabled = item.Sensor != null }; tray.Click += (_, _) => { ViewModel.SetSensorInTray(item, tray.IsChecked); SyncTraySensors(); }; flyout.Items.Add(tray); ToggleMenuFlyoutItem gadget = new() { Text = "Show in Gadget", IsChecked = ViewModel.IsSensorInGadget(item), IsEnabled = item.Sensor != null }; gadget.Click += (_, _) => { ViewModel.SetSensorInGadget(item, gadget.IsChecked); SyncGadgetSensors(); }; flyout.Items.Add(gadget); MenuFlyoutItem hide = CreateMenuItem(item.IsVisible ? "Hide Sensor" : "Unhide Sensor", (_, _) => { if (item.CanHide) { item.IsVisible = !item.IsVisible; RebuildSensorTree(); } }); hide.IsEnabled = item.CanHide; flyout.Items.Add(hide); flyout.Items.Add(new MenuFlyoutSeparator()); MenuFlyoutItem penColor = CreateMenuItem("Pen Color...", async (_, _) => await _dialogService.ShowPenColorAsync(item)); penColor.IsEnabled = item.Sensor != null; flyout.Items.Add(penColor); MenuFlyoutItem resetPenColor = CreateMenuItem("Reset Pen Color", (_, _) => ViewModel.SetSensorPenColor(item, null)); resetPenColor.IsEnabled = item.PenColor != null; flyout.Items.Add(resetPenColor); if (item.Sensor?.Control != null) { flyout.Items.Add(new MenuFlyoutSeparator()); flyout.Items.Add(BuildControlMenu(item.Sensor.Control)); } flyout.Items.Add(new MenuFlyoutSeparator()); flyout.Items.Add(CreateMenuItem("Reset Min/Max", (_, _) => ViewModel.ResetMinMax())); return flyout; } private void QueuePlotSeriesRefresh() { if (!DispatcherQueue.TryEnqueue(ViewModel.RefreshPlotSeries)) ViewModel.RefreshPlotSeries(); } private static MenuFlyoutSubItem BuildControlMenu(IControl control) { MenuFlyoutSubItem controlItem = new() { Text = "Control" }; ToggleMenuFlyoutItem defaultItem = new() { Text = "Default", IsChecked = control.ControlMode == ControlMode.Default }; defaultItem.Click += (_, _) => control.SetDefault(); controlItem.Items.Add(defaultItem); MenuFlyoutSubItem manual = new() { Text = "Manual" }; for (int value = 0; value <= 100; value += 5) { if (value > control.MaxSoftwareValue || value < control.MinSoftwareValue) continue; int controlValue = value; ToggleMenuFlyoutItem item = new() { Text = $"{controlValue} %", IsChecked = control.ControlMode == ControlMode.Software && Math.Abs(control.SoftwareValue - controlValue) < 0.1f }; item.Click += (_, _) => control.SetSoftware(controlValue); manual.Items.Add(item); } controlItem.Items.Add(manual); return controlItem; } private void SensorTree_SelectionChanged(TreeView sender, TreeViewSelectionChangedEventArgs args) { if (args.AddedItems.Count == 0) { ViewModel.SelectedItem = null; return; } object selected = args.AddedItems[0]; if (selected is TreeViewNode node && node.Content is FrameworkElement element) ViewModel.SelectedItem = element.DataContext as SensorTreeItemViewModel; } private void ResetPlot() { ViewModel.ResetPlot(); PlotControl.ResetZoom(); _secondaryWindows.RedrawPlot(); } private void DrawPlot() { if (ViewModel.PlotLocation == PlotLocation.Window) PlotControl.Clear(); else PlotControl.Redraw(); _secondaryWindows.RedrawPlot(); } private void UpdatePlotLayout() { ContentGrid.ColumnDefinitions[1].Width = ViewModel.ShowPlot && ViewModel.PlotLocation == PlotLocation.Right ? new GridLength(320) : new GridLength(0); ContentGrid.RowDefinitions[1].Height = ViewModel.ShowPlot && ViewModel.PlotLocation == PlotLocation.Bottom ? new GridLength(220) : new GridLength(0); PlotPane.Visibility = ViewModel.PlotVisibility; Grid.SetRow(PlotPane, ViewModel.PlotGridRow); Grid.SetColumn(PlotPane, ViewModel.PlotGridColumn); Grid.SetRowSpan(PlotPane, ViewModel.PlotGridRowSpan); Grid.SetColumnSpan(PlotPane, ViewModel.PlotGridColumnSpan); Grid.SetRowSpan(SensorPane, ViewModel.SensorGridRowSpan); Grid.SetColumnSpan(SensorPane, ViewModel.SensorGridColumnSpan); UpdatePlotWindowVisibility(); DrawPlot(); } private void ApplyTheme() { RootGrid.RequestedTheme = ViewModel.ThemeMode switch { AppThemeMode.Light => ElementTheme.Light, AppThemeMode.Dark or AppThemeMode.Black => ElementTheme.Dark, _ => ElementTheme.Default }; if (ViewModel.ThemeMode == AppThemeMode.Black) { SystemBackdrop = null; RootGrid.Background = new SolidColorBrush(Colors.Black); } else if (SystemBackdrop is MicaBackdrop) { RootGrid.Background = new SolidColorBrush(Colors.Transparent); } else { TryApplyMicaBackdrop(); RootGrid.Background = SystemBackdrop is MicaBackdrop ? new SolidColorBrush(Colors.Transparent) : (Brush)Application.Current.Resources["ApplicationPageBackgroundThemeBrush"]; } PlotControl.ApplyTheme(ViewModel.ThemeMode); _secondaryWindows.ApplyTheme(ViewModel.ThemeMode); } private void TryApplyMicaBackdrop() { if (SystemBackdrop is MicaBackdrop) return; try { if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported()) SystemBackdrop = new MicaBackdrop { Kind = Microsoft.UI.Composition.SystemBackdrops.MicaKind.Base }; } catch { // Mica unavailable (e.g. Win10) — fall back to theme brush in ApplyTheme. } } private void UpdateSensorColumnWidths() { MeasureStartup("MainWindow.UpdateSensorColumnWidths", _columnMeasurer.UpdateWidths, () => FormattableString.Invariant($"rows={_columnMeasurer.RowCount}, cacheEntries={_columnMeasurer.CacheEntryCount}, deviceWidth={_columnMeasurer.DeviceColumnWidth:F0}, settled={_columnMeasurer.IsSettled}")); } private TextBlock CreateHeaderText(string text, int column, Visibility visibility, string? bindingPath = null) { TextBlock textBlock = new() { Text = text, FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, Visibility = visibility }; if (bindingPath != null) Bind(textBlock, UIElement.VisibilityProperty, ViewModel, bindingPath); Grid.SetColumn(textBlock, column); return textBlock; } private static TextBlock CreateValueTextBlock(SensorTreeItemViewModel item, string textPath, string visibilityPath, int column) { TextBlock textBlock = new() { TextTrimming = TextTrimming.CharacterEllipsis }; Bind(textBlock, TextBlock.TextProperty, item, textPath); Bind(textBlock, UIElement.VisibilityProperty, item, visibilityPath); Grid.SetColumn(textBlock, column); return textBlock; } private static MenuFlyoutItem CreateMenuItem(string text, RoutedEventHandler handler) { MenuFlyoutItem item = new() { Text = text }; item.Click += handler; return item; } private static void Bind(DependencyObject target, DependencyProperty property, object source, string path, BindingMode mode = BindingMode.OneWay) { BindingOperations.SetBinding(target, property, new Binding { Source = source, Path = new PropertyPath(path), Mode = mode, UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged }); } }