Files

390 lines
13 KiB
C#

// 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.IO;
using System.Linq;
using System.Runtime.InteropServices;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
using static LibreHardwareMonitor.Windows.WinUI.Services.TrayIconInterop;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
// Orchestrates the system-tray icons: a main app icon (shown only when no per-sensor icons are present) plus one icon
// per user-selected sensor, their context menus, and routing of the shell callback messages. Win32 interop lives in
// TrayIconInterop; the per-sensor icon bitmaps are drawn by SensorIconRenderer.
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 readonly Dictionary<string, SensorTrayIcon> _sensorIconsByIdentifier = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<uint, SensorTrayIcon> _sensorIconsById = new();
private readonly Func<TemperatureUnit> _temperatureUnitProvider;
private readonly SensorIconRenderer _iconRenderer;
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<TemperatureUnit> temperatureUnitProvider)
{
_windowHandle = windowHandle;
_settings = settings;
_temperatureUnitProvider = temperatureUnitProvider;
_iconRenderer = new SensorIconRenderer(settings, 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 = _iconRenderer.CreateIcon(sensor);
_sensorIconsByIdentifier[identifier] = icon;
_sensorIconsById[icon.Id] = icon;
AddNotifyIcon(icon.Id, icon.IconHandle, GetSensorToolTip(sensor));
if (persist)
_settings.SetValue(SensorSelectionService.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<SensorTreeItemViewModel> sensorItems)
{
Dictionary<string, ISensor> selected = sensorItems
.Where(item => item.Sensor != null && _settings.GetValue(SensorSelectionService.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)
{
string renderKey = _iconRenderer.GetRenderKey(icon.Sensor);
if (icon.IconHandle != IntPtr.Zero && renderKey == icon.LastRenderKey)
{
// The icon pixels are unchanged; refresh only the (cheap) tooltip and skip the GDI re-render + handle churn.
ModifyNotifyIcon(icon.Id, icon.IconHandle, GetSensorToolTip(icon.Sensor));
return;
}
IntPtr previousIcon = icon.IconHandle;
icon.IconHandle = _iconRenderer.CreateIcon(icon.Sensor);
icon.LastRenderKey = renderKey;
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(SensorSelectionService.GetSensorSettingName(icon.Sensor, "tray"));
_settings.Remove(SensorSelectionService.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)
{
// With NOTIFYICON_VERSION_4 the event is packed into lParam: LOWORD = the mouse/keyboard message,
// HIWORD = the icon id. (wParam carries the cursor coordinates, which we don't use.)
uint packed = unchecked((uint)lParam.ToInt64());
uint mouseMessage = packed & 0xFFFF;
uint iconId = (packed >> 16) & 0xFFFF;
HandleTrayCallback(iconId, mouseMessage);
}
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<NotifyIconData>(),
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 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 string TrimForNotifyIcon(string text, int maximumLength)
{
return text.Length <= maximumLength ? text : text[..maximumLength];
}
private sealed class SensorTrayIcon(uint id, ISensor sensor)
{
public uint Id { get; } = id;
public IntPtr IconHandle { get; set; }
public ISensor Sensor { get; set; } = sensor;
/// <summary>Key of the pixels currently rendered into <see cref="IconHandle" />, used to skip redundant re-renders.</summary>
public string? LastRenderKey { get; set; }
}
}