MVP port complete

This commit is contained in:
2026-05-30 20:17:36 -05:00
parent a1cffce9fd
commit c54b5dc4f4
6 changed files with 1271 additions and 16 deletions
+260 -12
View File
@@ -10,6 +10,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Windows.WinUI.Services;
@@ -56,14 +57,20 @@ public sealed class MainWindow : Window
private readonly TreeView _sensorTree;
private readonly Grid _sensorPane;
private readonly WinUiStartupTrace? _startupTrace;
private readonly TrayIconService _trayIconService;
private readonly Dictionary<(string Text, bool Bold), double> _textMeasurementCache = new();
private readonly List<Grid> _sensorRowGrids = [];
private readonly double[] _sensorColumnWidths = [DefaultSensorColumnWidth, 120, 120, 120];
private PlotWindow? _plotWindow;
private Grid? _sensorHeader;
private SensorGadgetWindow? _gadgetWindow;
private double _stableDeviceColumnWidth = DefaultSensorColumnWidth;
private bool _deviceColumnWidthSettled;
private bool _firstLayoutRecorded;
private bool _isUpdating;
private bool _initialWindowStateApplied;
private bool _isClosingForExit;
private bool _isMainWindowHidden;
private bool _isMonitoringStarted;
private bool _startupCompletionRequested;
private bool _startupCompleteRecorded;
@@ -91,6 +98,12 @@ public sealed class MainWindow : Window
return appWindow;
});
_trayIconService = MeasureStartup("MainWindow.CreateTrayIconService", () => new TrayIconService(
WindowNative.GetWindowHandle(this),
settings,
() => ViewModel.TemperatureUnit));
_trayIconService.IsMainIconEnabled = ViewModel.MinimizeToTray;
Grid root = MeasureStartup("MainWindow.BuildRoot", BuildRoot);
MeasureStartup("MainWindow.AssignContent", () => Content = root);
_rootGrid = root;
@@ -143,6 +156,18 @@ public sealed class MainWindow : Window
or nameof(ViewModel.PlotGridRow))
UpdatePlotLayout();
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();
}
if (args.PropertyName == nameof(ViewModel.ShowHiddenSensors))
QueueSensorTreeRebuild();
@@ -156,6 +181,11 @@ public sealed class MainWindow : Window
};
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;
});
@@ -176,7 +206,11 @@ public sealed class MainWindow : Window
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();
@@ -330,7 +364,7 @@ public sealed class MainWindow : Window
hardware.Items.Add(CreateToggleItem("Battery", nameof(ViewModel.IsBatteryEnabled)));
file.Items.Add(hardware);
file.Items.Add(new MenuFlyoutSeparator());
file.Items.Add(CreateMenuItem("Exit", (_, _) => Close()));
file.Items.Add(CreateMenuItem("Exit", (_, _) => CloseApplication()));
menuBar.Items.Add(file);
MenuBarItem view = new() { Title = "View" };
@@ -349,6 +383,7 @@ public sealed class MainWindow : Window
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)));
@@ -367,6 +402,7 @@ public sealed class MainWindow : Window
("Fahrenheit", TemperatureUnit.Fahrenheit)
], () => ViewModel.TemperatureUnit, value => ViewModel.TemperatureUnit = value));
options.Items.Add(BuildRadioSubMenu("Plot Location", [
("Window", PlotLocation.Window),
("Bottom", PlotLocation.Bottom),
("Right", PlotLocation.Right)
], () => ViewModel.PlotLocation, value => ViewModel.PlotLocation = value));
@@ -607,6 +643,8 @@ public sealed class MainWindow : Window
{
await ViewModel.UpdateAsync();
UpdateSensorColumnWidths();
_trayIconService.Update();
_gadgetWindow?.UpdateSensors(ViewModel.GetGadgetSensorItems());
DrawPlot();
}
catch (Exception ex)
@@ -637,17 +675,170 @@ public sealed class MainWindow : Window
private void MainWindow_Closed(object sender, WindowEventArgs args)
{
if (!_isClosingForExit && ViewModel.MinimizeOnClose)
{
args.Handled = true;
MinimizeOrHideMainWindow();
return;
}
_timer.Stop();
_deviceColumnWidthSettleTimer.Stop();
_trayIconService.Dispose();
CloseSecondaryWindows();
SaveWindowBounds();
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 CloseSecondaryWindows()
{
if (_gadgetWindow != null)
{
SensorGadgetWindow gadgetWindow = _gadgetWindow;
_gadgetWindow = null;
gadgetWindow.CloseFromOwner();
}
if (_plotWindow != null)
{
PlotWindow plotWindow = _plotWindow;
_plotWindow = null;
plotWindow.CloseFromOwner();
}
}
private void HideShowMainWindow()
{
IntPtr hwnd = WindowNative.GetWindowHandle(this);
if (_isMainWindowHidden || !IsWindowVisible(hwnd) || IsIconic(hwnd))
RestoreMainWindow();
else
HideMainWindowToTray();
}
private void MinimizeOrHideMainWindow()
{
if (ViewModel.MinimizeToTray)
HideMainWindowToTray();
else
{
_isMainWindowHidden = false;
ShowWindow(WindowNative.GetWindowHandle(this), ShowWindowMinimize);
}
}
private void HideMainWindowToTray()
{
_isMainWindowHidden = true;
ShowWindow(WindowNative.GetWindowHandle(this), ShowWindowHide);
}
private void RestoreMainWindow()
{
IntPtr hwnd = WindowNative.GetWindowHandle(this);
_isMainWindowHidden = false;
ShowWindow(hwnd, ShowWindowShow);
ShowWindow(hwnd, ShowWindowRestore);
Activate();
}
private void SyncTraySensors()
{
_trayIconService.RestoreSelectedSensors(ViewModel.GetTraySensorItems());
}
private void SyncGadgetSensors()
{
_gadgetWindow?.UpdateSensors(ViewModel.GetGadgetSensorItems());
}
private void UpdateGadgetVisibility()
{
if (ViewModel.ShowGadget)
{
if (_gadgetWindow == null)
{
_gadgetWindow = new SensorGadgetWindow(ViewModel);
_gadgetWindow.HideShowMainWindowRequested += (_, _) => HideShowMainWindow();
_gadgetWindow.UserClosed += (_, _) =>
{
_gadgetWindow = null;
ViewModel.ShowGadget = false;
};
SyncGadgetSensors();
_gadgetWindow.Activate();
}
return;
}
if (_gadgetWindow != null)
{
SensorGadgetWindow gadgetWindow = _gadgetWindow;
_gadgetWindow = null;
gadgetWindow.CloseFromOwner();
}
}
private void UpdatePlotWindowVisibility()
{
if (ViewModel.IsPlotWindowVisible)
{
if (_plotWindow == null)
{
_plotWindow = new PlotWindow(ViewModel.Settings);
_plotWindow.PlotSizeChanged += (_, _) => DrawPlot();
_plotWindow.UserClosed += (_, _) =>
{
_plotWindow = null;
ViewModel.ShowPlot = false;
};
_plotWindow.Activate();
}
return;
}
if (_plotWindow != null)
{
PlotWindow plotWindow = _plotWindow;
_plotWindow = null;
plotWindow.CloseFromOwner();
}
}
private void RebuildSensorTree()
{
MeasureStartup("MainWindow.RebuildSensorTree", RebuildSensorTreeCore, GetRootSizeDetail);
ScheduleDeviceColumnWidthSettle();
SyncTraySensors();
SyncGadgetSensors();
if (_startupCompletionRequested)
CompleteStartupTraceIfReady();
}
@@ -764,6 +955,11 @@ public sealed class MainWindow : Window
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 ShowParametersAsync(item);
};
return row;
}
@@ -781,6 +977,32 @@ public sealed class MainWindow : Window
Bind(plot, ToggleMenuFlyoutItem.IsCheckedProperty, item, nameof(SensorTreeItemViewModel.Plot), BindingMode.TwoWay);
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)
@@ -1175,11 +1397,19 @@ public sealed class MainWindow : Window
private void DrawPlot()
{
if (_plotCanvas == null)
return;
if (ViewModel.PlotLocation == PlotLocation.Window)
_plotCanvas.Children.Clear();
else
DrawPlot(_plotCanvas);
_plotCanvas.Children.Clear();
if (!ViewModel.ShowPlot || ViewModel.PlotSeries.Count == 0 || _plotCanvas.ActualWidth <= 4 || _plotCanvas.ActualHeight <= 4)
if (_plotWindow != null)
DrawPlot(_plotWindow.PlotCanvas);
}
private void DrawPlot(Canvas canvas)
{
canvas.Children.Clear();
if (!ViewModel.ShowPlot || ViewModel.PlotSeries.Count == 0 || canvas.ActualWidth <= 4 || canvas.ActualHeight <= 4)
return;
double minTime = double.MaxValue;
@@ -1210,9 +1440,9 @@ public sealed class MainWindow : Window
minValue -= 1;
}
double width = _plotCanvas.ActualWidth;
double height = _plotCanvas.ActualHeight;
DrawAxis(width, height);
double width = canvas.ActualWidth;
double height = canvas.ActualHeight;
DrawAxis(canvas, width, height);
foreach (PlotSeriesViewModel plotSeries in ViewModel.PlotSeries)
{
@@ -1230,17 +1460,17 @@ public sealed class MainWindow : Window
line.Points.Add(new global::Windows.Foundation.Point(x, y));
}
_plotCanvas.Children.Add(line);
canvas.Children.Add(line);
}
}
private void DrawAxis(double width, double height)
private static void DrawAxis(Canvas canvas, double width, double height)
{
SolidColorBrush brush = new(Colors.Gray);
for (int i = 1; i < 4; i++)
{
double y = height * i / 4;
_plotCanvas.Children.Add(new Line
canvas.Children.Add(new Line
{
X1 = 0,
X2 = width,
@@ -1258,13 +1488,14 @@ public sealed class MainWindow : Window
_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.ShowPlot ? Visibility.Visible : Visibility.Collapsed;
_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();
}
@@ -1515,6 +1746,23 @@ public sealed class MainWindow : Window
}
}
private const int ShowWindowHide = 0;
private const int ShowWindowShow = 5;
private const int ShowWindowMinimize = 6;
private const int ShowWindowRestore = 9;
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ShowWindow(IntPtr windowHandle, int command);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsWindowVisible(IntPtr windowHandle);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsIconic(IntPtr windowHandle);
private sealed record ParameterEditorRow(IParameter Parameter, Grid Container, CheckBox UseDefault, TextBox Value);
private static void Bind(DependencyObject target, DependencyProperty property, object source, string path, BindingMode mode = BindingMode.OneWay)