mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
Unify monitor identification using stable Id property
Refactor monitor matching and persistence to use a single, stable Id (format: "{Source}_{EdidId}_{MonitorNumber}") across all components. Remove HardwareId and DeviceKey properties from Monitor, update ProfileMonitorSetting to use MonitorId, and simplify MonitorMatchingHelper logic. Update DDC/CI, WMI, ViewModels, Settings UI, and unit tests to rely on Id for all lookups, state management, and handle mapping. Improves reliability for multi-monitor setups and simplifies codebase by removing legacy fallback logic.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using PowerDisplay.Common.Models;
|
||||
using PowerDisplay.Common.Utils;
|
||||
|
||||
namespace PowerDisplay.UnitTests;
|
||||
@@ -15,72 +16,79 @@ namespace PowerDisplay.UnitTests;
|
||||
public class MonitorMatchingHelperTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void GetMonitorKey_WithHardwareId_ReturnsHardwareId()
|
||||
public void GetMonitorKey_WithMonitor_ReturnsId()
|
||||
{
|
||||
// Arrange
|
||||
var hardwareId = "HW_ID_123";
|
||||
var internalName = "Internal_Name";
|
||||
var name = "Display Name";
|
||||
var monitor = new Monitor { Id = "DDC_GSM5C6D_1", Name = "LG Monitor" };
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name);
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(monitor);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(hardwareId, result);
|
||||
Assert.AreEqual("DDC_GSM5C6D_1", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetMonitorKey_NoHardwareId_ReturnsInternalName()
|
||||
public void GetMonitorKey_NullMonitor_ReturnsEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
string? hardwareId = null;
|
||||
var internalName = "Internal_Name";
|
||||
var name = "Display Name";
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(internalName, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetMonitorKey_NoHardwareIdOrInternalName_ReturnsName()
|
||||
{
|
||||
// Arrange
|
||||
string? hardwareId = null;
|
||||
string? internalName = null;
|
||||
var name = "Display Name";
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(name, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetMonitorKey_AllNull_ReturnsEmptyString()
|
||||
{
|
||||
// Arrange & Act
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(null, null, null);
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(null);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetMonitorKey_EmptyHardwareId_FallsBackToInternalName()
|
||||
public void GetMonitorKey_EmptyId_ReturnsEmptyString()
|
||||
{
|
||||
// Arrange
|
||||
var hardwareId = string.Empty;
|
||||
var internalName = "Internal_Name";
|
||||
var name = "Display Name";
|
||||
var monitor = new Monitor { Id = string.Empty, Name = "Display Name" };
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name);
|
||||
var result = MonitorMatchingHelper.GetMonitorKey(monitor);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(internalName, result);
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AreMonitorsSame_SameId_ReturnsTrue()
|
||||
{
|
||||
// Arrange
|
||||
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
|
||||
var monitor2 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 2" };
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, monitor2);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AreMonitorsSame_DifferentId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
|
||||
var monitor2 = new Monitor { Id = "DDC_GSM5C6D_2", Name = "Monitor 2" };
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, monitor2);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AreMonitorsSame_NullMonitor_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
|
||||
|
||||
// Act
|
||||
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, null!);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,12 +492,12 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
|
||||
var monitorInfo = matchingInfos[i];
|
||||
|
||||
// Generate stable device key using DevicePath hash for uniqueness
|
||||
var deviceKey = !string.IsNullOrEmpty(monitorInfo.HardwareId)
|
||||
? $"{monitorInfo.HardwareId}_{monitorInfo.MonitorNumber}"
|
||||
: $"Unknown_{monitorInfo.MonitorNumber}";
|
||||
// Generate unique monitor Id
|
||||
var monitorId = !string.IsNullOrEmpty(monitorInfo.HardwareId)
|
||||
? $"DDC_{monitorInfo.HardwareId}_{monitorInfo.MonitorNumber}"
|
||||
: $"DDC_Unknown_{monitorInfo.MonitorNumber}";
|
||||
|
||||
var (handleToUse, _) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor);
|
||||
var (handleToUse, _) = _handleManager.ReuseOrCreateHandle(monitorId, physicalMonitor.HPhysicalMonitor);
|
||||
|
||||
var monitorToCreate = physicalMonitor;
|
||||
monitorToCreate.HPhysicalMonitor = handleToUse;
|
||||
@@ -585,7 +585,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
monitors.Add(monitor);
|
||||
newHandleMap[monitor.DeviceKey] = candidate.Handle;
|
||||
newHandleMap[monitor.Id] = candidate.Handle;
|
||||
|
||||
Logger.LogInfo($"DDC: Added monitor {monitor.Id} with {monitor.VcpCapabilitiesInfo?.SupportedVcpCodes.Count ?? 0} VCP codes");
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
try
|
||||
{
|
||||
// Get hardware ID and friendly name directly from MonitorDisplayInfo
|
||||
string hardwareId = monitorInfo.HardwareId ?? string.Empty;
|
||||
string edidId = monitorInfo.HardwareId ?? string.Empty;
|
||||
string name = physicalMonitor.GetDescription() ?? string.Empty;
|
||||
|
||||
// Use FriendlyName from QueryDisplayConfig if available and not generic
|
||||
@@ -141,12 +141,10 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
name = monitorInfo.FriendlyName;
|
||||
}
|
||||
|
||||
// Generate stable device key: "{HardwareId}_{MonitorNumber}"
|
||||
string deviceKey = !string.IsNullOrEmpty(hardwareId)
|
||||
? $"{hardwareId}_{monitorInfo.MonitorNumber}"
|
||||
: $"Unknown_{monitorInfo.MonitorNumber}";
|
||||
|
||||
string monitorId = $"DDC_{deviceKey}";
|
||||
// Generate unique monitor Id: "DDC_{EdidId}_{MonitorNumber}"
|
||||
string monitorId = !string.IsNullOrEmpty(edidId)
|
||||
? $"DDC_{edidId}_{monitorInfo.MonitorNumber}"
|
||||
: $"DDC_Unknown_{monitorInfo.MonitorNumber}";
|
||||
|
||||
// If still no good name, use default value
|
||||
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
|
||||
@@ -160,14 +158,12 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = monitorId,
|
||||
HardwareId = hardwareId,
|
||||
Name = name.Trim(),
|
||||
CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
IsAvailable = true,
|
||||
Handle = physicalMonitor.HPhysicalMonitor,
|
||||
DeviceKey = deviceKey,
|
||||
Capabilities = MonitorCapabilities.DdcCi,
|
||||
ConnectionType = "External",
|
||||
CommunicationMethod = "DDC/CI",
|
||||
|
||||
@@ -16,18 +16,18 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||
{
|
||||
// Mapping: deviceKey -> physical handle (thread-safe)
|
||||
private readonly LockedDictionary<string, IntPtr> _deviceKeyToHandleMap = new();
|
||||
// Mapping: monitorId -> physical handle (thread-safe)
|
||||
private readonly LockedDictionary<string, IntPtr> _monitorIdToHandleMap = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Get physical handle for monitor using stable deviceKey
|
||||
/// Get physical handle for monitor using its unique Id
|
||||
/// </summary>
|
||||
public IntPtr GetPhysicalHandle(Monitor monitor)
|
||||
{
|
||||
// Primary lookup: use stable deviceKey from EnumDisplayDevices
|
||||
if (!string.IsNullOrEmpty(monitor.DeviceKey) &&
|
||||
_deviceKeyToHandleMap.TryGetValue(monitor.DeviceKey, out var handle))
|
||||
// Primary lookup: use monitor Id
|
||||
if (!string.IsNullOrEmpty(monitor.Id) &&
|
||||
_monitorIdToHandleMap.TryGetValue(monitor.Id, out var handle))
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
@@ -45,18 +45,18 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// Try to reuse existing handle if valid; otherwise uses new handle
|
||||
/// Returns the handle to use and whether it was reused
|
||||
/// </summary>
|
||||
public (IntPtr Handle, bool WasReused) ReuseOrCreateHandle(string deviceKey, IntPtr newHandle)
|
||||
public (IntPtr Handle, bool WasReused) ReuseOrCreateHandle(string monitorId, IntPtr newHandle)
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceKey))
|
||||
if (string.IsNullOrEmpty(monitorId))
|
||||
{
|
||||
return (newHandle, false);
|
||||
}
|
||||
|
||||
return _deviceKeyToHandleMap.ExecuteWithLock(dict =>
|
||||
return _monitorIdToHandleMap.ExecuteWithLock(dict =>
|
||||
{
|
||||
// Try to reuse existing handle if it's still valid
|
||||
// Use quick connection check instead of full capabilities retrieval
|
||||
if (dict.TryGetValue(deviceKey, out var existingHandle) &&
|
||||
if (dict.TryGetValue(monitorId, out var existingHandle) &&
|
||||
existingHandle != IntPtr.Zero &&
|
||||
DdcCiNative.QuickConnectionCheck(existingHandle))
|
||||
{
|
||||
@@ -78,7 +78,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
/// </summary>
|
||||
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
|
||||
{
|
||||
_deviceKeyToHandleMap.ExecuteWithLock(dict =>
|
||||
_monitorIdToHandleMap.ExecuteWithLock(dict =>
|
||||
{
|
||||
// Clean up unused handles before updating
|
||||
CleanupUnusedHandles(dict, newHandleMap);
|
||||
@@ -140,7 +140,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
|
||||
// Release all physical monitor handles - get snapshot to avoid holding lock during cleanup
|
||||
var handles = _deviceKeyToHandleMap.GetValuesSnapshot();
|
||||
var handles = _monitorIdToHandleMap.GetValuesSnapshot();
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
@@ -157,7 +157,7 @@ namespace PowerDisplay.Common.Drivers.DDC
|
||||
}
|
||||
}
|
||||
|
||||
_deviceKeyToHandleMap.Clear();
|
||||
_monitorIdToHandleMap.Clear();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
@@ -354,19 +354,23 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
name = info.Name;
|
||||
}
|
||||
|
||||
// Extract HardwareId from InstanceName for state persistence
|
||||
// Extract EdidId from InstanceName
|
||||
// e.g., "DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0" -> "BOE0900"
|
||||
var hardwareId = ExtractHardwareIdFromInstanceName(instanceName);
|
||||
var edidId = ExtractHardwareIdFromInstanceName(instanceName);
|
||||
|
||||
// Get MonitorNumber from QueryDisplayConfig by matching HardwareId
|
||||
// Get MonitorNumber from QueryDisplayConfig by matching EdidId
|
||||
// This matches Windows Display Settings "Identify" feature
|
||||
int monitorNumber = GetMonitorNumberFromDisplayInfo(hardwareId, monitorDisplayInfos);
|
||||
int monitorNumber = GetMonitorNumberFromDisplayInfo(edidId, monitorDisplayInfos);
|
||||
|
||||
// Generate unique monitor Id: "WMI_{EdidId}_{MonitorNumber}"
|
||||
string monitorId = !string.IsNullOrEmpty(edidId)
|
||||
? $"WMI_{edidId}_{monitorNumber}"
|
||||
: $"WMI_Unknown_{monitorNumber}";
|
||||
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = $"WMI_{instanceName}",
|
||||
Id = monitorId,
|
||||
Name = name,
|
||||
HardwareId = hardwareId,
|
||||
CurrentBrightness = currentBrightness,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
@@ -375,7 +379,7 @@ namespace PowerDisplay.Common.Drivers.WMI
|
||||
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
|
||||
ConnectionType = "Internal",
|
||||
CommunicationMethod = "WMI",
|
||||
Manufacturer = hardwareId.Length >= 3 ? hardwareId.Substring(0, 3) : "Internal",
|
||||
Manufacturer = edidId.Length >= 3 ? edidId.Substring(0, 3) : "Internal",
|
||||
SupportsColorTemperature = false,
|
||||
MonitorNumber = monitorNumber,
|
||||
};
|
||||
|
||||
@@ -21,11 +21,6 @@ namespace PowerDisplay.Common.Interfaces
|
||||
/// </summary>
|
||||
string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hardware ID (EDID format like GSM5C6D).
|
||||
/// </summary>
|
||||
string HardwareId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current brightness value (0-100).
|
||||
/// </summary>
|
||||
|
||||
@@ -15,13 +15,8 @@ namespace PowerDisplay.Common.Models
|
||||
/// Implements IMonitorData to provide a common interface for monitor hardware values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Monitor Identifier Hierarchy:</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="Id"/>: Runtime identifier for UI and IPC (e.g., "DDC_GSM5C6D", "WMI_DISPLAY\BOE...")</item>
|
||||
/// <item><see cref="HardwareId"/>: EDID-based identifier for persistent storage (e.g., "GSM5C6D")</item>
|
||||
/// <item><see cref="DeviceKey"/>: Windows device path for handle management (e.g., "\\?\DISPLAY#...")</item>
|
||||
/// </list>
|
||||
/// <para>Use <see cref="Id"/> for lookups, <see cref="HardwareId"/> for saving state, <see cref="DeviceKey"/> for handle reuse.</para>
|
||||
/// <para><see cref="Id"/> is the unique identifier used for all purposes: UI lookups, IPC, persistent storage, and handle management.</para>
|
||||
/// <para>Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").</para>
|
||||
/// </remarks>
|
||||
public partial class Monitor : INotifyPropertyChanged, IMonitorData
|
||||
{
|
||||
@@ -31,25 +26,15 @@ namespace PowerDisplay.Common.Models
|
||||
private bool _isAvailable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime unique identifier for UI lookups and IPC communication.
|
||||
/// Unique identifier for all purposes: UI lookups, IPC, persistent storage, and handle management.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Format: "{Source}_{HardwareId}" where Source is "DDC" or "WMI".
|
||||
/// Examples: "DDC_GSM5C6D", "WMI_DISPLAY\BOE0900...".
|
||||
/// Use this for ViewModel lookups and MonitorManager method parameters.
|
||||
/// Format: "{Source}_{EdidId}_{MonitorNumber}" where Source is "DDC" or "WMI".
|
||||
/// Examples: "DDC_GSM5C6D_1", "WMI_BOE0900_2".
|
||||
/// Stable across reboots and unique even for multiple identical monitors.
|
||||
/// </remarks>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// EDID-based hardware identifier for persistent state storage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Format: Manufacturer code + product code from EDID (e.g., "GSM5C6D" for LG monitors).
|
||||
/// Use this for saving/loading monitor settings in MonitorStateManager.
|
||||
/// Stable across reboots but not guaranteed unique if multiple identical monitors are connected.
|
||||
/// </remarks>
|
||||
public string HardwareId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Display name
|
||||
/// </summary>
|
||||
@@ -242,17 +227,6 @@ namespace PowerDisplay.Common.Models
|
||||
/// </summary>
|
||||
public IntPtr Handle { get; set; } = IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Stable device key for persistent monitor identification and handle management.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Format: "{HardwareId}_{MonitorNumber}" (e.g., "GSM5C6D_1").
|
||||
/// HardwareId is from EDID (manufacturer+product code), MonitorNumber from QueryDisplayConfig.
|
||||
/// Used by PhysicalMonitorHandleManager to reuse handles across monitor discovery cycles.
|
||||
/// Same-model monitors on different ports will have different keys due to MonitorNumber.
|
||||
/// </remarks>
|
||||
public string DeviceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instance name (used by WMI)
|
||||
/// </summary>
|
||||
|
||||
@@ -11,8 +11,12 @@ namespace PowerDisplay.Common.Models
|
||||
/// </summary>
|
||||
public class ProfileMonitorSetting
|
||||
{
|
||||
[JsonPropertyName("hardwareId")]
|
||||
public string HardwareId { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the monitor's unique identifier.
|
||||
/// Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1").
|
||||
/// </summary>
|
||||
[JsonPropertyName("monitorId")]
|
||||
public string MonitorId { get; set; }
|
||||
|
||||
[JsonPropertyName("brightness")]
|
||||
public int? Brightness { get; set; }
|
||||
@@ -30,23 +34,18 @@ namespace PowerDisplay.Common.Models
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int? ColorTemperatureVcp { get; set; }
|
||||
|
||||
[JsonPropertyName("monitorInternalName")]
|
||||
public string MonitorInternalName { get; set; }
|
||||
|
||||
public ProfileMonitorSetting()
|
||||
{
|
||||
HardwareId = string.Empty;
|
||||
MonitorInternalName = string.Empty;
|
||||
MonitorId = string.Empty;
|
||||
}
|
||||
|
||||
public ProfileMonitorSetting(string hardwareId, int? brightness = null, int? colorTemperatureVcp = null, int? contrast = null, int? volume = null, string monitorInternalName = "")
|
||||
public ProfileMonitorSetting(string monitorId, int? brightness = null, int? colorTemperatureVcp = null, int? contrast = null, int? volume = null)
|
||||
{
|
||||
HardwareId = hardwareId;
|
||||
MonitorId = monitorId;
|
||||
Brightness = brightness;
|
||||
ColorTemperatureVcp = colorTemperatureVcp;
|
||||
Contrast = contrast;
|
||||
Volume = volume;
|
||||
MonitorInternalName = monitorInternalName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,33 +14,19 @@ namespace PowerDisplay.Common.Utils
|
||||
public static class MonitorMatchingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate a unique key for monitor matching based on hardware ID and internal name.
|
||||
/// Uses HardwareId if available; otherwise falls back to Id (InternalName) or Name.
|
||||
/// Generate a unique key for monitor matching based on Id.
|
||||
/// </summary>
|
||||
/// <param name="monitor">The monitor data to generate a key for.</param>
|
||||
/// <returns>A unique string key for the monitor.</returns>
|
||||
public static string GetMonitorKey(IMonitorData? monitor)
|
||||
=> GetMonitorKey(monitor?.HardwareId, monitor?.Id, monitor?.Name);
|
||||
=> monitor?.Id ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Generate a unique key for monitor matching using explicit values.
|
||||
/// Uses priority: HardwareId > InternalName > Name.
|
||||
/// </summary>
|
||||
/// <param name="hardwareId">The monitor's hardware ID.</param>
|
||||
/// <param name="internalName">The monitor's internal name (optional fallback).</param>
|
||||
/// <param name="name">The monitor's display name (optional fallback).</param>
|
||||
/// <returns>A unique string key for the monitor.</returns>
|
||||
public static string GetMonitorKey(string? hardwareId, string? internalName = null, string? name = null)
|
||||
=> !string.IsNullOrEmpty(hardwareId) ? hardwareId
|
||||
: !string.IsNullOrEmpty(internalName) ? internalName
|
||||
: name ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Check if two monitors are considered the same based on their keys.
|
||||
/// Check if two monitors are considered the same based on their Ids.
|
||||
/// </summary>
|
||||
/// <param name="monitor1">First monitor.</param>
|
||||
/// <param name="monitor2">Second monitor.</param>
|
||||
/// <returns>True if the monitors have the same key.</returns>
|
||||
/// <returns>True if the monitors have the same Id.</returns>
|
||||
public static bool AreMonitorsSame(IMonitorData monitor1, IMonitorData monitor2)
|
||||
{
|
||||
if (monitor1 == null || monitor2 == null)
|
||||
@@ -48,7 +34,7 @@ namespace PowerDisplay.Common.Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetMonitorKey(monitor1) == GetMonitorKey(monitor2);
|
||||
return !string.IsNullOrEmpty(monitor1.Id) && monitor1.Id == monitor2.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,16 +254,16 @@ public partial class MainViewModel
|
||||
|
||||
foreach (var setting in monitorSettings)
|
||||
{
|
||||
// Find monitor by InternalName (unique identifier)
|
||||
var monitorVm = Monitors.FirstOrDefault(m => m.InternalName == setting.MonitorInternalName);
|
||||
// Find monitor by Id (unique identifier)
|
||||
var monitorVm = Monitors.FirstOrDefault(m => m.Id == setting.MonitorId);
|
||||
|
||||
if (monitorVm == null)
|
||||
{
|
||||
Logger.LogWarning($"[Profile] Monitor with InternalName '{setting.MonitorInternalName}' not found (disconnected?)");
|
||||
Logger.LogWarning($"[Profile] Monitor with Id '{setting.MonitorId}' not found (disconnected?)");
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[Profile] Applying settings to monitor '{monitorVm.Name}' (InternalName: {setting.MonitorInternalName}, HardwareId: {setting.HardwareId})");
|
||||
Logger.LogInfo($"[Profile] Applying settings to monitor '{monitorVm.Name}' (Id: {setting.MonitorId})");
|
||||
|
||||
// Apply brightness if included in profile
|
||||
if (setting.Brightness.HasValue &&
|
||||
@@ -312,8 +312,8 @@ public partial class MainViewModel
|
||||
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
// Find and apply corresponding saved settings from state file using stable HardwareId
|
||||
var savedState = _stateManager.GetMonitorParameters(monitorVm.HardwareId);
|
||||
// Find and apply corresponding saved settings from state file using unique monitor Id
|
||||
var savedState = _stateManager.GetMonitorParameters(monitorVm.Id);
|
||||
if (!savedState.HasValue)
|
||||
{
|
||||
continue;
|
||||
@@ -384,18 +384,18 @@ public partial class MainViewModel
|
||||
/// Thread-safe save method that can be called from background threads.
|
||||
/// Does not access UI collections or update UI properties.
|
||||
/// </summary>
|
||||
public void SaveMonitorSettingDirect(string hardwareId, string property, int value)
|
||||
public void SaveMonitorSettingDirect(string monitorId, string property, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This is thread-safe - _stateManager has internal locking
|
||||
// No UI thread operations, no ObservableCollection access
|
||||
_stateManager.UpdateMonitorParameter(hardwareId, property, value);
|
||||
_stateManager.UpdateMonitorParameter(monitorId, property, value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Only log, don't update UI from background thread
|
||||
Logger.LogError($"Failed to queue setting save for HardwareId '{hardwareId}': {ex.Message}");
|
||||
Logger.LogError($"Failed to queue setting save for monitorId '{monitorId}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ public partial class MainViewModel
|
||||
var monitorInfo = new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo(
|
||||
name: vm.Name,
|
||||
internalName: vm.Id,
|
||||
hardwareId: vm.HardwareId,
|
||||
hardwareId: string.Empty, // Deprecated, use InternalName (Id) instead
|
||||
communicationMethod: vm.CommunicationMethod,
|
||||
currentBrightness: vm.Brightness,
|
||||
colorTemperatureVcp: vm.ColorTemperature)
|
||||
|
||||
@@ -163,7 +163,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[{HardwareId}] Setting color temperature to 0x{colorTemperature:X2}");
|
||||
Logger.LogInfo($"[{Id}] Setting color temperature to 0x{colorTemperature:X2}");
|
||||
|
||||
var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
|
||||
|
||||
@@ -173,18 +173,18 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
||||
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, nameof(ColorTemperature), colorTemperature);
|
||||
|
||||
Logger.LogInfo($"[{HardwareId}] Color temperature applied successfully");
|
||||
Logger.LogInfo($"[{Id}] Color temperature applied successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set color temperature: {result.ErrorMessage}");
|
||||
Logger.LogWarning($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting color temperature: {ex.Message}");
|
||||
Logger.LogError($"[{Id}] Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,22 +202,22 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"[{HardwareId}] Applying {propertyName.ToLowerInvariant()}: {value}%");
|
||||
Logger.LogDebug($"[{Id}] Applying {propertyName.ToLowerInvariant()}: {value}%");
|
||||
|
||||
var result = await setAsyncFunc(Id, value, default);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, propertyName, value);
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, propertyName, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set {propertyName.ToLowerInvariant()}: {result.ErrorMessage}");
|
||||
Logger.LogWarning($"[{Id}] Failed to set {propertyName.ToLowerInvariant()}: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting {propertyName.ToLowerInvariant()}: {ex.Message}");
|
||||
Logger.LogError($"[{Id}] Exception setting {propertyName.ToLowerInvariant()}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +266,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
public string Id => _monitor.Id;
|
||||
|
||||
public string HardwareId => _monitor.HardwareId;
|
||||
|
||||
public string InternalName => _monitor.Id;
|
||||
|
||||
public string Name => _monitor.Name;
|
||||
@@ -373,7 +371,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to show rotation controls (controlled by Settings UI, default false)
|
||||
/// Gets or sets a value indicating whether gets or sets whether to show rotation controls (controlled by Settings UI, default false)
|
||||
/// </summary>
|
||||
public bool ShowRotation
|
||||
{
|
||||
@@ -394,22 +392,22 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
public int CurrentRotation => _monitor.Orientation;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the current rotation is 0° (normal/default)
|
||||
/// Gets a value indicating whether gets whether the current rotation is 0° (normal/default)
|
||||
/// </summary>
|
||||
public bool IsRotation0 => CurrentRotation == 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the current rotation is 90° (rotated right)
|
||||
/// Gets a value indicating whether gets whether the current rotation is 90° (rotated right)
|
||||
/// </summary>
|
||||
public bool IsRotation1 => CurrentRotation == 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the current rotation is 180° (inverted)
|
||||
/// Gets a value indicating whether gets whether the current rotation is 180° (inverted)
|
||||
/// </summary>
|
||||
public bool IsRotation2 => CurrentRotation == 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the current rotation is 270° (rotated left)
|
||||
/// Gets a value indicating whether gets whether the current rotation is 270° (rotated left)
|
||||
/// </summary>
|
||||
public bool IsRotation3 => CurrentRotation == 3;
|
||||
|
||||
@@ -422,7 +420,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
// Validate orientation range (0=normal, 1=90°, 2=180°, 3=270°)
|
||||
if (orientation < 0 || orientation > 3)
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Invalid rotation value: {orientation}. Must be 0-3.");
|
||||
Logger.LogWarning($"[{Id}] Invalid rotation value: {orientation}. Must be 0-3.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -434,7 +432,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[{HardwareId}] Setting rotation to {orientation}");
|
||||
Logger.LogInfo($"[{Id}] Setting rotation to {orientation}");
|
||||
|
||||
var result = await _monitorManager.SetRotationAsync(Id, orientation);
|
||||
|
||||
@@ -447,16 +445,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
OnPropertyChanged(nameof(IsRotation2));
|
||||
OnPropertyChanged(nameof(IsRotation3));
|
||||
|
||||
Logger.LogInfo($"[{HardwareId}] Rotation set successfully to {orientation}");
|
||||
Logger.LogInfo($"[{Id}] Rotation set successfully to {orientation}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set rotation: {result.ErrorMessage}");
|
||||
Logger.LogWarning($"[{Id}] Failed to set rotation: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting rotation: {ex.Message}");
|
||||
Logger.LogError($"[{Id}] Exception setting rotation: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +484,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this monitor supports input source switching via VCP 0x60
|
||||
/// Gets a value indicating whether whether this monitor supports input source switching via VCP 0x60
|
||||
/// </summary>
|
||||
public bool SupportsInputSource => _monitor.SupportsInputSource;
|
||||
|
||||
@@ -548,7 +546,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[{HardwareId}] Setting input source to 0x{inputSource:X2}");
|
||||
Logger.LogInfo($"[{Id}] Setting input source to 0x{inputSource:X2}");
|
||||
|
||||
var result = await _monitorManager.SetInputSourceAsync(Id, inputSource);
|
||||
|
||||
@@ -558,16 +556,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
OnPropertyChanged(nameof(CurrentInputSourceName));
|
||||
RefreshAvailableInputSources();
|
||||
|
||||
Logger.LogInfo($"[{HardwareId}] Input source set successfully to {CurrentInputSourceName}");
|
||||
Logger.LogInfo($"[{Id}] Input source set successfully to {CurrentInputSourceName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set input source: {result.ErrorMessage}");
|
||||
Logger.LogWarning($"[{Id}] Failed to set input source: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting input source: {ex.Message}");
|
||||
Logger.LogError($"[{Id}] Exception setting input source: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
// Pre-fill monitor settings from existing profile
|
||||
foreach (var monitorSetting in profile.MonitorSettings)
|
||||
{
|
||||
var monitorItem = ViewModel.Monitors.FirstOrDefault(m => m.Monitor.InternalName == monitorSetting.MonitorInternalName);
|
||||
var monitorItem = ViewModel.Monitors.FirstOrDefault(m => m.Monitor.InternalName == monitorSetting.MonitorId);
|
||||
if (monitorItem != null)
|
||||
{
|
||||
monitorItem.IsSelected = true;
|
||||
|
||||
@@ -89,12 +89,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
var settings = _monitors
|
||||
.Where(m => m.IsSelected)
|
||||
.Select(m => new ProfileMonitorSetting(
|
||||
m.Monitor.HardwareId,
|
||||
m.Monitor.InternalName, // Monitor Id (unique identifier)
|
||||
m.IncludeBrightness ? (int?)m.Brightness : null,
|
||||
m.IncludeColorTemperature && m.SupportsColorTemperature ? (int?)m.ColorTemperature : null,
|
||||
m.IncludeContrast && m.SupportsContrast ? (int?)m.Contrast : null,
|
||||
m.IncludeVolume && m.SupportsVolume ? (int?)m.Volume : null,
|
||||
m.Monitor.InternalName))
|
||||
m.IncludeVolume && m.SupportsVolume ? (int?)m.Volume : null))
|
||||
.ToList();
|
||||
|
||||
return new PowerDisplayProfile(_profileName, settings);
|
||||
|
||||
Reference in New Issue
Block a user