diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Models/CustomVcpValueMapping.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Models/CustomVcpValueMapping.cs
new file mode 100644
index 0000000000..65131af103
--- /dev/null
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Models/CustomVcpValueMapping.cs
@@ -0,0 +1,88 @@
+// 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.Text.Json.Serialization;
+using PowerDisplay.Common.Utils;
+
+namespace PowerDisplay.Common.Models
+{
+ ///
+ /// Represents a custom name mapping for a VCP code value.
+ /// Used to override the default VCP value names with user-defined names.
+ /// This class is shared between PowerDisplay app and Settings UI.
+ ///
+ public class CustomVcpValueMapping
+ {
+ ///
+ /// Gets or sets the VCP code (e.g., 0x14 for color temperature, 0x60 for input source).
+ ///
+ [JsonPropertyName("vcpCode")]
+ public byte VcpCode { get; set; }
+
+ ///
+ /// Gets or sets the VCP value to map (e.g., 0x11 for HDMI-1).
+ ///
+ [JsonPropertyName("value")]
+ public int Value { get; set; }
+
+ ///
+ /// Gets or sets the custom name to display instead of the default name.
+ ///
+ [JsonPropertyName("customName")]
+ public string CustomName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a value indicating whether this mapping applies to all monitors.
+ /// When true, the mapping is applied globally. When false, only applies to TargetMonitorId.
+ ///
+ [JsonPropertyName("applyToAll")]
+ public bool ApplyToAll { get; set; } = true;
+
+ ///
+ /// Gets or sets the target monitor ID when ApplyToAll is false.
+ /// This is the monitor's unique identifier.
+ ///
+ [JsonPropertyName("targetMonitorId")]
+ public string TargetMonitorId { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the target monitor display name (for UI display only, not serialized).
+ ///
+ [JsonIgnore]
+ public string TargetMonitorName { get; set; } = string.Empty;
+
+ ///
+ /// Gets the display name for the VCP code (for UI display).
+ /// Uses VcpNames.GetCodeName() to get the standard MCCS VCP code name.
+ /// Note: For localized display in Settings UI, use VcpCodeToDisplayNameConverter instead.
+ ///
+ [JsonIgnore]
+ public string VcpCodeDisplayName => VcpNames.GetCodeName(VcpCode);
+
+ ///
+ /// Gets the display name for the VCP value (using built-in mapping).
+ ///
+ [JsonIgnore]
+ public string ValueDisplayName => VcpNames.GetFormattedValueName(VcpCode, Value);
+
+ ///
+ /// Gets a summary string for display in the UI list.
+ /// Format: "OriginalValue → CustomName" or "OriginalValue → CustomName (MonitorName)"
+ ///
+ [JsonIgnore]
+ public string DisplaySummary
+ {
+ get
+ {
+ var baseSummary = $"{VcpNames.GetValueName(VcpCode, Value) ?? $"0x{Value:X2}"} → {CustomName}";
+ if (!ApplyToAll && !string.IsNullOrEmpty(TargetMonitorName))
+ {
+ return $"{baseSummary} ({TargetMonitorName})";
+ }
+
+ return baseSummary;
+ }
+ }
+ }
+}
diff --git a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs
index 2d4fed19c6..1ecd5f150e 100644
--- a/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs
+++ b/src/modules/powerdisplay/PowerDisplay.Lib/Utils/VcpNames.cs
@@ -2,16 +2,27 @@
// 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.Linq;
+using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Utils
{
///
/// Provides human-readable names for VCP codes and their values based on MCCS v2.2a specification.
/// Combines VCP code names (e.g., 0x10 = "Brightness") and VCP value names (e.g., 0x14:0x05 = "6500K").
+ /// Supports localization through the LocalizedCodeNameProvider delegate.
///
public static class VcpNames
{
+ ///
+ /// Optional delegate to provide localized VCP code names.
+ /// Set this at application startup to enable localization.
+ /// The delegate receives a VCP code and should return the localized name, or null to use the default.
+ ///
+ public static Func? LocalizedCodeNameProvider { get; set; }
+
///
/// VCP code to name mapping
///
@@ -237,12 +248,21 @@ namespace PowerDisplay.Common.Utils
};
///
- /// Get the friendly name for a VCP code
+ /// Get the friendly name for a VCP code.
+ /// Uses LocalizedCodeNameProvider if set; falls back to built-in MCCS names if not.
///
/// VCP code (e.g., 0x10)
/// Friendly name, or hex representation if unknown
public static string GetCodeName(byte code)
{
+ // Try localized name first
+ var localizedName = LocalizedCodeNameProvider?.Invoke(code);
+ if (!string.IsNullOrEmpty(localizedName))
+ {
+ return localizedName;
+ }
+
+ // Fallback to built-in MCCS names
return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
}
@@ -389,6 +409,16 @@ namespace PowerDisplay.Common.Utils
},
};
+ ///
+ /// Get all known values for a VCP code
+ ///
+ /// VCP code (e.g., 0x14)
+ /// Dictionary of value to name mappings, or null if no mappings exist
+ public static IReadOnlyDictionary? GetValueMappings(byte vcpCode)
+ {
+ return ValueNames.TryGetValue(vcpCode, out var values) ? values : null;
+ }
+
///
/// Get human-readable name for a VCP value
///
@@ -424,5 +454,59 @@ namespace PowerDisplay.Common.Utils
return $"0x{value:X2}";
}
+
+ ///
+ /// Get human-readable name for a VCP value with custom mapping support.
+ /// Custom mappings take priority over built-in mappings.
+ /// Monitor ID is required to properly filter monitor-specific mappings.
+ ///
+ /// VCP code (e.g., 0x14)
+ /// Value to translate
+ /// Optional custom mappings that take priority
+ /// Monitor ID to filter mappings
+ /// Name string like "sRGB" or null if unknown
+ public static string? GetValueName(byte vcpCode, int value, IEnumerable? customMappings, string monitorId)
+ {
+ // 1. Priority: Check custom mappings first
+ if (customMappings != null)
+ {
+ // Find a matching custom mapping:
+ // - ApplyToAll = true (global), OR
+ // - ApplyToAll = false AND TargetMonitorId matches the given monitorId
+ var custom = customMappings.FirstOrDefault(m =>
+ m.VcpCode == vcpCode &&
+ m.Value == value &&
+ (m.ApplyToAll || (!m.ApplyToAll && m.TargetMonitorId == monitorId)));
+
+ if (custom != null && !string.IsNullOrEmpty(custom.CustomName))
+ {
+ return custom.CustomName;
+ }
+ }
+
+ // 2. Fallback to built-in mappings
+ return GetValueName(vcpCode, value);
+ }
+
+ ///
+ /// Get formatted display name for a VCP value with custom mapping support.
+ /// Custom mappings take priority over built-in mappings.
+ /// Monitor ID is required to properly filter monitor-specific mappings.
+ ///
+ /// VCP code (e.g., 0x14)
+ /// Value to translate
+ /// Optional custom mappings that take priority
+ /// Monitor ID to filter mappings
+ /// Formatted string like "sRGB (0x01)" or "0x01" if unknown
+ public static string GetFormattedValueName(byte vcpCode, int value, IEnumerable? customMappings, string monitorId)
+ {
+ var name = GetValueName(vcpCode, value, customMappings, monitorId);
+ if (name != null)
+ {
+ return $"{name} (0x{value:X2})";
+ }
+
+ return $"0x{value:X2}";
+ }
}
}
diff --git a/src/modules/powerdisplay/PowerDisplay/Program.cs b/src/modules/powerdisplay/PowerDisplay/Program.cs
index f893741e77..2553637b6f 100644
--- a/src/modules/powerdisplay/PowerDisplay/Program.cs
+++ b/src/modules/powerdisplay/PowerDisplay/Program.cs
@@ -125,14 +125,27 @@ namespace PowerDisplay
///
/// Called when an existing instance is activated by another process.
- /// This happens when EnsureProcessRunning() launches a new process while one is already running.
- /// We intentionally don't show the window here - window visibility should only be controlled via:
- /// - Toggle event (hotkey, tray icon click, Settings UI Launch button)
- /// - Standalone mode startup (handled in OnLaunched)
+ /// This happens when Quick Access or other launchers start the process while one is already running.
+ /// We toggle the window to show it - this allows Quick Access launch to work properly.
///
private static void OnActivated(object? sender, AppActivationArguments args)
{
- Logger.LogInfo("OnActivated: Redirect activation received - window visibility unchanged");
+ Logger.LogInfo("OnActivated: Redirect activation received - toggling window");
+
+ // Toggle the main window on redirect activation
+ if (_app?.MainWindow is MainWindow mainWindow)
+ {
+ // Dispatch to UI thread since OnActivated may be called from a different thread
+ mainWindow.DispatcherQueue.TryEnqueue(() =>
+ {
+ Logger.LogTrace("OnActivated: Toggling window from redirect activation");
+ mainWindow.ToggleWindow();
+ });
+ }
+ else
+ {
+ Logger.LogWarning("OnActivated: MainWindow not available for toggle");
+ }
}
}
}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs
index dc9e7ccbbd..d01af35c6c 100644
--- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.Settings.cs
@@ -43,6 +43,10 @@ public partial class MainViewModel
// UpdateMonitorList already handles filtering hidden monitors
UpdateMonitorList(_monitorManager.Monitors, isInitialLoad: false);
+ // Reload UI display settings first (includes custom VCP mappings)
+ // Must be loaded before ApplyUIConfiguration so names are available for UI refresh
+ LoadUIDisplaySettings();
+
// Apply UI configuration changes only (feature visibility toggles, etc.)
// Hardware parameters (brightness, color temperature) are applied via custom actions
var settings = _settingsUtils.GetSettingsOrDefault("PowerDisplay");
@@ -51,8 +55,11 @@ public partial class MainViewModel
// Reload profiles in case they were added/updated/deleted in Settings UI
LoadProfiles();
- // Reload UI display settings (profile switcher, identify button, color temp switcher)
- LoadUIDisplaySettings();
+ // Notify MonitorViewModels to refresh their custom VCP name displays
+ foreach (var monitor in Monitors)
+ {
+ monitor.RefreshCustomVcpNames();
+ }
}
catch (Exception ex)
{
@@ -304,7 +311,8 @@ public partial class MainViewModel
}
///
- /// Apply feature visibility settings to a monitor ViewModel
+ /// Apply feature visibility settings to a monitor ViewModel.
+ /// Only shows features that are both enabled by user AND supported by hardware.
///
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
{
@@ -313,12 +321,13 @@ public partial class MainViewModel
if (monitorSettings != null)
{
- monitorVm.ShowContrast = monitorSettings.EnableContrast;
- monitorVm.ShowVolume = monitorSettings.EnableVolume;
- monitorVm.ShowInputSource = monitorSettings.EnableInputSource;
+ // Only show features that are both enabled by user AND supported by hardware
+ monitorVm.ShowContrast = monitorSettings.EnableContrast && monitorVm.SupportsContrast;
+ monitorVm.ShowVolume = monitorSettings.EnableVolume && monitorVm.SupportsVolume;
+ monitorVm.ShowInputSource = monitorSettings.EnableInputSource && monitorVm.SupportsInputSource;
monitorVm.ShowRotation = monitorSettings.EnableRotation;
- monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
- monitorVm.ShowPowerState = monitorSettings.EnablePowerState;
+ monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature && monitorVm.SupportsColorTemperature;
+ monitorVm.ShowPowerState = monitorSettings.EnablePowerState && monitorVm.SupportsPowerState;
}
}
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
index d3a831a07d..e16b34cadb 100644
--- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
@@ -163,6 +163,23 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
}
+ // Custom VCP mappings - loaded from settings
+ private List _customVcpMappings = new();
+
+ ///
+ /// Gets or sets the custom VCP value name mappings.
+ /// These mappings override the default VCP value names for color temperature and input source.
+ ///
+ public List CustomVcpMappings
+ {
+ get => _customVcpMappings;
+ set
+ {
+ _customVcpMappings = value ?? new List();
+ OnPropertyChanged();
+ }
+ }
+
public bool IsScanning
{
get => _isScanning;
@@ -389,6 +406,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
var settings = _settingsUtils.GetSettingsOrDefault(PowerDisplaySettings.ModuleName);
ShowProfileSwitcher = settings.Properties.ShowProfileSwitcher;
ShowIdentifyMonitorsButton = settings.Properties.ShowIdentifyMonitorsButton;
+
+ // Load custom VCP mappings (now using shared type from PowerDisplay.Common.Models)
+ CustomVcpMappings = settings.Properties.CustomVcpMappings?.ToList() ?? new List();
+ Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings");
}
catch (Exception ex)
{
diff --git a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs
index 5629ebdacf..b50fcc03e4 100644
--- a/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs
+++ b/src/modules/powerdisplay/PowerDisplay/ViewModels/MonitorViewModel.cs
@@ -279,6 +279,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
// Advanced control display logic
public bool HasAdvancedControls => ShowContrast || ShowVolume;
+ ///
+ /// Gets a value indicating whether this monitor supports contrast control via VCP 0x12
+ ///
+ public bool SupportsContrast => _monitor.SupportsContrast;
+
+ ///
+ /// Gets a value indicating whether this monitor supports volume control via VCP 0x62
+ ///
+ public bool SupportsVolume => _monitor.SupportsVolume;
+
public bool ShowContrast
{
get => _showContrast;
@@ -456,8 +466,10 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
///
/// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
+ /// Uses custom mappings if available; falls back to built-in names if not.
///
- public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
+ public string ColorTemperaturePresetName =>
+ Common.Utils.VcpNames.GetFormattedValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings, _monitor.Id);
///
/// Gets a value indicating whether this monitor supports color temperature via VCP 0x14
@@ -537,7 +549,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
{
VcpValue = value,
- DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value),
+ DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value, _mainViewModel?.CustomVcpMappings, _monitor.Id),
IsSelected = value == _monitor.CurrentColorTemperature,
MonitorId = _monitor.Id,
}).ToList();
@@ -557,8 +569,11 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
///
/// Gets human-readable current input source name (e.g., "HDMI-1", "DisplayPort-1")
+ /// Uses custom mappings if available; falls back to built-in names if not.
///
- public string CurrentInputSourceName => _monitor.InputSourceName;
+ public string CurrentInputSourceName =>
+ Common.Utils.VcpNames.GetValueName(0x60, _monitor.CurrentInputSource, _mainViewModel?.CustomVcpMappings, _monitor.Id)
+ ?? $"Source 0x{_monitor.CurrentInputSource:X2}";
private List? _availableInputSources;
@@ -593,7 +608,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableInputSources = supportedSources.Select(value => new InputSourceItem
{
Value = value,
- Name = Common.Utils.VcpNames.GetValueName(0x60, value) ?? $"Source 0x{value:X2}",
+ Name = Common.Utils.VcpNames.GetValueName(0x60, value, _mainViewModel?.CustomVcpMappings, _monitor.Id) ?? $"Source 0x{value:X2}",
SelectionVisibility = value == _monitor.CurrentInputSource ? Visibility.Visible : Visibility.Collapsed,
MonitorId = _monitor.Id,
}).ToList();
@@ -601,6 +616,23 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(AvailableInputSources));
}
+ ///
+ /// Refresh custom VCP name displays after settings change.
+ /// Called when CustomVcpMappings is updated from Settings UI.
+ ///
+ public void RefreshCustomVcpNames()
+ {
+ // Refresh color temperature names
+ OnPropertyChanged(nameof(ColorTemperaturePresetName));
+ _availableColorPresets = null; // Force rebuild with new custom names
+ OnPropertyChanged(nameof(AvailableColorPresets));
+
+ // Refresh input source names
+ OnPropertyChanged(nameof(CurrentInputSourceName));
+ _availableInputSources = null; // Force rebuild with new custom names
+ OnPropertyChanged(nameof(AvailableInputSources));
+ }
+
///
/// Set input source for this monitor
///
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp
index 6f35629d3b..cf9ab171c2 100644
--- a/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayProcessManager.cpp
@@ -52,8 +52,11 @@ void PowerDisplayProcessManager::send_message(const std::wstring& message_type,
{
submit_task([this, message_type, message_arg] {
// Ensure process is running before sending message
- if (!is_process_running() && m_enabled)
+ // If process is not running, enable and start it - this allows Quick Access launch
+ // to work even when the module was not previously enabled
+ if (!is_process_running())
{
+ m_enabled = true;
refresh();
}
send_named_pipe_message(message_type, message_arg);
diff --git a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
index 871a8797ef..7360a34772 100644
--- a/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
+++ b/src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
@@ -9,6 +9,8 @@
#include
#include
#include
+#include
+#include
#include "resource.h"
@@ -48,6 +50,11 @@ private:
HANDLE m_hRefreshEvent = nullptr;
HANDLE m_hSendSettingsTelemetryEvent = nullptr;
+ // Toggle event handle and listener thread for Quick Access support
+ HANDLE m_hToggleEvent = nullptr;
+ HANDLE m_hStopEvent = nullptr; // Manual-reset event to signal thread termination
+ std::thread m_toggleEventThread;
+
public:
PowerDisplayModule()
{
@@ -62,16 +69,29 @@ public:
m_hSendSettingsTelemetryEvent = CreateDefaultEvent(CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT);
Logger::trace(L"Created SEND_SETTINGS_TELEMETRY_EVENT: handle={}", reinterpret_cast(m_hSendSettingsTelemetryEvent));
- if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent)
+ // Create Toggle event for Quick Access support
+ // This allows Quick Access to launch PowerDisplay even when module is not enabled
+ m_hToggleEvent = CreateDefaultEvent(CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT);
+ Logger::trace(L"Created TOGGLE_EVENT: handle={}", reinterpret_cast(m_hToggleEvent));
+
+ // Create manual-reset stop event for clean thread termination
+ m_hStopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ Logger::trace(L"Created STOP_EVENT: handle={}", reinterpret_cast(m_hStopEvent));
+
+ if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent || !m_hToggleEvent || !m_hStopEvent)
{
- Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}",
+ Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}, Toggle={}",
reinterpret_cast(m_hRefreshEvent),
- reinterpret_cast(m_hSendSettingsTelemetryEvent));
+ reinterpret_cast(m_hSendSettingsTelemetryEvent),
+ reinterpret_cast(m_hToggleEvent));
}
else
{
Logger::info(L"All Windows Events created successfully");
}
+
+ // Start toggle event listener thread for Quick Access support
+ StartToggleEventListener();
}
~PowerDisplayModule()
@@ -81,6 +101,9 @@ public:
disable();
}
+ // Stop toggle event listener thread
+ StopToggleEventListener();
+
// Clean up event handles
if (m_hRefreshEvent)
{
@@ -92,6 +115,99 @@ public:
CloseHandle(m_hSendSettingsTelemetryEvent);
m_hSendSettingsTelemetryEvent = nullptr;
}
+ if (m_hToggleEvent)
+ {
+ CloseHandle(m_hToggleEvent);
+ m_hToggleEvent = nullptr;
+ }
+ if (m_hStopEvent)
+ {
+ CloseHandle(m_hStopEvent);
+ m_hStopEvent = nullptr;
+ }
+ }
+
+ void StartToggleEventListener()
+ {
+ if (!m_hToggleEvent || !m_hStopEvent)
+ {
+ return;
+ }
+
+ // Reset stop event before starting thread
+ ResetEvent(m_hStopEvent);
+
+ m_toggleEventThread = std::thread([this]() {
+ Logger::info(L"Toggle event listener thread started");
+
+ HANDLE handles[] = { m_hToggleEvent, m_hStopEvent };
+ constexpr DWORD TOGGLE_EVENT_INDEX = 0;
+ constexpr DWORD STOP_EVENT_INDEX = 1;
+
+ while (true)
+ {
+ // Wait indefinitely for either toggle event or stop event
+ DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
+
+ if (result == WAIT_OBJECT_0 + TOGGLE_EVENT_INDEX)
+ {
+ Logger::trace(L"Toggle event received");
+ TogglePowerDisplay();
+ }
+ else if (result == WAIT_OBJECT_0 + STOP_EVENT_INDEX)
+ {
+ // Stop event signaled - exit the loop
+ Logger::trace(L"Stop event received, exiting toggle listener");
+ break;
+ }
+ else
+ {
+ // WAIT_FAILED or unexpected result
+ Logger::warn(L"WaitForMultipleObjects returned unexpected result: {}", result);
+ break;
+ }
+ }
+
+ Logger::info(L"Toggle event listener thread stopped");
+ });
+ }
+
+ void StopToggleEventListener()
+ {
+ if (m_hStopEvent)
+ {
+ // Signal the stop event to wake up the waiting thread
+ SetEvent(m_hStopEvent);
+ }
+
+ if (m_toggleEventThread.joinable())
+ {
+ m_toggleEventThread.join();
+ }
+ }
+
+ ///
+ /// Toggle PowerDisplay window visibility.
+ /// If process is running, launches again to trigger redirect activation (OnActivated handles toggle).
+ /// If process is not running, starts it via Named Pipe and sends toggle message.
+ ///
+ void TogglePowerDisplay()
+ {
+ if (m_processManager.is_running())
+ {
+ // Process running - launch to trigger single instance redirect, OnActivated will toggle
+ SHELLEXECUTEINFOW sei{ sizeof(sei) };
+ sei.fMask = SEE_MASK_FLAG_NO_UI;
+ sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
+ sei.nShow = SW_SHOWNORMAL;
+ ShellExecuteExW(&sei);
+ }
+ else
+ {
+ // Process not running - start and send toggle via Named Pipe
+ m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE);
+ }
+ Trace::ActivatePowerDisplay();
}
virtual void destroy() override
@@ -135,10 +251,7 @@ public:
if (action_object.get_name() == L"Launch")
{
Logger::trace(L"Launch action received");
-
- // Send Toggle message via Named Pipe (will start process if needed)
- m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE);
- Trace::ActivatePowerDisplay();
+ TogglePowerDisplay();
}
else if (action_object.get_name() == L"RefreshMonitors")
{
diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs
index b81c9638f3..1347ce86c1 100644
--- a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs
+++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs
@@ -119,6 +119,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
eventHandle.Set();
}
+ return true;
+ case ModuleType.PowerDisplay:
+ using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.TogglePowerDisplayEvent()))
+ {
+ eventHandle.Set();
+ }
+
return true;
default:
return false;
diff --git a/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs b/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs
index 25a4354474..5539daf0fd 100644
--- a/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs
@@ -23,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
ShowSystemTrayIcon = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
+ CustomVcpMappings = new List();
// Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app
@@ -61,5 +62,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
///
[JsonPropertyName("show_identify_monitors_button")]
public bool ShowIdentifyMonitorsButton { get; set; }
+
+ ///
+ /// Gets or sets custom VCP value name mappings shared across all monitors.
+ /// Allows users to define custom names for color temperature presets and input sources.
+ ///
+ [JsonPropertyName("custom_vcp_mappings")]
+ public List CustomVcpMappings { get; set; }
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml
new file mode 100644
index 0000000000..9f0bc51079
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml.cs
new file mode 100644
index 0000000000..915c891ada
--- /dev/null
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml.cs
@@ -0,0 +1,421 @@
+// 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.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using PowerDisplay.Common.Models;
+using PowerDisplay.Common.Utils;
+
+namespace Microsoft.PowerToys.Settings.UI.Views
+{
+ ///
+ /// Dialog for creating/editing custom VCP value name mappings
+ ///
+ public sealed partial class CustomVcpMappingEditorDialog : ContentDialog, INotifyPropertyChanged
+ {
+ ///
+ /// Special value to indicate "Custom value" option in the ComboBox
+ ///
+ private const int CustomValueMarker = -1;
+
+ ///
+ /// Represents a selectable VCP value item in the Value ComboBox
+ ///
+ public class VcpValueItem
+ {
+ public int Value { get; set; }
+
+ public string DisplayName { get; set; } = string.Empty;
+
+ public bool IsCustomOption => Value == CustomValueMarker;
+ }
+
+ ///
+ /// Represents a selectable monitor item in the Monitor ComboBox
+ ///
+ public class MonitorItem
+ {
+ public string Id { get; set; } = string.Empty;
+
+ public string DisplayName { get; set; } = string.Empty;
+ }
+
+ private readonly IEnumerable? _monitors;
+ private ObservableCollection _availableValues = new();
+ private ObservableCollection _availableMonitors = new();
+ private byte _selectedVcpCode;
+ private int _selectedValue;
+ private string _customName = string.Empty;
+ private bool _canSave;
+ private bool _showCustomValueInput;
+ private bool _showMonitorSelector;
+ private int _customValueParsed;
+ private bool _applyToAll = true;
+ private string _selectedMonitorId = string.Empty;
+ private string _selectedMonitorName = string.Empty;
+
+ public CustomVcpMappingEditorDialog(IEnumerable? monitors)
+ {
+ _monitors = monitors;
+ this.InitializeComponent();
+
+ // Set localized strings for ContentDialog
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ Title = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_Title");
+ PrimaryButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Save");
+ CloseButtonText = resourceLoader.GetString("PowerDisplay_Dialog_Cancel");
+
+ // Set VCP code ComboBox items content dynamically using localized names
+ VcpCodeItem_0x14.Content = GetFormattedVcpCodeName(resourceLoader, 0x14);
+ VcpCodeItem_0x60.Content = GetFormattedVcpCodeName(resourceLoader, 0x60);
+
+ // Populate monitor list
+ PopulateMonitorList();
+
+ // Default to Color Temperature (0x14)
+ VcpCodeComboBox.SelectedIndex = 0;
+ }
+
+ ///
+ /// Gets the result mapping after dialog closes with Primary button
+ ///
+ public CustomVcpValueMapping? ResultMapping { get; private set; }
+
+ ///
+ /// Gets the available values for the selected VCP code
+ ///
+ public ObservableCollection AvailableValues
+ {
+ get => _availableValues;
+ private set
+ {
+ _availableValues = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets the available monitors for selection
+ ///
+ public ObservableCollection AvailableMonitors
+ {
+ get => _availableMonitors;
+ private set
+ {
+ _availableMonitors = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the dialog can be saved
+ ///
+ public bool CanSave
+ {
+ get => _canSave;
+ private set
+ {
+ if (_canSave != value)
+ {
+ _canSave = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether to show the custom value input TextBox
+ ///
+ public Visibility ShowCustomValueInput => _showCustomValueInput ? Visibility.Visible : Visibility.Collapsed;
+
+ ///
+ /// Gets a value indicating whether to show the monitor selector ComboBox
+ ///
+ public Visibility ShowMonitorSelector => _showMonitorSelector ? Visibility.Visible : Visibility.Collapsed;
+
+ private void SetShowCustomValueInput(bool value)
+ {
+ if (_showCustomValueInput != value)
+ {
+ _showCustomValueInput = value;
+ OnPropertyChanged(nameof(ShowCustomValueInput));
+ }
+ }
+
+ private void SetShowMonitorSelector(bool value)
+ {
+ if (_showMonitorSelector != value)
+ {
+ _showMonitorSelector = value;
+ OnPropertyChanged(nameof(ShowMonitorSelector));
+ }
+ }
+
+ private void PopulateMonitorList()
+ {
+ AvailableMonitors = new ObservableCollection(
+ _monitors?.Select(m => new MonitorItem { Id = m.Id, DisplayName = m.DisplayName })
+ ?? Enumerable.Empty());
+
+ if (AvailableMonitors.Count > 0)
+ {
+ MonitorComboBox.SelectedIndex = 0;
+ }
+ }
+
+ ///
+ /// Pre-fill the dialog with existing mapping data for editing
+ ///
+ public void PreFillMapping(CustomVcpValueMapping mapping)
+ {
+ if (mapping is null)
+ {
+ return;
+ }
+
+ // Select the VCP code
+ VcpCodeComboBox.SelectedIndex = mapping.VcpCode == 0x14 ? 0 : 1;
+
+ // Populate values for the selected VCP code
+ PopulateValuesForVcpCode(mapping.VcpCode);
+
+ // Try to select the value in the ComboBox
+ var matchingItem = AvailableValues.FirstOrDefault(v => !v.IsCustomOption && v.Value == mapping.Value);
+ if (matchingItem is not null)
+ {
+ ValueComboBox.SelectedItem = matchingItem;
+ }
+ else
+ {
+ // Value not found in list, select "Custom value" option and fill the TextBox
+ ValueComboBox.SelectedItem = AvailableValues.FirstOrDefault(v => v.IsCustomOption);
+ CustomValueTextBox.Text = $"0x{mapping.Value:X2}";
+ _customValueParsed = mapping.Value;
+ }
+
+ // Set the custom name
+ CustomNameTextBox.Text = mapping.CustomName;
+ _customName = mapping.CustomName;
+
+ // Set apply scope
+ _applyToAll = mapping.ApplyToAll;
+ ApplyToAllToggle.IsOn = mapping.ApplyToAll;
+ SetShowMonitorSelector(!mapping.ApplyToAll);
+
+ // Select the target monitor if not applying to all
+ if (!mapping.ApplyToAll && !string.IsNullOrEmpty(mapping.TargetMonitorId))
+ {
+ var targetMonitor = AvailableMonitors.FirstOrDefault(m => m.Id == mapping.TargetMonitorId);
+ if (targetMonitor is not null)
+ {
+ MonitorComboBox.SelectedItem = targetMonitor;
+ _selectedMonitorId = targetMonitor.Id;
+ _selectedMonitorName = targetMonitor.DisplayName;
+ }
+ }
+
+ UpdateCanSave();
+ }
+
+ private void VcpCodeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (VcpCodeComboBox.SelectedItem is ComboBoxItem selectedItem &&
+ selectedItem.Tag is string tagValue &&
+ byte.TryParse(tagValue, out byte vcpCode))
+ {
+ _selectedVcpCode = vcpCode;
+ PopulateValuesForVcpCode(vcpCode);
+ UpdateCanSave();
+ }
+ }
+
+ private void PopulateValuesForVcpCode(byte vcpCode)
+ {
+ var values = new ObservableCollection();
+ var seenValues = new HashSet();
+
+ // Collect values from all monitors
+ if (_monitors is not null)
+ {
+ foreach (var monitor in _monitors)
+ {
+ if (monitor.VcpCodesFormatted is null)
+ {
+ continue;
+ }
+
+ // Find the VCP code entry
+ var vcpEntry = monitor.VcpCodesFormatted.FirstOrDefault(v =>
+ !string.IsNullOrEmpty(v.Code) &&
+ TryParseHexCode(v.Code, out int code) &&
+ code == vcpCode);
+
+ if (vcpEntry?.ValueList is null)
+ {
+ continue;
+ }
+
+ // Add each value from this monitor
+ foreach (var valueInfo in vcpEntry.ValueList)
+ {
+ if (TryParseHexCode(valueInfo.Value, out int vcpValue) && !seenValues.Contains(vcpValue))
+ {
+ seenValues.Add(vcpValue);
+ var displayName = !string.IsNullOrEmpty(valueInfo.Name)
+ ? $"{valueInfo.Name} (0x{vcpValue:X2})"
+ : VcpNames.GetFormattedValueName(vcpCode, vcpValue);
+ values.Add(new VcpValueItem
+ {
+ Value = vcpValue,
+ DisplayName = displayName,
+ });
+ }
+ }
+ }
+ }
+
+ // If no values found from monitors, fall back to built-in values from VcpNames
+ if (values.Count == 0)
+ {
+ var builtInValues = VcpNames.GetValueMappings(vcpCode);
+ if (builtInValues is not null)
+ {
+ foreach (var kvp in builtInValues)
+ {
+ values.Add(new VcpValueItem
+ {
+ Value = kvp.Key,
+ DisplayName = $"{kvp.Value} (0x{kvp.Key:X2})",
+ });
+ }
+ }
+ }
+
+ // Sort by value
+ var sortedValues = new ObservableCollection(values.OrderBy(v => v.Value));
+
+ // Add "Custom value" option at the end
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ sortedValues.Add(new VcpValueItem
+ {
+ Value = CustomValueMarker,
+ DisplayName = resourceLoader.GetString("PowerDisplay_CustomMappingEditor_CustomValueOption"),
+ });
+
+ AvailableValues = sortedValues;
+
+ // Select first item if available
+ if (sortedValues.Count > 0)
+ {
+ ValueComboBox.SelectedIndex = 0;
+ }
+ }
+
+ private static bool TryParseHexCode(string? hex, out int result)
+ {
+ result = 0;
+ if (string.IsNullOrEmpty(hex))
+ {
+ return false;
+ }
+
+ var cleanHex = hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex;
+ return int.TryParse(cleanHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out result);
+ }
+
+ private static string GetFormattedVcpCodeName(Windows.ApplicationModel.Resources.ResourceLoader resourceLoader, byte vcpCode)
+ {
+ var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
+ var localizedName = resourceLoader.GetString(resourceKey);
+ var name = string.IsNullOrEmpty(localizedName) ? VcpNames.GetCodeName(vcpCode) : localizedName;
+ return $"{name} (0x{vcpCode:X2})";
+ }
+
+ private void ValueComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (ValueComboBox.SelectedItem is VcpValueItem selectedItem)
+ {
+ SetShowCustomValueInput(selectedItem.IsCustomOption);
+ _selectedValue = selectedItem.IsCustomOption ? 0 : selectedItem.Value;
+ UpdateCanSave();
+ }
+ }
+
+ private void CustomValueTextBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ _customValueParsed = TryParseHexCode(CustomValueTextBox.Text?.Trim(), out int parsed) ? parsed : 0;
+ UpdateCanSave();
+ }
+
+ private void CustomNameTextBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ _customName = CustomNameTextBox.Text?.Trim() ?? string.Empty;
+ UpdateCanSave();
+ }
+
+ private void ApplyToAllToggle_Toggled(object sender, RoutedEventArgs e)
+ {
+ _applyToAll = ApplyToAllToggle.IsOn;
+ SetShowMonitorSelector(!_applyToAll);
+ UpdateCanSave();
+ }
+
+ private void MonitorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (MonitorComboBox.SelectedItem is MonitorItem selectedMonitor)
+ {
+ _selectedMonitorId = selectedMonitor.Id;
+ _selectedMonitorName = selectedMonitor.DisplayName;
+ UpdateCanSave();
+ }
+ }
+
+ private void UpdateCanSave()
+ {
+ var hasValidValue = _showCustomValueInput
+ ? _customValueParsed > 0
+ : ValueComboBox.SelectedItem is VcpValueItem item && !item.IsCustomOption;
+
+ CanSave = _selectedVcpCode > 0 &&
+ hasValidValue &&
+ !string.IsNullOrWhiteSpace(_customName) &&
+ (_applyToAll || !string.IsNullOrEmpty(_selectedMonitorId));
+ }
+
+ private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ if (CanSave)
+ {
+ int finalValue = _showCustomValueInput ? _customValueParsed : _selectedValue;
+ ResultMapping = new CustomVcpValueMapping
+ {
+ VcpCode = _selectedVcpCode,
+ Value = finalValue,
+ CustomName = _customName,
+ ApplyToAll = _applyToAll,
+ TargetMonitorId = _applyToAll ? string.Empty : _selectedMonitorId,
+ TargetMonitorName = _applyToAll ? string.Empty : _selectedMonitorName,
+ };
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml
index 08d736a0fc..899295ec66 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml
@@ -63,11 +63,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs
index 641311daad..d82746faf6 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/PowerDisplayPage.xaml.cs
@@ -133,6 +133,65 @@ namespace Microsoft.PowerToys.Settings.UI.Views
return ProfileHelper.GenerateUniqueProfileName(existingNames, baseName);
}
+ // Custom VCP Mapping event handlers
+ private async void AddCustomMapping_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new CustomVcpMappingEditorDialog(ViewModel.Monitors);
+ dialog.XamlRoot = this.XamlRoot;
+
+ var result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary && dialog.ResultMapping != null)
+ {
+ ViewModel.AddCustomVcpMapping(dialog.ResultMapping);
+ }
+ }
+
+ private async void EditCustomMapping_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Button button || button.Tag is not CustomVcpValueMapping mapping)
+ {
+ return;
+ }
+
+ var dialog = new CustomVcpMappingEditorDialog(ViewModel.Monitors);
+ dialog.XamlRoot = this.XamlRoot;
+ dialog.PreFillMapping(mapping);
+
+ var result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary && dialog.ResultMapping != null)
+ {
+ ViewModel.UpdateCustomVcpMapping(mapping, dialog.ResultMapping);
+ }
+ }
+
+ private async void DeleteCustomMapping_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not Button button || button.Tag is not CustomVcpValueMapping mapping)
+ {
+ return;
+ }
+
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ var dialog = new ContentDialog
+ {
+ XamlRoot = this.XamlRoot,
+ Title = resourceLoader.GetString("PowerDisplay_CustomMapping_Delete_Title"),
+ Content = resourceLoader.GetString("PowerDisplay_CustomMapping_Delete_Message"),
+ PrimaryButtonText = resourceLoader.GetString("Yes"),
+ CloseButtonText = resourceLoader.GetString("No"),
+ DefaultButton = ContentDialogButton.Close,
+ };
+
+ var result = await dialog.ShowAsync();
+
+ if (result == ContentDialogResult.Primary)
+ {
+ ViewModel.DeleteCustomVcpMapping(mapping);
+ }
+ }
+
// Flag to prevent reentrant handling during programmatic checkbox changes
private bool _isRestoringColorTempCheckbox;
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index ff7e61cccd..a11cbd72bc 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -5995,6 +5995,69 @@ The break timer font matches the text font.
Show or hide the identify monitors button in the Power Display flyout
+
+ Custom VCP Name Mappings
+
+
+ Custom name mappings
+
+
+ Define custom display names for color temperature presets and input sources
+
+
+ Add custom mapping
+
+
+ Add mapping
+
+
+ Custom VCP Name Mapping
+
+
+ VCP Code
+
+
+ Color Temperature
+
+
+ Input Source
+
+
+ Custom Name
+
+
+ Enter custom name
+
+
+ Value
+
+
+ Custom value...
+
+
+ Enter custom value (hex)
+
+
+ e.g., 0x11 or 17
+
+
+ Apply to all monitors
+
+
+ On
+
+
+ Off
+
+
+ Select monitor
+
+
+ Delete custom mapping?
+
+
+ This custom name mapping will be permanently removed.
+
Backup
diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs
index ce19e37467..6c96214d5b 100644
--- a/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs
@@ -36,6 +36,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public PowerDisplayViewModel(SettingsUtils settingsUtils, ISettingsRepository settingsRepository, ISettingsRepository powerDisplaySettingsRepository, Func ipcMSGCallBackFunc)
{
+ // Set up localized VCP code names for UI display
+ VcpNames.LocalizedCodeNameProvider = GetLocalizedVcpCodeName;
+
// To obtain the general settings configurations of PowerToys Settings.
ArgumentNullException.ThrowIfNull(settingsRepository);
@@ -56,9 +59,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// set the callback functions value to handle outgoing IPC message.
SendConfigMSG = ipcMSGCallBackFunc;
+ // Subscribe to collection changes for HasProfiles binding
+ _profiles.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasProfiles));
+
// Load profiles
LoadProfiles();
+ // Load custom VCP mappings
+ LoadCustomVcpMappings();
+
// Listen for monitor refresh events from PowerDisplay.exe
NativeEventWaiter.WaitForEventLoop(
Constants.RefreshPowerDisplayMonitorsEvent(),
@@ -446,21 +455,28 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Profile-related fields
private ObservableCollection _profiles = new ObservableCollection();
+ // Custom VCP mapping fields
+ private ObservableCollection _customVcpMappings;
+
///
- /// Gets or sets collection of available profiles (for button display)
+ /// Gets collection of custom VCP value name mappings
///
- public ObservableCollection Profiles
- {
- get => _profiles;
- set
- {
- if (_profiles != value)
- {
- _profiles = value;
- OnPropertyChanged();
- }
- }
- }
+ public ObservableCollection CustomVcpMappings => _customVcpMappings;
+
+ ///
+ /// Gets whether there are any custom VCP mappings (for UI binding)
+ ///
+ public bool HasCustomVcpMappings => _customVcpMappings?.Count > 0;
+
+ ///
+ /// Gets collection of available profiles (for button display)
+ ///
+ public ObservableCollection Profiles => _profiles;
+
+ ///
+ /// Gets whether there are any profiles (for UI binding)
+ ///
+ public bool HasProfiles => _profiles?.Count > 0;
public void RefreshEnabledState()
{
@@ -646,6 +662,109 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ ///
+ /// Load custom VCP mappings from settings
+ ///
+ private void LoadCustomVcpMappings()
+ {
+ List mappings;
+ try
+ {
+ mappings = _settings.Properties.CustomVcpMappings ?? new List();
+ Logger.LogInfo($"Loaded {mappings.Count} custom VCP mappings");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Failed to load custom VCP mappings: {ex.Message}");
+ mappings = new List();
+ }
+
+ _customVcpMappings = new ObservableCollection(mappings);
+ _customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings));
+ OnPropertyChanged(nameof(CustomVcpMappings));
+ OnPropertyChanged(nameof(HasCustomVcpMappings));
+ }
+
+ ///
+ /// Add a new custom VCP mapping.
+ /// No duplicate checking - mappings are resolved by order (first match wins in VcpNames).
+ ///
+ public void AddCustomVcpMapping(CustomVcpValueMapping mapping)
+ {
+ if (mapping == null)
+ {
+ return;
+ }
+
+ CustomVcpMappings.Add(mapping);
+ Logger.LogInfo($"Added custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2} -> {mapping.CustomName}");
+ SaveCustomVcpMappings();
+ }
+
+ ///
+ /// Update an existing custom VCP mapping
+ ///
+ public void UpdateCustomVcpMapping(CustomVcpValueMapping oldMapping, CustomVcpValueMapping newMapping)
+ {
+ if (oldMapping == null || newMapping == null)
+ {
+ return;
+ }
+
+ var index = CustomVcpMappings.IndexOf(oldMapping);
+ if (index >= 0)
+ {
+ CustomVcpMappings[index] = newMapping;
+ Logger.LogInfo($"Updated custom VCP mapping at index {index}");
+ SaveCustomVcpMappings();
+ }
+ }
+
+ ///
+ /// Delete a custom VCP mapping
+ ///
+ public void DeleteCustomVcpMapping(CustomVcpValueMapping mapping)
+ {
+ if (mapping == null)
+ {
+ return;
+ }
+
+ if (CustomVcpMappings.Remove(mapping))
+ {
+ Logger.LogInfo($"Deleted custom VCP mapping: VCP=0x{mapping.VcpCode:X2}, Value=0x{mapping.Value:X2}");
+ SaveCustomVcpMappings();
+ }
+ }
+
+ ///
+ /// Save custom VCP mappings to settings
+ ///
+ private void SaveCustomVcpMappings()
+ {
+ _settings.Properties.CustomVcpMappings = CustomVcpMappings.ToList();
+ NotifySettingsChanged();
+
+ // Signal PowerDisplay to reload settings
+ SignalSettingsUpdated();
+ }
+
+ ///
+ /// Provides localized VCP code names for UI display.
+ /// Looks for resource string with pattern "PowerDisplay_VcpCode_Name_0xXX".
+ /// Returns null for unknown codes to use the default MCCS name.
+ ///
+#nullable enable
+ private static string? GetLocalizedVcpCodeName(byte vcpCode)
+ {
+ var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
+ var localizedName = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey);
+
+ // ResourceLoader returns empty string if key not found
+ return string.IsNullOrEmpty(localizedName) ? null : localizedName;
+ }
+#nullable restore
+
private void NotifySettingsChanged()
{
// Skip during initialization when SendConfigMSG is not yet set