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:
Yu Leng
2025-12-10 13:34:36 +08:00
parent 0965f814ce
commit 4817709fda
13 changed files with 144 additions and 185 deletions

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils; using PowerDisplay.Common.Utils;
namespace PowerDisplay.UnitTests; namespace PowerDisplay.UnitTests;
@@ -15,72 +16,79 @@ namespace PowerDisplay.UnitTests;
public class MonitorMatchingHelperTests public class MonitorMatchingHelperTests
{ {
[TestMethod] [TestMethod]
public void GetMonitorKey_WithHardwareId_ReturnsHardwareId() public void GetMonitorKey_WithMonitor_ReturnsId()
{ {
// Arrange // Arrange
var hardwareId = "HW_ID_123"; var monitor = new Monitor { Id = "DDC_GSM5C6D_1", Name = "LG Monitor" };
var internalName = "Internal_Name";
var name = "Display Name";
// Act // Act
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name); var result = MonitorMatchingHelper.GetMonitorKey(monitor);
// Assert // Assert
Assert.AreEqual(hardwareId, result); Assert.AreEqual("DDC_GSM5C6D_1", result);
} }
[TestMethod] [TestMethod]
public void GetMonitorKey_NoHardwareId_ReturnsInternalName() public void GetMonitorKey_NullMonitor_ReturnsEmptyString()
{ {
// Arrange
string? hardwareId = null;
var internalName = "Internal_Name";
var name = "Display Name";
// Act // Act
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name); var result = MonitorMatchingHelper.GetMonitorKey(null);
// 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);
// Assert // Assert
Assert.AreEqual(string.Empty, result); Assert.AreEqual(string.Empty, result);
} }
[TestMethod] [TestMethod]
public void GetMonitorKey_EmptyHardwareId_FallsBackToInternalName() public void GetMonitorKey_EmptyId_ReturnsEmptyString()
{ {
// Arrange // Arrange
var hardwareId = string.Empty; var monitor = new Monitor { Id = string.Empty, Name = "Display Name" };
var internalName = "Internal_Name";
var name = "Display Name";
// Act // Act
var result = MonitorMatchingHelper.GetMonitorKey(hardwareId, internalName, name); var result = MonitorMatchingHelper.GetMonitorKey(monitor);
// Assert // 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);
} }
} }

View File

@@ -492,12 +492,12 @@ namespace PowerDisplay.Common.Drivers.DDC
var monitorInfo = matchingInfos[i]; var monitorInfo = matchingInfos[i];
// Generate stable device key using DevicePath hash for uniqueness // Generate unique monitor Id
var deviceKey = !string.IsNullOrEmpty(monitorInfo.HardwareId) var monitorId = !string.IsNullOrEmpty(monitorInfo.HardwareId)
? $"{monitorInfo.HardwareId}_{monitorInfo.MonitorNumber}" ? $"DDC_{monitorInfo.HardwareId}_{monitorInfo.MonitorNumber}"
: $"Unknown_{monitorInfo.MonitorNumber}"; : $"DDC_Unknown_{monitorInfo.MonitorNumber}";
var (handleToUse, _) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor); var (handleToUse, _) = _handleManager.ReuseOrCreateHandle(monitorId, physicalMonitor.HPhysicalMonitor);
var monitorToCreate = physicalMonitor; var monitorToCreate = physicalMonitor;
monitorToCreate.HPhysicalMonitor = handleToUse; monitorToCreate.HPhysicalMonitor = handleToUse;
@@ -585,7 +585,7 @@ namespace PowerDisplay.Common.Drivers.DDC
} }
monitors.Add(monitor); 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"); Logger.LogInfo($"DDC: Added monitor {monitor.Id} with {monitor.VcpCapabilitiesInfo?.SupportedVcpCodes.Count ?? 0} VCP codes");
} }

View File

@@ -131,7 +131,7 @@ namespace PowerDisplay.Common.Drivers.DDC
try try
{ {
// Get hardware ID and friendly name directly from MonitorDisplayInfo // 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; string name = physicalMonitor.GetDescription() ?? string.Empty;
// Use FriendlyName from QueryDisplayConfig if available and not generic // Use FriendlyName from QueryDisplayConfig if available and not generic
@@ -141,12 +141,10 @@ namespace PowerDisplay.Common.Drivers.DDC
name = monitorInfo.FriendlyName; name = monitorInfo.FriendlyName;
} }
// Generate stable device key: "{HardwareId}_{MonitorNumber}" // Generate unique monitor Id: "DDC_{EdidId}_{MonitorNumber}"
string deviceKey = !string.IsNullOrEmpty(hardwareId) string monitorId = !string.IsNullOrEmpty(edidId)
? $"{hardwareId}_{monitorInfo.MonitorNumber}" ? $"DDC_{edidId}_{monitorInfo.MonitorNumber}"
: $"Unknown_{monitorInfo.MonitorNumber}"; : $"DDC_Unknown_{monitorInfo.MonitorNumber}";
string monitorId = $"DDC_{deviceKey}";
// If still no good name, use default value // If still no good name, use default value
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP")) if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
@@ -160,14 +158,12 @@ namespace PowerDisplay.Common.Drivers.DDC
var monitor = new Monitor var monitor = new Monitor
{ {
Id = monitorId, Id = monitorId,
HardwareId = hardwareId,
Name = name.Trim(), Name = name.Trim(),
CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50, CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50,
MinBrightness = 0, MinBrightness = 0,
MaxBrightness = 100, MaxBrightness = 100,
IsAvailable = true, IsAvailable = true,
Handle = physicalMonitor.HPhysicalMonitor, Handle = physicalMonitor.HPhysicalMonitor,
DeviceKey = deviceKey,
Capabilities = MonitorCapabilities.DdcCi, Capabilities = MonitorCapabilities.DdcCi,
ConnectionType = "External", ConnectionType = "External",
CommunicationMethod = "DDC/CI", CommunicationMethod = "DDC/CI",

View File

@@ -16,18 +16,18 @@ namespace PowerDisplay.Common.Drivers.DDC
/// </summary> /// </summary>
public partial class PhysicalMonitorHandleManager : IDisposable public partial class PhysicalMonitorHandleManager : IDisposable
{ {
// Mapping: deviceKey -> physical handle (thread-safe) // Mapping: monitorId -> physical handle (thread-safe)
private readonly LockedDictionary<string, IntPtr> _deviceKeyToHandleMap = new(); private readonly LockedDictionary<string, IntPtr> _monitorIdToHandleMap = new();
private bool _disposed; private bool _disposed;
/// <summary> /// <summary>
/// Get physical handle for monitor using stable deviceKey /// Get physical handle for monitor using its unique Id
/// </summary> /// </summary>
public IntPtr GetPhysicalHandle(Monitor monitor) public IntPtr GetPhysicalHandle(Monitor monitor)
{ {
// Primary lookup: use stable deviceKey from EnumDisplayDevices // Primary lookup: use monitor Id
if (!string.IsNullOrEmpty(monitor.DeviceKey) && if (!string.IsNullOrEmpty(monitor.Id) &&
_deviceKeyToHandleMap.TryGetValue(monitor.DeviceKey, out var handle)) _monitorIdToHandleMap.TryGetValue(monitor.Id, out var handle))
{ {
return handle; return handle;
} }
@@ -45,18 +45,18 @@ namespace PowerDisplay.Common.Drivers.DDC
/// Try to reuse existing handle if valid; otherwise uses new handle /// Try to reuse existing handle if valid; otherwise uses new handle
/// Returns the handle to use and whether it was reused /// Returns the handle to use and whether it was reused
/// </summary> /// </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 (newHandle, false);
} }
return _deviceKeyToHandleMap.ExecuteWithLock(dict => return _monitorIdToHandleMap.ExecuteWithLock(dict =>
{ {
// Try to reuse existing handle if it's still valid // Try to reuse existing handle if it's still valid
// Use quick connection check instead of full capabilities retrieval // 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 && existingHandle != IntPtr.Zero &&
DdcCiNative.QuickConnectionCheck(existingHandle)) DdcCiNative.QuickConnectionCheck(existingHandle))
{ {
@@ -78,7 +78,7 @@ namespace PowerDisplay.Common.Drivers.DDC
/// </summary> /// </summary>
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap) public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
{ {
_deviceKeyToHandleMap.ExecuteWithLock(dict => _monitorIdToHandleMap.ExecuteWithLock(dict =>
{ {
// Clean up unused handles before updating // Clean up unused handles before updating
CleanupUnusedHandles(dict, newHandleMap); 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 // 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) foreach (var handle in handles)
{ {
if (handle != IntPtr.Zero) if (handle != IntPtr.Zero)
@@ -157,7 +157,7 @@ namespace PowerDisplay.Common.Drivers.DDC
} }
} }
_deviceKeyToHandleMap.Clear(); _monitorIdToHandleMap.Clear();
_disposed = true; _disposed = true;
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@@ -354,19 +354,23 @@ namespace PowerDisplay.Common.Drivers.WMI
name = info.Name; name = info.Name;
} }
// Extract HardwareId from InstanceName for state persistence // Extract EdidId from InstanceName
// e.g., "DISPLAY\BOE0900\4&10fd3ab1&0&UID265988_0" -> "BOE0900" // 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 // 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 var monitor = new Monitor
{ {
Id = $"WMI_{instanceName}", Id = monitorId,
Name = name, Name = name,
HardwareId = hardwareId,
CurrentBrightness = currentBrightness, CurrentBrightness = currentBrightness,
MinBrightness = 0, MinBrightness = 0,
MaxBrightness = 100, MaxBrightness = 100,
@@ -375,7 +379,7 @@ namespace PowerDisplay.Common.Drivers.WMI
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi, Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
ConnectionType = "Internal", ConnectionType = "Internal",
CommunicationMethod = "WMI", CommunicationMethod = "WMI",
Manufacturer = hardwareId.Length >= 3 ? hardwareId.Substring(0, 3) : "Internal", Manufacturer = edidId.Length >= 3 ? edidId.Substring(0, 3) : "Internal",
SupportsColorTemperature = false, SupportsColorTemperature = false,
MonitorNumber = monitorNumber, MonitorNumber = monitorNumber,
}; };

View File

@@ -21,11 +21,6 @@ namespace PowerDisplay.Common.Interfaces
/// </summary> /// </summary>
string Name { get; set; } string Name { get; set; }
/// <summary>
/// Gets or sets the hardware ID (EDID format like GSM5C6D).
/// </summary>
string HardwareId { get; set; }
/// <summary> /// <summary>
/// Gets or sets the current brightness value (0-100). /// Gets or sets the current brightness value (0-100).
/// </summary> /// </summary>

View File

@@ -15,13 +15,8 @@ namespace PowerDisplay.Common.Models
/// Implements IMonitorData to provide a common interface for monitor hardware values. /// Implements IMonitorData to provide a common interface for monitor hardware values.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para><b>Monitor Identifier Hierarchy:</b></para> /// <para><see cref="Id"/> is the unique identifier used for all purposes: UI lookups, IPC, persistent storage, and handle management.</para>
/// <list type="bullet"> /// <para>Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").</para>
/// <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>
/// </remarks> /// </remarks>
public partial class Monitor : INotifyPropertyChanged, IMonitorData public partial class Monitor : INotifyPropertyChanged, IMonitorData
{ {
@@ -31,25 +26,15 @@ namespace PowerDisplay.Common.Models
private bool _isAvailable = true; private bool _isAvailable = true;
/// <summary> /// <summary>
/// Runtime unique identifier for UI lookups and IPC communication. /// Unique identifier for all purposes: UI lookups, IPC, persistent storage, and handle management.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Format: "{Source}_{HardwareId}" where Source is "DDC" or "WMI". /// Format: "{Source}_{EdidId}_{MonitorNumber}" where Source is "DDC" or "WMI".
/// Examples: "DDC_GSM5C6D", "WMI_DISPLAY\BOE0900...". /// Examples: "DDC_GSM5C6D_1", "WMI_BOE0900_2".
/// Use this for ViewModel lookups and MonitorManager method parameters. /// Stable across reboots and unique even for multiple identical monitors.
/// </remarks> /// </remarks>
public string Id { get; set; } = string.Empty; 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> /// <summary>
/// Display name /// Display name
/// </summary> /// </summary>
@@ -242,17 +227,6 @@ namespace PowerDisplay.Common.Models
/// </summary> /// </summary>
public IntPtr Handle { get; set; } = IntPtr.Zero; 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> /// <summary>
/// Instance name (used by WMI) /// Instance name (used by WMI)
/// </summary> /// </summary>

View File

@@ -11,8 +11,12 @@ namespace PowerDisplay.Common.Models
/// </summary> /// </summary>
public class ProfileMonitorSetting public class ProfileMonitorSetting
{ {
[JsonPropertyName("hardwareId")] /// <summary>
public string HardwareId { get; set; } /// 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")] [JsonPropertyName("brightness")]
public int? Brightness { get; set; } public int? Brightness { get; set; }
@@ -30,23 +34,18 @@ namespace PowerDisplay.Common.Models
[JsonPropertyName("colorTemperature")] [JsonPropertyName("colorTemperature")]
public int? ColorTemperatureVcp { get; set; } public int? ColorTemperatureVcp { get; set; }
[JsonPropertyName("monitorInternalName")]
public string MonitorInternalName { get; set; }
public ProfileMonitorSetting() public ProfileMonitorSetting()
{ {
HardwareId = string.Empty; MonitorId = string.Empty;
MonitorInternalName = 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; Brightness = brightness;
ColorTemperatureVcp = colorTemperatureVcp; ColorTemperatureVcp = colorTemperatureVcp;
Contrast = contrast; Contrast = contrast;
Volume = volume; Volume = volume;
MonitorInternalName = monitorInternalName;
} }
} }
} }

View File

@@ -14,33 +14,19 @@ namespace PowerDisplay.Common.Utils
public static class MonitorMatchingHelper public static class MonitorMatchingHelper
{ {
/// <summary> /// <summary>
/// Generate a unique key for monitor matching based on hardware ID and internal name. /// Generate a unique key for monitor matching based on Id.
/// Uses HardwareId if available; otherwise falls back to Id (InternalName) or Name.
/// </summary> /// </summary>
/// <param name="monitor">The monitor data to generate a key for.</param> /// <param name="monitor">The monitor data to generate a key for.</param>
/// <returns>A unique string key for the monitor.</returns> /// <returns>A unique string key for the monitor.</returns>
public static string GetMonitorKey(IMonitorData? monitor) public static string GetMonitorKey(IMonitorData? monitor)
=> GetMonitorKey(monitor?.HardwareId, monitor?.Id, monitor?.Name); => monitor?.Id ?? string.Empty;
/// <summary> /// <summary>
/// Generate a unique key for monitor matching using explicit values. /// Check if two monitors are considered the same based on their Ids.
/// 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.
/// </summary> /// </summary>
/// <param name="monitor1">First monitor.</param> /// <param name="monitor1">First monitor.</param>
/// <param name="monitor2">Second 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) public static bool AreMonitorsSame(IMonitorData monitor1, IMonitorData monitor2)
{ {
if (monitor1 == null || monitor2 == null) if (monitor1 == null || monitor2 == null)
@@ -48,7 +34,7 @@ namespace PowerDisplay.Common.Utils
return false; return false;
} }
return GetMonitorKey(monitor1) == GetMonitorKey(monitor2); return !string.IsNullOrEmpty(monitor1.Id) && monitor1.Id == monitor2.Id;
} }
} }
} }

View File

@@ -254,16 +254,16 @@ public partial class MainViewModel
foreach (var setting in monitorSettings) foreach (var setting in monitorSettings)
{ {
// Find monitor by InternalName (unique identifier) // Find monitor by Id (unique identifier)
var monitorVm = Monitors.FirstOrDefault(m => m.InternalName == setting.MonitorInternalName); var monitorVm = Monitors.FirstOrDefault(m => m.Id == setting.MonitorId);
if (monitorVm == null) 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; 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 // Apply brightness if included in profile
if (setting.Brightness.HasValue && if (setting.Brightness.HasValue &&
@@ -312,8 +312,8 @@ public partial class MainViewModel
foreach (var monitorVm in Monitors) foreach (var monitorVm in Monitors)
{ {
// Find and apply corresponding saved settings from state file using stable HardwareId // Find and apply corresponding saved settings from state file using unique monitor Id
var savedState = _stateManager.GetMonitorParameters(monitorVm.HardwareId); var savedState = _stateManager.GetMonitorParameters(monitorVm.Id);
if (!savedState.HasValue) if (!savedState.HasValue)
{ {
continue; continue;
@@ -384,18 +384,18 @@ public partial class MainViewModel
/// Thread-safe save method that can be called from background threads. /// Thread-safe save method that can be called from background threads.
/// Does not access UI collections or update UI properties. /// Does not access UI collections or update UI properties.
/// </summary> /// </summary>
public void SaveMonitorSettingDirect(string hardwareId, string property, int value) public void SaveMonitorSettingDirect(string monitorId, string property, int value)
{ {
try try
{ {
// This is thread-safe - _stateManager has internal locking // This is thread-safe - _stateManager has internal locking
// No UI thread operations, no ObservableCollection access // No UI thread operations, no ObservableCollection access
_stateManager.UpdateMonitorParameter(hardwareId, property, value); _stateManager.UpdateMonitorParameter(monitorId, property, value);
} }
catch (Exception ex) catch (Exception ex)
{ {
// Only log, don't update UI from background thread // 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( var monitorInfo = new Microsoft.PowerToys.Settings.UI.Library.MonitorInfo(
name: vm.Name, name: vm.Name,
internalName: vm.Id, internalName: vm.Id,
hardwareId: vm.HardwareId, hardwareId: string.Empty, // Deprecated, use InternalName (Id) instead
communicationMethod: vm.CommunicationMethod, communicationMethod: vm.CommunicationMethod,
currentBrightness: vm.Brightness, currentBrightness: vm.Brightness,
colorTemperatureVcp: vm.ColorTemperature) colorTemperatureVcp: vm.ColorTemperature)

View File

@@ -163,7 +163,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
{ {
try 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); var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
@@ -173,18 +173,18 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(ColorTemperature)); OnPropertyChanged(nameof(ColorTemperature));
OnPropertyChanged(nameof(ColorTemperaturePresetName)); 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 else
{ {
Logger.LogWarning($"[{HardwareId}] Failed to set color temperature: {result.ErrorMessage}"); Logger.LogWarning($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
} }
} }
catch (Exception ex) 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 try
{ {
Logger.LogDebug($"[{HardwareId}] Applying {propertyName.ToLowerInvariant()}: {value}%"); Logger.LogDebug($"[{Id}] Applying {propertyName.ToLowerInvariant()}: {value}%");
var result = await setAsyncFunc(Id, value, default); var result = await setAsyncFunc(Id, value, default);
if (result.IsSuccess) if (result.IsSuccess)
{ {
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, propertyName, value); _mainViewModel?.SaveMonitorSettingDirect(_monitor.Id, propertyName, value);
} }
else else
{ {
Logger.LogWarning($"[{HardwareId}] Failed to set {propertyName.ToLowerInvariant()}: {result.ErrorMessage}"); Logger.LogWarning($"[{Id}] Failed to set {propertyName.ToLowerInvariant()}: {result.ErrorMessage}");
} }
} }
catch (Exception ex) 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 Id => _monitor.Id;
public string HardwareId => _monitor.HardwareId;
public string InternalName => _monitor.Id; public string InternalName => _monitor.Id;
public string Name => _monitor.Name; public string Name => _monitor.Name;
@@ -373,7 +371,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
} }
/// <summary> /// <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> /// </summary>
public bool ShowRotation public bool ShowRotation
{ {
@@ -394,22 +392,22 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
public int CurrentRotation => _monitor.Orientation; public int CurrentRotation => _monitor.Orientation;
/// <summary> /// <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> /// </summary>
public bool IsRotation0 => CurrentRotation == 0; public bool IsRotation0 => CurrentRotation == 0;
/// <summary> /// <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> /// </summary>
public bool IsRotation1 => CurrentRotation == 1; public bool IsRotation1 => CurrentRotation == 1;
/// <summary> /// <summary>
/// Gets whether the current rotation is 180° (inverted) /// Gets a value indicating whether gets whether the current rotation is 180° (inverted)
/// </summary> /// </summary>
public bool IsRotation2 => CurrentRotation == 2; public bool IsRotation2 => CurrentRotation == 2;
/// <summary> /// <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> /// </summary>
public bool IsRotation3 => CurrentRotation == 3; 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°) // Validate orientation range (0=normal, 1=90°, 2=180°, 3=270°)
if (orientation < 0 || orientation > 3) 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; return;
} }
@@ -434,7 +432,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
try try
{ {
Logger.LogInfo($"[{HardwareId}] Setting rotation to {orientation}"); Logger.LogInfo($"[{Id}] Setting rotation to {orientation}");
var result = await _monitorManager.SetRotationAsync(Id, orientation); var result = await _monitorManager.SetRotationAsync(Id, orientation);
@@ -447,16 +445,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(IsRotation2)); OnPropertyChanged(nameof(IsRotation2));
OnPropertyChanged(nameof(IsRotation3)); OnPropertyChanged(nameof(IsRotation3));
Logger.LogInfo($"[{HardwareId}] Rotation set successfully to {orientation}"); Logger.LogInfo($"[{Id}] Rotation set successfully to {orientation}");
} }
else else
{ {
Logger.LogWarning($"[{HardwareId}] Failed to set rotation: {result.ErrorMessage}"); Logger.LogWarning($"[{Id}] Failed to set rotation: {result.ErrorMessage}");
} }
} }
catch (Exception ex) 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; public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
/// <summary> /// <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> /// </summary>
public bool SupportsInputSource => _monitor.SupportsInputSource; public bool SupportsInputSource => _monitor.SupportsInputSource;
@@ -548,7 +546,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
{ {
try 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); var result = await _monitorManager.SetInputSourceAsync(Id, inputSource);
@@ -558,16 +556,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(CurrentInputSourceName)); OnPropertyChanged(nameof(CurrentInputSourceName));
RefreshAvailableInputSources(); RefreshAvailableInputSources();
Logger.LogInfo($"[{HardwareId}] Input source set successfully to {CurrentInputSourceName}"); Logger.LogInfo($"[{Id}] Input source set successfully to {CurrentInputSourceName}");
} }
else else
{ {
Logger.LogWarning($"[{HardwareId}] Failed to set input source: {result.ErrorMessage}"); Logger.LogWarning($"[{Id}] Failed to set input source: {result.ErrorMessage}");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError($"[{HardwareId}] Exception setting input source: {ex.Message}"); Logger.LogError($"[{Id}] Exception setting input source: {ex.Message}");
} }
} }

View File

@@ -58,7 +58,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// Pre-fill monitor settings from existing profile // Pre-fill monitor settings from existing profile
foreach (var monitorSetting in profile.MonitorSettings) 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) if (monitorItem != null)
{ {
monitorItem.IsSelected = true; monitorItem.IsSelected = true;

View File

@@ -89,12 +89,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
var settings = _monitors var settings = _monitors
.Where(m => m.IsSelected) .Where(m => m.IsSelected)
.Select(m => new ProfileMonitorSetting( .Select(m => new ProfileMonitorSetting(
m.Monitor.HardwareId, m.Monitor.InternalName, // Monitor Id (unique identifier)
m.IncludeBrightness ? (int?)m.Brightness : null, m.IncludeBrightness ? (int?)m.Brightness : null,
m.IncludeColorTemperature && m.SupportsColorTemperature ? (int?)m.ColorTemperature : null, m.IncludeColorTemperature && m.SupportsColorTemperature ? (int?)m.ColorTemperature : null,
m.IncludeContrast && m.SupportsContrast ? (int?)m.Contrast : null, m.IncludeContrast && m.SupportsContrast ? (int?)m.Contrast : null,
m.IncludeVolume && m.SupportsVolume ? (int?)m.Volume : null, m.IncludeVolume && m.SupportsVolume ? (int?)m.Volume : null))
m.Monitor.InternalName))
.ToList(); .ToList();
return new PowerDisplayProfile(_profileName, settings); return new PowerDisplayProfile(_profileName, settings);