diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index fdad3c4..af543c8 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -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 _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) diff --git a/LibreHardwareMonitor.Windows.WinUI/PlotWindow.cs b/LibreHardwareMonitor.Windows.WinUI/PlotWindow.cs new file mode 100644 index 0000000..8aa5001 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/PlotWindow.cs @@ -0,0 +1,82 @@ +// 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 LibreHardwareMonitor.Windows.WinUI.Services; +using Microsoft.UI; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Windows.Graphics; +using WinRT.Interop; + +namespace LibreHardwareMonitor.Windows.WinUI; + +public sealed class PlotWindow : Window +{ + private readonly AppSettings _settings; + private readonly AppWindow _appWindow; + private bool _closingFromOwner; + + public PlotWindow(AppSettings settings) + { + _settings = settings; + + IntPtr hwnd = WindowNative.GetWindowHandle(this); + _appWindow = AppWindow.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd)); + _appWindow.Title = "Sensor Plot"; + + PlotCanvas = new Canvas + { + MinWidth = 320, + MinHeight = 220, + Background = (Brush)Application.Current.Resources["SystemControlBackgroundAltHighBrush"] + }; + PlotCanvas.SizeChanged += (_, _) => PlotSizeChanged?.Invoke(this, EventArgs.Empty); + Content = PlotCanvas; + + RestoreBounds(); + Closed += PlotWindow_Closed; + } + + public event EventHandler? PlotSizeChanged; + + public event EventHandler? UserClosed; + + public Canvas PlotCanvas { get; } + + public void CloseFromOwner() + { + _closingFromOwner = true; + Close(); + } + + private void RestoreBounds() + { + int width = Math.Max(320, _settings.GetValue("plotForm.Width", 600)); + int height = Math.Max(220, _settings.GetValue("plotForm.Height", 400)); + _appWindow.Resize(new SizeInt32(width, height)); + + int x = _settings.GetValue("plotForm.Location.X", int.MinValue); + int y = _settings.GetValue("plotForm.Location.Y", int.MinValue); + if (x != int.MinValue && y != int.MinValue) + _appWindow.Move(new PointInt32(x, y)); + } + + private void PlotWindow_Closed(object sender, WindowEventArgs args) + { + SaveBounds(); + if (!_closingFromOwner) + UserClosed?.Invoke(this, EventArgs.Empty); + } + + private void SaveBounds() + { + _settings.SetValue("plotForm.Location.X", _appWindow.Position.X); + _settings.SetValue("plotForm.Location.Y", _appWindow.Position.Y); + _settings.SetValue("plotForm.Width", _appWindow.Size.Width); + _settings.SetValue("plotForm.Height", _appWindow.Size.Height); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/SensorGadgetWindow.cs b/LibreHardwareMonitor.Windows.WinUI/SensorGadgetWindow.cs new file mode 100644 index 0000000..2936591 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/SensorGadgetWindow.cs @@ -0,0 +1,201 @@ +// 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.Linq; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Microsoft.UI; +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 Windows.Graphics; +using WinRT.Interop; + +namespace LibreHardwareMonitor.Windows.WinUI; + +public sealed class SensorGadgetWindow : Window +{ + private readonly AppSettings _settings; + private readonly StackPanel _sensorList; + private readonly MainWindowViewModel _viewModel; + private readonly AppWindow _appWindow; + private bool _closingFromOwner; + + public SensorGadgetWindow(MainWindowViewModel viewModel) + { + _viewModel = viewModel; + _settings = viewModel.Settings; + + IntPtr hwnd = WindowNative.GetWindowHandle(this); + _appWindow = AppWindow.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd)); + _appWindow.Title = "Sensor Gadget"; + + _sensorList = new StackPanel { Spacing = 4, Padding = new Thickness(10) }; + ScrollViewer scrollViewer = new() + { + Content = _sensorList, + HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto + }; + + Grid root = new() + { + Background = (Brush)Application.Current.Resources["ApplicationPageBackgroundThemeBrush"] + }; + root.Children.Add(scrollViewer); + root.ContextFlyout = BuildContextMenu(); + Content = root; + + RestoreBounds(); + Closed += SensorGadgetWindow_Closed; + } + + public event EventHandler? HideShowMainWindowRequested; + + public event EventHandler? UserClosed; + + public void CloseFromOwner() + { + _closingFromOwner = true; + Close(); + } + + public void UpdateSensors(IEnumerable sensorItems) + { + _sensorList.Children.Clear(); + SensorTreeItemViewModel[] items = sensorItems.Where(item => item.Sensor != null).ToArray(); + if (items.Length == 0) + { + _sensorList.Children.Add(new TextBlock + { + Text = "No sensors selected.", + Opacity = 0.72, + TextWrapping = TextWrapping.Wrap + }); + return; + } + + foreach (IGrouping group in items.GroupBy(item => GetRootHardware(item.Sensor!.Hardware))) + { + _sensorList.Children.Add(new TextBlock + { + Text = group.Key.Name, + FontWeight = new global::Windows.UI.Text.FontWeight { Weight = 600 }, + Margin = new Thickness(0, 8, 0, 2), + TextTrimming = TextTrimming.CharacterEllipsis + }); + + foreach (SensorTreeItemViewModel item in group) + _sensorList.Children.Add(CreateSensorRow(item)); + } + } + + private Grid CreateSensorRow(SensorTreeItemViewModel item) + { + Grid row = new() + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, + new ColumnDefinition { Width = GridLength.Auto } + }, + ColumnSpacing = 12, + MinWidth = 220 + }; + + StackPanel namePanel = new() { Orientation = Orientation.Horizontal, Spacing = 6 }; + FontIcon icon = new() { FontSize = 12 }; + Bind(icon, FontIcon.GlyphProperty, item, nameof(SensorTreeItemViewModel.IconGlyph)); + namePanel.Children.Add(icon); + + TextBlock name = new() { TextTrimming = TextTrimming.CharacterEllipsis }; + Bind(name, TextBlock.TextProperty, item, nameof(SensorTreeItemViewModel.Text)); + namePanel.Children.Add(name); + row.Children.Add(namePanel); + + TextBlock value = new() + { + HorizontalAlignment = HorizontalAlignment.Right, + TextTrimming = TextTrimming.CharacterEllipsis + }; + Bind(value, TextBlock.TextProperty, item, nameof(SensorTreeItemViewModel.Value)); + Grid.SetColumn(value, 1); + row.Children.Add(value); + + MenuFlyout flyout = new(); + flyout.Items.Add(CreateMenuItem("Remove from Gadget", (_, _) => + { + _viewModel.SetSensorInGadget(item, false); + UpdateSensors(_viewModel.GetGadgetSensorItems()); + })); + row.ContextFlyout = flyout; + return row; + } + + private MenuFlyout BuildContextMenu() + { + MenuFlyout flyout = new(); + flyout.Items.Add(CreateMenuItem("Hide/Show Main Window", (_, _) => HideShowMainWindowRequested?.Invoke(this, EventArgs.Empty))); + return flyout; + } + + private void RestoreBounds() + { + int width = Math.Max(220, _settings.GetValue("sensorGadget.Width", 260)); + int height = Math.Max(160, _settings.GetValue("sensorGadget.Height", 360)); + _appWindow.Resize(new SizeInt32(width, height)); + + int x = _settings.GetValue("sensorGadget.Location.X", int.MinValue); + int y = _settings.GetValue("sensorGadget.Location.Y", int.MinValue); + if (x != int.MinValue && y != int.MinValue) + _appWindow.Move(new PointInt32(x, y)); + } + + private void SensorGadgetWindow_Closed(object sender, WindowEventArgs args) + { + SaveBounds(); + if (!_closingFromOwner) + UserClosed?.Invoke(this, EventArgs.Empty); + } + + private void SaveBounds() + { + _settings.SetValue("sensorGadget.Location.X", _appWindow.Position.X); + _settings.SetValue("sensorGadget.Location.Y", _appWindow.Position.Y); + _settings.SetValue("sensorGadget.Width", _appWindow.Size.Width); + _settings.SetValue("sensorGadget.Height", _appWindow.Size.Height); + } + + private static IHardware GetRootHardware(IHardware hardware) + { + while (hardware.Parent != null) + hardware = hardware.Parent; + + return hardware; + } + + 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 + }); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs b/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs new file mode 100644 index 0000000..4e2fde6 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Services/TrayIconService.cs @@ -0,0 +1,653 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Utilities; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using WinUIColor = Windows.UI.Color; + +namespace LibreHardwareMonitor.Windows.WinUI.Services; + +public sealed class TrayIconService : IDisposable +{ + private const uint CallbackMessage = 0x8000 + 0x42; + private const uint MainIconId = 1; + private const uint SensorIconIdStart = 1000; + private const uint NifMessage = 0x00000001; + private const uint NifIcon = 0x00000002; + private const uint NifTip = 0x00000004; + private const uint NimAdd = 0x00000000; + private const uint NimModify = 0x00000001; + private const uint NimDelete = 0x00000002; + private const uint NimSetVersion = 0x00000004; + private const uint NotifyIconVersion4 = 4; + private const uint WmLButtonDblClk = 0x0203; + private const uint WmRButtonUp = 0x0205; + private const uint MfSeparator = 0x00000800; + private const uint MfString = 0x00000000; + private const uint TpmReturNcmd = 0x0100; + private const uint TpmRightButton = 0x0002; + private const int IconSize = 16; + + private readonly Dictionary _sensorIconsByIdentifier = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _sensorIconsById = new(); + private readonly Func _temperatureUnitProvider; + private readonly IntPtr _windowHandle; + private readonly SubclassProc _subclassProc; + private readonly AppSettings _settings; + private bool _disposed; + private bool _destroyMainIconHandle = true; + private bool _isMainIconEnabled; + private bool _isMainIconVisible; + private IntPtr _mainIconHandle; + private uint _nextSensorIconId = SensorIconIdStart; + + public TrayIconService(IntPtr windowHandle, AppSettings settings, Func temperatureUnitProvider) + { + _windowHandle = windowHandle; + _settings = settings; + _temperatureUnitProvider = temperatureUnitProvider; + _subclassProc = WindowSubclassProc; + SetWindowSubclass(_windowHandle, _subclassProc, MainIconId, 0); + _mainIconHandle = LoadApplicationIcon(); + } + + public event EventHandler? HideShowRequested; + + public event EventHandler? ExitRequested; + + public bool IsMainIconEnabled + { + get => _isMainIconEnabled; + set + { + if (_isMainIconEnabled == value) + return; + + _isMainIconEnabled = value; + UpdateMainIconVisibility(); + } + } + + public void Add(ISensor sensor, bool persist = true) + { + string identifier = sensor.Identifier.ToString(); + if (_sensorIconsByIdentifier.TryGetValue(identifier, out SensorTrayIcon? existing)) + { + existing.Sensor = sensor; + Update(existing); + return; + } + + SensorTrayIcon icon = new(_nextSensorIconId++, sensor); + icon.IconHandle = CreateSensorIcon(sensor); + _sensorIconsByIdentifier[identifier] = icon; + _sensorIconsById[icon.Id] = icon; + AddNotifyIcon(icon.Id, icon.IconHandle, GetSensorToolTip(sensor)); + + if (persist) + _settings.SetValue(GetSensorSettingName(sensor, "tray"), true); + + UpdateMainIconVisibility(); + } + + public bool Contains(ISensor sensor) + { + return _sensorIconsByIdentifier.ContainsKey(sensor.Identifier.ToString()); + } + + public void Dispose() + { + if (_disposed) + return; + + _disposed = true; + foreach (SensorTrayIcon icon in _sensorIconsByIdentifier.Values.ToArray()) + DeleteSensorIcon(icon, deleteSettings: false); + + SetMainIconVisible(false); + if (_destroyMainIconHandle && _mainIconHandle != IntPtr.Zero) + { + DestroyIcon(_mainIconHandle); + _mainIconHandle = IntPtr.Zero; + } + + RemoveWindowSubclass(_windowHandle, _subclassProc, MainIconId); + } + + public void Remove(ISensor sensor, bool deleteSettings = true) + { + if (_sensorIconsByIdentifier.TryGetValue(sensor.Identifier.ToString(), out SensorTrayIcon? icon)) + DeleteSensorIcon(icon, deleteSettings); + } + + public void RestoreSelectedSensors(IEnumerable sensorItems) + { + Dictionary selected = sensorItems + .Where(item => item.Sensor != null && _settings.GetValue(GetSensorSettingName(item.Sensor, "tray"), false)) + .Select(item => item.Sensor!) + .ToDictionary(sensor => sensor.Identifier.ToString(), StringComparer.OrdinalIgnoreCase); + + foreach (SensorTrayIcon icon in _sensorIconsByIdentifier.Values.ToArray()) + { + if (!selected.ContainsKey(icon.Sensor.Identifier.ToString())) + DeleteSensorIcon(icon, deleteSettings: false); + } + + foreach (ISensor sensor in selected.Values) + Add(sensor, persist: false); + + UpdateMainIconVisibility(); + } + + public void Update() + { + foreach (SensorTrayIcon icon in _sensorIconsByIdentifier.Values) + Update(icon); + } + + private void Update(SensorTrayIcon icon) + { + IntPtr previousIcon = icon.IconHandle; + icon.IconHandle = CreateSensorIcon(icon.Sensor); + ModifyNotifyIcon(icon.Id, icon.IconHandle, GetSensorToolTip(icon.Sensor)); + if (previousIcon != IntPtr.Zero) + DestroyIcon(previousIcon); + } + + private void DeleteSensorIcon(SensorTrayIcon icon, bool deleteSettings) + { + DeleteNotifyIcon(icon.Id); + _sensorIconsByIdentifier.Remove(icon.Sensor.Identifier.ToString()); + _sensorIconsById.Remove(icon.Id); + if (icon.IconHandle != IntPtr.Zero) + { + DestroyIcon(icon.IconHandle); + icon.IconHandle = IntPtr.Zero; + } + + if (deleteSettings) + { + _settings.Remove(GetSensorSettingName(icon.Sensor, "tray")); + _settings.Remove(GetSensorSettingName(icon.Sensor, "traycolor")); + } + + UpdateMainIconVisibility(); + } + + private void UpdateMainIconVisibility() + { + SetMainIconVisible(_isMainIconEnabled && _sensorIconsByIdentifier.Count == 0); + } + + private void SetMainIconVisible(bool visible) + { + if (_isMainIconVisible == visible) + return; + + _isMainIconVisible = visible; + if (visible) + AddNotifyIcon(MainIconId, _mainIconHandle, "Libre Hardware Monitor"); + else + DeleteNotifyIcon(MainIconId); + } + + private IntPtr WindowSubclassProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam, nuint subclassId, nuint refData) + { + if (message == CallbackMessage) + HandleTrayCallback((uint)wParam.ToInt64(), (uint)lParam.ToInt64()); + + return DefSubclassProc(hWnd, message, wParam, lParam); + } + + private void HandleTrayCallback(uint iconId, uint mouseMessage) + { + if (mouseMessage == WmLButtonDblClk) + { + HideShowRequested?.Invoke(this, EventArgs.Empty); + return; + } + + if (mouseMessage != WmRButtonUp) + return; + + if (iconId == MainIconId) + { + ShowMainContextMenu(); + return; + } + + if (_sensorIconsById.TryGetValue(iconId, out SensorTrayIcon? icon)) + ShowSensorContextMenu(icon); + } + + private void ShowMainContextMenu() + { + const uint hideShowCommand = 1; + const uint exitCommand = 2; + + IntPtr menu = CreatePopupMenu(); + if (menu == IntPtr.Zero) + return; + + try + { + AppendMenu(menu, MfString, hideShowCommand, "Hide/Show"); + AppendMenu(menu, MfSeparator, 0, null); + AppendMenu(menu, MfString, exitCommand, "Exit"); + + uint command = TrackMenu(menu); + if (command == hideShowCommand) + HideShowRequested?.Invoke(this, EventArgs.Empty); + else if (command == exitCommand) + ExitRequested?.Invoke(this, EventArgs.Empty); + } + finally + { + DestroyMenu(menu); + } + } + + private void ShowSensorContextMenu(SensorTrayIcon icon) + { + const uint hideShowCommand = 1; + const uint removeCommand = 2; + const uint exitCommand = 3; + + IntPtr menu = CreatePopupMenu(); + if (menu == IntPtr.Zero) + return; + + try + { + AppendMenu(menu, MfString, hideShowCommand, "Hide/Show"); + AppendMenu(menu, MfSeparator, 0, null); + AppendMenu(menu, MfString, removeCommand, "Remove Sensor"); + AppendMenu(menu, MfSeparator, 0, null); + AppendMenu(menu, MfString, exitCommand, "Exit"); + + uint command = TrackMenu(menu); + if (command == hideShowCommand) + HideShowRequested?.Invoke(this, EventArgs.Empty); + else if (command == removeCommand) + DeleteSensorIcon(icon, deleteSettings: true); + else if (command == exitCommand) + ExitRequested?.Invoke(this, EventArgs.Empty); + } + finally + { + DestroyMenu(menu); + } + } + + private uint TrackMenu(IntPtr menu) + { + GetCursorPos(out Point point); + SetForegroundWindow(_windowHandle); + return (uint)TrackPopupMenuEx(menu, TpmReturNcmd | TpmRightButton, point.X, point.Y, _windowHandle, IntPtr.Zero); + } + + private void AddNotifyIcon(uint id, IntPtr icon, string tip) + { + NotifyIconData data = CreateNotifyIconData(id, icon, tip); + Shell_NotifyIcon(NimAdd, ref data); + data.uTimeoutOrVersion = NotifyIconVersion4; + Shell_NotifyIcon(NimSetVersion, ref data); + } + + private void ModifyNotifyIcon(uint id, IntPtr icon, string tip) + { + NotifyIconData data = CreateNotifyIconData(id, icon, tip); + Shell_NotifyIcon(NimModify, ref data); + } + + private void DeleteNotifyIcon(uint id) + { + NotifyIconData data = CreateNotifyIconData(id, IntPtr.Zero, ""); + Shell_NotifyIcon(NimDelete, ref data); + } + + private NotifyIconData CreateNotifyIconData(uint id, IntPtr icon, string tip) + { + return new NotifyIconData + { + cbSize = Marshal.SizeOf(), + hWnd = _windowHandle, + uID = id, + uFlags = NifMessage | NifIcon | NifTip, + uCallbackMessage = CallbackMessage, + hIcon = icon, + szTip = TrimForNotifyIcon(tip, 127), + szInfo = "", + szInfoTitle = "" + }; + } + + private IntPtr LoadApplicationIcon() + { + string iconPath = Path.Combine(AppContext.BaseDirectory, "Resources", "icon.ico"); + if (File.Exists(iconPath)) + { + IntPtr icon = LoadImage(IntPtr.Zero, iconPath, 1, 0, 0, 0x00000010); + if (icon != IntPtr.Zero) + return icon; + } + + _destroyMainIconHandle = false; + return LoadIcon(IntPtr.Zero, new IntPtr(32512)); + } + + private IntPtr CreateSensorIcon(ISensor sensor) + { + IntPtr hdc = GetDC(IntPtr.Zero); + IntPtr memoryDc = CreateCompatibleDC(hdc); + IntPtr bitmap = IntPtr.Zero; + IntPtr mask = IntPtr.Zero; + IntPtr oldBitmap = IntPtr.Zero; + IntPtr font = IntPtr.Zero; + IntPtr oldFont = IntPtr.Zero; + IntPtr brush = IntPtr.Zero; + + try + { + BitmapInfo bitmapInfo = BitmapInfo.Create(IconSize, IconSize); + bitmap = CreateDIBSection(hdc, ref bitmapInfo, 0, out _, IntPtr.Zero, 0); + if (bitmap == IntPtr.Zero) + return _mainIconHandle; + + oldBitmap = SelectObject(memoryDc, bitmap); + WinUIColor color = GetSensorTrayColor(sensor); + brush = CreateSolidBrush(ToColorRef(color)); + Rect fill = new() { Left = 0, Top = 0, Right = IconSize, Bottom = IconSize }; + FillRect(memoryDc, ref fill, brush); + + font = CreateFont(-11, 0, 0, 0, 700, 0, 0, 0, 0, 0, 0, 0, 0, "Segoe UI"); + oldFont = SelectObject(memoryDc, font); + SetBkMode(memoryDc, 1); + SetTextColor(memoryDc, ToColorRef(WinUIColor.FromArgb(255, 255, 255, 255))); + + string text = GetSensorIconText(sensor); + Rect textRect = new() { Left = 0, Top = 0, Right = IconSize, Bottom = IconSize }; + DrawText(memoryDc, text, text.Length, ref textRect, 0x00000001 | 0x00000004 | 0x00000020); + + mask = CreateBitmap(IconSize, IconSize, 1, 1, IntPtr.Zero); + IconInfo iconInfo = new() + { + IsIcon = true, + ColorBitmap = bitmap, + MaskBitmap = mask + }; + + return CreateIconIndirect(ref iconInfo); + } + finally + { + if (oldFont != IntPtr.Zero) + SelectObject(memoryDc, oldFont); + if (oldBitmap != IntPtr.Zero) + SelectObject(memoryDc, oldBitmap); + if (font != IntPtr.Zero) + DeleteObject(font); + if (brush != IntPtr.Zero) + DeleteObject(brush); + if (bitmap != IntPtr.Zero) + DeleteObject(bitmap); + if (mask != IntPtr.Zero) + DeleteObject(mask); + if (memoryDc != IntPtr.Zero) + DeleteDC(memoryDc); + if (hdc != IntPtr.Zero) + ReleaseDC(IntPtr.Zero, hdc); + } + } + + private WinUIColor GetSensorTrayColor(ISensor sensor) + { + WinUIColor defaultColor = sensor.SensorType is SensorType.Load or SensorType.Control or SensorType.Level + ? WinUIColor.FromArgb(255, 0x70, 0x8c, 0xf1) + : WinUIColor.FromArgb(255, 0x00, 0x78, 0xd4); + + return _settings.GetValue(GetSensorSettingName(sensor, "traycolor"), defaultColor); + } + + private string GetSensorIconText(ISensor sensor) + { + if (!sensor.Value.HasValue) + return "-"; + + float value = sensor.Value.Value; + if (sensor.SensorType == SensorType.Temperature && _temperatureUnitProvider() == TemperatureUnit.Fahrenheit) + value = value * 1.8f + 32; + + return sensor.SensorType switch + { + SensorType.TimeSpan => TimeSpan.FromSeconds(value).ToString("g", CultureInfo.CurrentCulture), + SensorType.Timing => value.ToString("F1", CultureInfo.CurrentCulture), + SensorType.Clock or SensorType.Fan or SensorType.Flow => (value * 1e-3f).ToString("F1", CultureInfo.CurrentCulture), + SensorType.Voltage or SensorType.Current or SensorType.SmallData or SensorType.Factor or SensorType.Throughput or SensorType.Conductivity => value.ToString("F1", CultureInfo.CurrentCulture), + _ => value.ToString("F0", CultureInfo.CurrentCulture) + }; + } + + private string GetSensorToolTip(ISensor sensor) + { + string value = SensorFormatter.FormatValue(sensor, sensor.Value, _temperatureUnitProvider()); + string text = $"{sensor.Hardware.Name}{Environment.NewLine}{sensor.Name}: {value}"; + return TrimForNotifyIcon(text, 127); + } + + private static uint ToColorRef(WinUIColor color) + { + return (uint)(color.R | (color.G << 8) | (color.B << 16)); + } + + private static string TrimForNotifyIcon(string text, int maximumLength) + { + return text.Length <= maximumLength ? text : text[..maximumLength]; + } + + private static string GetSensorSettingName(ISensor sensor, string suffix) + { + return new Identifier(sensor.Identifier, suffix).ToString(); + } + + private sealed class SensorTrayIcon(uint id, ISensor sensor) + { + public uint Id { get; } = id; + + public IntPtr IconHandle { get; set; } + + public ISensor Sensor { get; set; } = sensor; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct NotifyIconData + { + public int cbSize; + public IntPtr hWnd; + public uint uID; + public uint uFlags; + public uint uCallbackMessage; + public IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip; + public uint dwState; + public uint dwStateMask; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo; + public uint uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle; + public uint dwInfoFlags; + public Guid guidItem; + public IntPtr hBalloonIcon; + } + + [StructLayout(LayoutKind.Sequential)] + private struct Point + { + public int X; + public int Y; + } + + [StructLayout(LayoutKind.Sequential)] + private struct Rect + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct IconInfo + { + [MarshalAs(UnmanagedType.Bool)] + public bool IsIcon; + public int XHotspot; + public int YHotspot; + public IntPtr MaskBitmap; + public IntPtr ColorBitmap; + } + + [StructLayout(LayoutKind.Sequential)] + private struct BitmapInfoHeader + { + public uint Size; + public int Width; + public int Height; + public ushort Planes; + public ushort BitCount; + public uint Compression; + public uint SizeImage; + public int XPelsPerMeter; + public int YPelsPerMeter; + public uint ClrUsed; + public uint ClrImportant; + } + + [StructLayout(LayoutKind.Sequential)] + private struct BitmapInfo + { + public BitmapInfoHeader Header; + public uint Colors; + + public static BitmapInfo Create(int width, int height) + { + return new BitmapInfo + { + Header = new BitmapInfoHeader + { + Size = (uint)Marshal.SizeOf(), + Width = width, + Height = -height, + Planes = 1, + BitCount = 32 + } + }; + } + } + + private delegate IntPtr SubclassProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam, nuint subclassId, nuint refData); + + [DllImport("Comctl32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetWindowSubclass(IntPtr hWnd, SubclassProc subclassProc, nuint subclassId, nuint refData); + + [DllImport("Comctl32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool RemoveWindowSubclass(IntPtr hWnd, SubclassProc subclassProc, nuint subclassId); + + [DllImport("Comctl32.dll")] + private static extern IntPtr DefSubclassProc(IntPtr hWnd, uint message, IntPtr wParam, IntPtr lParam); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool Shell_NotifyIcon(uint message, ref NotifyIconData data); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr LoadImage(IntPtr instance, string name, uint type, int desiredWidth, int desiredHeight, uint load); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr LoadIcon(IntPtr instance, IntPtr iconName); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyIcon(IntPtr icon); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr CreatePopupMenu(); + + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AppendMenu(IntPtr menu, uint flags, uint newItemId, string? newItem); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyMenu(IntPtr menu); + + [DllImport("user32.dll", SetLastError = true)] + private static extern int TrackPopupMenuEx(IntPtr menu, uint flags, int x, int y, IntPtr windowHandle, IntPtr parameters); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetCursorPos(out Point point); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetForegroundWindow(IntPtr windowHandle); + + [DllImport("user32.dll")] + private static extern IntPtr GetDC(IntPtr windowHandle); + + [DllImport("user32.dll")] + private static extern int ReleaseDC(IntPtr windowHandle, IntPtr deviceContext); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateCompatibleDC(IntPtr deviceContext); + + [DllImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DeleteDC(IntPtr deviceContext); + + [DllImport("gdi32.dll")] + private static extern IntPtr SelectObject(IntPtr deviceContext, IntPtr gdiObject); + + [DllImport("gdi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DeleteObject(IntPtr gdiObject); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateDIBSection(IntPtr deviceContext, ref BitmapInfo bitmapInfo, uint usage, out IntPtr bits, IntPtr section, uint offset); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateBitmap(int width, int height, uint planes, uint bitCount, IntPtr bits); + + [DllImport("gdi32.dll")] + private static extern IntPtr CreateSolidBrush(uint color); + + [DllImport("user32.dll")] + private static extern int FillRect(IntPtr deviceContext, ref Rect rect, IntPtr brush); + + [DllImport("gdi32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr CreateFont(int height, int width, int escapement, int orientation, int weight, uint italic, uint underline, uint strikeOut, uint charSet, uint outputPrecision, uint clipPrecision, uint quality, uint pitchAndFamily, string faceName); + + [DllImport("gdi32.dll")] + private static extern int SetBkMode(IntPtr deviceContext, int mode); + + [DllImport("gdi32.dll")] + private static extern uint SetTextColor(IntPtr deviceContext, uint color); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern int DrawText(IntPtr deviceContext, string text, int count, ref Rect rect, uint format); + + [DllImport("user32.dll")] + private static extern IntPtr CreateIconIndirect(ref IconInfo iconInfo); +} diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index a168822..4c38833 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -129,7 +129,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable _showMinColumn = settings.GetValue("minMenuItem", false); _showMaxColumn = settings.GetValue("maxMenuItem", true); _showPlot = settings.GetValue("plotMenuItem", false); - _plotLocation = (PlotLocation)Math.Clamp(settings.GetValue("plotLocation", 0), 0, 1); + _plotLocation = (PlotLocation)Math.Clamp(settings.GetValue("plotLocation", 0), 0, 2); _plotStrokeThickness = Math.Clamp(settings.GetValue("plotStroke", 1) + 1, 1, 4); _temperatureUnit = (TemperatureUnit)Math.Clamp(settings.GetValue("TemperatureUnit", 0), 0, 1); _updateIntervalIndex = Math.Clamp(settings.GetValue("updateIntervalMenuItem", 2), 0, UpdateIntervals.Length - 1); @@ -144,13 +144,19 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable public event EventHandler? PlotInvalidated; + public event EventHandler? GadgetSensorsChanged; + + public event EventHandler? TraySensorsChanged; + public Visibility MaxColumnVisibility => ShowMaxColumn ? Visibility.Visible : Visibility.Collapsed; public Visibility MinColumnVisibility => ShowMinColumn ? Visibility.Visible : Visibility.Collapsed; public ObservableCollection PlotSeries { get; } = []; - public Visibility PlotVisibility => ShowPlot ? Visibility.Visible : Visibility.Collapsed; + public bool IsPlotWindowVisible => ShowPlot && PlotLocation == PlotLocation.Window; + + public Visibility PlotVisibility => ShowPlot && PlotLocation != PlotLocation.Window ? Visibility.Visible : Visibility.Collapsed; public ObservableCollection RootItems { get; } = []; @@ -361,6 +367,12 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable } } + public bool ShowGadget + { + get => Settings.GetValue("gadgetMenuItem", false); + set => SetBooleanSetting("gadgetMenuItem", value); + } + public bool RunWebServer { get => !IsWebServerUnavailable && Settings.GetValue("runWebServerMenuItem", false); @@ -426,6 +438,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable Settings.SetValue("plotLocation", (int)value); NotifyPlotLayoutChanged(); + PlotInvalidated?.Invoke(this, EventArgs.Empty); } } @@ -500,6 +513,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable Settings.SetValue("plotMenuItem", value); OnPropertyChanged(nameof(PlotVisibility)); + OnPropertyChanged(nameof(IsPlotWindowVisible)); NotifyPlotLayoutChanged(); PlotInvalidated?.Invoke(this, EventArgs.Empty); } @@ -680,6 +694,46 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable RestartWebServerIfRunning(); } + public bool IsSensorInGadget(SensorTreeItemViewModel item) + { + return item.Sensor != null && Settings.GetValue(GetSensorSettingName(item.Sensor, "gadget"), false); + } + + public bool IsSensorInTray(SensorTreeItemViewModel item) + { + return item.Sensor != null && Settings.GetValue(GetSensorSettingName(item.Sensor, "tray"), false); + } + + public IEnumerable GetGadgetSensorItems() + { + return RootItems.FirstOrDefault()?.EnumerateSensors().Where(IsSensorInGadget) ?? []; + } + + public IEnumerable GetTraySensorItems() + { + return RootItems.FirstOrDefault()?.EnumerateSensors().Where(IsSensorInTray) ?? []; + } + + public void SetSensorInGadget(SensorTreeItemViewModel item, bool value) + { + if (item.Sensor == null) + return; + + SetSensorBooleanSetting(item.Sensor, "gadget", value); + GadgetSensorsChanged?.Invoke(this, EventArgs.Empty); + } + + public void SetSensorInTray(SensorTreeItemViewModel item, bool value) + { + if (item.Sensor == null) + return; + + SetSensorBooleanSetting(item.Sensor, "tray", value); + if (!value) + Settings.Remove(GetSensorSettingName(item.Sensor, "traycolor")); + TraySensorsChanged?.Invoke(this, EventArgs.Empty); + } + public void SetSensorPenColor(SensorTreeItemViewModel item, Color? color) { if (item.Sensor == null) @@ -838,6 +892,8 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private void NotifyPlotLayoutChanged() { + OnPropertyChanged(nameof(PlotVisibility)); + OnPropertyChanged(nameof(IsPlotWindowVisible)); OnPropertyChanged(nameof(PlotGridRow)); OnPropertyChanged(nameof(PlotGridColumn)); OnPropertyChanged(nameof(PlotGridRowSpan)); @@ -855,6 +911,20 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable OnPropertyChanged(); } + private void SetSensorBooleanSetting(ISensor sensor, string suffix, bool value) + { + string settingName = GetSensorSettingName(sensor, suffix); + if (value) + Settings.SetValue(settingName, true); + else + Settings.Remove(settingName); + } + + private static string GetSensorSettingName(ISensor sensor, string suffix) + { + return new Identifier(sensor.Identifier, suffix).ToString(); + } + private void SetHardwareFlag(bool value, bool currentValue, Action setValue) { if (currentValue == value) diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs index b8d5a86..76c1289 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/PlotLocation.cs @@ -6,6 +6,7 @@ namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; public enum PlotLocation { - Bottom = 0, - Right = 1 + Window = 0, + Bottom = 1, + Right = 2 }