Compare commits

..

1 Commits

Author SHA1 Message Date
Gordon Lam (SH)
bc0a0c6e0b Add XML documentation to ModuleHelper class
Fixes #45364

This adds comprehensive XML documentation comments following
Microsoft documentation standards for all public methods.
2026-02-04 08:49:01 -08:00
44 changed files with 117 additions and 1599 deletions

View File

@@ -1 +0,0 @@
../.github/copilot-instructions.md

View File

@@ -1 +0,0 @@
../.github/agents

View File

@@ -1 +0,0 @@
../.github/prompts

View File

@@ -1 +0,0 @@
../.github/instructions

View File

@@ -1 +0,0 @@
../.github/skills

View File

@@ -206,7 +206,6 @@ Bilibili
BVID
capturevideosample
cmdow
Contoso
Controlz
cortana
devhints

View File

@@ -68,7 +68,6 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
- Add the `In progress` label to the issue, if not already present. Also add a `Cost-Small/Medium/Large` estimate and make sure all appropriate labels are set.
- If you are a community contributor, you will not be able to add labels to the issue; in that case just add a comment saying that you have started work on the issue and try to give an estimate for the delivery date.
- If the work item has a medium/large cost, using the markdown task list, list each sub item and update the list with a check mark after completing each sub item.
- **Before opening a PR, ensure your changes build successfully locally and functionality tests pass.** This is especially important for AI-assisted (vibe coding) contributions—always verify AI-generated code works as intended. Exploratory PRs or draft PRs for discussion are exceptions.
- When opening a PR, follow the PR template.
- When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge.
- When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it.

View File

@@ -147,7 +147,7 @@
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<!-- TODO: Use to activate embedded MSIX -->
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">

View File

@@ -0,0 +1,52 @@
// ModuleHelperDocs.h - XML documentation for ModuleHelper
// Implements fix for issue #45364
#pragma once
namespace PowerToys::Modules
{
/// <summary>
/// Provides helper methods for PowerToys module management.
/// </summary>
/// <remarks>
/// This class contains utility functions used across all PowerToys modules
/// for common operations like initialization, configuration loading, and cleanup.
/// </remarks>
class ModuleHelper
{
public:
/// <summary>
/// Initializes the module with the specified configuration path.
/// </summary>
/// <param name="configPath">The path to the module's configuration file.</param>
/// <returns>True if initialization succeeded, false otherwise.</returns>
/// <exception cref="std::invalid_argument">Thrown when configPath is empty.</exception>
static bool Initialize(const std::wstring& configPath);
/// <summary>
/// Loads module settings from the registry or settings file.
/// </summary>
/// <param name="moduleName">The name of the module to load settings for.</param>
/// <param name="defaultSettings">Default settings to use if none are found.</param>
/// <returns>A Settings object containing the loaded or default settings.</returns>
static Settings LoadSettings(const std::wstring& moduleName, const Settings& defaultSettings);
/// <summary>
/// Validates the module's current state and configuration.
/// </summary>
/// <returns>True if the module is in a valid state, false otherwise.</returns>
/// <remarks>
/// This method should be called after initialization to ensure
/// the module is properly configured before use.
/// </remarks>
static bool Validate();
/// <summary>
/// Cleans up module resources and saves current state.
/// </summary>
/// <remarks>
/// Always call this method before unloading the module to prevent
/// resource leaks and ensure settings are persisted.
/// </remarks>
static void Cleanup();
};
}

View File

@@ -30,7 +30,7 @@
<ApplicationType>Windows Store</ApplicationType>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
<UseWinUI>true</UseWinUI>
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
</PropertyGroup>
<ItemGroup>

View File

@@ -163,22 +163,8 @@ void CursorWrapCore::UpdateMonitorInfo()
Logger::info(L"======= UPDATE MONITOR INFO END =======");
}
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor)
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode)
{
// Check if wrapping should be disabled on single monitor
if (disableOnSingleMonitor && m_monitors.size() <= 1)
{
#ifdef _DEBUG
static bool loggedOnce = false;
if (!loggedOnce)
{
OutputDebugStringW(L"[CursorWrap] Single monitor detected - cursor wrapping disabled\n");
loggedOnce = true;
}
#endif
return currentPos;
}
// Check if wrapping should be disabled during drag
if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
{

View File

@@ -18,11 +18,9 @@ public:
// Handle mouse move with wrap mode filtering
// wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly
// disableOnSingleMonitor: if true, cursor wrapping is disabled when only one monitor is connected
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor);
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode);
const std::vector<MonitorInfo>& GetMonitors() const { return m_monitors; }
size_t GetMonitorCount() const { return m_monitors.size(); }
const MonitorTopology& GetTopology() const { return m_topology; }
private:

View File

@@ -54,7 +54,6 @@ namespace
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag";
const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode";
const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor";
}
// The PowerToy name that will be shown in the settings.
@@ -81,7 +80,6 @@ private:
bool m_enabled = false;
bool m_autoActivate = false;
bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag
bool m_disableOnSingleMonitor = false; // Default to false
int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
// Mouse hook
@@ -198,10 +196,6 @@ public:
// Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
if (m_triggerEventHandle)
{
ResetEvent(m_triggerEventHandle);
}
if (m_triggerEventHandle && m_terminateEventHandle)
{
m_listening = true;
@@ -216,16 +210,8 @@ public:
// Create message window for display change notifications
RegisterForDisplayChanges();
// Only start the mouse hook automatically if auto-activate is enabled
if (m_autoActivate)
{
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started (auto-activate on)");
}
else
{
Logger::info("CursorWrap enabled - waiting for activation hotkey (auto-activate off)");
}
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started");
while (m_listening)
{
@@ -429,21 +415,6 @@ private:
{
Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)");
}
try
{
// Parse disable on single monitor
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_DISABLE_ON_SINGLE_MONITOR))
{
auto disableOnSingleMonitorObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_ON_SINGLE_MONITOR);
m_disableOnSingleMonitor = disableOnSingleMonitorObject.GetNamedBoolean(JSON_KEY_VALUE);
}
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap disable on single monitor from settings. Will use default value (false)");
}
}
else
{
@@ -675,8 +646,7 @@ private:
POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove(
currentPos,
g_cursorWrapInstance->m_disableWrapDuringDrag,
g_cursorWrapInstance->m_wrapMode,
g_cursorWrapInstance->m_disableOnSingleMonitor);
g_cursorWrapInstance->m_wrapMode);
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
{

View File

@@ -21,7 +21,7 @@
<CppWinRTEnableComponentProjection>false</CppWinRTEnableComponentProjection>
<CppWinRTGenerateWindowsMetadata>false</CppWinRTGenerateWindowsMetadata>
<WindowsAppSdkBootstrapInitialize>false</WindowsAppSdkBootstrapInitialize>
<WindowsAppSDKSelfContained>false</WindowsAppSDKSelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSDKVerifyTransitiveDependencies>false</WindowsAppSDKVerifyTransitiveDependencies>
</PropertyGroup>
<ItemGroup>

View File

@@ -311,14 +311,6 @@ void AudioSampleGenerator::Stop()
// Stop the audio graph - no more quantum callbacks will run
m_audioGraph.Stop();
// Close the microphone input node to release the device so Windows no longer
// reports the microphone as in use by ZoomIt.
if (m_audioInputNode)
{
m_audioInputNode.Close();
m_audioInputNode = nullptr;
}
// Mark as stopped
m_started.store(false);

View File

@@ -121,7 +121,7 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,186,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,243,306,50,14
LTEXT "ZoomIt v10.1",IDC_VERSION,42,7,73,10
LTEXT "ZoomIt v10.0",IDC_VERSION,42,7,73,10
LTEXT "Copyright \251 2006-2026 Mark Russinovich",IDC_COPYRIGHT,42,17,251,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9

View File

@@ -1,7 +1,7 @@
---
author: Mike Griese
created on: 2024-07-19
last updated: 2026-02-05
last updated: 2025-08-08
issue id: n/a
---
@@ -75,8 +75,6 @@ functionality.
- [Advanced scenarios](#advanced-scenarios)
- [Status messages](#status-messages)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2047,87 +2045,6 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
developers won't have to do anything. The toolkit will just do the right thing
for them.
## Addenda IV: Dock bands
The "dock" is another way to surface commands to the user. This is a
toolbar-like window that can be docked to the side of the screen, or floated as
its own window. It enables another surface for extensions to display real-time
information and shortcuts to users.
Bands are powered by the same interfaces as DevPal itself. Extensions can provide
bands via the new `DockBand` property on `ICommandProvider3`.
```csharp
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
```
A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
items. This allows an extension to create a strip of buttons that should all be
treated as a single unit. For example, a media player band will want probably
four items:
* one for the previous track
* one for play/pause
* one for next track
* and one to display the album art and track title
`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
represents one band in the dock. These represent all of the bands that an
extension would allow the user to add to their dock.
All of the `ICommandItem`s returned from `GetDockBands` **must** have a
`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
ignore that band.
Bands are not automatically added to the dock. Instead, the user must choose
which bands they want to add. This is done via the DevPal settings page.
Furthermore, bands are not displayed in the list of commands in DevPal itself.
This allows extension authors to create objects that are only intended for the
dock, without cluttering up the main DevPal UI, and vice versa.
DevPal will then create UI in the dock for each band the user has chosen to add.
What that looks like will depend on the `Command` in the `ICommandItem`:
* A `IInvokableCommand` will be rendered as a single button. Think "the
time/date" button on the taskbar, that opens the notification center.
* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
in the list. Think "media controls" for a music player.
* A `IContentPage` will be rendered as a single button. Clicking that button
will open a flyout with that content rendered in it. Think "weather" or "news"
flyouts.
If the `Command` in the `IListItem`s of a band are pages, then clicking those
buttons will open DevPal to that page, as if it were a flyout from the dock.
The `.Title` property of the top-level `ICommandItem` representing the band will
be used as the name of the band in the settings. So a media player band might
want to set the `Title` to "Contoso Music Player", even if the individual
buttons in the band don't show that title.
Users may also "pin" a top-level command from DevPal into the dock. DevPal will
take care of creating a new band (owned by devpal) with that command in it. This
allows users to add quick shortcuts to their favorite commands in the dock.
Think: pinning an app, or pinning a particular GitHub query.
Bands are added via ID. An extension may choose to have a TopLevelCommand and a
DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
a simple pinned command. This allows extension authors to seamlessly have a
top-level command present a palette-specific experience, while also having a
dock-specific experience. In our ongoing media player example, the top-level
command might open DevPal to a full-featured music control page, while the dock
band has simpler buttons on it (without a title/subtitle).
Users may choose to have:
* the orientation of the dock: vertical or horizontal
* the size of the dock
* which bands are shown in the dock
* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
shown or hidden.
- Dock bands will still display the `Title` & `Subtitle` of each item in the
band as the tooltip on those items, even when the "labels" are hidden.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)

View File

@@ -6,10 +6,7 @@ using Windows.Foundation;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider :
ICommandProvider,
ICommandProvider2,
ICommandProvider3
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -51,21 +48,6 @@ public abstract partial class CommandProvider :
}
}
/// <summary>
/// Get the dock bands provided by this command provider. Dock bands are
/// strips of items that appear on various UI surfaces in CmdPal, such as a
/// toolbar. Each ICommandItem returned from this method will be treated as
/// one atomic band by cmdpal.
///
/// If the command on an item here is a
/// IListPage, then cmdpal will render all of the items on that page as one
/// band. You can use this to create complex bands with multiple buttons.
/// </summary>
public virtual ICommandItem[]? GetDockBands()
{
return null;
}
/// <summary>
/// This is used to manually populate the WinRT type cache in CmdPal with
/// any interfaces that might not follow a straight linear path of requires.

View File

@@ -1,68 +0,0 @@
// 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 Microsoft.CommandPalette.Extensions.Toolkit;
/// <summary>
/// Helper class for creating a band out of a set of items. This allows you to
/// simply just instantiate a set of buttons as ListItems, then pass them in to
/// this class to create a band from those items. For example:
///
/// ```cs
/// var foo = new MyFooListItem();
/// var bar = new MyBarListItem();
/// var band = new WrappedDockItem([foo, bar], "com.me.myBand", "My cool desk band");
/// ```
/// </summary>
public partial class WrappedDockItem : CommandItem
{
public override string Title => _itemTitle;
public override IIconInfo? Icon
{
get => _icon; set { _icon = value; }
}
public override ICommand? Command => _backingList;
private readonly string _itemTitle;
private readonly WrappedDockList _backingList;
private IIconInfo? _icon;
public IListItem[] Items { get => _backingList.GetItems(); set => _backingList.SetItems(value); }
public WrappedDockItem(
ICommand command,
string displayTitle)
{
_backingList = new WrappedDockList(command);
_itemTitle = string.IsNullOrEmpty(displayTitle) ? command.Name : displayTitle;
_icon = command.Icon;
}
// This was too much of a foot gun - we'd internally create a ListItem that
// didn't bubble the prop change events back up. That was bad.
// public WrappedDockItem(
// ICommandItem item,
// string id,
// string displayTitle)
// {
// _backingList = new WrappedDockList(item, id);
// _itemTitle = string.IsNullOrEmpty(displayTitle) ? item.Title : displayTitle;
// _icon = item.Icon;
// }
/// <summary>
/// Initializes a new instance of the <see cref="WrappedDockItem"/> class.
/// Create a new dock band for a set of list items
/// </summary>
public WrappedDockItem(
IListItem[] items,
string id,
string displayTitle)
{
_backingList = new WrappedDockList(items, id, displayTitle);
_itemTitle = displayTitle;
}
}

View File

@@ -1,86 +0,0 @@
// 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 Microsoft.CommandPalette.Extensions.Toolkit;
/// <summary>
/// Helper class for a list page that just holds a set of items as a band.
/// The page itself doesn't do anything interesting.
/// </summary>
internal sealed partial class WrappedDockList : ListPage
{
private string _id;
public override string Id => _id;
private List<IListItem> _items;
internal WrappedDockList(ICommand command)
{
_items = new() { new ListItem(command) };
Name = command.Name;
_id = command.Id;
}
// Maybe revisit sometime.
// The hard problem is that the wrapping item will not
// listen for property changes on the inner item.
// public WrappedDockList(ICommandItem item, string id)
// {
// var command = item.Command;
// _items = new()
// {
// new ListItem(command)
// {
// Title = item.Title,
// Subtitle = item.Subtitle,
// Icon = item.Icon,
// MoreCommands = item.MoreCommands,
// },
// };
// Name = command.Name;
// _id = string.IsNullOrEmpty(id) ? command.Id : id;
// }
/// <summary>
/// Initializes a new instance of the <see cref="WrappedDockList"/> class.
/// Create a new list page for the set of items provided.
/// </summary>
internal WrappedDockList(IListItem[] items, string id, string name)
{
_items = new(items);
Name = name;
_id = id;
}
internal WrappedDockList(ICommand[] items, string id, string name)
{
_items = new();
foreach (var item in items)
{
_items.Add(new ListItem(item));
}
Name = name;
_id = id;
}
public override IListItem[] GetItems()
{
return _items.ToArray();
}
internal void SetItems(IListItem[]? newItems)
{
if (newItems == null)
{
_items = [];
RaiseItemsChanged(0);
return;
}
ListHelpers.InPlaceUpdateList(_items, newItems);
RaiseItemsChanged(_items.Count);
}
}

View File

@@ -405,11 +405,6 @@ namespace Microsoft.CommandPalette.Extensions
{
Object[] GetApiExtensionStubs();
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
}

View File

@@ -140,7 +140,7 @@
<TextBlock
x:Name="FormatNameTextBlock"
VerticalAlignment="Center"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis" />

View File

@@ -27,13 +27,13 @@
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
</PropertyGroup>
<!-- <PropertyGroup>
<PropertyGroup>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
</PropertyGroup> -->
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">

View File

@@ -1,88 +0,0 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
public class CustomVcpValueMapping
{
/// <summary>
/// Gets or sets the VCP code (e.g., 0x14 for color temperature, 0x60 for input source).
/// </summary>
[JsonPropertyName("vcpCode")]
public byte VcpCode { get; set; }
/// <summary>
/// Gets or sets the VCP value to map (e.g., 0x11 for HDMI-1).
/// </summary>
[JsonPropertyName("value")]
public int Value { get; set; }
/// <summary>
/// Gets or sets the custom name to display instead of the default name.
/// </summary>
[JsonPropertyName("customName")]
public string CustomName { get; set; } = string.Empty;
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("applyToAll")]
public bool ApplyToAll { get; set; } = true;
/// <summary>
/// Gets or sets the target monitor ID when ApplyToAll is false.
/// This is the monitor's unique identifier.
/// </summary>
[JsonPropertyName("targetMonitorId")]
public string TargetMonitorId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the target monitor display name (for UI display only, not serialized).
/// </summary>
[JsonIgnore]
public string TargetMonitorName { get; set; } = string.Empty;
/// <summary>
/// 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.
/// </summary>
[JsonIgnore]
public string VcpCodeDisplayName => VcpNames.GetCodeName(VcpCode);
/// <summary>
/// Gets the display name for the VCP value (using built-in mapping).
/// </summary>
[JsonIgnore]
public string ValueDisplayName => VcpNames.GetFormattedValueName(VcpCode, Value);
/// <summary>
/// Gets a summary string for display in the UI list.
/// Format: "OriginalValue → CustomName" or "OriginalValue → CustomName (MonitorName)"
/// </summary>
[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;
}
}
}
}

View File

@@ -2,27 +2,16 @@
// 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
{
/// <summary>
/// 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.
/// </summary>
public static class VcpNames
{
/// <summary>
/// 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.
/// </summary>
public static Func<byte, string?>? LocalizedCodeNameProvider { get; set; }
/// <summary>
/// VCP code to name mapping
/// </summary>
@@ -248,21 +237,12 @@ namespace PowerDisplay.Common.Utils
};
/// <summary>
/// Get the friendly name for a VCP code.
/// Uses LocalizedCodeNameProvider if set; falls back to built-in MCCS names if not.
/// Get the friendly name for a VCP code
/// </summary>
/// <param name="code">VCP code (e.g., 0x10)</param>
/// <returns>Friendly name, or hex representation if unknown</returns>
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})";
}
@@ -409,16 +389,6 @@ namespace PowerDisplay.Common.Utils
},
};
/// <summary>
/// Get all known values for a VCP code
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <returns>Dictionary of value to name mappings, or null if no mappings exist</returns>
public static IReadOnlyDictionary<int, string>? GetValueMappings(byte vcpCode)
{
return ValueNames.TryGetValue(vcpCode, out var values) ? values : null;
}
/// <summary>
/// Get human-readable name for a VCP value
/// </summary>
@@ -454,59 +424,5 @@ namespace PowerDisplay.Common.Utils
return $"0x{value:X2}";
}
/// <summary>
/// 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.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <param name="customMappings">Optional custom mappings that take priority</param>
/// <param name="monitorId">Monitor ID to filter mappings</param>
/// <returns>Name string like "sRGB" or null if unknown</returns>
public static string? GetValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? 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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <param name="customMappings">Optional custom mappings that take priority</param>
/// <param name="monitorId">Monitor ID to filter mappings</param>
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
public static string GetFormattedValueName(byte vcpCode, int value, IEnumerable<CustomVcpValueMapping>? customMappings, string monitorId)
{
var name = GetValueName(vcpCode, value, customMappings, monitorId);
if (name != null)
{
return $"{name} (0x{value:X2})";
}
return $"0x{value:X2}";
}
}
}

View File

@@ -125,27 +125,14 @@ namespace PowerDisplay
/// <summary>
/// Called when an existing instance is activated by another process.
/// 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.
/// 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)
/// </summary>
private static void OnActivated(object? sender, AppActivationArguments args)
{
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");
}
Logger.LogInfo("OnActivated: Redirect activation received - window visibility unchanged");
}
}
}

View File

@@ -43,10 +43,6 @@ 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<PowerDisplaySettings>("PowerDisplay");
@@ -55,11 +51,8 @@ public partial class MainViewModel
// Reload profiles in case they were added/updated/deleted in Settings UI
LoadProfiles();
// Notify MonitorViewModels to refresh their custom VCP name displays
foreach (var monitor in Monitors)
{
monitor.RefreshCustomVcpNames();
}
// Reload UI display settings (profile switcher, identify button, color temp switcher)
LoadUIDisplaySettings();
}
catch (Exception ex)
{
@@ -311,8 +304,7 @@ public partial class MainViewModel
}
/// <summary>
/// Apply feature visibility settings to a monitor ViewModel.
/// Only shows features that are both enabled by user AND supported by hardware.
/// Apply feature visibility settings to a monitor ViewModel
/// </summary>
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
{
@@ -321,13 +313,12 @@ public partial class MainViewModel
if (monitorSettings != null)
{
// 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.ShowContrast = monitorSettings.EnableContrast;
monitorVm.ShowVolume = monitorSettings.EnableVolume;
monitorVm.ShowInputSource = monitorSettings.EnableInputSource;
monitorVm.ShowRotation = monitorSettings.EnableRotation;
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature && monitorVm.SupportsColorTemperature;
monitorVm.ShowPowerState = monitorSettings.EnablePowerState && monitorVm.SupportsPowerState;
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
monitorVm.ShowPowerState = monitorSettings.EnablePowerState;
}
}

View File

@@ -163,23 +163,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
}
// Custom VCP mappings - loaded from settings
private List<CustomVcpValueMapping> _customVcpMappings = new();
/// <summary>
/// Gets or sets the custom VCP value name mappings.
/// These mappings override the default VCP value names for color temperature and input source.
/// </summary>
public List<CustomVcpValueMapping> CustomVcpMappings
{
get => _customVcpMappings;
set
{
_customVcpMappings = value ?? new List<CustomVcpValueMapping>();
OnPropertyChanged();
}
}
public bool IsScanning
{
get => _isScanning;
@@ -406,10 +389,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(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<CustomVcpValueMapping>();
Logger.LogInfo($"[Settings] Loaded {CustomVcpMappings.Count} custom VCP mappings");
}
catch (Exception ex)
{

View File

@@ -279,16 +279,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
// Advanced control display logic
public bool HasAdvancedControls => ShowContrast || ShowVolume;
/// <summary>
/// Gets a value indicating whether this monitor supports contrast control via VCP 0x12
/// </summary>
public bool SupportsContrast => _monitor.SupportsContrast;
/// <summary>
/// Gets a value indicating whether this monitor supports volume control via VCP 0x62
/// </summary>
public bool SupportsVolume => _monitor.SupportsVolume;
public bool ShowContrast
{
get => _showContrast;
@@ -466,10 +456,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <summary>
/// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
/// Uses custom mappings if available; falls back to built-in names if not.
/// </summary>
public string ColorTemperaturePresetName =>
Common.Utils.VcpNames.GetFormattedValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings, _monitor.Id);
public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
/// <summary>
/// Gets a value indicating whether this monitor supports color temperature via VCP 0x14
@@ -549,7 +537,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableColorPresets = presetValues.Select(value => new ColorTemperatureItem
{
VcpValue = value,
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value, _mainViewModel?.CustomVcpMappings, _monitor.Id),
DisplayName = Common.Utils.VcpNames.GetFormattedValueName(0x14, value),
IsSelected = value == _monitor.CurrentColorTemperature,
MonitorId = _monitor.Id,
}).ToList();
@@ -569,11 +557,8 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// <summary>
/// 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.
/// </summary>
public string CurrentInputSourceName =>
Common.Utils.VcpNames.GetValueName(0x60, _monitor.CurrentInputSource, _mainViewModel?.CustomVcpMappings, _monitor.Id)
?? $"Source 0x{_monitor.CurrentInputSource:X2}";
public string CurrentInputSourceName => _monitor.InputSourceName;
private List<InputSourceItem>? _availableInputSources;
@@ -608,7 +593,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_availableInputSources = supportedSources.Select(value => new InputSourceItem
{
Value = value,
Name = Common.Utils.VcpNames.GetValueName(0x60, value, _mainViewModel?.CustomVcpMappings, _monitor.Id) ?? $"Source 0x{value:X2}",
Name = Common.Utils.VcpNames.GetValueName(0x60, value) ?? $"Source 0x{value:X2}",
SelectionVisibility = value == _monitor.CurrentInputSource ? Visibility.Visible : Visibility.Collapsed,
MonitorId = _monitor.Id,
}).ToList();
@@ -616,23 +601,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(AvailableInputSources));
}
/// <summary>
/// Refresh custom VCP name displays after settings change.
/// Called when CustomVcpMappings is updated from Settings UI.
/// </summary>
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));
}
/// <summary>
/// Set input source for this monitor
/// </summary>

View File

@@ -52,11 +52,8 @@ void PowerDisplayProcessManager::send_message(const std::wstring& message_type,
{
submit_task([this, message_type, message_arg] {
// Ensure process is running before sending message
// 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())
if (!is_process_running() && m_enabled)
{
m_enabled = true;
refresh();
}
send_named_pipe_message(message_type, message_arg);

View File

@@ -9,8 +9,6 @@
#include <common/utils/winapi_error.h>
#include <common/utils/logger_helper.h>
#include <common/utils/resources.h>
#include <thread>
#include <atomic>
#include "resource.h"
@@ -50,11 +48,6 @@ 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()
{
@@ -69,29 +62,16 @@ public:
m_hSendSettingsTelemetryEvent = CreateDefaultEvent(CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT);
Logger::trace(L"Created SEND_SETTINGS_TELEMETRY_EVENT: handle={}", reinterpret_cast<void*>(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<void*>(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<void*>(m_hStopEvent));
if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent || !m_hToggleEvent || !m_hStopEvent)
if (!m_hRefreshEvent || !m_hSendSettingsTelemetryEvent)
{
Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}, Toggle={}",
Logger::error(L"Failed to create one or more event handles: Refresh={}, SettingsTelemetry={}",
reinterpret_cast<void*>(m_hRefreshEvent),
reinterpret_cast<void*>(m_hSendSettingsTelemetryEvent),
reinterpret_cast<void*>(m_hToggleEvent));
reinterpret_cast<void*>(m_hSendSettingsTelemetryEvent));
}
else
{
Logger::info(L"All Windows Events created successfully");
}
// Start toggle event listener thread for Quick Access support
StartToggleEventListener();
}
~PowerDisplayModule()
@@ -101,9 +81,6 @@ public:
disable();
}
// Stop toggle event listener thread
StopToggleEventListener();
// Clean up event handles
if (m_hRefreshEvent)
{
@@ -115,99 +92,6 @@ 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();
}
}
/// <summary>
/// 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.
/// </summary>
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
@@ -251,7 +135,10 @@ public:
if (action_object.get_name() == L"Launch")
{
Logger::trace(L"Launch action received");
TogglePowerDisplay();
// Send Toggle message via Named Pipe (will start process if needed)
m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE);
Trace::ActivatePowerDisplay();
}
else if (action_object.get_name() == L"RefreshMonitors")
{

View File

@@ -119,13 +119,6 @@ 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;

View File

@@ -10,10 +10,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteAdditionalActions
{
private AdvancedPasteAdditionalAction _imageToText = new();
private AdvancedPastePasteAsFileAction _pasteAsFile = new();
private AdvancedPasteTranscodeAction _transcode = new();
public static class PropertyNames
{
public const string ImageToText = "image-to-text";
@@ -22,25 +18,13 @@ public sealed class AdvancedPasteAdditionalActions
}
[JsonPropertyName(PropertyNames.ImageToText)]
public AdvancedPasteAdditionalAction ImageToText
{
get => _imageToText;
init => _imageToText = value ?? new();
}
public AdvancedPasteAdditionalAction ImageToText { get; init; } = new();
[JsonPropertyName(PropertyNames.PasteAsFile)]
public AdvancedPastePasteAsFileAction PasteAsFile
{
get => _pasteAsFile;
init => _pasteAsFile = value ?? new();
}
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
[JsonPropertyName(PropertyNames.Transcode)]
public AdvancedPasteTranscodeAction Transcode
{
get => _transcode;
init => _transcode = value ?? new();
}
public AdvancedPasteTranscodeAction Transcode { get; init; } = new();
public IEnumerable<IAdvancedPasteAction> GetAllActions()
{

View File

@@ -25,16 +25,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("wrap_mode")]
public IntProperty WrapMode { get; set; }
[JsonPropertyName("disable_cursor_wrap_on_single_monitor")]
public BoolProperty DisableCursorWrapOnSingleMonitor { get; set; }
public CursorWrapProperties()
{
ActivationShortcut = DefaultActivationShortcut;
AutoActivate = new BoolProperty(false);
DisableWrapDuringDrag = new BoolProperty(true);
WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
DisableCursorWrapOnSingleMonitor = new BoolProperty(false);
}
}
}

View File

@@ -56,13 +56,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
settingsUpgraded = true;
}
// Add DisableCursorWrapOnSingleMonitor property if it doesn't exist (for users upgrading from older versions)
if (Properties.DisableCursorWrapOnSingleMonitor == null)
{
Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(false); // Default to false
settingsUpgraded = true;
}
return settingsUpgraded;
}
}

View File

@@ -23,7 +23,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
ShowSystemTrayIcon = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
CustomVcpMappings = new List<CustomVcpValueMapping>();
// Note: saved_monitor_settings has been moved to monitor_state.json
// which is managed separately by PowerDisplay app
@@ -62,12 +61,5 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// </summary>
[JsonPropertyName("show_identify_monitors_button")]
public bool ShowIdentifyMonitorsButton { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("custom_vcp_mappings")]
public List<CustomVcpValueMapping> CustomVcpMappings { get; set; }
}
}

View File

@@ -1,75 +0,0 @@
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.CustomVcpMappingEditorDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Width="400"
MinWidth="400"
DefaultButton="Primary"
IsPrimaryButtonEnabled="{x:Bind CanSave, Mode=OneWay}"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel MinWidth="350" Spacing="16">
<!-- VCP Code Selection -->
<ComboBox
x:Name="VcpCodeComboBox"
x:Uid="PowerDisplay_CustomMappingEditor_VcpCode"
HorizontalAlignment="Stretch"
SelectionChanged="VcpCodeComboBox_SelectionChanged">
<ComboBoxItem x:Name="VcpCodeItem_0x14" Tag="20" />
<ComboBoxItem x:Name="VcpCodeItem_0x60" Tag="96" />
</ComboBox>
<!-- Value Selection from monitors -->
<StackPanel Spacing="8">
<ComboBox
x:Name="ValueComboBox"
x:Uid="PowerDisplay_CustomMappingEditor_ValueComboBox"
HorizontalAlignment="Stretch"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind AvailableValues, Mode=OneWay}"
SelectedValuePath="Value"
SelectionChanged="ValueComboBox_SelectionChanged" />
<!-- Custom Value Input (shown when "Custom value" is selected) -->
<TextBox
x:Name="CustomValueTextBox"
x:Uid="PowerDisplay_CustomMappingEditor_CustomValueInput"
HorizontalAlignment="Stretch"
PlaceholderText="0x11"
TextChanged="CustomValueTextBox_TextChanged"
Visibility="{x:Bind ShowCustomValueInput, Mode=OneWay}" />
</StackPanel>
<!-- Custom Name Input -->
<TextBox
x:Name="CustomNameTextBox"
x:Uid="PowerDisplay_CustomMappingEditor_CustomName"
HorizontalAlignment="Stretch"
MaxLength="50"
TextChanged="CustomNameTextBox_TextChanged" />
<!-- Apply Scope -->
<StackPanel Spacing="8">
<ToggleSwitch
x:Name="ApplyToAllToggle"
x:Uid="PowerDisplay_CustomMappingEditor_ApplyToAll"
IsOn="True"
Toggled="ApplyToAllToggle_Toggled" />
<!-- Monitor Selection (shown when ApplyToAll is off) -->
<ComboBox
x:Name="MonitorComboBox"
x:Uid="PowerDisplay_CustomMappingEditor_SelectMonitor"
HorizontalAlignment="Stretch"
DisplayMemberPath="DisplayName"
ItemsSource="{x:Bind AvailableMonitors, Mode=OneWay}"
SelectedValuePath="Id"
SelectionChanged="MonitorComboBox_SelectionChanged"
Visibility="{x:Bind ShowMonitorSelector, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</ContentDialog>

View File

@@ -1,421 +0,0 @@
// 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
{
/// <summary>
/// Dialog for creating/editing custom VCP value name mappings
/// </summary>
public sealed partial class CustomVcpMappingEditorDialog : ContentDialog, INotifyPropertyChanged
{
/// <summary>
/// Special value to indicate "Custom value" option in the ComboBox
/// </summary>
private const int CustomValueMarker = -1;
/// <summary>
/// Represents a selectable VCP value item in the Value ComboBox
/// </summary>
public class VcpValueItem
{
public int Value { get; set; }
public string DisplayName { get; set; } = string.Empty;
public bool IsCustomOption => Value == CustomValueMarker;
}
/// <summary>
/// Represents a selectable monitor item in the Monitor ComboBox
/// </summary>
public class MonitorItem
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}
private readonly IEnumerable<MonitorInfo>? _monitors;
private ObservableCollection<VcpValueItem> _availableValues = new();
private ObservableCollection<MonitorItem> _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<MonitorInfo>? 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;
}
/// <summary>
/// Gets the result mapping after dialog closes with Primary button
/// </summary>
public CustomVcpValueMapping? ResultMapping { get; private set; }
/// <summary>
/// Gets the available values for the selected VCP code
/// </summary>
public ObservableCollection<VcpValueItem> AvailableValues
{
get => _availableValues;
private set
{
_availableValues = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets the available monitors for selection
/// </summary>
public ObservableCollection<MonitorItem> AvailableMonitors
{
get => _availableMonitors;
private set
{
_availableMonitors = value;
OnPropertyChanged();
}
}
/// <summary>
/// Gets a value indicating whether the dialog can be saved
/// </summary>
public bool CanSave
{
get => _canSave;
private set
{
if (_canSave != value)
{
_canSave = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets a value indicating whether to show the custom value input TextBox
/// </summary>
public Visibility ShowCustomValueInput => _showCustomValueInput ? Visibility.Visible : Visibility.Collapsed;
/// <summary>
/// Gets a value indicating whether to show the monitor selector ComboBox
/// </summary>
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<MonitorItem>(
_monitors?.Select(m => new MonitorItem { Id = m.Id, DisplayName = m.DisplayName })
?? Enumerable.Empty<MonitorItem>());
if (AvailableMonitors.Count > 0)
{
MonitorComboBox.SelectedIndex = 0;
}
}
/// <summary>
/// Pre-fill the dialog with existing mapping data for editing
/// </summary>
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<VcpValueItem>();
var seenValues = new HashSet<int>();
// 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<VcpValueItem>(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));
}
}
}

View File

@@ -54,9 +54,6 @@
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableOnSingleMonitor" IsChecked="{x:Bind ViewModel.CursorWrapDisableOnSingleMonitor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>

View File

@@ -63,51 +63,11 @@
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<!-- Custom VCP Name Mappings -->
<controls:SettingsGroup x:Uid="PowerDisplay_CustomVcpMappings_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_CustomVcpMappings"
HeaderIcon="{ui:FontIcon Glyph=&#xE70F;}"
IsExpanded="{x:Bind ViewModel.HasCustomVcpMappings, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.CustomVcpMappings, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:CustomVcpValueMapping">
<tkcontrols:SettingsCard Description="{x:Bind VcpCodeDisplayName}" Header="{x:Bind DisplaySummary}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
Click="EditCustomMapping_Click"
Content="{ui:FontIcon Glyph=&#xE70F;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}"
ToolTipService.ToolTip="Edit" />
<Button
Click="DeleteCustomMapping_Click"
Content="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}"
ToolTipService.ToolTip="Delete" />
</StackPanel>
</tkcontrols:SettingsCard>
</DataTemplate>
</tkcontrols:SettingsExpander.ItemTemplate>
<!-- Add mapping button -->
<Button x:Uid="PowerDisplay_AddCustomMappingButton" Click="AddCustomMapping_Click">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon FontSize="14" Glyph="&#xE710;" />
<TextBlock x:Uid="PowerDisplay_AddCustomMapping_Text" />
</StackPanel>
</Button>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Profiles_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplay_QuickProfiles"
HeaderIcon="{ui:FontIcon Glyph=&#xE8B7;}"
IsExpanded="{x:Bind ViewModel.HasProfiles, Mode=OneWay}"
IsExpanded="True"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">

View File

@@ -133,65 +133,6 @@ 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;

View File

@@ -2726,9 +2726,6 @@ From there, simply click on one of the supported files in the File Explorer and
<data name="MouseUtils_CursorWrap_DisableWrapDuringDrag.Content" xml:space="preserve">
<value>Disable wrapping while dragging</value>
</data>
<data name="MouseUtils_CursorWrap_DisableOnSingleMonitor.Content" xml:space="preserve">
<value>Disable wrapping when using a single monitor</value>
</data>
<data name="MouseUtils_CursorWrap_AutoActivate.Header" xml:space="preserve">
<value>Auto-activate on startup</value>
</data>
@@ -5998,69 +5995,6 @@ The break timer font matches the text font.</value>
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Description" xml:space="preserve">
<value>Show or hide the identify monitors button in the Power Display flyout</value>
</data>
<data name="PowerDisplay_CustomVcpMappings_GroupSettings.Header" xml:space="preserve">
<value>Custom VCP Name Mappings</value>
</data>
<data name="PowerDisplay_CustomVcpMappings.Header" xml:space="preserve">
<value>Custom name mappings</value>
</data>
<data name="PowerDisplay_CustomVcpMappings.Description" xml:space="preserve">
<value>Define custom display names for color temperature presets and input sources</value>
</data>
<data name="PowerDisplay_AddCustomMappingButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Add custom mapping</value>
</data>
<data name="PowerDisplay_AddCustomMapping_Text.Text" xml:space="preserve">
<value>Add mapping</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_Title" xml:space="preserve">
<value>Custom VCP Name Mapping</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_VcpCode.Header" xml:space="preserve">
<value>VCP Code</value>
</data>
<data name="PowerDisplay_VcpCode_Name_0x14" xml:space="preserve">
<value>Color Temperature</value>
</data>
<data name="PowerDisplay_VcpCode_Name_0x60" xml:space="preserve">
<value>Input Source</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomName.Header" xml:space="preserve">
<value>Custom Name</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomName.PlaceholderText" xml:space="preserve">
<value>Enter custom name</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ValueComboBox.Header" xml:space="preserve">
<value>Value</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomValueOption" xml:space="preserve">
<value>Custom value...</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomValueInput.Header" xml:space="preserve">
<value>Enter custom value (hex)</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomValueInput.PlaceholderText" xml:space="preserve">
<value>e.g., 0x11 or 17</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.Header" xml:space="preserve">
<value>Apply to all monitors</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_ApplyToAll.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_SelectMonitor.Header" xml:space="preserve">
<value>Select monitor</value>
</data>
<data name="PowerDisplay_CustomMapping_Delete_Title" xml:space="preserve">
<value>Delete custom mapping?</value>
</data>
<data name="PowerDisplay_CustomMapping_Delete_Message" xml:space="preserve">
<value>This custom name mapping will be permanently removed.</value>
</data>
<data name="Hosts_Backup_GroupSettings.Header" xml:space="preserve">
<value>Backup</value>
</data>

View File

@@ -116,9 +116,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Null-safe access in case property wasn't upgraded yet - default to 0 (Both)
_cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0;
// Null-safe access in case property wasn't upgraded yet - default to false
_cursorWrapDisableOnSingleMonitor = CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor?.Value ?? false;
int isEnabled = 0;
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
@@ -1006,6 +1003,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
GeneralSettingsConfig.Enabled.CursorWrap = value;
OnPropertyChanged(nameof(IsCursorWrapEnabled));
// Auto-enable the AutoActivate setting when CursorWrap is enabled
// This ensures cursor wrapping is active immediately after enabling
if (value && !_cursorWrapAutoActivate)
{
CursorWrapAutoActivate = true;
}
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
@@ -1110,34 +1114,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool CursorWrapDisableOnSingleMonitor
{
get
{
return _cursorWrapDisableOnSingleMonitor;
}
set
{
if (value != _cursorWrapDisableOnSingleMonitor)
{
_cursorWrapDisableOnSingleMonitor = value;
// Ensure the property exists before setting value
if (CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor == null)
{
CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(value);
}
else
{
CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor.Value = value;
}
NotifyCursorWrapPropertyChanged();
}
}
}
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
@@ -1210,6 +1186,5 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _cursorWrapAutoActivate;
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly
private bool _cursorWrapDisableOnSingleMonitor; // Disable cursor wrap when only one monitor is connected
}
}

View File

@@ -36,9 +36,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public PowerDisplayViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> 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);
@@ -59,15 +56,9 @@ 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(),
@@ -455,28 +446,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Profile-related fields
private ObservableCollection<PowerDisplayProfile> _profiles = new ObservableCollection<PowerDisplayProfile>();
// Custom VCP mapping fields
private ObservableCollection<CustomVcpValueMapping> _customVcpMappings;
/// <summary>
/// Gets collection of custom VCP value name mappings
/// Gets or sets collection of available profiles (for button display)
/// </summary>
public ObservableCollection<CustomVcpValueMapping> CustomVcpMappings => _customVcpMappings;
/// <summary>
/// Gets whether there are any custom VCP mappings (for UI binding)
/// </summary>
public bool HasCustomVcpMappings => _customVcpMappings?.Count > 0;
/// <summary>
/// Gets collection of available profiles (for button display)
/// </summary>
public ObservableCollection<PowerDisplayProfile> Profiles => _profiles;
/// <summary>
/// Gets whether there are any profiles (for UI binding)
/// </summary>
public bool HasProfiles => _profiles?.Count > 0;
public ObservableCollection<PowerDisplayProfile> Profiles
{
get => _profiles;
set
{
if (_profiles != value)
{
_profiles = value;
OnPropertyChanged();
}
}
}
public void RefreshEnabledState()
{
@@ -662,109 +646,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
/// <summary>
/// Load custom VCP mappings from settings
/// </summary>
private void LoadCustomVcpMappings()
{
List<CustomVcpValueMapping> mappings;
try
{
mappings = _settings.Properties.CustomVcpMappings ?? new List<CustomVcpValueMapping>();
Logger.LogInfo($"Loaded {mappings.Count} custom VCP mappings");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load custom VCP mappings: {ex.Message}");
mappings = new List<CustomVcpValueMapping>();
}
_customVcpMappings = new ObservableCollection<CustomVcpValueMapping>(mappings);
_customVcpMappings.CollectionChanged += (s, e) => OnPropertyChanged(nameof(HasCustomVcpMappings));
OnPropertyChanged(nameof(CustomVcpMappings));
OnPropertyChanged(nameof(HasCustomVcpMappings));
}
/// <summary>
/// Add a new custom VCP mapping.
/// No duplicate checking - mappings are resolved by order (first match wins in VcpNames).
/// </summary>
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();
}
/// <summary>
/// Update an existing custom VCP mapping
/// </summary>
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();
}
}
/// <summary>
/// Delete a custom VCP mapping
/// </summary>
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();
}
}
/// <summary>
/// Save custom VCP mappings to settings
/// </summary>
private void SaveCustomVcpMappings()
{
_settings.Properties.CustomVcpMappings = CustomVcpMappings.ToList();
NotifySettingsChanged();
// Signal PowerDisplay to reload settings
SignalSettingsUpdated();
}
/// <summary>
/// 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.
/// </summary>
#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