// 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.ObjectModel; using System.Linq; using System.Text; using LibreHardwareMonitor.Hardware; using LibreHardwareMonitor.Windows.WinUI.Services; using LibreHardwareMonitor.Windows.WinUI.Utilities; using Microsoft.UI.Xaml; using Windows.UI; namespace LibreHardwareMonitor.Windows.WinUI.ViewModels; public sealed class SensorTreeItemViewModel : ViewModelBase { private readonly AppSettings? _settings; private SensorTreeItemViewModel? _parent; private string? _expandedSettingName; private bool _isExpanded = true; private bool _isVisible = true; private bool _plot; private bool _showHiddenSensors; private Visibility _maxColumnVisibility = Visibility.Visible; private Visibility _minColumnVisibility = Visibility.Collapsed; private Visibility _rowVisibility = Visibility.Visible; private Visibility _valueColumnVisibility = Visibility.Visible; private Color? _penColor; private TemperatureUnit _temperatureUnit; private string? _lastValue; private string? _lastMin; private string? _lastMax; private string? _lastToolTip; private SensorTreeItemViewModel(SensorTreeItemKind kind, AppSettings? settings) { Kind = kind; _settings = settings; } internal event EventHandler? DisplayColumnsChanged; public ObservableCollection Children { get; } = []; public bool CanHide => Sensor != null; public bool CanRename => Sensor != null || Hardware != null; public IHardware? Hardware { get; private init; } public string IconGlyph { get; private init; } = "\uE950"; public string? IconImageName { get; private init; } public bool IsExpanded { get => _isExpanded; set { if (SetProperty(ref _isExpanded, value) && _expandedSettingName != null) _settings?.SetValue(_expandedSettingName, value); } } public bool IsVisible { get => _isVisible; set { if (!SetProperty(ref _isVisible, value)) return; if (Sensor != null) _settings?.SetValue(new Identifier(Sensor.Identifier, "hidden").ToString(), !value); UpdateVisibilityState(); // A SensorType group's visibility depends on whether any child is visible, so recompute the parent too; // otherwise hiding the last visible sensor leaves an empty group header (and the reverse on show). _parent?.UpdateVisibilityState(); } } public SensorTreeItemKind Kind { get; } public string Max => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Max, TemperatureUnit); public Visibility MaxColumnVisibility { get => _maxColumnVisibility; private set => SetProperty(ref _maxColumnVisibility, value); } public string Min => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Min, TemperatureUnit); public Visibility MinColumnVisibility { get => _minColumnVisibility; private set => SetProperty(ref _minColumnVisibility, value); } public bool Plot { get => _plot; set { if (Sensor == null || !SetProperty(ref _plot, value)) return; _settings?.SetValue(new Identifier(Sensor.Identifier, "plot").ToString(), value); } } public Color? PenColor { get => _penColor; set { if (Sensor == null || Nullable.Equals(_penColor, value)) return; _penColor = value; string settingName = new Identifier(Sensor.Identifier, "penColor").ToString(); if (value.HasValue) _settings?.SetValue(settingName, value.Value); else _settings?.Remove(settingName); OnPropertyChanged(); } } public Visibility PlotCheckVisibility => Sensor == null ? Visibility.Collapsed : Visibility.Visible; public Visibility RowVisibility { get => _rowVisibility; private set => SetProperty(ref _rowVisibility, value); } public ISensor? Sensor { get; private init; } public string Text { get { if (Sensor != null) return Sensor.Name; if (Hardware != null) return Hardware.Name; return _text; } set { if (Text == value) return; if (Sensor != null) Sensor.Name = value; else if (Hardware != null) Hardware.Name = value; else _text = value; OnPropertyChanged(); OnDisplayColumnsChanged(SensorDisplayColumn.Sensor); } } public string ToolTip { get { if (Sensor != null) return SensorFormatter.GetToolTip(Sensor, TemperatureUnit); if (Hardware?.Properties.Count > 0) { StringBuilder builder = new(); builder.AppendLine("Hardware properties:"); foreach (KeyValuePair property in Hardware.Properties) builder.AppendFormat(" - {0}: {1}{2}", property.Key, property.Value, Environment.NewLine); return builder.ToString(); } return ""; } } public TemperatureUnit TemperatureUnit { get => _temperatureUnit; private set => SetProperty(ref _temperatureUnit, value); } public string Value => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Value, TemperatureUnit); public Visibility ValueColumnVisibility { get => _valueColumnVisibility; private set => SetProperty(ref _valueColumnVisibility, value); } private string _text = ""; public static SensorTreeItemViewModel CreateRoot(string text) { return new SensorTreeItemViewModel(SensorTreeItemKind.Root, null) { _text = text, IconGlyph = "\uE7F4", IconImageName = "computer.png", _isExpanded = true }; } public static SensorTreeItemViewModel FromHardware(IHardware hardware, AppSettings settings) { string expandedSettingName = new Identifier(hardware.Identifier, "expanded").ToString(); SensorTreeItemViewModel item = new(SensorTreeItemKind.Hardware, settings) { Hardware = hardware, IconGlyph = SensorTypeDisplay.GetHardwareGlyph(hardware.HardwareType), IconImageName = SensorTypeDisplay.GetHardwareImageFile(hardware.HardwareType), _expandedSettingName = expandedSettingName, _isExpanded = settings.GetValue(expandedSettingName, true) }; foreach (SensorType sensorType in Enum.GetValues()) { ISensor[] sensors = hardware.Sensors .Where(sensor => sensor.SensorType == sensorType) .OrderBy(sensor => sensor.Index) .ToArray(); if (sensors.Length == 0) continue; item.Children.Add(FromSensorType(hardware.Identifier, sensorType, sensors, settings)); } foreach (IHardware subHardware in hardware.SubHardware.OrderBy(subHardware => subHardware.HardwareType).ThenBy(subHardware => subHardware.Name, StringComparer.OrdinalIgnoreCase)) item.Children.Add(FromHardware(subHardware, settings)); item.UpdateVisibilityState(); return item; } public void Configure(TemperatureUnit temperatureUnit, bool showHiddenSensors, bool showValueColumn, bool showMinColumn, bool showMaxColumn) { SetTemperatureUnit(temperatureUnit); SetShowHiddenSensors(showHiddenSensors); SetColumnVisibility(showValueColumn, showMinColumn, showMaxColumn); } public IEnumerable Enumerate() { yield return this; foreach (SensorTreeItemViewModel child in Children) { foreach (SensorTreeItemViewModel node in child.Enumerate()) yield return node; } } public IEnumerable EnumerateSensors() { return Enumerate().Where(item => item.Sensor != null); } public void RefreshValues() { // Value/Min/Max/ToolTip are computed (no backing field), so raise PropertyChanged only when the formatted text // actually changed. Unconditionally notifying all four for every item on every update tick made the native // binding engine re-marshal unchanged values across the boundary continuously — most sensors don't change each // tick, so this cuts the per-tick interop churn (and its retained wrappers) by a large factor. SensorDisplayColumn changedColumns = SensorDisplayColumn.None; string value = Value; if (value != _lastValue) { _lastValue = value; OnPropertyChanged(nameof(Value)); changedColumns |= SensorDisplayColumn.Value; } string min = Min; if (min != _lastMin) { _lastMin = min; OnPropertyChanged(nameof(Min)); changedColumns |= SensorDisplayColumn.Min; } string max = Max; if (max != _lastMax) { _lastMax = max; OnPropertyChanged(nameof(Max)); changedColumns |= SensorDisplayColumn.Max; } string toolTip = ToolTip; if (toolTip != _lastToolTip) { _lastToolTip = toolTip; OnPropertyChanged(nameof(ToolTip)); } OnDisplayColumnsChanged(changedColumns); foreach (SensorTreeItemViewModel child in Children) child.RefreshValues(); } public void SetAllExpanded(bool isExpanded) { IsExpanded = isExpanded; foreach (SensorTreeItemViewModel child in Children) child.SetAllExpanded(isExpanded); } public void SetColumnVisibility(bool showValueColumn, bool showMinColumn, bool showMaxColumn) { ValueColumnVisibility = showValueColumn ? Visibility.Visible : Visibility.Collapsed; MinColumnVisibility = showMinColumn ? Visibility.Visible : Visibility.Collapsed; MaxColumnVisibility = showMaxColumn ? Visibility.Visible : Visibility.Collapsed; foreach (SensorTreeItemViewModel child in Children) child.SetColumnVisibility(showValueColumn, showMinColumn, showMaxColumn); } public void SetShowHiddenSensors(bool showHiddenSensors) { _showHiddenSensors = showHiddenSensors; UpdateVisibilityState(); foreach (SensorTreeItemViewModel child in Children) child.SetShowHiddenSensors(showHiddenSensors); } public void SetTemperatureUnit(TemperatureUnit temperatureUnit) { if (SetTemperatureUnitCore(temperatureUnit)) RefreshValues(); } private static SensorTreeItemViewModel FromSensor(ISensor sensor, AppSettings settings) { string hiddenSettingName = new Identifier(sensor.Identifier, "hidden").ToString(); string plotSettingName = new Identifier(sensor.Identifier, "plot").ToString(); string penColorSettingName = new Identifier(sensor.Identifier, "penColor").ToString(); return new SensorTreeItemViewModel(SensorTreeItemKind.Sensor, settings) { Sensor = sensor, IconGlyph = SensorTypeDisplay.GetGlyph(sensor.SensorType), IconImageName = SensorTypeDisplay.GetImageFile(sensor.SensorType), _isVisible = !settings.GetValue(hiddenSettingName, sensor.IsDefaultHidden), _plot = settings.GetValue(plotSettingName, false), _penColor = settings.Contains(penColorSettingName) ? settings.GetValue(penColorSettingName, Color.FromArgb(255, 0, 0, 0)) : null }; } private static SensorTreeItemViewModel FromSensorType(Identifier parentIdentifier, SensorType sensorType, IEnumerable sensors, AppSettings settings) { string expandedSettingName = new Identifier(parentIdentifier, sensorType.ToString(), ".expanded").ToString(); SensorTreeItemViewModel item = new(SensorTreeItemKind.SensorType, settings) { _text = SensorTypeDisplay.GetText(sensorType), IconGlyph = SensorTypeDisplay.GetGlyph(sensorType), IconImageName = SensorTypeDisplay.GetImageFile(sensorType), _expandedSettingName = expandedSettingName, _isExpanded = settings.GetValue(expandedSettingName, true) }; foreach (ISensor sensor in sensors) { SensorTreeItemViewModel child = FromSensor(sensor, settings); child._parent = item; item.Children.Add(child); } item.UpdateVisibilityState(); return item; } private bool SetTemperatureUnitCore(TemperatureUnit temperatureUnit) { bool changed = SetProperty(ref _temperatureUnit, temperatureUnit, nameof(TemperatureUnit)); foreach (SensorTreeItemViewModel child in Children) changed |= child.SetTemperatureUnitCore(temperatureUnit); return changed; } private void OnDisplayColumnsChanged(SensorDisplayColumn columns) { if (columns != SensorDisplayColumn.None) DisplayColumnsChanged?.Invoke(this, columns); } private void UpdateVisibilityState() { bool visible = Kind switch { SensorTreeItemKind.Sensor => _showHiddenSensors || IsVisible, SensorTreeItemKind.SensorType => _showHiddenSensors || Children.Any(child => child.IsVisible), _ => true }; RowVisibility = visible ? Visibility.Visible : Visibility.Collapsed; } }