From c6ef3edeff3c91280e68ae7290bffbe4e89c648e Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 2 Jun 2026 13:28:34 -0500 Subject: [PATCH] Convert MenuBar to MainWindow.xaml (Phase 5b step 3) Declare File/View/Options/Help menus and their toggle items in XAML with x:Bind (TwoWay) to ViewModel properties. Radio/indexed submenus (Temperature Unit, Plot Location, Theme, Stroke Thickness, File Rotation, Update Interval, Logging Interval, Sensor Values Time Window) are declared as named empty MenuFlyoutSubItems and populated by PopulateRadioSubMenu in code-behind so dynamic check-state and Tag-based selection are preserved. Click handlers for non-toggle items (save report, reset, expand/collapse, web server dialogs, about, exit) live on the code-behind. Delete BuildMenuBar, CreateToggleItem, CreateToggleSettingItem, BuildRadioSubMenu, BuildIndexedSubMenu. CreateMenuItem is retained for the still-imperative sensor row context menu. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MainWindow.cs | 234 +++++------------- .../MainWindow.xaml | 67 ++++- 2 files changed, 131 insertions(+), 170 deletions(-) diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index bbadb23..84c1c6a 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -95,7 +95,7 @@ public sealed partial class MainWindow : Window RootGrid.LayoutUpdated += RootGrid_LayoutUpdated; Bind(ContentGrid, UIElement.IsHitTestVisibleProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled)); - MeasureStartup("MainWindow.BuildMenuBar", () => MenuHost.Children.Add(BuildMenuBar())); + MeasureStartup("MainWindow.PopulateMenuSubmenus", PopulateMenuSubmenus); TreeView? builtTree = null; Grid sensorPane = MeasureStartup("MainWindow.BuildSensorPane", () => @@ -288,147 +288,89 @@ public sealed partial class MainWindow : Window await _startupTrace.MeasureAsync(phase, action); } - private MenuBar BuildMenuBar() + private void PopulateMenuSubmenus() { - MenuBar menuBar = new(); - Bind(menuBar, Control.IsEnabledProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled)); - - MenuBarItem file = new() { Title = "File" }; - file.Items.Add(CreateMenuItem("Save Report...", async (_, _) => await _dialogService.SaveReportAsync())); - file.Items.Add(new MenuFlyoutSeparator()); - file.Items.Add(CreateMenuItem("Reset", (_, _) => ViewModel.ResetHardware())); - MenuFlyoutSubItem hardware = new() { Text = "Hardware" }; - hardware.Items.Add(CreateToggleItem("Motherboard", nameof(ViewModel.IsMotherboardEnabled))); - hardware.Items.Add(CreateToggleItem("CPU", nameof(ViewModel.IsCpuEnabled))); - hardware.Items.Add(CreateToggleItem("Memory", nameof(ViewModel.IsMemoryEnabled))); - hardware.Items.Add(CreateToggleItem("GPU", nameof(ViewModel.IsGpuEnabled))); - hardware.Items.Add(CreateToggleItem("Power Monitors", nameof(ViewModel.IsPowerMonitorEnabled))); - hardware.Items.Add(CreateToggleItem("Fan Controllers", nameof(ViewModel.IsControllerEnabled))); - hardware.Items.Add(CreateToggleItem("Storage Devices", nameof(ViewModel.IsStorageEnabled))); - hardware.Items.Add(CreateToggleItem("Network", nameof(ViewModel.IsNetworkEnabled))); - hardware.Items.Add(CreateToggleItem("Power Supplies", nameof(ViewModel.IsPsuEnabled))); - hardware.Items.Add(CreateToggleItem("Battery", nameof(ViewModel.IsBatteryEnabled))); - file.Items.Add(hardware); - file.Items.Add(new MenuFlyoutSeparator()); - file.Items.Add(CreateMenuItem("Exit", (_, _) => CloseApplication())); - menuBar.Items.Add(file); - - MenuBarItem view = new() { Title = "View" }; - view.Items.Add(CreateMenuItem("Reset Min/Max", (_, _) => ViewModel.ResetMinMax())); - view.Items.Add(CreateMenuItem("Expand All Nodes", (_, _) => - { - ViewModel.SetAllExpanded(true); - RebuildSensorTree(); - })); - view.Items.Add(CreateMenuItem("Collapse All Nodes", (_, _) => - { - ViewModel.SetAllExpanded(false); - RebuildSensorTree(); - })); - view.Items.Add(CreateMenuItem("Reset Plot", (_, _) => ResetPlot())); - view.Items.Add(new MenuFlyoutSeparator()); - view.Items.Add(CreateToggleItem("Show Hidden Sensors", nameof(ViewModel.ShowHiddenSensors))); - view.Items.Add(CreateToggleItem("Show Plot", nameof(ViewModel.ShowPlot))); - view.Items.Add(CreateToggleItem("Gadget", nameof(ViewModel.ShowGadget))); - MenuFlyoutSubItem columns = new() { Text = "Columns" }; - columns.Items.Add(CreateToggleItem("Value", nameof(ViewModel.ShowValueColumn))); - columns.Items.Add(CreateToggleItem("Min", nameof(ViewModel.ShowMinColumn))); - columns.Items.Add(CreateToggleItem("Max", nameof(ViewModel.ShowMaxColumn))); - view.Items.Add(columns); - menuBar.Items.Add(view); - - MenuBarItem options = new() { Title = "Options" }; - options.Items.Add(CreateToggleItem("Start Minimized", nameof(ViewModel.StartMinimized))); - options.Items.Add(CreateToggleItem("Minimize To Tray", nameof(ViewModel.MinimizeToTray))); - options.Items.Add(CreateToggleItem("Minimize On Close", nameof(ViewModel.MinimizeOnClose))); - options.Items.Add(CreateToggleItem("Run On Windows Startup", nameof(ViewModel.RunOnStartup))); - options.Items.Add(new MenuFlyoutSeparator()); - options.Items.Add(BuildRadioSubMenu("Temperature Unit", [ + PopulateRadioSubMenu(TemperatureUnitMenu, [ ("Celsius", TemperatureUnit.Celsius), ("Fahrenheit", TemperatureUnit.Fahrenheit) - ], () => ViewModel.TemperatureUnit, value => ViewModel.TemperatureUnit = value)); - options.Items.Add(BuildRadioSubMenu("Plot Location", [ + ], () => ViewModel.TemperatureUnit, v => ViewModel.TemperatureUnit = v); + PopulateRadioSubMenu(PlotLocationMenu, [ ("Window", PlotLocation.Window), ("Bottom", PlotLocation.Bottom), ("Right", PlotLocation.Right) - ], () => ViewModel.PlotLocation, value => ViewModel.PlotLocation = value)); - options.Items.Add(BuildRadioSubMenu("Theme", [ + ], () => ViewModel.PlotLocation, v => ViewModel.PlotLocation = v); + PopulateRadioSubMenu(ThemeMenu, [ ("Auto", AppThemeMode.Auto), ("Light", AppThemeMode.Light), ("Dark", AppThemeMode.Dark), ("Black", AppThemeMode.Black) - ], () => ViewModel.ThemeMode, value => ViewModel.ThemeMode = value)); - options.Items.Add(BuildIndexedSubMenu("Stroke Thickness", [ + ], () => ViewModel.ThemeMode, v => ViewModel.ThemeMode = v); + PopulateRadioSubMenu(StrokeThicknessMenu, [ ("1pt", 1), ("2pt", 2), ("3pt", 3), ("4pt", 4) - ], () => (int)ViewModel.PlotStrokeThickness, value => ViewModel.PlotStrokeThickness = value)); - options.Items.Add(new MenuFlyoutSeparator()); - options.Items.Add(CreateToggleItem("Log Sensors", nameof(ViewModel.LogSensors))); - options.Items.Add(CreateToggleItem("Force Drive Wakeup", nameof(ViewModel.ForceDriveWakeup))); - options.Items.Add(CreateToggleItem("Throttle ATA Storage", nameof(ViewModel.ThrottleAtaUpdate))); - options.Items.Add(BuildRadioSubMenu("File Rotation Method", [ + ], () => (int)ViewModel.PlotStrokeThickness, v => ViewModel.PlotStrokeThickness = v); + PopulateRadioSubMenu(FileRotationMenu, [ ("Per Session", LoggerFileRotation.PerSession), ("Daily", LoggerFileRotation.Daily) - ], () => ViewModel.FileRotationMethod, value => ViewModel.FileRotationMethod = value)); - options.Items.Add(BuildIndexedSubMenu("Update Interval", [ - ("250ms", 0), - ("500ms", 1), - ("1s", 2), - ("2s", 3), - ("5s", 4), - ("10s", 5) - ], () => ViewModel.UpdateIntervalIndex, value => ViewModel.UpdateIntervalIndex = value)); - options.Items.Add(BuildIndexedSubMenu("Logging Interval", [ - ("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, value => ViewModel.LoggingIntervalIndex = value)); - options.Items.Add(BuildIndexedSubMenu("Sensor Values Time Window", [ - ("30s", 0), - ("1min", 1), - ("2min", 2), - ("5min", 3), - ("10min", 4), - ("30min", 5), - ("1h", 6), - ("2h", 7), - ("6h", 8), - ("12h", 9), - ("24h", 10) - ], () => ViewModel.SensorValuesTimeWindowIndex, value => ViewModel.SensorValuesTimeWindowIndex = value)); - options.Items.Add(new MenuFlyoutSeparator()); - MenuFlyoutSubItem webServer = new() { Text = "Remote Web Server" }; - ToggleMenuFlyoutItem runWebServer = CreateToggleItem("Run", nameof(ViewModel.RunWebServer)); - runWebServer.IsEnabled = !ViewModel.IsWebServerUnavailable; - webServer.Items.Add(runWebServer); - MenuFlyoutItem interfacePort = CreateMenuItem("Interface / Port", async (_, _) => await _dialogService.ShowWebServerSettingsAsync()); - interfacePort.IsEnabled = !ViewModel.IsWebServerUnavailable; - webServer.Items.Add(interfacePort); - MenuFlyoutItem authentication = CreateMenuItem("Authentication", async (_, _) => await _dialogService.ShowWebServerAuthenticationAsync()); - authentication.IsEnabled = !ViewModel.IsWebServerUnavailable; - webServer.Items.Add(authentication); - options.Items.Add(webServer); - menuBar.Items.Add(options); + ], () => 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); - MenuBarItem help = new() { Title = "Help" }; - help.Items.Add(CreateMenuItem("About", async (_, _) => await _dialogService.ShowAboutAsync())); - menuBar.Items.Add(help); - - return menuBar; + 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 Grid BuildSensorPane(out TreeView tree) { Grid pane = new() @@ -993,34 +935,6 @@ public sealed partial class MainWindow : Window return textBlock; } - private static MenuFlyoutSubItem BuildIndexedSubMenu(string text, (string Label, int Value)[] items, Func getter, Action setter) - { - return BuildRadioSubMenu(text, items, getter, setter); - } - - private static MenuFlyoutSubItem BuildRadioSubMenu(string text, (string Label, T Value)[] items, Func getter, Action setter) - { - MenuFlyoutSubItem subItem = new() { Text = text }; - foreach ((string label, T value) in items) - { - ToggleMenuFlyoutItem item = new() - { - Text = label, - IsChecked = Equals(getter(), value) - }; - item.Click += (_, _) => - { - setter(value); - foreach (ToggleMenuFlyoutItem sibling in subItem.Items.OfType()) - sibling.IsChecked = Equals(sibling.Tag, value); - }; - item.Tag = value; - subItem.Items.Add(item); - } - - return subItem; - } - private static MenuFlyoutItem CreateMenuItem(string text, RoutedEventHandler handler) { MenuFlyoutItem item = new() { Text = text }; @@ -1028,24 +942,6 @@ public sealed partial class MainWindow : Window return item; } - private ToggleMenuFlyoutItem CreateToggleItem(string text, string bindingPath) - { - ToggleMenuFlyoutItem item = new() { Text = text }; - Bind(item, ToggleMenuFlyoutItem.IsCheckedProperty, ViewModel, bindingPath, BindingMode.TwoWay); - return item; - } - - private static ToggleMenuFlyoutItem CreateToggleSettingItem(string text, Func getter, Action setter) - { - ToggleMenuFlyoutItem item = new() - { - Text = text, - IsChecked = getter() - }; - item.Click += (_, _) => setter(item.IsChecked); - return item; - } - private static void Bind(DependencyObject target, DependencyProperty property, object source, string path, BindingMode mode = BindingMode.OneWay) { BindingOperations.SetBinding(target, property, new Binding diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml b/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml index 198c9b1..6128d5c 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.xaml @@ -12,7 +12,72 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +