[PD] Re-enable PowerDisplay (#46489)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
1. Re-enable PowerDisplay for PowerToys.
2. Add PowerDisplay back into installer.
3. Use new PowerDisplay icon and logo.
4. Fix some DPI related issue.
5. UI/UX improvement.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #1052
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Pull new code from this branch. Set up PowerDisplay.UI as startup
project. Click run in VS.

Or, build whole solution, set up runner as startup project. Click run to
test full experience.

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
moooyo
2026-04-10 15:14:41 +08:00
committed by GitHub
parent 3e2914a0b2
commit 0089de33bd
71 changed files with 1584 additions and 1813 deletions

View File

@@ -332,6 +332,7 @@ REGSTR
INVOKEIDLIST
MEMORYSTATUSEX
ABE
Mdt
HTCAPTION
POSCHANGED
QUERYPOS

View File

@@ -1692,6 +1692,7 @@ UOI
UPDATENOW
updown
UPGRADINGPRODUCTCODE
upserts
Uptool
urld
Usb
@@ -2173,7 +2174,7 @@ nodiscard
nologo
nomove
nosize
notopmost
NOTOPMOST
Notupdated
notwindows
nowarn

View File

@@ -217,7 +217,11 @@
"PowerToys.PowerAccentModuleInterface.dll",
"PowerToys.PowerAccentKeyboardService.dll",
"PowerToys.PowerDisplayModuleInterface.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
"PowerDisplay.Lib.dll",
"PowerDisplay.Models.dll",
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
"WinUI3Apps\\PowerToys.PowerRename.exe",

View File

@@ -709,17 +709,19 @@
</Project>
</Folder>
<Folder Name="/modules/PowerDisplay/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Models/PowerDisplay.Models.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<!-- TEMPORARILY_DISABLED: PowerDisplay
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
-->
</Folder>
<Folder Name="/modules/PowerDisplay/Tests/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">

View File

@@ -1594,6 +1594,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
L"PowerToys.LightSwitchService.exe",
L"PowerToys.PowerDisplay.exe",
L"PowerToys.GcodeThumbnailProvider.exe",
L"PowerToys.BgcodeThumbnailProvider.exe",
L"PowerToys.PdfThumbnailProvider.exe",

View File

@@ -47,6 +47,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="PowerDisplay.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />

View File

@@ -53,6 +53,7 @@
<ComponentGroupRef Id="LightSwitchComponentGroup" />
<ComponentGroupRef Id="PeekComponentGroup" />
<ComponentGroupRef Id="PowerRenameComponentGroup" />
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
<ComponentGroupRef Id="RunComponentGroup" />
<ComponentGroupRef Id="SettingsComponentGroup" />

View File

@@ -212,6 +212,10 @@ Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRo
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs
#PowerDisplay
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay\"
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
#RegistryPreview
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs

View File

@@ -150,7 +150,6 @@
<decimal value="0" />
</disabledValue>
</policy>
<!-- TEMPORARILY_DISABLED: PowerDisplay
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
@@ -161,7 +160,6 @@
<decimal value="0" />
</disabledValue>
</policy>
-->
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />

View File

@@ -249,7 +249,7 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
<!-- <string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string> --><!-- TEMPORARILY_DISABLED: PowerDisplay -->
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>

View File

@@ -0,0 +1,48 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
namespace PowerDisplay.Common.Extensions
{
/// <summary>
/// Extension methods for <see cref="CustomVcpValueMapping"/> that provide display-related properties.
/// These depend on <see cref="VcpNames"/> and are therefore kept in PowerDisplay.Lib
/// rather than in the shared PowerDisplay.Models project.
/// </summary>
public static class CustomVcpValueMappingExtensions
{
/// <summary>
/// Gets the display name for the VCP code (e.g., "Select Color Preset").
/// </summary>
public static string GetVcpCodeDisplayName(this CustomVcpValueMapping mapping)
{
return VcpNames.GetCodeName(mapping.VcpCode);
}
/// <summary>
/// Gets the formatted display name for the VCP value (e.g., "6500K (0x05)").
/// </summary>
public static string GetValueDisplayName(this CustomVcpValueMapping mapping)
{
return VcpNames.GetFormattedValueName(mapping.VcpCode, mapping.Value);
}
/// <summary>
/// Gets a summary string for display in the UI list.
/// Format: "OriginalValue → CustomName" or "OriginalValue → CustomName (MonitorName)"
/// </summary>
public static string GetDisplaySummary(this CustomVcpValueMapping mapping)
{
var baseSummary = $"{VcpNames.GetValueName(mapping.VcpCode, mapping.Value) ?? $"0x{mapping.Value:X2}"} \u2192 {mapping.CustomName}";
if (!mapping.ApplyToAll && !string.IsNullOrEmpty(mapping.TargetMonitorName))
{
return $"{baseSummary} ({mapping.TargetMonitorName})";
}
return baseSummary;
}
}
}

View File

@@ -2,13 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
namespace PowerDisplay.Common.Interfaces
{
/// <summary>
/// Interface for profile management service.
/// Provides abstraction for loading, saving, and managing PowerDisplay profiles.
/// Provides abstraction for loading and saving PowerDisplay profiles.
/// Enables dependency injection and unit testing.
/// </summary>
public interface IProfileService
@@ -25,38 +25,5 @@ namespace PowerDisplay.Common.Interfaces
/// <param name="profiles">The profiles collection to save.</param>
/// <returns>True if save was successful, false otherwise.</returns>
bool SaveProfiles(PowerDisplayProfiles profiles);
/// <summary>
/// Adds or updates a profile in the collection and persists to disk.
/// </summary>
/// <param name="profile">The profile to add or update.</param>
/// <returns>True if operation was successful, false otherwise.</returns>
bool AddOrUpdateProfile(PowerDisplayProfile profile);
/// <summary>
/// Removes a profile by name and persists to disk.
/// </summary>
/// <param name="profileName">The name of the profile to remove.</param>
/// <returns>True if profile was found and removed, false otherwise.</returns>
bool RemoveProfile(string profileName);
/// <summary>
/// Gets a profile by name.
/// </summary>
/// <param name="profileName">The name of the profile to retrieve.</param>
/// <returns>The profile if found, null otherwise.</returns>
PowerDisplayProfile? GetProfile(string profileName);
/// <summary>
/// Checks if the profiles file exists.
/// </summary>
/// <returns>True if profiles file exists, false otherwise.</returns>
bool ProfilesFileExists();
/// <summary>
/// Gets the path to the profiles file.
/// </summary>
/// <returns>The full path to the profiles file.</returns>
string GetProfilesFilePath();
}
}

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

@@ -39,5 +39,6 @@
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\PowerDisplay.Models\PowerDisplay.Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Serialization
{
/// <summary>
/// JSON serialization context for MonitorState types.
/// Provides source-generated serialization for Native AOT compatibility.
/// Separated from ProfileSerializationContext which moved to PowerDisplay.Models.
/// </summary>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IncludeFields = true)]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(Dictionary<string, MonitorStateEntry>))]
public partial class MonitorStateSerializationContext : JsonSerializerContext
{
}
}

View File

@@ -156,7 +156,7 @@ namespace PowerDisplay.Common.Services
}
var json = File.ReadAllText(_stateFilePath);
var stateFile = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.MonitorStateFile);
var stateFile = JsonSerializer.Deserialize(json, MonitorStateSerializationContext.Default.MonitorStateFile);
if (stateFile?.Monitors != null)
{
@@ -257,7 +257,7 @@ namespace PowerDisplay.Common.Services
};
}
return JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
return JsonSerializer.Serialize(stateFile, MonitorStateSerializationContext.Default.MonitorStateFile);
}
/// <summary>

View File

@@ -2,249 +2,52 @@
// 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.IO;
using System.Text.Json;
using ManagedCommon;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Serialization;
using PowerDisplay.Models;
using ModelsProfileHelper = PowerDisplay.Models.ProfileHelper;
namespace PowerDisplay.Common.Services
{
/// <summary>
/// Centralized service for managing PowerDisplay profiles storage and retrieval.
/// Provides unified access to profile data for PowerDisplay, Settings UI, and LightSwitch modules.
/// Thread-safe and AOT-compatible.
/// Thin facade over <see cref="ProfileHelper"/> that satisfies <see cref="IProfileService"/>
/// and provides named static entry points for PowerDisplay.exe callers.
/// All locking and compound-operation atomicity is handled by <see cref="PowerDisplay.Models.ProfileHelper"/>.
/// </summary>
public class ProfileService : IProfileService
{
private const string LogPrefix = "[ProfileService]";
private static readonly object _lock = new object();
/// <summary>
/// Gets the singleton instance of the ProfileService.
/// Use this for dependency injection or when interface-based access is needed.
/// </summary>
public static IProfileService Instance { get; } = new ProfileService();
/// <summary>
/// Initializes a new instance of the <see cref="ProfileService"/> class.
/// Private constructor to enforce singleton pattern for instance-based access.
/// Static methods remain available for backward compatibility.
/// </summary>
private ProfileService()
{
}
/// <summary>
/// Loads PowerDisplay profiles from disk.
/// Thread-safe operation with automatic legacy profile cleanup.
/// </summary>
/// <returns>PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails</returns>
public static PowerDisplayProfiles LoadProfiles()
{
lock (_lock)
{
var (profiles, _) = LoadProfilesInternal();
return profiles;
}
}
/// <inheritdoc cref="ModelsProfileHelper.LoadProfiles"/>
public static PowerDisplayProfiles LoadProfiles() => ModelsProfileHelper.LoadProfiles();
/// <summary>
/// Saves PowerDisplay profiles to disk.
/// Thread-safe operation with automatic timestamp update and legacy profile cleanup.
/// </summary>
/// <param name="profiles">The profiles collection to save</param>
/// <returns>True if save was successful, false otherwise</returns>
public static bool SaveProfiles(PowerDisplayProfiles profiles)
{
lock (_lock)
{
if (profiles == null)
{
Logger.LogWarning($"{LogPrefix} Cannot save null profiles");
return false;
}
/// <inheritdoc cref="ModelsProfileHelper.SaveProfiles"/>
public static bool SaveProfiles(PowerDisplayProfiles profiles) => ModelsProfileHelper.SaveProfiles(profiles);
var (success, _) = SaveProfilesInternal(profiles);
return success;
}
}
/// <inheritdoc cref="ModelsProfileHelper.AddOrUpdateProfile"/>
public static bool AddOrUpdateProfile(PowerDisplayProfile profile) => ModelsProfileHelper.AddOrUpdateProfile(profile);
/// <summary>
/// Adds or updates a profile in the collection and persists to disk.
/// Thread-safe operation.
/// </summary>
/// <param name="profile">The profile to add or update</param>
/// <returns>True if operation was successful, false otherwise</returns>
public static bool AddOrUpdateProfile(PowerDisplayProfile profile)
{
lock (_lock)
{
if (profile == null || !profile.IsValid())
{
Logger.LogWarning($"{LogPrefix} Cannot add invalid profile");
return false;
}
/// <inheritdoc cref="ModelsProfileHelper.RenameAndUpdateProfile"/>
public static bool RenameAndUpdateProfile(string oldName, PowerDisplayProfile newProfile) => ModelsProfileHelper.RenameAndUpdateProfile(oldName, newProfile);
var (profiles, _) = LoadProfilesInternal();
profiles.SetProfile(profile);
/// <inheritdoc cref="ModelsProfileHelper.RemoveProfile"/>
public static bool RemoveProfile(string profileName) => ModelsProfileHelper.RemoveProfile(profileName);
var (success, _) = SaveProfilesInternal(profiles);
return success;
}
}
/// <inheritdoc cref="ModelsProfileHelper.GetProfile"/>
public static PowerDisplayProfile? GetProfile(string profileName) => ModelsProfileHelper.GetProfile(profileName);
/// <summary>
/// Removes a profile by name and persists to disk.
/// Thread-safe operation.
/// </summary>
/// <param name="profileName">The name of the profile to remove</param>
/// <returns>True if profile was found and removed, false otherwise</returns>
public static bool RemoveProfile(string profileName)
{
lock (_lock)
{
var (profiles, _) = LoadProfilesInternal();
bool removed = profiles.RemoveProfile(profileName);
if (removed)
{
SaveProfilesInternal(profiles);
}
return removed;
}
}
/// <summary>
/// Gets a profile by name.
/// Thread-safe operation.
/// </summary>
/// <param name="profileName">The name of the profile to retrieve</param>
/// <returns>The profile if found, null otherwise</returns>
public static PowerDisplayProfile? GetProfile(string profileName)
{
lock (_lock)
{
var (profiles, _) = LoadProfilesInternal();
return profiles.GetProfile(profileName);
}
}
/// <summary>
/// Checks if the profiles file exists.
/// </summary>
/// <returns>True if profiles file exists, false otherwise</returns>
public static bool ProfilesFileExists()
{
try
{
return File.Exists(PathConstants.ProfilesFilePath);
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Error checking if profiles file exists: {ex.Message}");
return false;
}
}
/// <summary>
/// Gets the path to the profiles file.
/// </summary>
/// <returns>The full path to the profiles file</returns>
public static string GetProfilesFilePath()
{
return PathConstants.ProfilesFilePath;
}
// Internal methods without lock for use within already-locked contexts
// Returns tuple with result and optional log message
private static (PowerDisplayProfiles Profiles, string? Message) LoadProfilesInternal()
{
try
{
var filePath = PathConstants.ProfilesFilePath;
PathConstants.EnsurePowerDisplayFolderExists();
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath);
var profiles = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.PowerDisplayProfiles);
if (profiles != null)
{
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
return (profiles, $"Loaded {profiles.Profiles.Count} profiles from {filePath}");
}
}
else
{
return (new PowerDisplayProfiles(), $"No profiles file found at {filePath}, returning empty collection");
}
return (new PowerDisplayProfiles(), null);
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Failed to load profiles: {ex.Message}");
return (new PowerDisplayProfiles(), null);
}
}
// Returns tuple with success status and optional log message
private static (bool Success, string? Message) SaveProfilesInternal(PowerDisplayProfiles profiles)
{
try
{
if (profiles == null)
{
return (false, null);
}
PathConstants.EnsurePowerDisplayFolderExists();
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
profiles.LastUpdated = DateTime.UtcNow;
var json = JsonSerializer.Serialize(profiles, ProfileSerializationContext.Default.PowerDisplayProfiles);
var filePath = PathConstants.ProfilesFilePath;
File.WriteAllText(filePath, json);
return (true, $"Saved {profiles.Profiles.Count} profiles to {filePath}");
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Failed to save profiles: {ex.Message}");
return (false, null);
}
}
// IProfileService Implementation
// Explicit interface implementation to satisfy IProfileService
// These methods delegate to the static methods for backward compatibility
// IProfileService explicit implementation
/// <inheritdoc/>
PowerDisplayProfiles IProfileService.LoadProfiles() => LoadProfiles();
PowerDisplayProfiles IProfileService.LoadProfiles() => ModelsProfileHelper.LoadProfiles();
/// <inheritdoc/>
bool IProfileService.SaveProfiles(PowerDisplayProfiles profiles) => SaveProfiles(profiles);
/// <inheritdoc/>
bool IProfileService.AddOrUpdateProfile(PowerDisplayProfile profile) => AddOrUpdateProfile(profile);
/// <inheritdoc/>
bool IProfileService.RemoveProfile(string profileName) => RemoveProfile(profileName);
/// <inheritdoc/>
PowerDisplayProfile? IProfileService.GetProfile(string profileName) => GetProfile(profileName);
/// <inheritdoc/>
bool IProfileService.ProfilesFileExists() => ProfilesFileExists();
/// <inheritdoc/>
string IProfileService.GetProfilesFilePath() => GetProfilesFilePath();
bool IProfileService.SaveProfiles(PowerDisplayProfiles profiles) => ModelsProfileHelper.SaveProfiles(profiles);
}
}

View File

@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
namespace PowerDisplay.Common.Utils
{

View File

@@ -5,14 +5,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using PowerDisplay.Common.Models;
using PowerDisplay.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
{
@@ -249,20 +248,11 @@ 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.
/// </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})";
}
@@ -410,17 +400,7 @@ 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
/// Get human-readable name for a VCP value.
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
@@ -439,7 +419,7 @@ namespace PowerDisplay.Common.Utils
}
/// <summary>
/// Get formatted display name for a VCP value (with hex value in parentheses)
/// Get formatted display name for a VCP value (with hex value in parentheses).
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
@@ -458,21 +438,17 @@ namespace PowerDisplay.Common.Utils
/// <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>
/// <param name="customMappings">Custom mappings that take priority over built-in names</param>
/// <param name="monitorId">Monitor ID to filter monitor-specific 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 &&
@@ -487,26 +463,5 @@ namespace PowerDisplay.Common.Utils
// 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

@@ -6,7 +6,7 @@ using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
namespace PowerDisplay.Models
{
/// <summary>
/// Represents a color temperature preset item for VCP code 0x14.

View File

@@ -4,12 +4,12 @@
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
namespace PowerDisplay.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 is a local copy maintained in Settings.UI.Library to avoid a dependency on PowerDisplay.Lib.
/// This class is shared between PowerDisplay app and Settings UI.
/// </summary>
public class CustomVcpValueMapping
{
@@ -53,16 +53,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// <summary>
/// Gets the display name for the VCP code (for UI display).
/// Uses simple hex formatting. For richer names, use extension methods in PowerDisplay.Lib.
/// </summary>
[JsonIgnore]
public string VcpCodeDisplayName => $"VCP 0x{VcpCode:X2}";
/// <summary>
/// Gets the display name for the VCP value.
/// </summary>
[JsonIgnore]
public string ValueDisplayName => $"0x{Value:X2}";
/// <summary>
/// Gets a summary string for display in the UI list.
/// Format: "0xVV → CustomName" or "0xVV → CustomName (MonitorName)"

View File

@@ -0,0 +1,23 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
<!-- Licensed under the MIT License. See LICENSE file in the project root for license information. -->
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>PowerDisplay.Models</RootNamespace>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
<AssemblyName>PowerDisplay.Models</AssemblyName>
</PropertyGroup>
<!-- Native AOT Configuration -->
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>
</Project>

View File

@@ -6,7 +6,7 @@ using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
namespace PowerDisplay.Models
{
/// <summary>
/// Represents a PowerDisplay profile containing monitor settings

View File

@@ -9,17 +9,13 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
namespace PowerDisplay.Models
{
/// <summary>
/// Container for all PowerDisplay profiles
/// </summary>
public class PowerDisplayProfiles
{
// NOTE: Custom profile concept has been removed. Profiles are now templates, not states.
// This constant is kept for backward compatibility (cleaning up legacy Custom profiles).
public const string CustomProfileName = "Custom";
[JsonPropertyName("profiles")]
public List<PowerDisplayProfile> Profiles { get; set; }

View File

@@ -0,0 +1,196 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Text.Json;
namespace PowerDisplay.Models
{
/// <summary>
/// Helper for loading and saving PowerDisplay profiles from/to disk.
/// Provides shared file I/O logic used by both Settings UI and PowerDisplay module.
/// Thread-safe and AOT-compatible.
/// All compound operations (load → modify → save) are atomic within a single process.
/// </summary>
public static class ProfileHelper
{
private static readonly object _lock = new object();
private static readonly Lazy<string> _profilesFilePath = new Lazy<string>(() =>
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"PowerToys",
"PowerDisplay",
"profiles.json"));
/// <summary>
/// Gets the full path to the profiles JSON file.
/// </summary>
public static string ProfilesFilePath => _profilesFilePath.Value;
/// <summary>
/// Loads PowerDisplay profiles from disk.
/// Thread-safe operation.
/// </summary>
/// <returns>PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails.</returns>
public static PowerDisplayProfiles LoadProfiles()
{
lock (_lock)
{
return LoadProfilesCore();
}
}
/// <summary>
/// Saves PowerDisplay profiles to disk.
/// Thread-safe operation with automatic timestamp update.
/// </summary>
/// <param name="profiles">The profiles collection to save.</param>
/// <returns>True if save was successful, false otherwise.</returns>
public static bool SaveProfiles(PowerDisplayProfiles profiles)
{
lock (_lock)
{
return SaveProfilesCore(profiles);
}
}
/// <summary>
/// Adds or updates a profile and persists to disk atomically.
/// </summary>
/// <param name="profile">The profile to add or update.</param>
/// <returns>True if the operation was successful, false otherwise.</returns>
public static bool AddOrUpdateProfile(PowerDisplayProfile profile)
{
if (profile == null || !profile.IsValid())
{
return false;
}
lock (_lock)
{
var profiles = LoadProfilesCore();
profiles.SetProfile(profile);
return SaveProfilesCore(profiles);
}
}
/// <summary>
/// Renames and updates a profile atomically (for rename or edit operations).
/// Removes the old entry by <paramref name="oldName"/> and upserts the updated profile.
/// </summary>
/// <param name="oldName">The current name of the profile to replace.</param>
/// <param name="newProfile">The updated profile.</param>
/// <returns>True if the operation was successful, false otherwise.</returns>
public static bool RenameAndUpdateProfile(string oldName, PowerDisplayProfile newProfile)
{
if (newProfile == null || !newProfile.IsValid())
{
return false;
}
lock (_lock)
{
var profiles = LoadProfilesCore();
profiles.RemoveProfile(oldName);
profiles.SetProfile(newProfile);
return SaveProfilesCore(profiles);
}
}
/// <summary>
/// Removes a profile by name and persists to disk atomically.
/// </summary>
/// <param name="profileName">The name of the profile to remove.</param>
/// <returns>True if the profile was found and removed, false otherwise.</returns>
public static bool RemoveProfile(string profileName)
{
lock (_lock)
{
var profiles = LoadProfilesCore();
bool removed = profiles.RemoveProfile(profileName);
if (removed)
{
SaveProfilesCore(profiles);
}
return removed;
}
}
/// <summary>
/// Gets a profile by name.
/// </summary>
/// <param name="profileName">The name of the profile to retrieve.</param>
/// <returns>The profile if found, null otherwise.</returns>
public static PowerDisplayProfile? GetProfile(string profileName)
{
lock (_lock)
{
return LoadProfilesCore().GetProfile(profileName);
}
}
// Lock-free core methods — only call from within a lock (_lock) block.
private static PowerDisplayProfiles LoadProfilesCore()
{
try
{
EnsureFolderExists();
if (File.Exists(ProfilesFilePath))
{
var json = File.ReadAllText(ProfilesFilePath);
var profiles = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.PowerDisplayProfiles);
if (profiles != null)
{
return profiles;
}
}
return new PowerDisplayProfiles();
}
catch (Exception)
{
return new PowerDisplayProfiles();
}
}
private static bool SaveProfilesCore(PowerDisplayProfiles profiles)
{
try
{
if (profiles == null)
{
return false;
}
EnsureFolderExists();
profiles.LastUpdated = DateTime.UtcNow;
var json = JsonSerializer.Serialize(profiles, ProfileSerializationContext.Default.PowerDisplayProfiles);
File.WriteAllText(ProfilesFilePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
private static void EnsureFolderExists()
{
var folder = Path.GetDirectoryName(ProfilesFilePath);
if (folder != null && !Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
}
}
}

View File

@@ -4,7 +4,7 @@
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
namespace PowerDisplay.Models
{
/// <summary>
/// Monitor settings for a specific profile

View File

@@ -4,9 +4,8 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Serialization
namespace PowerDisplay.Models
{
/// <summary>
/// JSON serialization context for PowerDisplay Profile types.
@@ -23,11 +22,6 @@ namespace PowerDisplay.Common.Serialization
[JsonSerializable(typeof(PowerDisplayProfile))]
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
[JsonSerializable(typeof(PowerDisplayProfiles))]
// Monitor State Types
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(Dictionary<string, MonitorStateEntry>))]
public partial class ProfileSerializationContext : JsonSerializerContext
{
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

After

Width:  |  Height:  |  Size: 191 KiB

View File

@@ -14,11 +14,25 @@ namespace PowerDisplay.Configuration
/// </summary>
public static class UI
{
// Window dimensions
public const int WindowWidth = 362;
public const int MinWindowHeight = 100;
public const int MaxWindowHeight = 650;
public const int WindowRightMargin = 12;
// Main flyout dimensions in device-independent pixels (DIP)
public const int WindowWidthDip = 362;
public const int WindowMinHeightDip = 100;
public const int WindowMaxHeightDip = 650;
public const int WindowRightMarginDip = 12;
public const int WindowBottomMarginDip = WindowRightMarginDip;
public const double WindowMaxWorkAreaHeightRatio = 0.75;
// Adaptive flyout bounds in device-independent pixels (DIP)
public const int FlyoutContextMenuMaxWidthDip = 320;
public const int FlyoutContextMenuAdaptiveMaxWidthDip = 420;
public const double FlyoutContextMenuMaxWorkAreaWidthRatio = 0.35;
// Identify overlay bounds in device-independent pixels (DIP)
public const int IdentifyWindowPreferredWidthDip = 300;
public const int IdentifyWindowPreferredHeightDip = 280;
public const int IdentifyWindowMinWidthDip = 160;
public const int IdentifyWindowMinHeightDip = 160;
public const double IdentifyWindowMaxWorkAreaRatio = 0.28;
/// <summary>
/// Icon glyph for internal/laptop displays (WMI)

View File

@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
using WinUIEx;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Subclasses a window's WndProc to suppress WM_DPICHANGED messages during
/// cross-monitor MoveAndResize calls. Without suppression, the framework
/// auto-scales it a second time, causing double-scaling artifacts.
///
/// Usage:
/// var suppressor = new DpiSuppressor(window);
/// using (suppressor.Suppress())
/// {
/// window.AppWindow.MoveAndResize(rect, displayArea);
/// }
/// </summary>
internal sealed partial class DpiSuppressor : IDisposable
{
// Optional external WndProc handler (e.g., HotkeyService) called before default processing.
// Return true to indicate the message was handled.
private readonly Func<uint, nuint, nint, bool>? _preProcessor;
private const int GwlWndProc = -4;
private const uint WmDpiChanged = 0x02E0;
private readonly nint _hwnd;
private nint _originalWndProc;
private WndProcDelegate? _wndProcDelegate;
private bool _suppressing;
private bool _disposed;
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
private static partial nint CallWindowProc(nint lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
/// <summary>
/// Initializes a new instance of the <see cref="DpiSuppressor"/> class.
/// Subclass the window's WndProc to enable DPI suppression.
/// </summary>
/// <param name="window">Window to subclass.</param>
/// <param name="preProcessor">Optional callback invoked for every message before default processing.
/// Receives (uMsg, wParam, lParam). Return true to swallow the message.</param>
public DpiSuppressor(WindowEx window, Func<uint, nuint, nint, bool>? preProcessor = null)
{
_hwnd = window.GetWindowHandle();
_preProcessor = preProcessor;
_wndProcDelegate = WndProc;
var ptr = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate);
_originalWndProc = SetWindowLongPtr(_hwnd, GwlWndProc, ptr);
}
/// <summary>
/// Returns a disposable scope during which WM_DPICHANGED is suppressed.
/// </summary>
public SuppressScope Suppress() => new(this);
private nint WndProc(nint hwnd, uint uMsg, nuint wParam, nint lParam)
{
// Let external handler process first (e.g., hotkey messages)
if (_preProcessor?.Invoke(uMsg, wParam, lParam) == true)
{
return 0;
}
if (uMsg == WmDpiChanged && _suppressing)
{
return 0;
}
return CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
// Restore original WndProc
if (_originalWndProc != 0)
{
SetWindowLongPtr(_hwnd, GwlWndProc, _originalWndProc);
_originalWndProc = 0;
}
_wndProcDelegate = null;
}
internal readonly struct SuppressScope : IDisposable
{
private readonly DpiSuppressor _owner;
internal SuppressScope(DpiSuppressor owner)
{
_owner = owner;
_owner._suppressing = true;
}
public void Dispose()
{
_owner._suppressing = false;
}
}
}
}

View File

@@ -6,7 +6,6 @@ using System;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using WinRT.Interop;
namespace PowerDisplay.Helpers
{
@@ -23,10 +22,6 @@ namespace PowerDisplay.Helpers
private readonly Action _hotkeyAction;
private nint _hwnd;
private nint _originalWndProc;
// Must keep delegate reference to prevent GC collection
private WndProcDelegate? _hotkeyWndProc;
private bool _isRegistered;
private bool _disposed;
@@ -38,25 +33,37 @@ namespace PowerDisplay.Helpers
/// <summary>
/// Initialize the hotkey service with a window handle.
/// Must be called after window is created.
/// Must be called after window is created and WndProcService is attached.
/// </summary>
/// <param name="window">The WinUI window to attach to.</param>
public void Initialize(Microsoft.UI.Xaml.Window window)
public void Initialize(nint hwnd)
{
_hwnd = WindowNative.GetWindowHandle(window);
// LOAD BEARING: If you don't stick the pointer to the WndProc into a
// member (and instead use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our WndProc will explode.
_hotkeyWndProc = HotkeyWndProc;
var wndProcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = SetWindowLongPtrNative(_hwnd, GwlWndProc, wndProcPointer);
// Register hotkey based on current settings
_hwnd = hwnd;
ReloadSettings();
}
/// <summary>
/// Handle WM_HOTKEY messages. Called by WndProcService.
/// </summary>
/// <returns>True if the message was handled.</returns>
public bool HandleMessage(uint uMsg, nuint wParam)
{
if (uMsg == WmHotkey && (int)wParam == HotkeyId)
{
try
{
_hotkeyAction?.Invoke();
}
catch (Exception ex)
{
Logger.LogError($"[HotkeyService] Hotkey action failed: {ex.Message}");
}
return true;
}
return false;
}
/// <summary>
/// Reload settings and re-register hotkey.
/// Call this when settings change.
@@ -119,25 +126,6 @@ namespace PowerDisplay.Helpers
_isRegistered = false;
}
private nint HotkeyWndProc(nint hwnd, uint uMsg, nuint wParam, nint lParam)
{
if (uMsg == WmHotkey && (int)wParam == HotkeyId)
{
try
{
_hotkeyAction?.Invoke();
}
catch (Exception ex)
{
Logger.LogError($"[HotkeyService] Hotkey action failed: {ex.Message}");
}
return 0;
}
return CallWindowProcNative(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
public void Dispose()
{
if (_disposed)
@@ -150,7 +138,6 @@ namespace PowerDisplay.Helpers
}
// P/Invoke constants
private const int GwlWndProc = -4;
private const uint WmHotkey = 0x0312;
// HOT_KEY_MODIFIERS flags
@@ -160,12 +147,6 @@ namespace PowerDisplay.Helpers
private const uint ModWin = 0x0008;
private const uint ModNoRepeat = 0x4000;
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtrNative(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
private static partial nint CallWindowProcNative(nint lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
[LibraryImport("user32.dll", EntryPoint = "RegisterHotKey", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool RegisterHotKeyNative(nint hWnd, int id, uint fsModifiers, uint vk);

View File

@@ -144,44 +144,6 @@ namespace PowerDisplay.Helpers
}
}
/// <summary>
/// Get brightness of the specified monitor
/// </summary>
public async Task<VcpFeatureValue> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.Invalid;
}
try
{
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
// Update cached brightness value
if (brightnessInfo.IsValid)
{
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
}
return brightnessInfo;
}
catch (Exception ex)
{
// Mark monitor as unavailable
Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}");
monitor.IsAvailable = false;
return VcpFeatureValue.Invalid;
}
}
/// <summary>
/// Set brightness of the specified monitor
/// </summary>
@@ -215,33 +177,6 @@ namespace PowerDisplay.Helpers
(mon, val) => mon.CurrentVolume = val,
cancellationToken);
/// <summary>
/// Get monitor color temperature
/// </summary>
public async Task<VcpFeatureValue> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.Invalid;
}
try
{
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return VcpFeatureValue.Invalid;
}
}
/// <summary>
/// Set monitor color temperature
/// </summary>
@@ -253,33 +188,6 @@ namespace PowerDisplay.Helpers
(mon, val) => mon.CurrentColorTemperature = val,
cancellationToken);
/// <summary>
/// Get current input source for a monitor
/// </summary>
public async Task<VcpFeatureValue> GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.Invalid;
}
try
{
return await controller.GetInputSourceAsync(monitor, cancellationToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return VcpFeatureValue.Invalid;
}
}
/// <summary>
/// Set input source for a monitor
/// </summary>

View File

@@ -250,7 +250,6 @@ namespace PowerDisplay.Helpers
break;
case PInvoke.WM_LBUTTONUP:
case PInvoke.WM_LBUTTONDBLCLK:
_toggleWindowAction?.Invoke();
break;
}

View File

@@ -3,9 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using WinUIEx;
@@ -26,6 +24,9 @@ namespace PowerDisplay.Helpers
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetCursorPos(out POINT lpPoint);
[LibraryImport("shcore.dll")]
private static partial int GetDpiForMonitor(nint hMonitor, uint dpiType, out uint dpiX, out uint dpiY);
// Window Styles
private const int GwlStyle = -16;
private const int WsCaption = 0x00C00000;
@@ -40,17 +41,11 @@ namespace PowerDisplay.Helpers
private const int WsExWindowedge = 0x00000100;
private const int WsExClientedge = 0x00000200;
private const int WsExStaticedge = 0x00020000;
private const int WsExToolwindow = 0x00000080;
private const uint SwpNosize = 0x0001;
private const uint SwpNomove = 0x0002;
private const uint SwpFramechanged = 0x0020;
private const nint HwndTopmost = -1;
private const nint HwndNotopmost = -2;
// ShowWindow commands
private const int SwHide = 0;
private const int SwShow = 5;
private const uint MdtEffectiveDpi = 0;
private const int DefaultDpi = 96;
// P/Invoke declarations (64-bit only - PowerToys only builds for x64/ARM64)
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
@@ -70,22 +65,6 @@ namespace PowerDisplay.Helpers
int cy,
uint uFlags);
[LibraryImport("user32.dll", EntryPoint = "ShowWindow")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindowNative(nint hWnd, int nCmdShow);
[LibraryImport("user32.dll", EntryPoint = "IsWindowVisible")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool IsWindowVisibleNative(nint hWnd);
/// <summary>
/// Check if window is visible
/// </summary>
public static bool IsWindowVisible(nint hWnd)
{
return IsWindowVisibleNative(hWnd);
}
/// <summary>
/// Disable window moving and resizing functionality
/// </summary>
@@ -123,54 +102,6 @@ namespace PowerDisplay.Helpers
SwpNomove | SwpNosize | SwpFramechanged);
}
/// <summary>
/// Set whether window is topmost
/// </summary>
public static void SetWindowTopmost(nint hWnd, bool topmost)
{
SetWindowPos(
hWnd,
topmost ? HwndTopmost : HwndNotopmost,
0,
0,
0,
0,
SwpNomove | SwpNosize);
}
/// <summary>
/// Show or hide window
/// </summary>
public static void ShowWindow(nint hWnd, bool show)
{
ShowWindowNative(hWnd, show ? SwShow : SwHide);
}
/// <summary>
/// Hide window from taskbar
/// </summary>
public static void HideFromTaskbar(nint hWnd)
{
// Get current extended style
nint exStyle = GetWindowLongPtr(hWnd, GwlExstyle);
// Add WS_EX_TOOLWINDOW style to hide window from taskbar
exStyle |= WsExToolwindow;
// Set new extended style
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
// Refresh window frame
SetWindowPos(
hWnd,
0,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
/// <summary>
/// Get the DPI scale factor for a window (relative to standard 96 DPI)
/// </summary>
@@ -178,18 +109,39 @@ namespace PowerDisplay.Helpers
/// <returns>DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%)</returns>
public static double GetDpiScale(WindowEx window)
{
return (float)window.GetDpiForWindow() / 96.0;
return (double)window.GetDpiForWindow() / DefaultDpi;
}
/// <summary>
/// Convert device-independent units (DIU) to physical pixels
/// Get the DPI scale factor for a display area (relative to standard 96 DPI)
/// </summary>
/// <param name="diu">Device-independent unit value</param>
/// <param name="displayArea">Target display area</param>
/// <returns>DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%)</returns>
public static double GetDpiScale(DisplayArea displayArea)
{
return (double)GetEffectiveDpi(global::Microsoft.UI.Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId)) / DefaultDpi;
}
/// <summary>
/// Convert device-independent pixels (DIP) to physical pixels.
/// </summary>
/// <param name="dip">Device-independent pixel value</param>
/// <param name="dpiScale">DPI scale factor</param>
/// <returns>Physical pixel value</returns>
public static int ScaleToPhysicalPixels(int diu, double dpiScale)
public static int ScaleToPhysicalPixels(int dip, double dpiScale)
{
return (int)Math.Ceiling(diu * dpiScale);
return (int)Math.Ceiling(dip * dpiScale);
}
/// <summary>
/// Convert physical pixels to device-independent pixels (DIP).
/// </summary>
/// <param name="physicalPixels">Physical pixel value</param>
/// <param name="dpiScale">DPI scale factor</param>
/// <returns>Device-independent pixel value</returns>
public static int ScaleToDip(int physicalPixels, double dpiScale)
{
return (int)Math.Floor(physicalPixels / dpiScale);
}
/// <summary>
@@ -200,62 +152,108 @@ namespace PowerDisplay.Helpers
/// - Different DPI settings
/// </summary>
/// <param name="window">WinUIEx window to position</param>
/// <param name="width">Window width in device-independent units (DIU)</param>
/// <param name="height">Window height in device-independent units (DIU)</param>
/// <param name="rightMargin">Right margin in device-independent units (DIU)</param>
/// <param name="widthDip">Window width in device-independent pixels (DIP)</param>
/// <param name="heightDip">Window height in device-independent pixels (DIP)</param>
/// <param name="rightMarginDip">Right margin in device-independent pixels (DIP)</param>
/// <param name="bottomMarginDip">Bottom margin in device-independent pixels (DIP)</param>
public static void PositionWindowBottomRight(
WindowEx window,
int width,
int height,
int rightMargin = 0)
int widthDip,
int heightDip,
int rightMarginDip = 0,
int bottomMarginDip = 0)
{
// RectWork already includes correct offsets for taskbar position
var monitors = MonitorInfo.GetDisplayMonitors();
if (monitors == null || monitors.Count == 0)
if (!TryGetDisplayAreaAtCursor(out var displayArea) || displayArea is null)
{
ManagedCommon.Logger.LogWarning("PositionWindowBottomRight: No monitors found, skipping positioning");
ManagedCommon.Logger.LogWarning("PositionWindowBottomRight: Unable to determine target display from cursor, skipping positioning");
return;
}
// Find the monitor where the mouse cursor is located
var targetMonitor = GetMonitorAtCursor(monitors);
var workArea = targetMonitor.RectWork;
double dpiScale = GetDpiScale(window);
// Calculate bottom-right position
// RectWork.Right/Bottom already account for taskbar position
double x = workArea.Right - (dpiScale * (width + rightMargin));
double y = workArea.Bottom - (dpiScale * height);
window.MoveAndResize(x, y, width, height);
MoveWindowBottomRight(window, displayArea, widthDip, heightDip, rightMarginDip, bottomMarginDip);
}
/// <summary>
/// Get the monitor where the mouse cursor is currently located.
/// Falls back to primary monitor if cursor position cannot be determined.
/// Center a window within the specified display area's work area.
/// </summary>
/// <param name="monitors">List of available monitors</param>
/// <returns>MonitorInfo of the monitor containing the cursor</returns>
private static MonitorInfo GetMonitorAtCursor(IList<MonitorInfo> monitors)
public static void CenterWindowOnDisplay(
WindowEx window,
DisplayArea displayArea,
int widthDip,
int heightDip)
{
// Try to get cursor position using Win32 API
if (GetCursorPos(out var cursorPos))
double dpiScale = GetDpiScale(displayArea);
int w = ScaleToPhysicalPixels(widthDip, dpiScale);
int h = ScaleToPhysicalPixels(heightDip, dpiScale);
// WorkArea relative to DisplayArea (accounts for taskbar position)
var rel = GetWorkAreaRelativeToDisplay(displayArea);
int x = rel.X + ((rel.Width - w) / 2);
int y = rel.Y + ((rel.Height - h) / 2);
window.AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, w, h), displayArea);
}
private static void MoveWindowBottomRight(
WindowEx window,
DisplayArea displayArea,
int widthDip,
int heightDip,
int rightMarginDip,
int bottomMarginDip)
{
double dpiScale = GetDpiScale(displayArea);
int w = ScaleToPhysicalPixels(widthDip, dpiScale);
int h = ScaleToPhysicalPixels(heightDip, dpiScale);
int marginRight = ScaleToPhysicalPixels(rightMarginDip, dpiScale);
int marginBottom = ScaleToPhysicalPixels(bottomMarginDip, dpiScale);
// WorkArea relative to DisplayArea (accounts for taskbar position)
var rel = GetWorkAreaRelativeToDisplay(displayArea);
int x = rel.X + rel.Width - w - marginRight;
int y = rel.Y + rel.Height - h - marginBottom;
window.AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(x, y, w, h), displayArea);
}
/// <summary>
/// Get the work area rectangle relative to the display area's origin.
/// MoveAndResize(rect, displayArea) expects coordinates relative to the DisplayArea,
/// but WorkArea.X/Y are in absolute screen coordinates, so we subtract the DisplayArea origin.
/// The resulting rect describes where the usable area is within the display (e.g., offset by taskbar).
/// </summary>
private static Windows.Graphics.RectInt32 GetWorkAreaRelativeToDisplay(DisplayArea displayArea)
{
var outer = displayArea.OuterBounds;
var work = displayArea.WorkArea;
return new Windows.Graphics.RectInt32(
work.X - outer.X,
work.Y - outer.Y,
work.Width,
work.Height);
}
internal static bool TryGetDisplayAreaAtCursor(out DisplayArea? displayArea)
{
displayArea = null;
if (!GetCursorPos(out var cursorPos))
{
// Find the monitor that contains the cursor point
foreach (var monitor in monitors)
{
if (cursorPos.X >= monitor.RectMonitor.Left &&
cursorPos.X < monitor.RectMonitor.Right &&
cursorPos.Y >= monitor.RectMonitor.Top &&
cursorPos.Y < monitor.RectMonitor.Bottom)
{
return monitor;
}
}
return false;
}
// Fallback to first monitor (typically primary)
return monitors[0];
displayArea = DisplayArea.GetFromPoint(new Windows.Graphics.PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.None);
return displayArea is not null;
}
private static int GetEffectiveDpi(nint hMonitor)
{
if (hMonitor == 0)
{
return DefaultDpi;
}
var hr = GetDpiForMonitor(hMonitor, MdtEffectiveDpi, out var dpiX, out _);
return hr >= 0 && dpiX > 0 ? (int)dpiX : DefaultDpi;
}
}
}

View File

@@ -22,6 +22,7 @@
<ProjectPriFileName>PowerToys.PowerDisplay.pri</ProjectPriFileName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
<!-- Disable XAML-generated Main method, use custom Program.cs -->
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
@@ -33,6 +34,12 @@
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<ItemGroup>
<!-- Hide build log files from Solution Explorer -->
<None Remove="*.log" />
@@ -81,11 +88,13 @@
</ItemGroup>
<ItemGroup>
<!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally -->
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<!-- Removed ManagedCsWin32 - using CsWin32 directly for TrayIcon APIs -->
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
<ProjectReference Include="..\PowerDisplay.Models\PowerDisplay.Models.csproj" />
</ItemGroup>
<!-- Copy Assets folder to output directory -->
<ItemGroup>

View File

@@ -10,15 +10,20 @@
IsResizable="False"
IsTitleBarVisible="False"
mc:Ignorable="d">
<Grid Background="#1A000000">
<TextBlock
x:Name="NumberText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="Segoe UI"
FontSize="200"
FontWeight="Bold"
Foreground="White"
Text="1" />
<Grid Background="{ThemeResource SolidBackgroundFillColorBaseBrush}">
<Grid.Resources>
<Thickness x:Key="IdentifyWindowContentMargin">24</Thickness>
</Grid.Resources>
<Viewbox Margin="{StaticResource IdentifyWindowContentMargin}" Stretch="Uniform">
<TextBlock
x:Name="NumberText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="Bold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="1"
TextAlignment="Center" />
</Viewbox>
</Grid>
</winuiex:WindowEx>

View File

@@ -5,7 +5,8 @@
using System;
using System.Threading.Tasks;
using Microsoft.UI.Windowing;
using Windows.Graphics;
using PowerDisplay.Configuration;
using PowerDisplay.Helpers;
using WinUIEx;
namespace PowerDisplay.PowerDisplayXAML
@@ -13,13 +14,9 @@ namespace PowerDisplay.PowerDisplayXAML
/// <summary>
/// Interaction logic for IdentifyWindow.xaml
/// </summary>
public sealed partial class IdentifyWindow : WindowEx
public sealed partial class IdentifyWindow : WindowEx, IDisposable
{
// Window size in device-independent units (DIU)
private const int WindowWidthDiu = 300;
private const int WindowHeightDiu = 280;
private double _dpiScale = 1.0;
private DpiSuppressor? _dpiSuppressor;
public IdentifyWindow(string displayText)
{
@@ -33,13 +30,13 @@ namespace PowerDisplay.PowerDisplayXAML
{
// WinUI will throw if explorer is not running, safely ignore
}
catch (Exception)
{
}
// Configure window style
ConfigureWindow();
// Subclass WndProc to suppress WM_DPICHANGED during cross-DPI positioning
_dpiSuppressor = new DpiSuppressor(this);
// Auto close after 3 seconds
Task.Delay(3000).ContinueWith(_ =>
{
@@ -52,13 +49,8 @@ namespace PowerDisplay.PowerDisplayXAML
private void ConfigureWindow()
{
_dpiScale = this.GetDpiForWindow() / 96.0;
// Set window size scaled for DPI
// AppWindow.Resize expects physical pixels
int physicalWidth = (int)(WindowWidthDiu * _dpiScale);
int physicalHeight = (int)(WindowHeightDiu * _dpiScale);
this.AppWindow.Resize(new SizeInt32 { Width = physicalWidth, Height = physicalHeight });
// Set a preferred size in DIP. PositionOnDisplay will clamp it for the target monitor.
this.SetWindowSize(AppConstants.UI.IdentifyWindowPreferredWidthDip, AppConstants.UI.IdentifyWindowPreferredHeightDip);
this.IsAlwaysOnTop = true;
}
@@ -66,19 +58,42 @@ namespace PowerDisplay.PowerDisplayXAML
/// Position the window at the center of the specified display area
/// </summary>
public void PositionOnDisplay(DisplayArea displayArea)
{
var (windowWidthDip, windowHeightDip) = GetAdaptiveWindowSizeDip(displayArea);
// Suppress WM_DPICHANGED during MoveAndResize to prevent double-scaling
// when positioning on a monitor with different DPI than the primary.
using (_dpiSuppressor?.Suppress() ?? default)
{
WindowHelper.CenterWindowOnDisplay(this, displayArea, windowWidthDip, windowHeightDip);
}
}
private static (int WidthDip, int HeightDip) GetAdaptiveWindowSizeDip(DisplayArea displayArea)
{
var workArea = displayArea.WorkArea;
double dpiScale = WindowHelper.GetDpiScale(displayArea);
// Window size in physical pixels (already scaled for DPI)
int physicalWidth = (int)(WindowWidthDiu * _dpiScale);
int physicalHeight = (int)(WindowHeightDiu * _dpiScale);
int maxWidthDip = Math.Max(
AppConstants.UI.IdentifyWindowMinWidthDip,
WindowHelper.ScaleToDip((int)Math.Floor(workArea.Width * AppConstants.UI.IdentifyWindowMaxWorkAreaRatio), dpiScale));
int maxHeightDip = Math.Max(
AppConstants.UI.IdentifyWindowMinHeightDip,
WindowHelper.ScaleToDip((int)Math.Floor(workArea.Height * AppConstants.UI.IdentifyWindowMaxWorkAreaRatio), dpiScale));
// Calculate center position (WorkArea coordinates are in physical pixels)
int x = workArea.X + ((workArea.Width - physicalWidth) / 2);
int y = workArea.Y + ((workArea.Height - physicalHeight) / 2);
int widthDip = Math.Max(
AppConstants.UI.IdentifyWindowMinWidthDip,
Math.Min(AppConstants.UI.IdentifyWindowPreferredWidthDip, maxWidthDip));
int heightDip = Math.Max(
AppConstants.UI.IdentifyWindowMinHeightDip,
Math.Min(AppConstants.UI.IdentifyWindowPreferredHeightDip, maxHeightDip));
// Use WindowEx's AppWindow property
this.AppWindow.Move(new PointInt32(x, y));
return (widthDip, heightDip);
}
public void Dispose()
{
_dpiSuppressor?.Dispose();
}
}
}

View File

@@ -5,8 +5,7 @@
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:helpers="using:PowerDisplay.Helpers"
xmlns:local="using:PowerDisplay"
xmlns:models="using:PowerDisplay.Common.Models"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:models="using:PowerDisplay.Models"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:PowerDisplay.ViewModels"
xmlns:winuiex="using:WinUIEx"
@@ -25,13 +24,41 @@
IsTabStop="True"
TabFocusNavigation="Local">
<Grid.Resources>
<GridLength x:Key="FlyoutStatusBarHeight">48</GridLength>
<x:Double x:Key="FlyoutSeparatorHeight">1</x:Double>
<GridLength x:Key="FlyoutSliderIconColumnWidth">20</GridLength>
<x:Double x:Key="FlyoutActionGlyphFontSize">16</x:Double>
<x:Double x:Key="FlyoutActionGlyphSmallFontSize">14</x:Double>
<x:Double x:Key="FlyoutActionGlyphExtraSmallFontSize">12</x:Double>
<x:Double x:Key="FlyoutSecondaryTextFontSize">12</x:Double>
<x:Double x:Key="FlyoutRotationGlyphFontSize">14</x:Double>
<x:Double x:Key="FlyoutButtonSize">32</x:Double>
<x:Double x:Key="FlyoutCompactSpacing">4</x:Double>
<x:Double x:Key="FlyoutStandardSpacing">8</x:Double>
<x:Double x:Key="FlyoutMonitorSectionSpacing">32</x:Double>
<x:Double x:Key="FlyoutScanningIndicatorSize">24</x:Double>
<x:Double x:Key="FlyoutScanningSpacing">16</x:Double>
<Thickness x:Key="FlyoutButtonPadding">6</Thickness>
<Thickness x:Key="FlyoutScanningMargin">0,16,0,16</Thickness>
<Thickness x:Key="FlyoutContentPadding">16</Thickness>
<Thickness x:Key="FlyoutListHeaderMargin">16,0,8,0</Thickness>
<Thickness x:Key="FlyoutListItemPadding">0,4</Thickness>
<Thickness x:Key="FlyoutSelectionGlyphMargin">8,0,0,0</Thickness>
<Thickness x:Key="FlyoutSectionTopMargin">0,4,0,0</Thickness>
<Thickness x:Key="FlyoutSectionContainerPadding">8,0,8,8</Thickness>
<Thickness x:Key="FlyoutSeparatorMargin">8,8</Thickness>
<Thickness x:Key="FlyoutStatusBarMargin">4,0,4,0</Thickness>
<Thickness x:Key="MonitorHeaderOffsetMargin">10,0,0,0</Thickness>
<Thickness x:Key="MonitorNameBaselineMargin">2,0,0,2</Thickness>
<x:Double x:Key="FlyoutContextMenuMinWidth">240</x:Double>
<x:Double x:Key="FlyoutContextMenuMaxWidth">320</x:Double>
<Style
x:Key="FlyoutButtonStyle"
BasedOn="{StaticResource SubtleButtonStyle}"
TargetType="Button">
<Setter Property="Padding" Value="6" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="Padding" Value="{StaticResource FlyoutButtonPadding}" />
<Setter Property="Width" Value="{StaticResource FlyoutButtonSize}" />
<Setter Property="Height" Value="{StaticResource FlyoutButtonSize}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
</Style>
</Grid.Resources>
@@ -39,7 +66,7 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="48" />
<RowDefinition Height="{StaticResource FlyoutStatusBarHeight}" />
</Grid.RowDefinitions>
<!-- Main Content Area with modern design -->
@@ -50,22 +77,21 @@
BorderThickness="0,0,0,1">
<Grid>
<StackPanel
Margin="0,16,0,16"
Margin="{StaticResource FlyoutScanningMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="16"
Spacing="{StaticResource FlyoutScanningSpacing}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.IsScanning), Mode=OneWay}">
<ProgressRing
Width="24"
Height="24"
Width="{StaticResource FlyoutScanningIndicatorSize}"
Height="{StaticResource FlyoutScanningIndicatorSize}"
Foreground="{ThemeResource AccentFillColorDefaultBrush}"
IsActive="True" />
<TextBlock
x:Name="ScanningMonitorsTextBlock"
x:Uid="ScanningMonitorsText"
HorizontalAlignment="Center"
FontSize="12"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextAlignment="Center" />
</StackPanel>
@@ -77,94 +103,188 @@
IconSource="{ui:FontIconSource Glyph=&#xE7F4;}"
IsClosable="False"
IsOpen="{x:Bind ViewModel.ShowNoMonitorsMessage, Mode=OneWay}"
Severity="Informational"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.ShowNoMonitorsMessage), Mode=OneWay}" />
Severity="Informational" />
<!-- Content Area -->
<ScrollViewer
x:Name="MainScrollViewer"
Padding="16,16,16,16"
VerticalAlignment="Stretch"
Padding="{StaticResource FlyoutContentPadding}"
HorizontalScrollBarVisibility="Disabled"
HorizontalScrollMode="Disabled"
IsTabStop="False"
VerticalScrollBarVisibility="Auto"
ZoomMode="Disabled">
VerticalScrollBarVisibility="Auto">
<!-- Monitors List with modern card design -->
<ItemsRepeater
x:Name="MonitorsRepeater"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
TabFocusNavigation="Local"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.HasMonitors), Mode=OneWay}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" Spacing="32" />
<StackLayout Orientation="Vertical" Spacing="{StaticResource FlyoutMonitorSectionSpacing}" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="vm:MonitorViewModel">
<StackPanel
HorizontalAlignment="Stretch"
Spacing="4"
TabFocusNavigation="Local">
<StackPanel Spacing="{StaticResource FlyoutCompactSpacing}" TabFocusNavigation="Local">
<!-- Monitor Name with Icon -->
<Grid Margin="2,0,0,0" HorizontalAlignment="Stretch">
<Grid
Margin="{StaticResource MonitorHeaderOffsetMargin}"
HorizontalAlignment="Stretch"
ColumnSpacing="{StaticResource FlyoutStandardSpacing}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<local:MonitorIcon
VerticalAlignment="Center"
IsBuiltIn="{x:Bind IsInternal, Mode=OneWay}"
MonitorNumber="{x:Bind MonitorNumber, Mode=OneWay}" />
<TextBlock
Grid.Column="2"
Margin="0,0,0,2"
Grid.Column="1"
Margin="{StaticResource MonitorNameBaselineMargin}"
VerticalAlignment="Center"
Text="{x:Bind DisplayName, Mode=OneWay}" />
Text="{x:Bind DisplayName, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
ToolTipService.ToolTip="{x:Bind DisplayName, Mode=OneWay}" />
<!-- Icon buttons for InputSource and ColorTemperature -->
<StackPanel
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="4">
Spacing="{StaticResource FlyoutCompactSpacing}">
<!-- Color Temperature Button -->
<Button
x:Uid="ColorTemperatureTooltip"
Content="{ui:FontIcon Glyph=&#xE790;,
FontSize=16}"
FontSize={StaticResource FlyoutActionGlyphFontSize}}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowColorTemperature), Mode=OneWay}">
<Button.Flyout>
<Flyout Opened="Flyout_Opened" ShouldConstrainToRootBounds="False">
<StackPanel Orientation="Vertical">
<ListView
ItemsSource="{x:Bind AvailableColorPresets, Mode=OneWay}"
SelectionChanged="{x:Bind HandleColorTemperatureSelectionChanged}"
SelectionMode="Single">
<ListView.Header>
<TextBlock
x:Uid="ColorTemperatureHeader"
Margin="{StaticResource FlyoutListHeaderMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:ColorTemperatureItem">
<Grid Padding="{StaticResource FlyoutListItemPadding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
Text="{x:Bind DisplayName}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<FontIcon
Grid.Column="1"
Margin="{StaticResource FlyoutSelectionGlyphMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Glyph="&#xE73E;"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(IsSelected)}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
</Button.Flyout>
</Button>
<!-- More Button (Input Source + Power State) -->
<Button
x:Uid="MoreOptionsTooltip"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize={StaticResource FlyoutActionGlyphFontSize}}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowMoreButton), Mode=OneWay}">
<Button.Flyout>
<Flyout
Opened="Flyout_Opened"
Placement="BottomEdgeAlignedRight"
ShouldConstrainToRootBounds="False">
<StackPanel MinWidth="{StaticResource FlyoutContextMenuMinWidth}" MaxWidth="{StaticResource FlyoutContextMenuMaxWidth}">
<!-- Input Source Section -->
<ListView
ItemsSource="{x:Bind AvailableColorPresets, Mode=OneWay}"
SelectionChanged="ColorTemperatureListView_SelectionChanged"
SelectionMode="Single">
ItemsSource="{x:Bind AvailableInputSources, Mode=OneWay}"
SelectionChanged="{x:Bind HandleInputSourceSelectionChanged}"
SelectionMode="Single"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowInputSource), Mode=OneWay}">
<ListView.Header>
<TextBlock
x:Uid="ColorTemperatureHeader"
Margin="16,0,8,0"
FontSize="12"
x:Uid="InputSourceHeader"
Margin="{StaticResource FlyoutListHeaderMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:ColorTemperatureItem">
<Grid Padding="0,4">
<DataTemplate x:DataType="vm:InputSourceItem">
<Grid Padding="{StaticResource FlyoutListItemPadding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="{x:Bind DisplayName}" />
<TextBlock
VerticalAlignment="Center"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<FontIcon
Grid.Column="1"
Margin="8,0,0,0"
FontSize="12"
Margin="{StaticResource FlyoutSelectionGlyphMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Glyph="&#xE73E;"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(IsSelected)}" />
Visibility="{x:Bind SelectionVisibility}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<!-- Separator between Input Source and Power State -->
<Border
Height="{StaticResource FlyoutSeparatorHeight}"
Margin="{StaticResource FlyoutSeparatorMargin}"
Background="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowSeparatorAfterInputSource), Mode=OneWay}" />
<!-- Power State Section -->
<ListView
ItemsSource="{x:Bind AvailablePowerStates, Mode=OneWay}"
SelectionChanged="{x:Bind HandlePowerStateSelectionChanged}"
SelectionMode="Single"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowPowerState), Mode=OneWay}">
<ListView.Header>
<TextBlock
x:Uid="PowerStateHeader"
Margin="{StaticResource FlyoutListHeaderMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:PowerStateItem">
<Grid Padding="{StaticResource FlyoutListItemPadding}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
VerticalAlignment="Center"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<FontIcon
Grid.Column="1"
Margin="{StaticResource FlyoutSelectionGlyphMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Glyph="&#xE73E;"
Visibility="{x:Bind SelectionVisibility}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
@@ -173,101 +293,11 @@
</Flyout>
</Button.Flyout>
</Button>
<!-- More Button (Input Source + Power State) -->
<Button
x:Uid="MoreOptionsTooltip"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowMoreButton), Mode=OneWay}">
<Button.Flyout>
<Flyout
Opened="Flyout_Opened"
Placement="BottomEdgeAlignedRight"
ShouldConstrainToRootBounds="False">
<StackPanel
MinWidth="240"
MaxWidth="320"
Orientation="Vertical">
<!-- Input Source Section -->
<StackPanel Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowInputSource), Mode=OneWay}">
<ListView
ItemsSource="{x:Bind AvailableInputSources, Mode=OneWay}"
SelectionChanged="InputSourceListView_SelectionChanged"
SelectionMode="Single">
<ListView.Header>
<TextBlock
x:Uid="InputSourceHeader"
Margin="16,0,8,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:InputSourceItem">
<Grid Padding="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="{x:Bind Name}" />
<FontIcon
Grid.Column="1"
Margin="8,0,0,0"
FontSize="12"
Glyph="&#xE73E;"
Visibility="{x:Bind SelectionVisibility}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<!-- Separator between Input Source and Power State -->
<Border
Height="1"
Margin="8,8"
Background="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowSeparatorAfterInputSource), Mode=OneWay}" />
<!-- Power State Section -->
<StackPanel Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowPowerState), Mode=OneWay}">
<ListView
ItemsSource="{x:Bind AvailablePowerStates, Mode=OneWay}"
SelectionChanged="PowerStateListView_SelectionChanged"
SelectionMode="Single">
<ListView.Header>
<TextBlock
x:Uid="PowerStateHeader"
Margin="16,0,8,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:PowerStateItem">
<Grid Padding="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="{x:Bind Name}" />
<FontIcon
Grid.Column="1"
Margin="8,0,0,0"
FontSize="12"
Glyph="&#xE73E;"
Visibility="{x:Bind SelectionVisibility}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
<StackPanel
Margin="0,8,0,0"
Padding="8,0,16,8"
Margin="{StaticResource FlyoutSectionTopMargin}"
Padding="{StaticResource FlyoutSectionContainerPadding}"
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
@@ -275,190 +305,167 @@
TabFocusNavigation="Local"
XYFocusKeyboardNavigation="Enabled">
<!-- Brightness Control -->
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
<Grid Margin="{StaticResource FlyoutSectionTopMargin}" ColumnSpacing="{StaticResource FlyoutStandardSpacing}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="{StaticResource FlyoutSliderIconColumnWidth}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="BrightnessTooltip"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="18"
FontSize="{StaticResource FlyoutActionGlyphFontSize}"
Glyph="&#xEC8A;" />
<Slider
x:Uid="BrightnessAutomation"
Grid.Column="2"
HorizontalAlignment="Stretch"
Grid.Column="1"
VerticalAlignment="Center"
DataContext="{x:Bind}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
KeyUp="Slider_KeyUp"
KeyUp="{x:Bind HandleBrightnessKeyUp}"
Maximum="{x:Bind MaxBrightness, Mode=OneWay}"
Minimum="{x:Bind MinBrightness, Mode=OneWay}"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Brightness"
PointerCaptureLost="{x:Bind HandleBrightnessPointerCaptureLost}"
Value="{x:Bind Brightness, Mode=OneWay}" />
</Grid>
<!-- Contrast Control -->
<Grid
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Margin="{StaticResource FlyoutSectionTopMargin}"
ColumnSpacing="{StaticResource FlyoutStandardSpacing}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowContrast), Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="{StaticResource FlyoutSliderIconColumnWidth}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="ContrastTooltip"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="12"
FontSize="{StaticResource FlyoutActionGlyphExtraSmallFontSize}"
Glyph="&#xE7A1;" />
<Slider
x:Uid="ContrastAutomation"
Grid.Column="2"
Grid.Column="1"
VerticalAlignment="Center"
DataContext="{x:Bind}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
KeyUp="Slider_KeyUp"
KeyUp="{x:Bind HandleContrastKeyUp}"
Maximum="100"
Minimum="0"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Contrast"
PointerCaptureLost="{x:Bind HandleContrastPointerCaptureLost}"
Value="{x:Bind ContrastPercent, Mode=OneWay}" />
</Grid>
<!-- Volume Control -->
<Grid
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Margin="{StaticResource FlyoutSectionTopMargin}"
ColumnSpacing="{StaticResource FlyoutStandardSpacing}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowVolume), Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="{StaticResource FlyoutSliderIconColumnWidth}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="VolumeTooltip"
Margin="2,0,0,0"
Margin="4,0,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="16"
Glyph="&#xE993;" />
FontSize="{StaticResource FlyoutActionGlyphFontSize}"
Glyph="&#xE994;" />
<Slider
x:Uid="VolumeAutomation"
Grid.Column="2"
HorizontalAlignment="Stretch"
Grid.Column="1"
VerticalAlignment="Center"
DataContext="{x:Bind}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
KeyUp="Slider_KeyUp"
KeyUp="{x:Bind HandleVolumeKeyUp}"
Maximum="{x:Bind MaxVolume, Mode=OneWay}"
Minimum="{x:Bind MinVolume, Mode=OneWay}"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Volume"
PointerCaptureLost="{x:Bind HandleVolumePointerCaptureLost}"
Value="{x:Bind Volume, Mode=OneWay}" />
</Grid>
<!-- Rotation Controls -->
<Grid
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Margin="{StaticResource FlyoutSectionTopMargin}"
ColumnSpacing="{StaticResource FlyoutStandardSpacing}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowRotation), Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="{StaticResource FlyoutSliderIconColumnWidth}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="RotationTooltip"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="16"
FontSize="{StaticResource FlyoutActionGlyphSmallFontSize}"
Glyph="&#xE7AD;" />
<Grid
Grid.Column="2"
<ComboBox
Grid.Column="1"
HorizontalAlignment="Stretch"
TabFocusNavigation="Local"
XYFocusKeyboardNavigation="Enabled">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Normal (0°) -->
<ToggleButton
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
SelectedIndex="{x:Bind SelectedRotationIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="RotationOptionLandscape" />
<ComboBoxItem x:Uid="RotationOptionPortrait" />
<ComboBoxItem x:Uid="RotationOptionLandscapeFlipped" />
<ComboBoxItem x:Uid="RotationOptionPortraitFlipped" />
</ComboBox>
<!-- Normal (0°) -->
<!--<ToggleButton
x:Uid="RotateNormalTooltip"
Grid.Column="0"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
Click="{x:Bind HandleRotation0Click}"
IsChecked="{x:Bind IsRotation0, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
Tag="0">
<FontIcon FontSize="14" Glyph="&#xE74A;" />
IsTabStop="True">
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="&#xE74A;" />
</ToggleButton>
<!-- Left (270°) -->
-->
<!-- Left (270°) -->
<!--
<ToggleButton
x:Uid="RotateLeftTooltip"
Grid.Column="2"
Grid.Column="1"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
Click="{x:Bind HandleRotation3Click}"
IsChecked="{x:Bind IsRotation3, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
Tag="3">
<FontIcon FontSize="14" Glyph="&#xE76B;" />
IsTabStop="True">
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="&#xE76B;" />
</ToggleButton>
<!-- Right (90°) -->
-->
<!-- Right (90°) -->
<!--
<ToggleButton
x:Uid="RotateRightTooltip"
Grid.Column="4"
Grid.Column="2"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
Click="{x:Bind HandleRotation1Click}"
IsChecked="{x:Bind IsRotation1, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
Tag="1">
<FontIcon FontSize="14" Glyph="&#xE76C;" />
IsTabStop="True">
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="&#xE76C;" />
</ToggleButton>
<!-- Inverted (180°) -->
-->
<!-- Inverted (180°) -->
<!--
<ToggleButton
x:Uid="RotateInvertedTooltip"
Grid.Column="6"
Grid.Column="3"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
Click="{x:Bind HandleRotation2Click}"
IsChecked="{x:Bind IsRotation2, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
IsTabStop="True"
Tag="2">
<FontIcon FontSize="14" Glyph="&#xE74B;" />
</ToggleButton>
</Grid>
IsTabStop="True">
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="&#xE74B;" />
</ToggleButton>-->
</Grid>
</StackPanel>
</StackPanel>
@@ -469,72 +476,86 @@
</Grid>
</Border>
<Grid x:Name="StatusBar" Grid.Row="1">
<Grid
x:Name="StatusBar"
Grid.Row="1"
Padding="{StaticResource FlyoutStatusBarMargin}">
<!-- Action Buttons -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button
x:Name="ProfilesButton"
x:Uid="ProfilesTooltip"
Content="{ui:FontIcon Glyph=&#xE748;,
FontSize={StaticResource FlyoutActionGlyphFontSize}}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.ShowProfileSwitcherButton), Mode=OneWay}">
<Button.Flyout>
<Flyout
x:Name="ProfilesFlyout"
Opened="Flyout_Opened"
ShouldConstrainToRootBounds="False">
<ListView
x:Name="ProfilesListView"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}"
SelectionChanged="ProfileListView_SelectionChanged"
SelectionMode="Single">
<ListView.Header>
<TextBlock
x:Uid="ProfilesHeader"
Margin="{StaticResource FlyoutListHeaderMargin}"
FontSize="{StaticResource FlyoutSecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:PowerDisplayProfile">
<TextBlock
Padding="{StaticResource FlyoutListItemPadding}"
Text="{x:Bind Name}"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
</Button.Flyout>
</Button>
<StackPanel
Margin="0,0,8,8"
Grid.Column="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
Spacing="{StaticResource FlyoutStandardSpacing}">
<Button
x:Name="ProfilesButton"
x:Uid="ProfilesTooltip"
Content="{ui:FontIcon Glyph=&#xE748;,
x:Uid="RefreshTooltip"
Content="{ui:FontIcon Glyph=&#xE777;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.ShowProfileSwitcherButton), Mode=OneWay}">
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<Flyout
x:Name="ProfilesFlyout"
Opened="Flyout_Opened"
ShouldConstrainToRootBounds="False">
<ListView
x:Name="ProfilesListView"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}"
SelectionChanged="ProfileListView_SelectionChanged"
SelectionMode="Single">
<ListView.Header>
<TextBlock
x:Uid="ProfilesHeader"
Margin="16,0,8,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:PowerDisplayProfile">
<TextBlock Padding="0,4" Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Name="RefreshButton"
x:Uid="RefreshText"
Click="OnRefreshClick"
Icon="{ui:FontIcon Glyph=&#xE777;,
FontSize=16}" />
<MenuFlyoutItem
x:Name="IdentifyButton"
x:Uid="IdentifyText"
Command="{x:Bind ViewModel.IdentifyMonitorsCommand}"
Icon="{ui:FontIcon Glyph=&#xE71E;}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.ShowIdentifyMonitorsButton), Mode=OneWay}" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button
x:Name="RefreshButton"
x:Uid="RefreshTooltip"
Click="OnRefreshClick"
Content="{ui:FontIcon Glyph=&#xE72C;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
<Button
x:Name="IdentifyButton"
x:Uid="IdentifyTooltip"
Command="{x:Bind ViewModel.IdentifyMonitorsCommand}"
Content="{ui:FontIcon Glyph=&#xE9D9;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.ShowIdentifyMonitorsButton), Mode=OneWay}" />
<Button
x:Name="SettingsBtn"
x:Uid="SettingsTooltip"
Padding="6"
Click="OnSettingsClick"
Style="{StaticResource FlyoutButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SettingsTooltip" />
</ToolTipService.ToolTip>
<AnimatedIcon x:Name="SearchAnimatedIcon">
<AnimatedIcon x:Name="SettingsAnimatedIcon">
<AnimatedIcon.Source>
<animatedVisuals:AnimatedSettingsVisualSource />
</AnimatedIcon.Source>

View File

@@ -4,18 +4,16 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using PowerDisplay.Common.Models;
using PowerDisplay.Configuration;
using PowerDisplay.Helpers;
using PowerDisplay.Models;
using PowerDisplay.ViewModels;
using Windows.Graphics;
using WinUIEx;
@@ -32,6 +30,7 @@ namespace PowerDisplay
private readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
private MainViewModel? _viewModel;
private HotkeyService? _hotkeyService;
private DpiSuppressor? _dpiSuppressor;
// Expose ViewModel as property for x:Bind
public MainViewModel ViewModel => _viewModel ?? throw new InvalidOperationException("ViewModel not initialized");
@@ -65,10 +64,15 @@ namespace PowerDisplay
Logger.LogTrace("MainWindow constructor: Event handlers registered");
// 5. Initialize HotkeyService for in-process hotkey handling (CmdPal pattern)
// This avoids IPC timing issues with Runner's centralized hotkey mechanism
Logger.LogTrace("MainWindow constructor: Initializing HotkeyService");
var hwnd = this.GetWindowHandle();
_hotkeyService = new HotkeyService(_settingsUtils, ToggleWindow);
_hotkeyService.Initialize(this);
_hotkeyService.Initialize(hwnd);
// 6. Subclass WndProc for hotkey handling and DPI change suppression
Logger.LogTrace("MainWindow constructor: Setting up DpiSuppressor");
_dpiSuppressor = new DpiSuppressor(this, (uMsg, wParam, _) =>
_hotkeyService?.HandleMessage(uMsg, wParam) == true);
Logger.LogTrace("MainWindow constructor: HotkeyService initialized");
Logger.LogTrace("MainWindow constructor: Setting IsShownInSwitchers property");
@@ -118,6 +122,7 @@ namespace PowerDisplay
}
private bool _hasInitialized;
private bool _isShowingWindow;
private void ShowError(string message)
{
@@ -126,10 +131,12 @@ namespace PowerDisplay
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
{
Logger.LogTrace($"OnWindowActivated: WindowActivationState={args.WindowActivationState}");
Logger.LogTrace($"OnWindowActivated: WindowActivationState={args.WindowActivationState}, _isShowingWindow={_isShowingWindow}");
// Auto-hide window when it loses focus (deactivated)
if (args.WindowActivationState == WindowActivationState.Deactivated)
// But NOT during ShowWindow() sequence — the first Activate() for positioning
// may briefly deactivate before the final Activate()/Show() brings it back.
if (args.WindowActivationState == WindowActivationState.Deactivated && !_isShowingWindow)
{
Logger.LogInfo("OnWindowActivated: Window deactivated, hiding window");
HideWindow();
@@ -146,6 +153,7 @@ namespace PowerDisplay
public void ShowWindow()
{
Logger.LogInfo($"ShowWindow: Called, _hasInitialized={_hasInitialized}");
_isShowingWindow = true;
try
{
// If not initialized, log warning but continue showing
@@ -154,55 +162,36 @@ namespace PowerDisplay
Logger.LogWarning("ShowWindow: Window not fully initialized yet, showing anyway");
}
// Adjust size BEFORE showing to prevent flicker
// This measures content and positions window at correct size
Logger.LogTrace("ShowWindow: Adjusting window size to content");
// Adjust size and position on the correct monitor
// (DPI change suppression is handled inside AdjustWindowSizeToContent)
AdjustWindowSizeToContent();
// CRITICAL: WinUI3 windows must be Activated at least once to display properly.
// In PowerToys mode, window is created but never activated until first show.
// Without Activate(), Show() may not actually render the window on screen.
Logger.LogTrace("ShowWindow: Calling this.Activate()");
this.Activate();
// Now show the window - it should appear at the correct size
Logger.LogTrace("ShowWindow: Calling this.Show()");
this.Show();
// Ensure window stays on top of other windows
this.IsAlwaysOnTop = true;
Logger.LogTrace("ShowWindow: IsAlwaysOnTop set to true");
// Ensure window gets keyboard focus using WinUIEx's BringToFront
// This is necessary for Tab navigation to work without clicking first
// Ensure window gets keyboard focus
this.BringToFront();
Logger.LogTrace("ShowWindow: BringToFront called");
// Clear focus from any interactive element (e.g., Slider) to prevent
// showing the value tooltip when the window opens
RootGrid.Focus(FocusState.Programmatic);
// Verify window is visible
bool isVisible = IsWindowVisible();
Logger.LogInfo($"ShowWindow: Window visibility after show: {isVisible}");
if (!isVisible)
{
Logger.LogError("ShowWindow: Window not visible after show attempt, forcing visibility");
this.Activate();
this.Show();
this.BringToFront();
Logger.LogInfo($"ShowWindow: After forced show, visibility: {IsWindowVisible()}");
}
else
{
Logger.LogInfo("ShowWindow: Window shown successfully");
}
Logger.LogInfo($"ShowWindow: Window shown successfully, visibility={IsWindowVisible()}");
}
catch (Exception ex)
{
Logger.LogError($"ShowWindow: Failed to show window: {ex.Message}\n{ex.StackTrace}");
throw;
}
finally
{
_isShowingWindow = false;
}
}
public void HideWindow()
@@ -323,9 +312,9 @@ namespace PowerDisplay
// Window properties (IsResizable, IsMaximizable, IsMinimizable,
// IsTitleBarVisible, IsShownInSwitchers) are set in XAML
// Set minimal initial window size - will be adjusted before showing
// Using minimal height to prevent "large window shrinking" flicker
this.AppWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = 100 });
// Set a minimal initial window size in DIP - it will be adjusted before showing.
// Using minimal height prevents the "large window shrinking" flicker.
this.SetWindowSize(AppConstants.UI.WindowWidthDip, AppConstants.UI.WindowMinHeightDip);
// Position window at bottom right corner
PositionWindowAtBottomRight();
@@ -379,14 +368,26 @@ namespace PowerDisplay
// Force layout update and measure content height
RootGrid.UpdateLayout();
MainContainer?.Measure(new Windows.Foundation.Size(AppConstants.UI.WindowWidth, double.PositiveInfinity));
MainContainer?.Measure(new Windows.Foundation.Size(AppConstants.UI.WindowWidthDip, double.PositiveInfinity));
var contentHeight = (int)Math.Ceiling(MainContainer?.DesiredSize.Height ?? 0);
var maxHeightDip = GetAdaptiveWindowMaxHeightDip();
// Apply min/max height limits and reposition (WindowEx handles DPI automatically)
// Apply min/max height limits and reposition using DIP values.
// Min height ensures window is visible even if content hasn't loaded yet
var finalHeight = Math.Max(AppConstants.UI.MinWindowHeight, Math.Min(contentHeight, AppConstants.UI.MaxWindowHeight));
Logger.LogTrace($"AdjustWindowSizeToContent: contentHeight={contentHeight}, finalHeight={finalHeight}");
WindowHelper.PositionWindowBottomRight(this, AppConstants.UI.WindowWidth, finalHeight, AppConstants.UI.WindowRightMargin);
var finalHeightDip = Math.Min(contentHeight, maxHeightDip);
Logger.LogTrace($"AdjustWindowSizeToContent: contentHeight={contentHeight}, maxHeightDip={maxHeightDip}, finalHeightDip={finalHeightDip}");
// Suppress WM_DPICHANGED during MoveAndResize to prevent double-scaling
// when moving across monitors with different DPI settings.
using (_dpiSuppressor?.Suppress() ?? default)
{
WindowHelper.PositionWindowBottomRight(
this,
AppConstants.UI.WindowWidthDip,
finalHeightDip,
AppConstants.UI.WindowRightMarginDip,
AppConstants.UI.WindowBottomMarginDip);
}
}
catch (Exception ex)
{
@@ -394,16 +395,48 @@ namespace PowerDisplay
}
}
private static int GetAdaptiveWindowMaxHeightDip()
{
if (!WindowHelper.TryGetDisplayAreaAtCursor(out var displayArea) || displayArea is null)
{
return AppConstants.UI.WindowMaxHeightDip;
}
double dpiScale = WindowHelper.GetDpiScale(displayArea);
int workAreaHeightDip = WindowHelper.ScaleToDip(displayArea.WorkArea.Height, dpiScale);
return (int)Math.Floor(workAreaHeightDip * AppConstants.UI.WindowMaxWorkAreaHeightRatio);
}
private static double GetAdaptiveFlyoutMaxWidthDip()
{
if (!WindowHelper.TryGetDisplayAreaAtCursor(out var displayArea) || displayArea is null)
{
return AppConstants.UI.FlyoutContextMenuMaxWidthDip;
}
double dpiScale = WindowHelper.GetDpiScale(displayArea);
int workAreaWidthDip = WindowHelper.ScaleToDip(displayArea.WorkArea.Width, dpiScale);
double adaptiveMaxWidthDip = Math.Floor(workAreaWidthDip * AppConstants.UI.FlyoutContextMenuMaxWorkAreaWidthRatio);
return Math.Max(
AppConstants.UI.FlyoutContextMenuMaxWidthDip,
Math.Min(AppConstants.UI.FlyoutContextMenuAdaptiveMaxWidthDip, adaptiveMaxWidthDip));
}
private void PositionWindowAtBottomRight()
{
try
{
var windowSize = this.AppWindow.Size;
var windowHeightDip = this.Height > 0
? (int)Math.Ceiling(this.Height)
: AppConstants.UI.WindowMinHeightDip;
WindowHelper.PositionWindowBottomRight(
this, // MainWindow inherits from WindowEx
AppConstants.UI.WindowWidth,
windowSize.Height,
AppConstants.UI.WindowRightMargin);
AppConstants.UI.WindowWidthDip,
windowHeightDip,
AppConstants.UI.WindowRightMarginDip,
AppConstants.UI.WindowBottomMarginDip);
}
catch (Exception)
{
@@ -411,207 +444,6 @@ namespace PowerDisplay
}
}
/// <summary>
/// Slider PointerCaptureLost event handler - updates ViewModel when drag completes
/// This is the WinUI3 recommended way to detect drag completion
/// </summary>
private void Slider_PointerCaptureLost(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
var slider = sender as Slider;
if (slider == null)
{
return;
}
var propertyName = slider.Tag as string;
var monitorVm = slider.DataContext as MonitorViewModel;
if (monitorVm == null || propertyName == null)
{
return;
}
// Get final value after drag completes
int finalValue = (int)slider.Value;
// Now update the ViewModel, which will trigger hardware operation
switch (propertyName)
{
case "Brightness":
monitorVm.Brightness = finalValue;
break;
case "Contrast":
monitorVm.ContrastPercent = finalValue;
break;
case "Volume":
monitorVm.Volume = finalValue;
break;
}
}
/// <summary>
/// Slider KeyUp event handler - updates ViewModel when arrow keys are released
/// This handles keyboard navigation for accessibility
/// </summary>
private void Slider_KeyUp(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
{
// Only handle arrow keys (Left, Right, Up, Down)
if (e.Key != Windows.System.VirtualKey.Left &&
e.Key != Windows.System.VirtualKey.Right &&
e.Key != Windows.System.VirtualKey.Up &&
e.Key != Windows.System.VirtualKey.Down)
{
return;
}
var slider = sender as Slider;
if (slider == null)
{
return;
}
var propertyName = slider.Tag as string;
var monitorVm = slider.DataContext as MonitorViewModel;
if (monitorVm == null || propertyName == null)
{
return;
}
// Get the current value after key press
int finalValue = (int)slider.Value;
// Update the ViewModel, which will trigger hardware operation
switch (propertyName)
{
case "Brightness":
monitorVm.Brightness = finalValue;
break;
case "Contrast":
monitorVm.ContrastPercent = finalValue;
break;
case "Volume":
monitorVm.Volume = finalValue;
break;
}
}
/// <summary>
/// Input source ListView selection changed handler - switches the monitor input source
/// </summary>
private async void InputSourceListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView)
{
return;
}
// Get the selected input source item
var selectedItem = listView.SelectedItem as InputSourceItem;
if (selectedItem == null)
{
return;
}
Logger.LogInfo($"[UI] InputSourceListView_SelectionChanged: Selected {selectedItem.Name} (0x{selectedItem.Value:X2}) for monitor {selectedItem.MonitorId}");
// Find the monitor by ID
MonitorViewModel? monitorVm = null;
if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
{
monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
}
if (monitorVm == null)
{
Logger.LogWarning("[UI] InputSourceListView_SelectionChanged: Could not find MonitorViewModel");
return;
}
// Set the input source
await monitorVm.SetInputSourceAsync(selectedItem.Value);
}
/// <summary>
/// Power state ListView selection changed handler - switches the monitor power state.
/// Note: Selecting any state other than "On" will turn off the display.
/// </summary>
private async void PowerStateListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView)
{
return;
}
// Get the selected power state item
var selectedItem = listView.SelectedItem as PowerStateItem;
if (selectedItem == null)
{
return;
}
// Skip if "On" is selected - the monitor is already on
if (selectedItem.Value == PowerStateItem.PowerStateOn)
{
return;
}
Logger.LogInfo($"[UI] PowerStateListView_SelectionChanged: Selected {selectedItem.Name} (0x{selectedItem.Value:X2}) for monitor {selectedItem.MonitorId}");
// Find the monitor by ID
MonitorViewModel? monitorVm = null;
if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
{
monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
}
if (monitorVm == null)
{
Logger.LogWarning("[UI] PowerStateListView_SelectionChanged: Could not find MonitorViewModel");
return;
}
// Set the power state - this will turn off the display
await monitorVm.SetPowerStateAsync(selectedItem.Value);
}
/// <summary>
/// Rotation button click handler - changes monitor orientation
/// </summary>
private async void RotationButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Microsoft.UI.Xaml.Controls.Primitives.ToggleButton toggleButton)
{
return;
}
// Get the orientation from the Tag
if (toggleButton.Tag is not string tagStr || !int.TryParse(tagStr, out int orientation))
{
Logger.LogWarning("[UI] RotationButton_Click: Invalid Tag");
return;
}
var monitorVm = toggleButton.DataContext as MonitorViewModel;
if (monitorVm == null)
{
Logger.LogWarning("[UI] RotationButton_Click: Could not find MonitorViewModel");
return;
}
// If clicking the current orientation, restore the checked state and do nothing
if (monitorVm.CurrentRotation == orientation)
{
toggleButton.IsChecked = true;
return;
}
Logger.LogInfo($"[UI] RotationButton_Click: Setting rotation for {monitorVm.Name} to {orientation}");
// Set the rotation
await monitorVm.SetRotationAsync(orientation);
}
/// <summary>
/// Profile selection changed handler - applies the selected profile
/// </summary>
@@ -643,44 +475,6 @@ namespace PowerDisplay
listView.SelectedItem = null;
}
/// <summary>
/// Color temperature selection changed handler - applies the selected color temperature preset
/// </summary>
private async void ColorTemperatureListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView)
{
return;
}
var selectedItem = listView.SelectedItem as ColorTemperatureItem;
if (selectedItem == null)
{
return;
}
Logger.LogInfo($"[UI] ColorTemperatureListView_SelectionChanged: Selected {selectedItem.DisplayName} (0x{selectedItem.VcpValue:X2}) for monitor {selectedItem.MonitorId}");
// Find the monitor by ID
MonitorViewModel? monitorVm = null;
if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
{
monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
}
if (monitorVm == null)
{
Logger.LogWarning("[UI] ColorTemperatureListView_SelectionChanged: Could not find MonitorViewModel");
return;
}
// Apply the color temperature
await monitorVm.SetColorTemperatureAsync(selectedItem.VcpValue);
// Clear selection to allow reselecting the same preset
listView.SelectedItem = null;
}
/// <summary>
/// Flyout opened event handler - sets focus to the first focusable element inside the flyout.
/// This enables keyboard navigation when the flyout opens.
@@ -689,6 +483,8 @@ namespace PowerDisplay
{
if (sender is Flyout flyout && flyout.Content is FrameworkElement content)
{
content.MaxWidth = GetAdaptiveFlyoutMaxWidthDip();
// Use DispatcherQueue to ensure the flyout content is fully rendered before setting focus
DispatcherQueue.TryEnqueue(() =>
{
@@ -704,6 +500,7 @@ namespace PowerDisplay
public void Dispose()
{
_hotkeyService?.Dispose();
_dpiSuppressor?.Dispose();
_viewModel?.Dispose();
GC.SuppressFinalize(this);
}

View File

@@ -1,49 +1,55 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="PowerDisplay.MonitorIcon"
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:local="using:PowerDisplay"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
AutomationProperties.AccessibilityView="Raw"
IsTabStop="False"
mc:Ignorable="d">
<UserControl.Resources>
<x:Double x:Key="MonitorGlyphFontSize">22</x:Double>
<x:Double x:Key="MonitorNumberFontSize">10</x:Double>
<Thickness x:Key="ExternalMonitorNumberMargin">0,0,0,4</Thickness>
<Thickness x:Key="BuiltInMonitorNumberMargin">0,0,0,6</Thickness>
</UserControl.Resources>
<Grid>
<Viewbox>
<!-- Decorative badge: keep fixed text scaling for the glyph overlay and rely on adjacent DisplayName text for accessible monitor numbering. -->
<Viewbox Width="16">
<Grid>
<Grid x:Name="MonitorGrid">
<Grid x:Name="MonitorGrid" x:Uid="ExternalMonitorTooltip">
<FontIcon
x:Uid="MonitorTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="22"
FontSize="{StaticResource MonitorGlyphFontSize}"
Glyph="&#xE7F4;" />
<TextBlock
Margin="0,0,0,4"
Margin="{StaticResource ExternalMonitorNumberMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
FontSize="{StaticResource MonitorNumberFontSize}"
FontWeight="SemiBold"
IsTextScaleFactorEnabled="False"
Text="{x:Bind MonitorNumber, Mode=OneWay}" />
</Grid>
<Grid
x:Name="BuiltInDisplayGrid"
Padding="0,0,0,-4"
x:Uid="BuiltInMonitorTooltip"
Visibility="Collapsed">
<FontIcon
x:Uid="MonitorTooltip"
Margin="0,0,0,0"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="22"
FontSize="{StaticResource MonitorGlyphFontSize}"
Glyph="&#xE7FB;" />
<TextBlock
Margin="0,0,0,6"
Margin="{StaticResource BuiltInMonitorNumberMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
FontSize="{StaticResource MonitorNumberFontSize}"
FontWeight="SemiBold"
IsTextScaleFactorEnabled="False"
Text="{x:Bind MonitorNumber, Mode=OneWay}" />
</Grid>
</Grid>

View File

@@ -34,6 +34,13 @@ namespace PowerDisplay
// Initialize COM wrappers first (needed for AppInstance)
WinRT.ComWrappersSupport.InitializeComWrappers();
// Exit before instance registration so a blocked launch cannot redirect
// activation to an already-running instance.
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredPowerDisplayEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
return 0;
}
// Single instance check BEFORE logger initialization to avoid creating extra log files
// Command Palette pattern: check for existing instance first
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();

View File

@@ -7,6 +7,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
namespace PowerDisplay.Serialization
{

View File

@@ -24,8 +24,11 @@
<data name="SettingsTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
<data name="ExternalMonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Monitor</value>
</data>
<data name="BuiltInMonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Built-in display</value>
</data>
<data name="BrightnessTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Brightness</value>
@@ -50,6 +53,18 @@
</data>
<data name="RotateInvertedTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Inverted (180°)</value>
</data>
<data name="RotationOptionLandscape.Content" xml:space="preserve">
<value>Landscape</value>
</data>
<data name="RotationOptionPortrait.Content" xml:space="preserve">
<value>Portrait</value>
</data>
<data name="RotationOptionLandscapeFlipped.Content" xml:space="preserve">
<value>Landscape (flipped)</value>
</data>
<data name="RotationOptionPortraitFlipped.Content" xml:space="preserve">
<value>Portrait (flipped)</value>
</data>
<data name="VolumeAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Volume</value>
@@ -72,8 +87,11 @@
<data name="ProfilesTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Quick apply profiles</value>
</data>
<data name="IdentifyTooltip.ToolTipService.ToolTip" xml:space="preserve">
<data name="IdentifyText.Text" xml:space="preserve">
<value>Identify monitors</value>
</data>
<data name="RefreshText.Text" xml:space="preserve">
<value>Refresh</value>
</data>
<data name="InputSourceHeader.Text" xml:space="preserve">
<value>Input source</value>

View File

@@ -12,6 +12,7 @@ using Microsoft.PowerToys.Telemetry;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
using PowerDisplay.Serialization;
using PowerDisplay.Services;
using PowerDisplay.Telemetry.Events;

View File

@@ -5,13 +5,12 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -23,6 +22,7 @@ using PowerDisplay.Common.Drivers.DDC;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Helpers;
using PowerDisplay.Models;
using PowerDisplay.PowerDisplayXAML;
namespace PowerDisplay.ViewModels;
@@ -35,7 +35,7 @@ namespace PowerDisplay.ViewModels;
/// - MainViewModel.Settings.cs: Settings UI synchronization and profiles
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
public partial class MainViewModel : INotifyPropertyChanged, IDisposable
public partial class MainViewModel : ObservableObject, IDisposable
{
[LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -48,12 +48,23 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
private readonly MonitorStateManager _stateManager;
private readonly DisplayChangeWatcher _displayChangeWatcher;
private ObservableCollection<MonitorViewModel> _monitors;
private ObservableCollection<PowerDisplayProfile> _profiles;
private bool _isScanning;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasMonitors))]
[NotifyPropertyChangedFor(nameof(ShowNoMonitorsMessage))]
[NotifyPropertyChangedFor(nameof(IsInteractionEnabled))]
public partial bool IsScanning { get; set; }
private bool _isInitialized;
private bool _isLoading;
[ObservableProperty]
public partial ObservableCollection<MonitorViewModel> Monitors { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasProfiles))]
[NotifyPropertyChangedFor(nameof(ShowProfileSwitcherButton))]
public partial ObservableCollection<PowerDisplayProfile> Profiles { get; set; }
/// <summary>
/// Event triggered when UI refresh is requested due to settings changes
/// </summary>
@@ -69,9 +80,11 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_cancellationTokenSource = new CancellationTokenSource();
_monitors = new ObservableCollection<MonitorViewModel>();
_profiles = new ObservableCollection<PowerDisplayProfile>();
_isScanning = true;
Monitors = new ObservableCollection<MonitorViewModel>();
Profiles = new ObservableCollection<PowerDisplayProfile>();
IsScanning = true;
ShowProfileSwitcher = true;
ShowIdentifyMonitorsButton = true;
// Initialize settings utils
_settingsUtils = SettingsUtils.Default;
@@ -97,71 +110,21 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
_ = InitializeAsync(_cancellationTokenSource.Token);
}
public ObservableCollection<MonitorViewModel> Monitors
{
get => _monitors;
set
{
_monitors = value;
OnPropertyChanged();
}
}
public ObservableCollection<PowerDisplayProfile> Profiles
{
get => _profiles;
set
{
_profiles = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasProfiles));
}
}
public bool HasProfiles => Profiles.Count > 0;
// UI display control properties - loaded from settings
private bool _showProfileSwitcher = true;
private bool _showIdentifyMonitorsButton = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowProfileSwitcherButton))]
public partial bool ShowProfileSwitcher { get; set; }
[ObservableProperty]
public partial bool ShowIdentifyMonitorsButton { get; set; }
/// <summary>
/// Gets a value indicating whether to show the profile switcher button.
/// Combines settings value with HasProfiles check.
/// </summary>
public bool ShowProfileSwitcherButton => _showProfileSwitcher && HasProfiles;
/// <summary>
/// Gets or sets a value indicating whether to show the profile switcher (from settings).
/// </summary>
public bool ShowProfileSwitcher
{
get => _showProfileSwitcher;
set
{
if (_showProfileSwitcher != value)
{
_showProfileSwitcher = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowProfileSwitcherButton));
}
}
}
/// <summary>
/// Gets or sets a value indicating whether to show the identify monitors button.
/// </summary>
public bool ShowIdentifyMonitorsButton
{
get => _showIdentifyMonitorsButton;
set
{
if (_showIdentifyMonitorsButton != value)
{
_showIdentifyMonitorsButton = value;
OnPropertyChanged();
}
}
}
public bool ShowProfileSwitcherButton => ShowProfileSwitcher && HasProfiles;
// Custom VCP mappings - loaded from settings
private List<CustomVcpValueMapping> _customVcpMappings = new();
@@ -180,24 +143,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
}
public bool IsScanning
{
get => _isScanning;
set
{
if (_isScanning != value)
{
_isScanning = value;
OnPropertyChanged();
// Dependent properties that change with IsScanning
OnPropertyChanged(nameof(HasMonitors));
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
OnPropertyChanged(nameof(IsInteractionEnabled));
}
}
}
public bool HasMonitors => !IsScanning && Monitors.Count > 0;
public bool ShowNoMonitorsMessage => !IsScanning && Monitors.Count == 0;
@@ -285,8 +230,8 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
// Create and position identify window
var identifyWindow = new IdentifyWindow(displayText);
identifyWindow.PositionOnDisplay(displayArea);
identifyWindow.Activate();
identifyWindow.PositionOnDisplay(displayArea);
windowsCreated++;
}
}
@@ -305,13 +250,6 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void Dispose()
{
// Cancel all async operations first
@@ -381,10 +319,10 @@ public partial class MainViewModel : INotifyPropertyChanged, IDisposable
try
{
var profilesData = ProfileService.LoadProfiles();
_profiles.Clear();
Profiles.Clear();
foreach (var profile in profilesData.Profiles)
{
_profiles.Add(profile);
Profiles.Add(profile);
}
OnPropertyChanged(nameof(HasProfiles));

View File

@@ -6,16 +6,20 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using PowerDisplay.Common.Models;
using PowerDisplay.Configuration;
using PowerDisplay.Helpers;
using PowerDisplay.Models;
using Windows.System;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.ViewModels;
@@ -23,7 +27,7 @@ namespace PowerDisplay.ViewModels;
/// <summary>
/// ViewModel for individual monitor
/// </summary>
public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
public partial class MonitorViewModel : ObservableObject, IDisposable
{
private readonly Monitor _monitor;
private readonly MonitorManager _monitorManager;
@@ -32,13 +36,27 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
private int _brightness;
private int _contrast;
private int _volume;
private bool _isAvailable;
[ObservableProperty]
public partial bool IsAvailable { get; set; }
// Visibility settings (controlled by Settings UI)
private bool _showContrast;
private bool _showVolume;
private bool _showInputSource;
private bool _showRotation;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasAdvancedControls))]
public partial bool ShowContrast { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasAdvancedControls))]
public partial bool ShowVolume { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowMoreButton))]
[NotifyPropertyChangedFor(nameof(ShowSeparatorAfterInputSource))]
public partial bool ShowInputSource { get; set; }
[ObservableProperty]
public partial bool ShowRotation { get; set; }
private bool _showPowerState;
/// <summary>
@@ -205,9 +223,9 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_monitor.PropertyChanged += OnMonitorPropertyChanged;
// Initialize Show properties based on hardware capabilities
_showContrast = monitor.SupportsContrast;
_showVolume = monitor.SupportsVolume;
_showInputSource = monitor.SupportsInputSource;
ShowContrast = monitor.SupportsContrast;
ShowVolume = monitor.SupportsVolume;
ShowInputSource = monitor.SupportsInputSource;
_showPowerState = monitor.SupportsPowerState;
_showColorTemperature = monitor.SupportsColorTemperature;
@@ -215,7 +233,7 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
_brightness = monitor.CurrentBrightness;
_contrast = monitor.CurrentContrast;
_volume = monitor.CurrentVolume;
_isAvailable = monitor.IsAvailable;
IsAvailable = monitor.IsAvailable;
}
public string Id => _monitor.Id;
@@ -289,48 +307,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// </summary>
public bool SupportsVolume => _monitor.SupportsVolume;
public bool ShowContrast
{
get => _showContrast;
set
{
if (_showContrast != value)
{
_showContrast = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public bool ShowVolume
{
get => _showVolume;
set
{
if (_showVolume != value)
{
_showVolume = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public bool ShowInputSource
{
get => _showInputSource;
set
{
if (_showInputSource != value)
{
_showInputSource = value;
OnPropertyChanged();
OnMoreButtonPropertiesChanged();
}
}
}
/// <summary>
/// Gets or sets a value indicating whether to show power state control in the More Button flyout.
/// </summary>
@@ -369,22 +345,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(ShowSeparatorAfterInputSource));
}
/// <summary>
/// Gets or sets a value indicating whether to show rotation controls (controlled by Settings UI, default false).
/// </summary>
public bool ShowRotation
{
get => _showRotation;
set
{
if (_showRotation != value)
{
_showRotation = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets the current rotation/orientation of the monitor (0=normal, 1=90°, 2=180°, 3=270°)
/// </summary>
@@ -410,6 +370,16 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
/// </summary>
public bool IsRotation3 => CurrentRotation == 3;
/// <summary>
/// Gets or sets the selected rotation index for binding to a ComboBox.
/// Maps directly to <see cref="CurrentRotation"/>: 0=Landscape, 1=Portrait, 2=Landscape (flipped), 3=Portrait (flipped).
/// </summary>
public int SelectedRotationIndex
{
get => CurrentRotation;
set => _ = SetRotationAsync(value);
}
/// <summary>
/// Set rotation/orientation for this monitor.
/// Note: MonitorManager.SetRotationAsync will refresh all monitors' orientations after success,
@@ -468,8 +438,14 @@ 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.
/// </summary>
public string ColorTemperaturePresetName =>
Common.Utils.VcpNames.GetFormattedValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings, _monitor.Id);
public string ColorTemperaturePresetName
{
get
{
var name = Common.Utils.VcpNames.GetValueName(0x14, _monitor.CurrentColorTemperature, _mainViewModel?.CustomVcpMappings, _monitor.Id);
return name != null ? $"{name} (0x{_monitor.CurrentColorTemperature:X2})" : $"0x{_monitor.CurrentColorTemperature:X2}";
}
}
/// <summary>
/// Gets a value indicating whether this monitor supports color temperature via VCP 0x14
@@ -549,7 +525,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.GetValueName(0x14, value, _mainViewModel?.CustomVcpMappings, _monitor.Id) is string n ? $"{n} (0x{value:X2})" : $"0x{value:X2}",
IsSelected = value == _monitor.CurrentColorTemperature,
MonitorId = _monitor.Id,
}).ToList();
@@ -781,16 +757,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
}
}
public bool IsAvailable
{
get => _isAvailable;
set
{
_isAvailable = value;
OnPropertyChanged();
}
}
[RelayCommand]
private void SetBrightness(int? brightness)
{
@@ -850,13 +816,6 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
return min + (int)Math.Round(percent * (max - min) / 100.0);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
@@ -882,9 +841,125 @@ public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
OnPropertyChanged(nameof(IsRotation1));
OnPropertyChanged(nameof(IsRotation2));
OnPropertyChanged(nameof(IsRotation3));
OnPropertyChanged(nameof(SelectedRotationIndex));
}
}
// Slider commit handlers — fire hardware update only on pointer release or arrow key up
public void HandleBrightnessPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
if (sender is Slider slider)
{
Brightness = (int)slider.Value;
}
}
public void HandleBrightnessKeyUp(object sender, KeyRoutedEventArgs e)
{
if (IsArrowKey(e.Key) && sender is Slider slider)
{
Brightness = (int)slider.Value;
}
}
public void HandleContrastPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
if (sender is Slider slider)
{
ContrastPercent = (int)slider.Value;
}
}
public void HandleContrastKeyUp(object sender, KeyRoutedEventArgs e)
{
if (IsArrowKey(e.Key) && sender is Slider slider)
{
ContrastPercent = (int)slider.Value;
}
}
public void HandleVolumePointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
if (sender is Slider slider)
{
Volume = (int)slider.Value;
}
}
public void HandleVolumeKeyUp(object sender, KeyRoutedEventArgs e)
{
if (IsArrowKey(e.Key) && sender is Slider slider)
{
Volume = (int)slider.Value;
}
}
private static bool IsArrowKey(VirtualKey key) =>
key == VirtualKey.Left || key == VirtualKey.Right ||
key == VirtualKey.Up || key == VirtualKey.Down;
// Rotation button handlers — one per orientation to avoid Tag string parsing
public async void HandleRotation0Click(object sender, RoutedEventArgs e) => await CommitRotationClickAsync(0);
public async void HandleRotation1Click(object sender, RoutedEventArgs e) => await CommitRotationClickAsync(1);
public async void HandleRotation2Click(object sender, RoutedEventArgs e) => await CommitRotationClickAsync(2);
public async void HandleRotation3Click(object sender, RoutedEventArgs e) => await CommitRotationClickAsync(3);
private async Task CommitRotationClickAsync(int orientation)
{
if (CurrentRotation == orientation)
{
// Force-notify to restore the ToggleButton checked state
// (ToggleButton auto-unchecks on click; OneWay binding only re-pushes on change)
OnPropertyChanged(nameof(IsRotation0));
OnPropertyChanged(nameof(IsRotation1));
OnPropertyChanged(nameof(IsRotation2));
OnPropertyChanged(nameof(IsRotation3));
return;
}
await SetRotationAsync(orientation);
}
// ListView selection handlers
public async void HandleColorTemperatureSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView || listView.SelectedItem is not ColorTemperatureItem item)
{
return;
}
await SetColorTemperatureAsync(item.VcpValue);
listView.SelectedItem = null;
}
public async void HandleInputSourceSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView || listView.SelectedItem is not InputSourceItem item)
{
return;
}
await SetInputSourceAsync(item.Value);
}
public async void HandlePowerStateSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView || listView.SelectedItem is not PowerStateItem item)
{
return;
}
if (item.Value == PowerStateItem.PowerStateOn)
{
return;
}
await SetPowerStateAsync(item.Value);
}
public void Dispose()
{
// Unsubscribe from MainViewModel events

View File

@@ -51,14 +51,25 @@ void PowerDisplayProcessManager::stop()
void PowerDisplayProcessManager::send_message(const std::wstring& message_type, const std::wstring& message_arg)
{
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 (!m_enabled)
{
Logger::warn(L"Ignoring '{}' message because PowerDisplay is disabled", message_type);
return;
}
// If the process exited unexpectedly while the module is still enabled,
// bring it back before delivering the message.
if (!is_process_running())
{
m_enabled = true;
refresh();
if (!is_process_running())
{
Logger::warn(L"PowerDisplay process is not running; '{}' message was not delivered", message_type);
return;
}
}
send_named_pipe_message(message_type, message_arg);
});
}

View File

@@ -70,7 +70,8 @@ public:
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
// Listener registration is tied to enable()/disable() so activation still
// flows through the module's runtime enable/GPO checks.
m_hToggleEvent = CreateDefaultEvent(CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT);
Logger::trace(L"Created TOGGLE_EVENT: handle={}", reinterpret_cast<void*>(m_hToggleEvent));
@@ -90,8 +91,6 @@ public:
Logger::info(L"All Windows Events created successfully");
}
// Start toggle event listener thread for Quick Access support
StartToggleEventListener();
}
~PowerDisplayModule()
@@ -129,7 +128,7 @@ public:
void StartToggleEventListener()
{
if (!m_hToggleEvent || !m_hStopEvent)
if (!m_hToggleEvent || !m_hStopEvent || m_toggleEventThread.joinable())
{
return;
}
@@ -152,7 +151,7 @@ public:
if (result == WAIT_OBJECT_0 + TOGGLE_EVENT_INDEX)
{
Logger::trace(L"Toggle event received");
TogglePowerDisplay();
TryTogglePowerDisplay(L"Toggle event");
}
else if (result == WAIT_OBJECT_0 + STOP_EVENT_INDEX)
{
@@ -172,6 +171,34 @@ public:
});
}
bool IsActivationAllowed(const wchar_t* source)
{
if (gpo_policy_enabled_configuration() == powertoys_gpo::gpo_rule_configured_t::gpo_rule_configured_disabled)
{
Logger::warn(L"{} ignored because PowerDisplay is disabled by GPO", source);
return false;
}
if (!m_enabled)
{
Logger::info(L"{} ignored because PowerDisplay module is disabled", source);
return false;
}
return true;
}
bool TrySendMessage(const std::wstring& message_type, const std::wstring& message_arg, const wchar_t* source)
{
if (!IsActivationAllowed(source))
{
return false;
}
m_processManager.send_message(message_type, message_arg);
return true;
}
void StopToggleEventListener()
{
if (m_hStopEvent)
@@ -191,8 +218,13 @@ public:
/// 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()
bool TryTogglePowerDisplay(const wchar_t* source)
{
if (!IsActivationAllowed(source))
{
return false;
}
if (m_processManager.is_running())
{
// Process running - launch to trigger single instance redirect, OnActivated will toggle
@@ -208,6 +240,7 @@ public:
m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_TOGGLE_MESSAGE);
}
Trace::ActivatePowerDisplay();
return true;
}
virtual void destroy() override
@@ -251,7 +284,7 @@ public:
if (action_object.get_name() == L"Launch")
{
Logger::trace(L"Launch action received");
TogglePowerDisplay();
TryTogglePowerDisplay(L"Launch action");
}
else if (action_object.get_name() == L"RefreshMonitors")
{
@@ -274,7 +307,7 @@ public:
Logger::trace(L"ApplyProfile: profile name = '{}'", profileName);
// Send ApplyProfile message with profile name via Named Pipe
m_processManager.send_message(CommonSharedConstants::POWER_DISPLAY_APPLY_PROFILE_MESSAGE, profileName);
TrySendMessage(CommonSharedConstants::POWER_DISPLAY_APPLY_PROFILE_MESSAGE, profileName, L"ApplyProfile action");
}
}
catch (std::exception&)
@@ -297,6 +330,8 @@ public:
m_enabled = true;
Trace::EnablePowerDisplay(true);
StartToggleEventListener();
// Start the process manager (launches PowerDisplay.exe with Named Pipe)
m_processManager.start();
@@ -307,13 +342,12 @@ public:
{
Logger::trace(L"PowerDisplay::disable()");
if (m_enabled)
{
// Stop the process manager (sends terminate message and waits for exit)
m_processManager.stop();
}
m_enabled = false;
StopToggleEventListener();
// Stop the process manager (sends terminate message and waits for exit)
m_processManager.stop();
Trace::EnablePowerDisplay(false);
}

View File

@@ -286,7 +286,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"PowerToys.CmdPalModuleInterface.dll",
L"PowerToys.ZoomItModuleInterface.dll",
L"PowerToys.LightSwitchModuleInterface.dll",
// L"PowerToys.PowerDisplayModuleInterface.dll", // TEMPORARILY_DISABLED: PowerDisplay
L"PowerToys.PowerDisplayModuleInterface.dll",
};
for (auto moduleSubdir : knownModules)

View File

@@ -77,12 +77,6 @@ public sealed class AllAppsViewModel : Observable
continue;
}
// TEMPORARILY_DISABLED: PowerDisplay
if (moduleType == ModuleType.PowerDisplay)
{
continue;
}
_allFlyoutMenuItems.Add(new FlyoutMenuItem
{
Tag = moduleType,

View File

@@ -76,8 +76,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
AddFlyoutMenuItem(ModuleType.Hosts);
AddFlyoutMenuItem(ModuleType.KeyboardManager);
AddFlyoutMenuItem(ModuleType.LightSwitch);
// AddFlyoutMenuItem(ModuleType.PowerDisplay); // TEMPORARILY_DISABLED: PowerDisplay
AddFlyoutMenuItem(ModuleType.PowerDisplay);
AddFlyoutMenuItem(ModuleType.PowerLauncher);
AddFlyoutMenuItem(ModuleType.PowerOCR);
AddFlyoutMenuItem(ModuleType.RegistryPreview);

View File

@@ -1,89 +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.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
#nullable enable
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Represents a color temperature preset item for VCP code 0x14.
/// Used to display available color temperature presets in UI components.
/// This is a local copy maintained in Settings.UI.Library to avoid a dependency on PowerDisplay.Lib.
/// </summary>
public partial class ColorPresetItem : INotifyPropertyChanged
{
private int _vcpValue;
private string _displayName = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="ColorPresetItem"/> class.
/// </summary>
public ColorPresetItem()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPresetItem"/> class.
/// </summary>
/// <param name="vcpValue">The VCP value for the color temperature preset.</param>
/// <param name="displayName">The display name for UI.</param>
public ColorPresetItem(int vcpValue, string displayName)
{
_vcpValue = vcpValue;
_displayName = displayName;
}
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Gets or sets the VCP value for this color temperature preset.
/// </summary>
[JsonPropertyName("vcpValue")]
public int VcpValue
{
get => _vcpValue;
set
{
if (_vcpValue != value)
{
_vcpValue = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the display name for UI.
/// </summary>
[JsonPropertyName("displayName")]
public string DisplayName
{
get => _displayName;
set
{
if (_displayName != value)
{
_displayName = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using PowerDisplay.Models;
namespace Microsoft.PowerToys.Settings.UI.Library
{

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PowerDisplay.Models;
using Settings.UI.Library.Attributes;
namespace Microsoft.PowerToys.Settings.UI.Library

View File

@@ -23,6 +23,7 @@
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\modules\MouseUtils\MouseJump.Common\MouseJump.Common.csproj" />
<ProjectReference Include="..\..\modules\powerdisplay\PowerDisplay.Models\PowerDisplay.Models.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using PowerDisplay.Models;
using SettingsUILibrary = Settings.UI.Library;
using SettingsUILibraryHelpers = Settings.UI.Library.Helpers;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -120,7 +120,7 @@
<ProjectReference Include="..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\..\common\LanguageModelProvider\LanguageModelProvider.csproj" />
<ProjectReference Include="..\..\modules\MouseUtils\MouseJump.Common\MouseJump.Common.csproj" />
<ProjectReference Include="..\..\modules\powerdisplay\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
<ProjectReference Include="..\..\modules\powerdisplay\PowerDisplay.Models\PowerDisplay.Models.csproj" />
<ProjectReference Include="..\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\Settings.UI.Controls\Settings.UI.Controls.csproj" />
</ItemGroup>

View File

@@ -126,12 +126,10 @@
x:Uid="Shell_Peek"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/Peek.png}"
Tag="Peek" />
<!-- TEMPORARILY_DISABLED: PowerDisplay
<NavigationViewItem
x:Uid="Shell_PowerDisplay"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}"
Tag="PowerDisplay" />
-->
<NavigationViewItem
x:Uid="Shell_PowerRename"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerRename.png}"

View File

@@ -15,9 +15,7 @@ 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;
using CustomVcpValueMapping = Microsoft.PowerToys.Settings.UI.Library.CustomVcpValueMapping;
using PowerDisplay.Models;
namespace Microsoft.PowerToys.Settings.UI.Views
{
@@ -276,7 +274,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
seenValues.Add(vcpValue);
var displayName = !string.IsNullOrEmpty(valueInfo.Name)
? $"{valueInfo.Name} (0x{vcpValue:X2})"
: VcpNames.GetFormattedValueName(vcpCode, vcpValue);
: $"0x{vcpValue:X2}";
values.Add(new VcpValueItem
{
Value = vcpValue,
@@ -287,23 +285,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
}
}
// 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));
@@ -340,7 +321,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
var resourceKey = $"PowerDisplay_VcpCode_Name_0x{vcpCode:X2}";
var localizedName = resourceLoader.GetString(resourceKey);
var name = string.IsNullOrEmpty(localizedName) ? VcpNames.GetCodeName(vcpCode) : localizedName;
var name = string.IsNullOrEmpty(localizedName) ? $"0x{vcpCode:X2}" : localizedName;
return $"{name} (0x{vcpCode:X2})";
}

View File

@@ -7,15 +7,32 @@
xmlns:library="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pdmodels="using:PowerDisplay.Common.Models"
xmlns:pdmodels="using:PowerDisplay.Models"
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
AutomationProperties.LandmarkType="Main"
mc:Ignorable="d">
<local:NavigablePage.Resources>
<x:Double x:Key="PowerDisplayCompactActionControlMinWidth">120</x:Double>
<x:Double x:Key="PowerDisplayVcpDetailsFlyoutWidth">420</x:Double>
<x:Double x:Key="PowerDisplayVcpDetailsFlyoutMaxHeight">480</x:Double>
<x:Double x:Key="PowerDisplayActionButtonSpacing">8</x:Double>
<x:Double x:Key="PowerDisplayCompactSpacing">4</x:Double>
<x:Double x:Key="PowerDisplayInlineIconSpacing">6</x:Double>
<x:Double x:Key="PowerDisplayPrimaryGlyphFontSize">14</x:Double>
<x:Double x:Key="PowerDisplaySecondaryGlyphFontSize">12</x:Double>
<x:Double x:Key="PowerDisplayMoreButtonGlyphFontSize">16</x:Double>
<Thickness x:Key="PowerDisplayMonitorExpanderMargin">0,0,0,2</Thickness>
<Thickness x:Key="PowerDisplayVcpDetailsHeaderMargin">0,0,0,12</Thickness>
<Thickness x:Key="PowerDisplayVcpCopyButtonPadding">8,4</Thickness>
<Thickness x:Key="PowerDisplayVcpValueMargin">0,0,0,8</Thickness>
</local:NavigablePage.Resources>
<controls:SettingsPageControl x:Uid="PowerDisplay" ModuleImageSource="ms-appx:///Assets/Settings/Modules/PowerDisplay.png">
<controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_Enable_PowerDisplay"
@@ -25,42 +42,40 @@
</tkcontrols:SettingsCard>
</controls:GPOInfoControl>
<controls:SettingsGroup x:Uid="Shortcut" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ActivationShortcut" HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_Configuration_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard
x:Uid="PowerDisplay_LaunchButtonControl"
ActionIcon="{ui:FontIcon Glyph=&#xE8A7;}"
Command="{x:Bind ViewModel.LaunchEventHandler}"
<controls:SettingsGroup x:Uid="PowerDisplayGroupHeader" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsExpander
x:Uid="PowerDisplayFlyoutHeader"
HeaderIcon="{ui:FontIcon Glyph=&#xE770;}"
IsClickEnabled="True" />
<tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=&#xE7B8;}">
<ToggleSwitch x:Uid="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowSystemTrayIcon" HeaderIcon="{ui:FontIcon Glyph=&#xE75B;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
IsExpanded="True">
<Button
x:Uid="PowerDisplay_LaunchButton"
Command="{x:Bind ViewModel.LaunchEventHandler}"
Style="{StaticResource AccentButtonStyle}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ActivationShortcut">
<controls:ShortcutControl HotkeySettings="{x:Bind Path=ViewModel.ActivationShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_MonitorRefreshDelay" HeaderIcon="{ui:FontIcon Glyph=&#xE916;}">
<ComboBox
MinWidth="120"
ItemsSource="{x:Bind ViewModel.MonitorRefreshDelayOptions}"
SelectedItem="{x:Bind ViewModel.MonitorRefreshDelay, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
<controls:SettingsGroup x:Uid="PowerDisplay_FlyoutOptions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowProfileSwitcher" HeaderIcon="{ui:FontIcon Glyph=&#xE748;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowProfileSwitcher, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowIdentifyMonitorsButton" HeaderIcon="{ui:FontIcon Glyph=&#xE9D9;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowIdentifyMonitorsButton, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PowerDisplay_MonitorRefreshDelay">
<ComboBox
MinWidth="{StaticResource PowerDisplayCompactActionControlMinWidth}"
ItemsSource="{x:Bind ViewModel.MonitorRefreshDelayOptions}"
SelectedItem="{x:Bind ViewModel.MonitorRefreshDelay, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_RestoreSettingsOnStartup" IsChecked="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_ShowSystemTrayIcon" IsChecked="{x:Bind ViewModel.ShowSystemTrayIcon, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_ShowProfileSwitcher" IsChecked="{x:Bind ViewModel.ShowProfileSwitcher, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<CheckBox x:Uid="PowerDisplay_ShowIdentifyMonitorsButton" IsChecked="{x:Bind ViewModel.ShowIdentifyMonitorsButton, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup>
<!-- Custom VCP Name Mappings -->
@@ -71,23 +86,23 @@
IsExpanded="{x:Bind ViewModel.HasCustomVcpMappings, Mode=OneWay}"
ItemsSource="{x:Bind ViewModel.CustomVcpMappings, Mode=OneWay}">
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="library:CustomVcpValueMapping">
<DataTemplate x:DataType="pdmodels:CustomVcpValueMapping">
<tkcontrols:SettingsCard Description="{x:Bind VcpCodeDisplayName}" Header="{x:Bind DisplaySummary}">
<StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayActionButtonSpacing}">
<Button
x:Uid="PowerDisplay_EditCustomMapping_Button"
Click="EditCustomMapping_Click"
Content="{ui:FontIcon Glyph=&#xE70F;,
FontSize=14}"
FontSize={StaticResource PowerDisplayPrimaryGlyphFontSize}}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}"
ToolTipService.ToolTip="Edit" />
Tag="{x:Bind}" />
<Button
x:Uid="PowerDisplay_DeleteCustomMapping_Button"
Click="DeleteCustomMapping_Click"
Content="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
FontSize={StaticResource PowerDisplayPrimaryGlyphFontSize}}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}"
ToolTipService.ToolTip="Delete" />
Tag="{x:Bind}" />
</StackPanel>
</tkcontrols:SettingsCard>
</DataTemplate>
@@ -95,8 +110,8 @@
<!-- Add mapping button -->
<Button x:Uid="PowerDisplay_AddCustomMappingButton" Click="AddCustomMapping_Click">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon FontSize="14" Glyph="&#xE710;" />
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayInlineIconSpacing}">
<FontIcon FontSize="{StaticResource PowerDisplayPrimaryGlyphFontSize}" Glyph="&#xE710;" />
<TextBlock x:Uid="PowerDisplay_AddCustomMapping_Text" />
</StackPanel>
</Button>
@@ -112,7 +127,7 @@
<tkcontrols:SettingsExpander.ItemTemplate>
<DataTemplate x:DataType="pdmodels:PowerDisplayProfile">
<tkcontrols:SettingsCard Header="{x:Bind Name}">
<StackPanel Orientation="Horizontal" Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayActionButtonSpacing}">
<Button
x:Uid="PowerDisplay_Profile_ApplyButton"
Click="ProfileButton_Click"
@@ -120,7 +135,7 @@
<Button
x:Uid="PowerDisplay_Profile_MoreButton"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=16}"
FontSize={StaticResource PowerDisplayMoreButtonGlyphFontSize}}"
Style="{StaticResource SubtleButtonStyle}"
Tag="{x:Bind}">
<Button.Flyout>
@@ -146,8 +161,8 @@
<!-- Add profile button -->
<Button x:Uid="PowerDisplay_AddProfileButton" Click="AddProfileButton_Click">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon FontSize="14" Glyph="&#xE710;" />
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayInlineIconSpacing}">
<FontIcon FontSize="{StaticResource PowerDisplayPrimaryGlyphFontSize}" Glyph="&#xE710;" />
<TextBlock x:Uid="PowerDisplay_AddProfile_Text" />
</StackPanel>
</Button>
@@ -164,20 +179,18 @@
<!-- Monitor list -->
<ItemsControl
x:Name="MonitorsList"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="library:MonitorInfo">
<tkcontrols:SettingsExpander
Margin="0,0,0,2"
Margin="{StaticResource PowerDisplayMonitorExpanderMargin}"
Description="{x:Bind Id, Mode=OneWay}"
Header="{x:Bind DisplayName, Mode=OneWay}"
IsExpanded="False">
Header="{x:Bind DisplayName, Mode=OneWay}">
<tkcontrols:SettingsExpander.HeaderIcon>
<FontIcon Glyph="{x:Bind MonitorIconGlyph, Mode=OneWay}" />
</tkcontrols:SettingsExpander.HeaderIcon>
<TextBlock Text="{x:Bind CommunicationMethod, Mode=OneWay}" />
<ptcontrols:IsEnabledTextBlock Text="{x:Bind CommunicationMethod, Mode=OneWay}" />
<tkcontrols:SettingsExpander.ItemsHeader>
<!-- Capabilities warning -->
<InfoBar
@@ -185,9 +198,8 @@
BorderThickness="0"
CornerRadius="0"
IsClosable="False"
IsOpen="True"
Severity="Warning"
Visibility="{x:Bind ShowCapabilitiesWarning, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
IsOpen="{x:Bind ShowCapabilitiesWarning, Mode=OneWay}"
Severity="Warning" />
</tkcontrols:SettingsExpander.ItemsHeader>
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind SupportsContrast, Mode=OneWay}">
@@ -220,38 +232,36 @@
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_VcpCapabilities" Visibility="{x:Bind HasCapabilities, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<Button
x:Uid="PowerDisplay_Monitor_VcpDetails_Button"
Content="&#xE946;"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Content="{ui:FontIcon Glyph=&#xE946;}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<Flyout ShouldConstrainToRootBounds="False">
<Grid Width="420">
<Grid Width="{StaticResource PowerDisplayVcpDetailsFlyoutWidth}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition />
</Grid.RowDefinitions>
<!-- Header with Copy Button -->
<Grid Margin="0,0,0,12">
<Grid Margin="{StaticResource PowerDisplayVcpDetailsHeaderMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
x:Uid="PowerDisplay_Monitor_VcpCodes_Header"
Grid.Column="0"
VerticalAlignment="Center"
FontSize="13"
FontSize="{StaticResource PowerDisplayPrimaryGlyphFontSize}"
FontWeight="SemiBold" />
<Button
x:Uid="PowerDisplay_Monitor_VcpCodes_CopyButton"
Grid.Column="1"
Padding="8,4"
Padding="{StaticResource PowerDisplayVcpCopyButtonPadding}"
VerticalAlignment="Center"
Click="CopyVcpCodes_Click"
Tag="{x:Bind}">
<StackPanel Orientation="Horizontal" Spacing="4">
<FontIcon FontSize="12" Glyph="&#xE8C8;" />
<TextBlock x:Uid="PowerDisplay_Monitor_VcpCodes_CopyText" FontSize="12" />
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayCompactSpacing}">
<FontIcon FontSize="{StaticResource PowerDisplaySecondaryGlyphFontSize}" Glyph="&#xE8C8;" />
<TextBlock x:Uid="PowerDisplay_Monitor_VcpCodes_CopyText" FontSize="{StaticResource PowerDisplaySecondaryGlyphFontSize}" />
</StackPanel>
</Button>
</Grid>
@@ -259,25 +269,25 @@
<!-- VCP Codes List -->
<ScrollViewer
Grid.Row="1"
MaxHeight="480"
MaxHeight="{StaticResource PowerDisplayVcpDetailsFlyoutMaxHeight}"
HorizontalScrollBarVisibility="Disabled"
HorizontalScrollMode="Disabled"
VerticalScrollBarVisibility="Auto">
<ItemsControl HorizontalAlignment="Stretch" ItemsSource="{x:Bind VcpCodesFormatted, Mode=OneWay}">
<ItemsControl ItemsSource="{x:Bind VcpCodesFormatted, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="library:VcpCodeDisplayInfo">
<StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
<StackPanel>
<TextBlock
FontSize="12"
FontSize="{StaticResource PowerDisplaySecondaryGlyphFontSize}"
Text="{x:Bind Title}"
TextWrapping="Wrap" />
<TextBlock
Margin="0,0,0,8"
FontSize="12"
Margin="{StaticResource PowerDisplayVcpValueMargin}"
FontSize="{StaticResource PowerDisplaySecondaryGlyphFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind Values}"
TextWrapping="Wrap"
Visibility="{x:Bind HasValues}" />
Visibility="{x:Bind HasValues, Converter={StaticResource BoolToVisibilityConverter}}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>

View File

@@ -12,10 +12,8 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
using Windows.ApplicationModel.DataTransfer;
using CustomVcpValueMapping = Microsoft.PowerToys.Settings.UI.Library.CustomVcpValueMapping;
namespace Microsoft.PowerToys.Settings.UI.Views
{
@@ -127,11 +125,29 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private string GenerateDefaultProfileName()
{
// Use shared ProfileHelper for consistent profile name generation
var existingNames = ViewModel.Profiles.Select(p => p.Name).ToHashSet();
var existingNames = ViewModel.Profiles.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
var baseName = resourceLoader.GetString("PowerDisplay_Profile_DefaultBaseName");
return ProfileHelper.GenerateUniqueProfileName(existingNames, baseName);
if (string.IsNullOrEmpty(baseName))
{
baseName = "Profile";
}
if (!existingNames.Contains(baseName))
{
return baseName;
}
for (int i = 2; i < 1000; i++)
{
var candidate = $"{baseName} {i}";
if (!existingNames.Contains(candidate))
{
return candidate;
}
}
return $"{baseName} {DateTime.Now.Ticks}";
}
// Custom VCP Mapping event handlers

View File

@@ -1,4 +1,4 @@
<ContentDialog
<ContentDialog
x:Class="Microsoft.PowerToys.Settings.UI.Views.ProfileEditorDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -50,7 +50,10 @@
</tkcontrols:SettingsExpander.HeaderIcon>
<ToggleSwitch IsOn="{x:Bind IsSelected, Mode=TwoWay}" />
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsBrightness, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard
Padding="16,8,16,8"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}"
Visibility="{x:Bind Monitor.SupportsBrightness, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeBrightness, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -70,7 +73,10 @@
</tkcontrols:SettingsCard.Resources>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsContrast, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard
Padding="16,8,16,8"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}"
Visibility="{x:Bind Monitor.SupportsContrast, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeContrast, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -86,7 +92,10 @@
Minimum="0"
Value="{x:Bind Contrast, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsVolume, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard
Padding="16,8,16,8"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}"
Visibility="{x:Bind Monitor.SupportsVolume, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeVolume, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -102,7 +111,10 @@
Minimum="0"
Value="{x:Bind Volume, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Padding="16,8,16,8" Visibility="{x:Bind Monitor.SupportsColorTemperature, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard
Padding="16,8,16,8"
IsEnabled="{x:Bind IsSelected, Mode=OneWay}"
Visibility="{x:Bind Monitor.SupportsColorTemperature, Converter={StaticResource BoolToVisibilityConverter}}">
<tkcontrols:SettingsCard.Header>
<CheckBox IsChecked="{x:Bind IncludeColorTemperature, Mode=TwoWay}">
<StackPanel Orientation="Horizontal" Spacing="8">

View File

@@ -11,7 +11,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
namespace Microsoft.PowerToys.Settings.UI.Views
{

View File

@@ -209,18 +209,6 @@
helpers:NavHelper.NavigateTo="views:LightSwitchPage"
AutomationProperties.AutomationId="LightSwitchNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/LightSwitch.png}" />
<!-- TEMPORARILY_DISABLED: PowerDisplay
<NavigationViewItem
x:Name="PowerDisplayNavigationItem"
x:Uid="Shell_PowerDisplay"
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
AutomationProperties.AutomationId="PowerDisplayNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
-->
<NavigationViewItem
x:Name="PowerLauncherNavigationItem"
x:Uid="Shell_PowerLauncher"
@@ -305,11 +293,7 @@
x:Uid="Shell_KeyboardManager"
helpers:NavHelper.NavigateTo="views:KeyboardManagerPage"
AutomationProperties.AutomationId="KeyboardManagerNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/KeyboardManager.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/KeyboardManager.png}" />
<NavigationViewItem
x:Name="MouseUtilitiesNavigationItem"
x:Uid="Shell_MouseUtilities"
@@ -322,6 +306,16 @@
helpers:NavHelper.NavigateTo="views:MouseWithoutBordersPage"
AutomationProperties.AutomationId="MouseWithoutBordersNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseWithoutBorders.png}" />
<NavigationViewItem
x:Name="PowerDisplayNavigationItem"
x:Uid="Shell_PowerDisplay"
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
AutomationProperties.AutomationId="PowerDisplayNavItem"
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
<NavigationViewItem.InfoBadge>
<InfoBadge Style="{StaticResource NewInfoBadge}" />
</NavigationViewItem.InfoBadge>
</NavigationViewItem>
<NavigationViewItem
x:Name="QuickAccentNavigationItem"
x:Uid="Shell_QuickAccent"

View File

@@ -5384,9 +5384,6 @@ The break timer font matches the text font.</value>
<data name="PowerDisplay_Enable_PowerDisplay.Header" xml:space="preserve">
<value>Enable Power Display</value>
</data>
<data name="PowerDisplay_Configuration_GroupSettings.Header" xml:space="preserve">
<value>Configuration</value>
</data>
<data name="PowerDisplay_ToggleWindow" xml:space="preserve">
<value>Toggle Power Display</value>
<comment>Dashboard: Label for the PowerDisplay activation hotkey</comment>
@@ -5397,28 +5394,19 @@ The break timer font matches the text font.</value>
</data>
<data name="PowerDisplay_LaunchButtonControl.Header" xml:space="preserve">
<value>Open Power Display</value>
</data>
<data name="PowerDisplay_LaunchButton.Content" xml:space="preserve">
<value>Open Power Display</value>
</data>
<data name="PowerDisplay_LaunchButtonControl.Description" xml:space="preserve">
<value>Launch the Power Display utility</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup.Header" xml:space="preserve">
<value>Restore settings on startup</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup.Description" xml:space="preserve">
<data name="PowerDisplay_RestoreSettingsOnStartup.Content" xml:space="preserve">
<value>Restore monitor brightness and color temperature when Power Display launches</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch.OnContent" xml:space="preserve">
<value>On</value>
</data>
<data name="PowerDisplay_RestoreSettingsOnStartup_ToggleSwitch.OffContent" xml:space="preserve">
<value>Off</value>
</data>
<data name="PowerDisplay_ShowSystemTrayIcon.Header" xml:space="preserve">
<data name="PowerDisplay_ShowSystemTrayIcon.Content" xml:space="preserve">
<value>Show system tray icon</value>
</data>
<data name="PowerDisplay_ShowSystemTrayIcon.Description" xml:space="preserve">
<value>Choose if PowerDisplay is visible in the system tray</value>
</data>
<data name="PowerDisplay_MonitorRefreshDelay.Header" xml:space="preserve">
<value>Monitor refresh delay</value>
</data>
@@ -5604,26 +5592,23 @@ The break timer font matches the text font.</value>
<data name="PowerDisplay_Monitor_VcpCodes_CopyText.Text" xml:space="preserve">
<value>Copy</value>
</data>
<data name="PowerDisplay_FlyoutOptions_GroupSettings.Header" xml:space="preserve">
<value>Flyout options</value>
<data name="PowerDisplay_ShowProfileSwitcher.Content" xml:space="preserve">
<value>Show profile switcher button</value>
</data>
<data name="PowerDisplay_ShowProfileSwitcher.Header" xml:space="preserve">
<value>Show profile switcher</value>
</data>
<data name="PowerDisplay_ShowProfileSwitcher.Description" xml:space="preserve">
<value>Show or hide the profile switcher button in the Power Display flyout</value>
</data>
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Header" xml:space="preserve">
<data name="PowerDisplay_ShowIdentifyMonitorsButton.Content" xml:space="preserve">
<value>Show identify monitors button</value>
</data>
<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>
<value>Custom VCP name mappings</value>
</data>
<data name="PowerDisplay_CustomVcpMappings.Header" xml:space="preserve">
<value>Custom name mappings</value>
</data>
<data name="PowerDisplayGroupHeader.Header" xml:space="preserve">
<value>Power Display</value>
</data>
<data name="PowerDisplayFlyoutHeader.Header" xml:space="preserve">
<value>Power Display flyout</value>
</data>
<data name="PowerDisplay_CustomVcpMappings.Description" xml:space="preserve">
<value>Define custom display names for color temperature presets and input sources</value>
@@ -5631,23 +5616,29 @@ The break timer font matches the text font.</value>
<data name="PowerDisplay_AddCustomMappingButton.ToolTipService.ToolTip" xml:space="preserve">
<value>Add custom mapping</value>
</data>
<data name="PowerDisplay_EditCustomMapping_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>Edit</value>
</data>
<data name="PowerDisplay_DeleteCustomMapping_Button.ToolTipService.ToolTip" xml:space="preserve">
<value>Delete</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>
<value>Custom VCP name mapping</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_VcpCode.Header" xml:space="preserve">
<value>VCP Code</value>
<value>VCP code</value>
</data>
<data name="PowerDisplay_VcpCode_Name_0x14" xml:space="preserve">
<value>Color Temperature</value>
<value>Color temperature</value>
</data>
<data name="PowerDisplay_VcpCode_Name_0x60" xml:space="preserve">
<value>Input Source</value>
<value>Input source</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomName.Header" xml:space="preserve">
<value>Custom Name</value>
<value>Custom name</value>
</data>
<data name="PowerDisplay_CustomMappingEditor_CustomName.PlaceholderText" xml:space="preserve">
<value>Enter custom name</value>

View File

@@ -213,12 +213,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
continue;
}
// TEMPORARILY_DISABLED: PowerDisplay
if (moduleType == ModuleType.PowerDisplay)
{
continue;
}
GpoRuleConfigured gpo = ModuleGpoHelper.GetModuleGpoConfiguration(moduleType);
var newItem = new DashboardListItem()
{

View File

@@ -20,8 +20,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Newtonsoft.Json.Linq;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Models;
using PowerToys.GPOWrapper;
using Settings.UI.Library;
using Settings.UI.Library.Helpers;
@@ -725,7 +724,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
try
{
var profilesData = ProfileService.LoadProfiles();
var profilesData = ProfileHelper.LoadProfiles();
AvailableProfiles.Clear();

View File

@@ -9,6 +9,7 @@ using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
@@ -17,11 +18,8 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using PowerDisplay.Models;
using PowerToys.Interop;
using CustomVcpValueMapping = Microsoft.PowerToys.Settings.UI.Library.CustomVcpValueMapping;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
@@ -37,9 +35,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);
@@ -180,7 +175,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
if (SetSettingsProperty(_settings.Properties.ActivationShortcut, value, v => _settings.Properties.ActivationShortcut = v))
{
// Signal PowerDisplay.exe to re-register the hotkey
EventHelper.SignalEvent(Constants.HotkeyUpdatedPowerDisplayEvent());
SignalNamedEvent(Constants.HotkeyUpdatedPowerDisplayEvent());
Logger.LogInfo($"ActivationShortcut changed, signaled HotkeyUpdatedPowerDisplayEvent");
}
}
@@ -358,10 +353,23 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
/// </summary>
private void SignalSettingsUpdated()
{
EventHelper.SignalEvent(Constants.SettingsUpdatedPowerDisplayEvent());
SignalNamedEvent(Constants.SettingsUpdatedPowerDisplayEvent());
Logger.LogInfo("Signaled SettingsUpdatedPowerDisplayEvent for feature visibility change");
}
private static void SignalNamedEvent(string eventName)
{
try
{
using var handle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
handle.Set();
}
catch (Exception ex)
{
Logger.LogError($"Failed to signal event '{eventName}': {ex.Message}");
}
}
public void Launch()
{
var actionMessage = new PowerDisplayActionMessage
@@ -505,7 +513,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
try
{
var profilesData = ProfileService.LoadProfiles();
var profilesData = ProfileHelper.LoadProfiles();
// Load profile objects (no Custom - it's not a profile anymore)
Profiles.Clear();
@@ -577,9 +585,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Logger.LogInfo($"Creating profile: {profile.Name}");
var profilesData = ProfileService.LoadProfiles();
profilesData.SetProfile(profile);
ProfileService.SaveProfiles(profilesData);
ProfileHelper.AddOrUpdateProfile(profile);
// Reload profile list
LoadProfiles();
@@ -610,12 +616,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Logger.LogInfo($"Updating profile: {oldName} -> {newProfile.Name}");
var profilesData = ProfileService.LoadProfiles();
// Remove old profile and add updated one
profilesData.RemoveProfile(oldName);
profilesData.SetProfile(newProfile);
ProfileService.SaveProfiles(profilesData);
ProfileHelper.RenameAndUpdateProfile(oldName, newProfile);
// Reload profile list
LoadProfiles();
@@ -645,9 +646,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Logger.LogInfo($"Deleting profile: {profileName}");
var profilesData = ProfileService.LoadProfiles();
profilesData.RemoveProfile(profileName);
ProfileService.SaveProfiles(profilesData);
ProfileHelper.RemoveProfile(profileName);
// Reload profile list
LoadProfiles();
@@ -750,22 +749,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
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

View File

@@ -9,7 +9,7 @@ using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Common.Models;
using PowerDisplay.Models;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{