mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-05-18 05:05:25 +02:00
[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:
1
.github/actions/spell-check/allow/code.txt
vendored
1
.github/actions/spell-check/allow/code.txt
vendored
@@ -332,6 +332,7 @@ REGSTR
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
Mdt
|
||||
HTCAPTION
|
||||
POSCHANGED
|
||||
QUERYPOS
|
||||
|
||||
3
.github/actions/spell-check/expect.txt
vendored
3
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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)"
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
|
||||
196
src/modules/powerdisplay/PowerDisplay.Models/ProfileHelper.cs
Normal file
196
src/modules/powerdisplay/PowerDisplay.Models/ProfileHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PowerDisplay.Common.Models
|
||||
namespace PowerDisplay.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor settings for a specific profile
|
||||
@@ -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 |
@@ -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)
|
||||
|
||||
116
src/modules/powerdisplay/PowerDisplay/Helpers/DpiSuppressor.cs
Normal file
116
src/modules/powerdisplay/PowerDisplay/Helpers/DpiSuppressor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -250,7 +250,6 @@ namespace PowerDisplay.Helpers
|
||||
|
||||
break;
|
||||
case PInvoke.WM_LBUTTONUP:
|
||||
case PInvoke.WM_LBUTTONDBLCLK:
|
||||
_toggleWindowAction?.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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=}"
|
||||
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=,
|
||||
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=""
|
||||
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=,
|
||||
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=""
|
||||
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=""
|
||||
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=,
|
||||
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=""
|
||||
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=""
|
||||
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="" />
|
||||
<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="" />
|
||||
|
||||
<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="" />
|
||||
FontSize="{StaticResource FlyoutActionGlyphFontSize}"
|
||||
Glyph="" />
|
||||
<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="" />
|
||||
|
||||
<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="" />
|
||||
IsTabStop="True">
|
||||
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="" />
|
||||
</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="" />
|
||||
IsTabStop="True">
|
||||
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="" />
|
||||
</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="" />
|
||||
IsTabStop="True">
|
||||
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="" />
|
||||
</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="" />
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
IsTabStop="True">
|
||||
<FontIcon FontSize="{StaticResource FlyoutRotationGlyphFontSize}" Glyph="" />
|
||||
</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=,
|
||||
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=,
|
||||
x:Uid="RefreshTooltip"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
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=,
|
||||
FontSize=16}" />
|
||||
<MenuFlyoutItem
|
||||
x:Name="IdentifyButton"
|
||||
x:Uid="IdentifyText"
|
||||
Command="{x:Bind ViewModel.IdentifyMonitorsCommand}"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
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=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
<Button
|
||||
x:Name="IdentifyButton"
|
||||
x:Uid="IdentifyTooltip"
|
||||
Command="{x:Bind ViewModel.IdentifyMonitorsCommand}"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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="" />
|
||||
<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="" />
|
||||
<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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -77,12 +77,6 @@ public sealed class AllAppsViewModel : Observable
|
||||
continue;
|
||||
}
|
||||
|
||||
// TEMPORARILY_DISABLED: PowerDisplay
|
||||
if (moduleType == ModuleType.PowerDisplay)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_allFlyoutMenuItems.Add(new FlyoutMenuItem
|
||||
{
|
||||
Tag = moduleType,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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})";
|
||||
}
|
||||
|
||||
|
||||
@@ -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=}">
|
||||
<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=}"
|
||||
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=}"
|
||||
IsClickEnabled="True" />
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_RestoreSettingsOnStartup" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<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=}">
|
||||
<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=}">
|
||||
<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=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.ShowProfileSwitcher, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_ShowIdentifyMonitorsButton" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<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=,
|
||||
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=,
|
||||
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="" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayInlineIconSpacing}">
|
||||
<FontIcon FontSize="{StaticResource PowerDisplayPrimaryGlyphFontSize}" Glyph="" />
|
||||
<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=,
|
||||
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="" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayInlineIconSpacing}">
|
||||
<FontIcon FontSize="{StaticResource PowerDisplayPrimaryGlyphFontSize}" Glyph="" />
|
||||
<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=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
Content="{ui:FontIcon Glyph=}"
|
||||
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="" />
|
||||
<TextBlock x:Uid="PowerDisplay_Monitor_VcpCodes_CopyText" FontSize="12" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="{StaticResource PowerDisplayCompactSpacing}">
|
||||
<FontIcon FontSize="{StaticResource PowerDisplaySecondaryGlyphFontSize}" Glyph="" />
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user