Refactor and enhance monitor management system

Refactored namespaces to improve modularity, including moving `PowerDisplay.Native` to `PowerDisplay.Common.Drivers`. Introduced the `IMonitorData` interface for better abstraction of monitor hardware data. Replaced `ColorTemperature` with `ColorTemperatureVcp` for precise VCP-based color temperature control, adding utilities for Kelvin conversion.

Enhanced monitor state management with a new `MonitorStateFile` for JSON persistence and updated `MonitorStateManager` for debounced saves. Added `MonitorMatchingHelper` for consistent monitor identification and `ProfileHelper` for profile management operations.

Refactored P/Invoke declarations into helper classes, updated UI bindings for `ColorTemperatureVcp`, and improved logging for better runtime visibility. Removed redundant code, added new utility classes (`MonitorValueConverter`, `MonitorMatchingHelper`), and ensured backward compatibility.

These changes improve code organization, maintainability, and extensibility while aligning with hardware-level control standards.
This commit is contained in:
Yu Leng
2025-11-24 21:58:34 +08:00
parent 580651b47a
commit 15746e8f45
32 changed files with 819 additions and 283 deletions

View File

@@ -11,19 +11,18 @@ using ManagedCommon;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using PowerDisplay.Helpers;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates;
using static PowerDisplay.Native.PInvoke;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.NativeDelegates;
using static PowerDisplay.Common.Drivers.PInvoke;
using Monitor = PowerDisplay.Common.Models.Monitor;
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Native.DDC
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// DDC/CI monitor controller for controlling external monitors

View File

@@ -6,25 +6,25 @@ using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using ManagedCommon;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates;
using static PowerDisplay.Native.PInvoke;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.NativeDelegates;
using static PowerDisplay.Common.Drivers.PInvoke;
// Type aliases for Windows API naming conventions compatibility
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Native.DISPLAYCONFIG_PATH_INFO;
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Native.DISPLAYCONFIG_TARGET_DEVICE_NAME;
using LUID = PowerDisplay.Native.Luid;
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
using DISPLAY_DEVICE = PowerDisplay.Common.Drivers.DisplayDevice;
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Common.Drivers.DISPLAYCONFIG_DEVICE_INFO_HEADER;
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Common.Drivers.DISPLAYCONFIG_MODE_INFO;
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Common.Drivers.DISPLAYCONFIG_PATH_INFO;
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Common.Drivers.DISPLAYCONFIG_TARGET_DEVICE_NAME;
using LUID = PowerDisplay.Common.Drivers.Luid;
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
using RECT = PowerDisplay.Common.Drivers.Rect;
#pragma warning disable SA1649 // File name should match first type name - Multiple related types for DDC/CI
#pragma warning disable SA1402 // File may only contain a single type - Related DDC/CI types grouped together
namespace PowerDisplay.Native.DDC
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Display device information class
@@ -227,7 +227,7 @@ namespace PowerDisplay.Native.DDC
/// Gets all monitor friendly names by enumerating display configurations
/// </summary>
/// <returns>Mapping of device path to friendly name</returns>
public static Dictionary<string, string> GetAllMonitorFriendlyNames()
public static unsafe Dictionary<string, string> GetAllMonitorFriendlyNames()
{
var friendlyNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
@@ -244,11 +244,17 @@ namespace PowerDisplay.Native.DDC
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// Query display configuration
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
if (result != 0)
// Query display configuration using fixed pointer
fixed (DISPLAYCONFIG_PATH_INFO* pathsPtr = paths)
{
return friendlyNames;
fixed (DISPLAYCONFIG_MODE_INFO* modesPtr = modes)
{
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, pathsPtr, ref modeCount, modesPtr, IntPtr.Zero);
if (result != 0)
{
return friendlyNames;
}
}
}
// Get friendly name for each path
@@ -345,7 +351,7 @@ namespace PowerDisplay.Native.DDC
/// Gets complete information for all monitors, including friendly name and hardware ID
/// </summary>
/// <returns>Dictionary containing monitor information</returns>
public static Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
public static unsafe Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
{
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
@@ -362,11 +368,17 @@ namespace PowerDisplay.Native.DDC
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// Query display configuration
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
if (result != 0)
// Query display configuration using fixed pointer
fixed (DISPLAYCONFIG_PATH_INFO* pathsPtr = paths)
{
return monitorInfo;
fixed (DISPLAYCONFIG_MODE_INFO* modesPtr = modes)
{
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, pathsPtr, ref modeCount, modesPtr, IntPtr.Zero);
if (result != 0)
{
return monitorInfo;
}
}
}
// Get information for each path

View File

@@ -8,14 +8,14 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ManagedCommon;
using PowerDisplay.Common.Models;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.PInvoke;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.PInvoke;
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
using MONITORINFOEX = PowerDisplay.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Native.DDC
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Helper class for discovering and creating monitor objects

View File

@@ -6,9 +6,9 @@ using System;
using System.Collections.Generic;
using ManagedCommon;
using PowerDisplay.Common.Models;
using static PowerDisplay.Native.PInvoke;
using static PowerDisplay.Common.Drivers.PInvoke;
namespace PowerDisplay.Native.DDC
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Manages physical monitor handles - reuse, cleanup, and validation

View File

@@ -4,7 +4,7 @@
using System;
namespace PowerDisplay.Native
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Windows API constant definitions

View File

@@ -5,10 +5,7 @@
using System;
using System.Runtime.InteropServices;
// Type aliases for Windows API naming conventions compatibility
using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native;
namespace PowerDisplay.Common.Drivers;
/// <summary>
/// Native delegate type definitions

View File

@@ -7,7 +7,7 @@ using System.Runtime.InteropServices;
#pragma warning disable SA1649 // File name should match first type name - Multiple related P/Invoke structures
namespace PowerDisplay.Native
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// Physical monitor structure for DDC/CI

View File

@@ -5,118 +5,13 @@
using System;
using System.Runtime.InteropServices;
namespace PowerDisplay.Native
namespace PowerDisplay.Common.Drivers
{
/// <summary>
/// P/Invoke declarations using LibraryImport source generator
/// </summary>
internal static partial class PInvoke
{
// ==================== User32.dll - Window Management ====================
// GetWindowLong: On 64-bit use GetWindowLongPtrW, on 32-bit use GetWindowLongW
#if WIN64
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
internal static partial IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
#else
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongW")]
internal static partial int GetWindowLong(IntPtr hWnd, int nIndex);
#endif
// SetWindowLong: On 64-bit use SetWindowLongPtrW, on 32-bit use SetWindowLongW
#if WIN64
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
internal static partial IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
#else
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongW")]
internal static partial int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
#endif
// SetWindowLongPtr: Always uses the Ptr variant (64-bit)
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
internal static partial IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint uFlags);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool ShowWindow(IntPtr hWnd, int nCmdShow);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetForegroundWindow(IntPtr hWnd);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool IsWindowVisible(IntPtr hWnd);
// ==================== User32.dll - Window Creation and Messaging ====================
[LibraryImport("user32.dll", EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial IntPtr CreateWindowEx(
uint dwExStyle,
[MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
[MarshalAs(UnmanagedType.LPWStr)] string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DestroyWindow(IntPtr hWnd);
[LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")]
internal static partial IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[LibraryImport("user32.dll")]
internal static partial IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
[LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial uint RegisterWindowMessage([MarshalAs(UnmanagedType.LPWStr)] string lpString);
// ==================== User32.dll - Menu Functions ====================
[LibraryImport("user32.dll")]
internal static partial IntPtr CreatePopupMenu();
[LibraryImport("user32.dll", EntryPoint = "AppendMenuW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool AppendMenu(
IntPtr hMenu,
uint uFlags,
uint uIDNewItem,
[MarshalAs(UnmanagedType.LPWStr)] string lpNewItem);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DestroyMenu(IntPtr hMenu);
[LibraryImport("user32.dll")]
internal static partial int TrackPopupMenu(
IntPtr hMenu,
uint uFlags,
int x,
int y,
int nReserved,
IntPtr hWnd,
IntPtr prcRect);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetCursorPos(out POINT lpPoint);
// ==================== User32.dll - Display Configuration ====================
[LibraryImport("user32.dll")]
internal static partial int GetDisplayConfigBufferSizes(
@@ -124,14 +19,14 @@ namespace PowerDisplay.Native
out uint numPathArrayElements,
out uint numModeInfoArrayElements);
// With DisableRuntimeMarshalling, LibraryImport can handle struct arrays
// Use unsafe pointer to avoid runtime marshalling
[LibraryImport("user32.dll")]
internal static partial int QueryDisplayConfig(
internal static unsafe partial int QueryDisplayConfig(
uint flags,
ref uint numPathArrayElements,
[Out] DISPLAYCONFIG_PATH_INFO[] pathArray,
DISPLAYCONFIG_PATH_INFO* pathArray,
ref uint numModeInfoArrayElements,
[Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
IntPtr currentTopologyId);
[LibraryImport("user32.dll")]

View File

@@ -13,7 +13,7 @@ using PowerDisplay.Common.Models;
using WmiLight;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.Native.WMI
namespace PowerDisplay.Common.Drivers.WMI
{
/// <summary>
/// WMI monitor controller for controlling internal laptop displays.

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerDisplay.Common.Interfaces
{
/// <summary>
/// Core interface representing monitor hardware data.
/// This interface defines the actual hardware values for a monitor.
/// Implementations can add UI-specific properties and use converters for display formatting.
/// </summary>
public interface IMonitorData
{
/// <summary>
/// Gets or sets the unique identifier for the monitor.
/// </summary>
string Id { get; set; }
/// <summary>
/// Gets or sets the display name of the monitor.
/// </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>
int Brightness { get; set; }
/// <summary>
/// Gets or sets the current contrast value (0-100).
/// </summary>
int Contrast { get; set; }
/// <summary>
/// Gets or sets the current volume value (0-100).
/// </summary>
int Volume { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
/// Use MonitorValueConverter to convert to/from human-readable Kelvin values.
/// </summary>
int ColorTemperatureVcp { get; set; }
}
}

View File

@@ -14,8 +14,12 @@ namespace PowerDisplay.Common.Models
[JsonPropertyName("monitor_id")]
public string MonitorId { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP preset value.
/// JSON property name kept as "color_temperature" for IPC compatibility.
/// </summary>
[JsonPropertyName("color_temperature")]
public int ColorTemperature { get; set; }
public int ColorTemperatureVcp { get; set; }
public ColorTemperatureOperation()
{

View File

@@ -5,14 +5,16 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Monitor model that implements property change notification
/// Monitor model that implements property change notification.
/// Implements IMonitorData to provide a common interface for monitor hardware values.
/// </summary>
public partial class Monitor : INotifyPropertyChanged
public partial class Monitor : INotifyPropertyChanged, IMonitorData
{
private int _currentBrightness;
private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
@@ -252,5 +254,33 @@ namespace PowerDisplay.Common.Models
LastUpdate = DateTime.Now;
}
}
/// <inheritdoc />
int IMonitorData.Brightness
{
get => CurrentBrightness;
set => CurrentBrightness = value;
}
/// <inheritdoc />
int IMonitorData.Contrast
{
get => CurrentContrast;
set => CurrentContrast = value;
}
/// <inheritdoc />
int IMonitorData.Volume
{
get => CurrentVolume;
set => CurrentVolume = value;
}
/// <inheritdoc />
int IMonitorData.ColorTemperatureVcp
{
get => CurrentColorTemperature;
set => CurrentColorTemperature = value;
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Individual monitor state entry for JSON persistence.
/// Stores the current state of a monitor's adjustable parameters.
/// </summary>
public sealed class MonitorStateEntry
{
/// <summary>
/// Gets or sets the brightness level (0-100).
/// </summary>
[JsonPropertyName("brightness")]
public int Brightness { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP value.
/// </summary>
[JsonPropertyName("colorTemperature")]
public int ColorTemperatureVcp { get; set; }
/// <summary>
/// Gets or sets the contrast level (0-100).
/// </summary>
[JsonPropertyName("contrast")]
public int Contrast { get; set; }
/// <summary>
/// Gets or sets the volume level (0-100).
/// </summary>
[JsonPropertyName("volume")]
public int Volume { get; set; }
/// <summary>
/// Gets or sets the raw capabilities string from DDC/CI.
/// </summary>
[JsonPropertyName("capabilitiesRaw")]
public string? CapabilitiesRaw { get; set; }
/// <summary>
/// Gets or sets when this entry was last updated.
/// </summary>
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Monitor state file structure for JSON persistence.
/// Contains all monitor states indexed by HardwareId.
/// </summary>
public sealed class MonitorStateFile
{
/// <summary>
/// Gets or sets the monitor states dictionary.
/// Key is the monitor's HardwareId.
/// </summary>
[JsonPropertyName("monitors")]
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
/// <summary>
/// Gets or sets when the file was last updated.
/// </summary>
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@@ -23,8 +23,12 @@ namespace PowerDisplay.Common.Models
[JsonPropertyName("volume")]
public int? Volume { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP preset value.
/// JSON property name kept as "colorTemperature" for backward compatibility.
/// </summary>
[JsonPropertyName("colorTemperature")]
public int? ColorTemperature { get; set; }
public int? ColorTemperatureVcp { get; set; }
[JsonPropertyName("monitorInternalName")]
public string MonitorInternalName { get; set; }
@@ -35,11 +39,11 @@ namespace PowerDisplay.Common.Models
MonitorInternalName = string.Empty;
}
public ProfileMonitorSetting(string hardwareId, int? brightness = null, int? colorTemperature = null, int? contrast = null, int? volume = null, string monitorInternalName = "")
public ProfileMonitorSetting(string hardwareId, int? brightness = null, int? colorTemperatureVcp = null, int? contrast = null, int? volume = null, string monitorInternalName = "")
{
HardwareId = hardwareId;
Brightness = brightness;
ColorTemperature = colorTemperature;
ColorTemperatureVcp = colorTemperatureVcp;
Contrast = contrast;
Volume = volume;
MonitorInternalName = monitorInternalName;

View File

@@ -65,6 +65,17 @@ namespace PowerDisplay.Common
/// </summary>
public const string SettingsFileName = "settings.json";
/// <summary>
/// The name of the monitor state file.
/// </summary>
public const string MonitorStateFileName = "monitor_state.json";
/// <summary>
/// Gets the monitor state file path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\monitor_state.json
/// </summary>
public static string MonitorStateFilePath => Path.Combine(PowerDisplayFolderPath, MonitorStateFileName);
/// <summary>
/// Event name for LightSwitch theme change notifications.
/// </summary>

View File

@@ -27,6 +27,11 @@ namespace PowerDisplay.Common.Serialization
[JsonSerializable(typeof(List<ProfileOperation>))]
[JsonSerializable(typeof(ColorTemperatureOperation))]
[JsonSerializable(typeof(List<ColorTemperatureOperation>))]
// Monitor State Types
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(Dictionary<string, MonitorStateEntry>))]
public partial class ProfileSerializationContext : JsonSerializerContext
{
}

View File

@@ -8,12 +8,11 @@ using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Common;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Serialization;
using PowerDisplay.Common.Utils;
using PowerDisplay.Configuration;
using PowerDisplay.Serialization;
namespace PowerDisplay.Helpers
namespace PowerDisplay.Common.Services
{
/// <summary>
/// Manages monitor parameter state in a separate file from main settings.
@@ -39,7 +38,7 @@ namespace PowerDisplay.Helpers
{
public int Brightness { get; set; }
public int ColorTemperature { get; set; }
public int ColorTemperatureVcp { get; set; }
public int Contrast { get; set; }
@@ -48,11 +47,15 @@ namespace PowerDisplay.Helpers
public string? CapabilitiesRaw { get; set; }
}
/// <summary>
/// Initializes a new instance of the <see cref="MonitorStateManager"/> class.
/// Uses PathConstants for consistent path management.
/// </summary>
public MonitorStateManager()
{
// Use PathConstants for consistent path management
PathConstants.EnsurePowerDisplayFolderExists();
_stateFilePath = Path.Combine(PathConstants.PowerDisplayFolderPath, AppConstants.State.StateFileName);
_stateFilePath = PathConstants.MonitorStateFilePath;
// Initialize debouncer for batching rapid updates (e.g., slider drag)
_saveDebouncer = new SimpleDebouncer(SaveDebounceMs);
@@ -68,6 +71,9 @@ namespace PowerDisplay.Helpers
/// Uses HardwareId as the stable key.
/// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
/// </summary>
/// <param name="hardwareId">The monitor's hardware ID.</param>
/// <param name="property">The property name to update (Brightness, ColorTemperature, Contrast, or Volume).</param>
/// <param name="value">The new value.</param>
public void UpdateMonitorParameter(string hardwareId, string property, int value)
{
try
@@ -94,7 +100,7 @@ namespace PowerDisplay.Helpers
state.Brightness = value;
break;
case "ColorTemperature":
state.ColorTemperature = value;
state.ColorTemperatureVcp = value;
break;
case "Contrast":
state.Contrast = value;
@@ -121,9 +127,11 @@ namespace PowerDisplay.Helpers
}
/// <summary>
/// Get saved parameters for a monitor using HardwareId
/// Get saved parameters for a monitor using HardwareId.
/// </summary>
public (int Brightness, int ColorTemperature, int Contrast, int Volume)? GetMonitorParameters(string hardwareId)
/// <param name="hardwareId">The monitor's hardware ID.</param>
/// <returns>A tuple of (Brightness, ColorTemperatureVcp, Contrast, Volume) or null if not found.</returns>
public (int Brightness, int ColorTemperatureVcp, int Contrast, int Volume)? GetMonitorParameters(string hardwareId)
{
if (string.IsNullOrEmpty(hardwareId))
{
@@ -134,7 +142,7 @@ namespace PowerDisplay.Helpers
{
if (_states.TryGetValue(hardwareId, out var state))
{
return (state.Brightness, state.ColorTemperature, state.Contrast, state.Volume);
return (state.Brightness, state.ColorTemperatureVcp, state.Contrast, state.Volume);
}
}
@@ -142,7 +150,7 @@ namespace PowerDisplay.Helpers
}
/// <summary>
/// Load state from disk
/// Load state from disk.
/// </summary>
private void LoadStateFromDisk()
{
@@ -155,7 +163,7 @@ namespace PowerDisplay.Helpers
}
var json = File.ReadAllText(_stateFilePath);
var stateFile = JsonSerializer.Deserialize(json, AppJsonContext.Default.MonitorStateFile);
var stateFile = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.MonitorStateFile);
if (stateFile?.Monitors != null)
{
@@ -169,7 +177,7 @@ namespace PowerDisplay.Helpers
_states[monitorKey] = new MonitorState
{
Brightness = entry.Brightness,
ColorTemperature = entry.ColorTemperature,
ColorTemperatureVcp = entry.ColorTemperatureVcp,
Contrast = entry.Contrast,
Volume = entry.Volume,
CapabilitiesRaw = entry.CapabilitiesRaw,
@@ -217,7 +225,7 @@ namespace PowerDisplay.Helpers
stateFile.Monitors[monitorId] = new MonitorStateEntry
{
Brightness = state.Brightness,
ColorTemperature = state.ColorTemperature,
ColorTemperatureVcp = state.ColorTemperatureVcp,
Contrast = state.Contrast,
Volume = state.Volume,
CapabilitiesRaw = state.CapabilitiesRaw,
@@ -227,7 +235,7 @@ namespace PowerDisplay.Helpers
}
// Write to disk asynchronously
var json = JsonSerializer.Serialize(stateFile, AppJsonContext.Default.MonitorStateFile);
var json = JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
await File.WriteAllTextAsync(_stateFilePath, json);
// Clear dirty flag after successful save
@@ -244,6 +252,9 @@ namespace PowerDisplay.Helpers
}
}
/// <summary>
/// Disposes the MonitorStateManager, flushing any pending state changes.
/// </summary>
public void Dispose()
{
if (_disposed)

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerDisplay.Common.Interfaces;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for monitor matching and identification.
/// Provides consistent logic for matching monitors across different data sources.
/// </summary>
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.
/// </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)
{
if (monitor == null)
{
return string.Empty;
}
// Use hardware ID if available (most stable identifier)
if (!string.IsNullOrEmpty(monitor.HardwareId))
{
return monitor.HardwareId;
}
// Fall back to Id (InternalName in MonitorInfo)
if (!string.IsNullOrEmpty(monitor.Id))
{
return monitor.Id;
}
// Last resort: use display name
return monitor.Name ?? string.Empty;
}
/// <summary>
/// Generate a unique key for monitor matching using explicit values.
/// Useful when you don't have an IMonitorData object.
/// </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)
{
// Use hardware ID if available (most stable identifier)
if (!string.IsNullOrEmpty(hardwareId))
{
return hardwareId;
}
// Fall back to internal name
if (!string.IsNullOrEmpty(internalName))
{
return internalName;
}
// Last resort: use display name
return name ?? string.Empty;
}
/// <summary>
/// Check if two monitors are considered the same based on their keys.
/// </summary>
/// <param name="monitor1">First monitor.</param>
/// <param name="monitor2">Second monitor.</param>
/// <returns>True if the monitors have the same key.</returns>
public static bool AreMonitorsSame(IMonitorData monitor1, IMonitorData monitor2)
{
if (monitor1 == null || monitor2 == null)
{
return false;
}
return GetMonitorKey(monitor1) == GetMonitorKey(monitor2);
}
}
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Provides conversion utilities for monitor hardware values.
/// Use this class to convert between raw hardware values and display-friendly formats.
/// </summary>
public static class MonitorValueConverter
{
/// <summary>
/// Standard VCP color temperature preset to Kelvin value mapping.
/// Based on MCCS (Monitor Control Command Set) standard.
/// </summary>
private static readonly Dictionary<int, int> VcpToKelvinMap = new()
{
[0x03] = 4000,
[0x04] = 5000,
[0x05] = 6500,
[0x06] = 7500,
[0x08] = 9300,
[0x09] = 10000,
[0x0A] = 11500,
};
/// <summary>
/// Reverse mapping from Kelvin to VCP value.
/// </summary>
private static readonly Dictionary<int, int> KelvinToVcpMap = new()
{
[4000] = 0x03,
[5000] = 0x04,
[6500] = 0x05,
[7500] = 0x06,
[9300] = 0x08,
[10000] = 0x09,
[11500] = 0x0A,
};
/// <summary>
/// Converts a VCP color temperature preset value to approximate Kelvin temperature.
/// </summary>
/// <param name="vcpValue">The VCP preset value (e.g., 0x05).</param>
/// <returns>The Kelvin temperature (e.g., 6500), or 0 if unknown.</returns>
public static int VcpToKelvin(int vcpValue)
{
return VcpToKelvinMap.TryGetValue(vcpValue, out var kelvin) ? kelvin : 0;
}
/// <summary>
/// Converts a Kelvin temperature to VCP color temperature preset value.
/// </summary>
/// <param name="kelvin">The Kelvin temperature (e.g., 6500).</param>
/// <returns>The VCP preset value (e.g., 0x05), or 0x05 (6500K) as default if unknown.</returns>
public static int KelvinToVcp(int kelvin)
{
return KelvinToVcpMap.TryGetValue(kelvin, out var vcpValue) ? vcpValue : 0x05;
}
/// <summary>
/// Formats a VCP color temperature value as a Kelvin string for display.
/// </summary>
/// <param name="vcpValue">The VCP preset value (e.g., 0x05).</param>
/// <returns>Formatted string like "6500K" or "Unknown (0x05)" if not a standard preset.</returns>
public static string FormatVcpAsKelvin(int vcpValue)
{
var kelvin = VcpToKelvin(vcpValue);
if (kelvin > 0)
{
return $"{kelvin}K";
}
// Use VcpValueNames for special presets like sRGB, User 1, etc.
var name = VcpValueNames.GetName(ColorTemperatureHelper.ColorTemperatureVcpCode, vcpValue);
return name ?? $"Unknown (0x{vcpValue:X2})";
}
/// <summary>
/// Formats a VCP color temperature value as a display name with preset name.
/// </summary>
/// <param name="vcpValue">The VCP preset value (e.g., 0x05).</param>
/// <returns>Formatted string like "6500K (0x05)" or "sRGB (0x01)".</returns>
public static string FormatColorTemperatureDisplay(int vcpValue)
{
return ColorTemperatureHelper.FormatColorTemperatureDisplayName(vcpValue);
}
/// <summary>
/// Gets the preset name for a VCP color temperature value.
/// </summary>
/// <param name="vcpValue">The VCP preset value (e.g., 0x05).</param>
/// <returns>Preset name like "6500K", "sRGB", or null if unknown.</returns>
public static string? GetColorTemperaturePresetName(int vcpValue)
{
return VcpValueNames.GetName(ColorTemperatureHelper.ColorTemperatureVcpCode, vcpValue);
}
/// <summary>
/// Formats a brightness value for display.
/// </summary>
/// <param name="brightness">Brightness value (0-100).</param>
/// <returns>Formatted string like "50%".</returns>
public static string FormatBrightness(int brightness)
{
return $"{brightness}%";
}
/// <summary>
/// Formats a contrast value for display.
/// </summary>
/// <param name="contrast">Contrast value (0-100).</param>
/// <returns>Formatted string like "50%".</returns>
public static string FormatContrast(int contrast)
{
return $"{contrast}%";
}
/// <summary>
/// Formats a volume value for display.
/// </summary>
/// <param name="volume">Volume value (0-100).</param>
/// <returns>Formatted string like "50%".</returns>
public static string FormatVolume(int volume)
{
return $"{volume}%";
}
/// <summary>
/// Checks if a VCP value represents a known color temperature preset.
/// </summary>
/// <param name="vcpValue">The VCP preset value.</param>
/// <returns>True if the value is a known preset.</returns>
public static bool IsKnownColorTemperaturePreset(int vcpValue)
{
return VcpToKelvinMap.ContainsKey(vcpValue) ||
VcpValueNames.GetName(ColorTemperatureHelper.ColorTemperatureVcpCode, vcpValue) != null;
}
}
}

View File

@@ -0,0 +1,125 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for profile management operations.
/// Provides utilities for profile name generation and validation.
/// </summary>
public static class ProfileHelper
{
/// <summary>
/// Default base name for new profiles.
/// </summary>
public const string DefaultProfileBaseName = "Profile";
/// <summary>
/// Generate a unique profile name that doesn't conflict with existing profiles.
/// </summary>
/// <param name="existingProfiles">The collection of existing profiles.</param>
/// <param name="baseName">The base name to use (default: "Profile").</param>
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
public static string GenerateUniqueProfileName(PowerDisplayProfiles existingProfiles, string baseName = DefaultProfileBaseName)
{
var existingNames = new HashSet<string>();
if (existingProfiles?.Profiles != null)
{
foreach (var profile in existingProfiles.Profiles)
{
if (!string.IsNullOrEmpty(profile.Name))
{
existingNames.Add(profile.Name);
}
}
}
return GenerateUniqueProfileName(existingNames, baseName);
}
/// <summary>
/// Generate a unique profile name that doesn't conflict with existing names.
/// </summary>
/// <param name="existingNames">Set of existing profile names.</param>
/// <param name="baseName">The base name to use (default: "Profile").</param>
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
public static string GenerateUniqueProfileName(ISet<string> existingNames, string baseName = DefaultProfileBaseName)
{
if (existingNames == null)
{
return $"{baseName} 1";
}
int counter = 1;
string name;
do
{
name = $"{baseName} {counter}";
counter++;
}
while (existingNames.Contains(name));
return name;
}
/// <summary>
/// Generate a unique profile name from a collection of profile names.
/// </summary>
/// <param name="existingNames">Enumerable of existing profile names.</param>
/// <param name="baseName">The base name to use (default: "Profile").</param>
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
public static string GenerateUniqueProfileName(IEnumerable<string> existingNames, string baseName = DefaultProfileBaseName)
{
var nameSet = new HashSet<string>(existingNames ?? Enumerable.Empty<string>());
return GenerateUniqueProfileName(nameSet, baseName);
}
/// <summary>
/// Validate that a profile has at least one monitor with at least one setting.
/// </summary>
/// <param name="profile">The profile to validate.</param>
/// <returns>True if the profile has valid settings.</returns>
public static bool HasValidSettings(PowerDisplayProfile profile)
{
if (profile == null || profile.MonitorSettings == null || profile.MonitorSettings.Count == 0)
{
return false;
}
// Check that at least one monitor has at least one setting
return profile.MonitorSettings.Any(m =>
m.Brightness.HasValue ||
m.Contrast.HasValue ||
m.Volume.HasValue ||
m.ColorTemperatureVcp.HasValue);
}
/// <summary>
/// Check if a profile name is available (not already in use).
/// </summary>
/// <param name="name">The name to check.</param>
/// <param name="existingProfiles">The collection of existing profiles.</param>
/// <returns>True if the name is available.</returns>
public static bool IsNameAvailable(string name, PowerDisplayProfiles existingProfiles)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
if (existingProfiles?.Profiles == null || existingProfiles.Profiles.Count == 0)
{
return true;
}
return !existingProfiles.Profiles.Any(p =>
string.Equals(p.Name, name, System.StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@@ -8,13 +8,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Drivers.DDC;
using PowerDisplay.Common.Drivers.WMI;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Native;
using PowerDisplay.Native.DDC;
using PowerDisplay.Native.WMI;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.Core

View File

@@ -3,11 +3,11 @@
// See the LICENSE file in the project root for more information.
using System;
using static PowerDisplay.Native.PInvoke;
using System.Runtime.InteropServices;
namespace PowerDisplay.Native
namespace PowerDisplay.Helpers
{
internal static class WindowHelper
internal static partial class WindowHelper
{
// Window Styles
private const int GwlStyle = -16;
@@ -42,6 +42,50 @@ namespace PowerDisplay.Native
private const int SwMinimize = 6;
private const int SwRestore = 9;
// P/Invoke declarations
#if WIN64
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
private static partial IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
#else
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongW")]
private static partial int GetWindowLong(IntPtr hWnd, int nIndex);
#endif
#if WIN64
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
#else
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongW")]
private static partial int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
#endif
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint uFlags);
[LibraryImport("user32.dll", EntryPoint = "ShowWindow")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindowNative(IntPtr hWnd, int nCmdShow);
[LibraryImport("user32.dll", EntryPoint = "IsWindowVisible")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool IsWindowVisibleNative(IntPtr hWnd);
/// <summary>
/// Check if window is visible
/// </summary>
public static bool IsWindowVisible(IntPtr hWnd)
{
return IsWindowVisibleNative(hWnd);
}
/// <summary>
/// Disable window moving and resizing functionality
/// </summary>
@@ -115,7 +159,7 @@ namespace PowerDisplay.Native
/// </summary>
public static void ShowWindow(IntPtr hWnd, bool show)
{
PInvoke.ShowWindow(hWnd, show ? SwShow : SwHide);
ShowWindowNative(hWnd, show ? SwShow : SwHide);
}
/// <summary>
@@ -123,7 +167,7 @@ namespace PowerDisplay.Native
/// </summary>
public static void MinimizeWindow(IntPtr hWnd)
{
PInvoke.ShowWindow(hWnd, SwMinimize);
ShowWindowNative(hWnd, SwMinimize);
}
/// <summary>
@@ -131,7 +175,7 @@ namespace PowerDisplay.Native
/// </summary>
public static void RestoreWindow(IntPtr hWnd)
{
PInvoke.ShowWindow(hWnd, SwRestore);
ShowWindowNative(hWnd, SwRestore);
}
/// <summary>

View File

@@ -20,11 +20,9 @@ using PowerDisplay.Configuration;
using PowerDisplay.Core;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Helpers;
using PowerDisplay.Native;
using PowerDisplay.ViewModels;
using Windows.Graphics;
using WinRT.Interop;
using static PowerDisplay.Native.PInvoke;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay
@@ -308,7 +306,7 @@ namespace PowerDisplay
public bool IsWindowVisible()
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
return PInvoke.IsWindowVisible(hWnd);
return WindowHelper.IsWindowVisible(hWnd);
}
/// <summary>
@@ -319,15 +317,20 @@ namespace PowerDisplay
try
{
bool isVisible = IsWindowVisible();
Logger.LogInfo($"[ToggleWindow] IsWindowVisible returned: {isVisible}");
if (isVisible)
{
Logger.LogInfo("[ToggleWindow] Window is visible, calling HideWindow");
HideWindow();
}
else
{
Logger.LogInfo("[ToggleWindow] Window is hidden, calling ShowWindow");
ShowWindow();
}
Logger.LogInfo("[ToggleWindow] Toggle completed");
}
catch (Exception ex)
{

View File

@@ -2,7 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -16,11 +15,11 @@ namespace PowerDisplay.Serialization
/// <summary>
/// JSON source generation context for AOT compatibility.
/// Eliminates reflection-based JSON serialization.
/// Note: MonitorStateFile and MonitorStateEntry are now in PowerDisplay.Lib
/// and should be serialized using ProfileSerializationContext from the Lib.
/// </summary>
[JsonSerializable(typeof(MonitorInfoData))]
[JsonSerializable(typeof(IPCMessageAction))]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(PowerDisplaySettings))]
[JsonSerializable(typeof(ColorTemperatureOperation))]
[JsonSerializable(typeof(ProfileOperation))]
@@ -58,42 +57,4 @@ namespace PowerDisplay.Serialization
[JsonPropertyName("action")]
public string? Action { get; set; }
}
/// <summary>
/// Monitor state file structure for JSON persistence.
/// Made internal (from private) to support source generation.
/// </summary>
internal sealed class MonitorStateFile
{
[JsonPropertyName("monitors")]
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
/// <summary>
/// Individual monitor state entry.
/// Made internal (from private) to support source generation.
/// </summary>
internal sealed class MonitorStateEntry
{
[JsonPropertyName("brightness")]
public int Brightness { get; set; }
[JsonPropertyName("colorTemperature")]
public int ColorTemperature { get; set; }
[JsonPropertyName("contrast")]
public int Contrast { get; set; }
[JsonPropertyName("volume")]
public int Volume { get; set; }
[JsonPropertyName("capabilitiesRaw")]
public string? CapabilitiesRaw { get; set; }
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@@ -101,7 +101,7 @@ public partial class MainViewModel
if (pendingOp != null && !string.IsNullOrEmpty(pendingOp.MonitorId))
{
Logger.LogInfo($"[Settings] Processing pending color temperature operation: Monitor '{pendingOp.MonitorId}' -> 0x{pendingOp.ColorTemperature:X2}");
Logger.LogInfo($"[Settings] Processing pending color temperature operation: Monitor '{pendingOp.MonitorId}' -> 0x{pendingOp.ColorTemperatureVcp:X2}");
// Find the monitor by internal name (ID)
var monitorVm = Monitors.FirstOrDefault(m => m.Id == pendingOp.MonitorId);
@@ -109,7 +109,7 @@ public partial class MainViewModel
if (monitorVm != null)
{
// Apply color temperature directly
await ApplyColorTemperatureAsync(monitorVm, pendingOp.ColorTemperature);
await ApplyColorTemperatureAsync(monitorVm, pendingOp.ColorTemperatureVcp);
Logger.LogInfo($"[Settings] Successfully applied color temperature to monitor '{pendingOp.MonitorId}'");
}
else
@@ -277,9 +277,9 @@ public partial class MainViewModel
}
// Apply color temperature if included in profile
if (setting.ColorTemperature.HasValue && setting.ColorTemperature.Value > 0)
if (setting.ColorTemperatureVcp.HasValue && setting.ColorTemperatureVcp.Value > 0)
{
updateTasks.Add(monitorVm.SetColorTemperatureAsync(setting.ColorTemperature.Value, fromProfile: true));
updateTasks.Add(monitorVm.SetColorTemperatureAsync(setting.ColorTemperatureVcp.Value, fromProfile: true));
}
}
@@ -358,10 +358,10 @@ public partial class MainViewModel
// Color temperature: VCP 0x14 preset value (discrete values, no range check needed)
// Note: ColorTemperature is now read-only in flyout UI, controlled via Settings UI
if (savedState.Value.ColorTemperature > 0)
if (savedState.Value.ColorTemperatureVcp > 0)
{
// Validation will happen in Settings UI when applying preset values
monitorVm.UpdatePropertySilently(nameof(monitorVm.ColorTemperature), savedState.Value.ColorTemperature);
monitorVm.UpdatePropertySilently(nameof(monitorVm.ColorTemperature), savedState.Value.ColorTemperatureVcp);
}
// Contrast validation - only apply if hardware supports it
@@ -540,7 +540,7 @@ public partial class MainViewModel
hardwareId: vm.HardwareId,
communicationMethod: vm.CommunicationMethod,
currentBrightness: vm.Brightness,
colorTemperature: vm.ColorTemperature)
colorTemperatureVcp: vm.ColorTemperature)
{
CapabilitiesRaw = vm.CapabilitiesRaw,
VcpCodes = BuildVcpCodesList(vm),

View File

@@ -7,19 +7,22 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MonitorInfo : Observable
public class MonitorInfo : Observable, IMonitorData
{
private string _name = string.Empty;
private string _internalName = string.Empty;
private string _hardwareId = string.Empty;
private string _communicationMethod = string.Empty;
private int _currentBrightness;
private int _colorTemperature = 6500;
private int _colorTemperatureVcp = 0x05; // Default to 6500K preset (VCP 0x14 value)
private int _contrast;
private int _volume;
private bool _isHidden;
private bool _enableContrast;
private bool _enableVolume;
@@ -48,14 +51,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
CommunicationMethod = communicationMethod;
}
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, int currentBrightness, int colorTemperature)
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, int currentBrightness, int colorTemperatureVcp)
{
Name = name;
InternalName = internalName;
HardwareId = hardwareId;
CommunicationMethod = communicationMethod;
CurrentBrightness = currentBrightness;
ColorTemperature = colorTemperature;
ColorTemperatureVcp = colorTemperatureVcp;
}
[JsonPropertyName("name")]
@@ -128,21 +131,68 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
[JsonPropertyName("colorTemperature")]
public int ColorTemperature
/// <summary>
/// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
/// Use MonitorValueConverter to convert to human-readable Kelvin values for display.
/// </summary>
[JsonPropertyName("colorTemperatureVcp")]
public int ColorTemperatureVcp
{
get => _colorTemperature;
get => _colorTemperatureVcp;
set
{
if (_colorTemperature != value)
if (_colorTemperatureVcp != value)
{
_colorTemperature = value;
_colorTemperatureVcp = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorTemperatureDisplay));
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
}
}
}
/// <summary>
/// Gets the color temperature as a human-readable display string.
/// Converts the VCP value to a Kelvin temperature display.
/// </summary>
[JsonIgnore]
public string ColorTemperatureDisplay => MonitorValueConverter.FormatColorTemperatureDisplay(ColorTemperatureVcp);
/// <summary>
/// Gets or sets the current contrast value (0-100).
/// </summary>
[JsonPropertyName("contrast")]
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
_contrast = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the current volume value (0-100).
/// </summary>
[JsonPropertyName("volume")]
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
_volume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("isHidden")]
public bool IsHidden
{
@@ -407,7 +457,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
// Check if current value is in the preset list
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperature);
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperatureVcp);
if (currentValueInList)
{
@@ -419,8 +469,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
var displayList = new List<ColorPresetItem>();
// Add current value with "Custom" indicator using shared helper
var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperature);
displayList.Add(new ColorPresetItem(_colorTemperature, displayName));
var displayName = ColorTemperatureHelper.FormatCustomColorTemperatureDisplayName(_colorTemperatureVcp);
displayList.Add(new ColorPresetItem(_colorTemperatureVcp, displayName));
// Add all supported presets
displayList.AddRange(presets);
@@ -502,7 +552,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
HardwareId = other.HardwareId;
CommunicationMethod = other.CommunicationMethod;
CurrentBrightness = other.CurrentBrightness;
ColorTemperature = other.ColorTemperature;
Contrast = other.Contrast;
Volume = other.Volume;
ColorTemperatureVcp = other.ColorTemperatureVcp;
IsHidden = other.IsHidden;
EnableContrast = other.EnableContrast;
EnableVolume = other.EnableVolume;
@@ -516,6 +568,41 @@ namespace Microsoft.PowerToys.Settings.UI.Library
CapabilitiesStatus = other.CapabilitiesStatus;
}
/// <inheritdoc />
string IMonitorData.Id
{
get => InternalName;
set => InternalName = value;
}
/// <inheritdoc />
int IMonitorData.Brightness
{
get => CurrentBrightness;
set => CurrentBrightness = value;
}
/// <inheritdoc />
int IMonitorData.Contrast
{
get => Contrast;
set => Contrast = value;
}
/// <inheritdoc />
int IMonitorData.Volume
{
get => Volume;
set => Volume = value;
}
/// <inheritdoc />
int IMonitorData.ColorTemperatureVcp
{
get => ColorTemperatureVcp;
set => ColorTemperatureVcp = value;
}
/// <summary>
/// Type alias for ColorPresetItem to maintain backward compatibility with XAML bindings.
/// Inherits from PowerDisplay.Common.Models.ColorPresetItem.

View File

@@ -201,7 +201,7 @@
x:Name="ColorTemperatureComboBox"
MinWidth="200"
ItemsSource="{Binding ColorPresetsForDisplay, Mode=OneWay}"
SelectedValue="{Binding ColorTemperature, Mode=TwoWay}"
SelectedValue="{Binding ColorTemperatureVcp, Mode=TwoWay}"
SelectedValuePath="VcpValue"
DisplayMemberPath="DisplayName"
PlaceholderText="Not available"

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.WinUI.Controls;
using Microsoft.PowerToys.Settings.UI.Helpers;
@@ -12,6 +13,7 @@ using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using Windows.ApplicationModel.DataTransfer;
namespace Microsoft.PowerToys.Settings.UI.Views
@@ -70,7 +72,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
// Store the initial value
if (!_previousColorTemperatureValues.ContainsKey(monitor.HardwareId))
{
_previousColorTemperatureValues[monitor.HardwareId] = monitor.ColorTemperature;
_previousColorTemperatureValues[monitor.HardwareId] = monitor.ColorTemperatureVcp;
}
return;
@@ -87,7 +89,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
int previousValue;
if (!_previousColorTemperatureValues.TryGetValue(monitor.HardwareId, out previousValue))
{
previousValue = monitor.ColorTemperature;
previousValue = monitor.ColorTemperatureVcp;
}
// Show confirmation dialog
@@ -137,7 +139,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
// User confirmed, apply the change
// Setting the property will trigger save to settings file via OnPropertyChanged
monitor.ColorTemperature = newValue.Value;
monitor.ColorTemperatureVcp = newValue.Value;
_previousColorTemperatureValues[monitor.HardwareId] = newValue.Value;
// Send IPC message to PowerDisplay with monitor ID and new color temperature value
@@ -235,22 +237,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private string GenerateDefaultProfileName()
{
var existingNames = new HashSet<string>();
foreach (var profile in ViewModel.Profiles)
{
existingNames.Add(profile.Name);
}
int counter = 1;
string name;
do
{
name = $"Profile {counter}";
counter++;
}
while (existingNames.Contains(name));
return name;
// Use shared ProfileHelper for consistent profile name generation
var existingNames = ViewModel.Profiles.Select(p => p.Name);
return ProfileHelper.GenerateUniqueProfileName(existingNames);
}
}
}

View File

@@ -71,10 +71,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
// Set color temperature if included in profile
if (monitorSetting.ColorTemperature.HasValue)
if (monitorSetting.ColorTemperatureVcp.HasValue)
{
monitorItem.IncludeColorTemperature = true;
monitorItem.ColorTemperature = monitorSetting.ColorTemperature.Value;
monitorItem.ColorTemperature = monitorSetting.ColorTemperatureVcp.Value;
}
// Set contrast if included in profile

View File

@@ -25,6 +25,7 @@ using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.UI.Services;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerToys.Interop;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
@@ -216,17 +217,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
/// <summary>
/// Generate a unique key for monitor matching based on hardware ID and internal name
/// Generate a unique key for monitor matching based on hardware ID and internal name.
/// Uses shared MonitorMatchingHelper from PowerDisplay.Lib for consistency.
/// </summary>
private string GetMonitorKey(MonitorInfo monitor)
private static string GetMonitorKey(MonitorInfo monitor)
{
// Use hardware ID if available, otherwise fall back to internal name
if (!string.IsNullOrEmpty(monitor.HardwareId))
{
return monitor.HardwareId;
}
return monitor.InternalName ?? monitor.Name ?? string.Empty;
// Use shared helper for consistent monitor matching logic
return MonitorMatchingHelper.GetMonitorKey(monitor);
}
/// <summary>
@@ -344,7 +341,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_settings.Properties.PendingColorTemperatureOperation = new ColorTemperatureOperation
{
MonitorId = monitorInternalName,
ColorTemperature = colorTemperature,
ColorTemperatureVcp = colorTemperature,
};
// Save settings to persist the operation

View File

@@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Brightness = monitor.CurrentBrightness,
Contrast = 50, // Default value (MonitorInfo doesn't store contrast)
Volume = 50, // Default value (MonitorInfo doesn't store volume)
ColorTemperature = monitor.ColorTemperature,
ColorTemperature = monitor.ColorTemperatureVcp,
};
item.SuppressAutoSelection = false;