mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Introduce PowerDisplay
This commit is contained in:
@@ -85,6 +85,7 @@
|
|||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||||
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
|
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
|
||||||
|
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
|
||||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
|
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
|
||||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
|
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
|
||||||
@@ -111,6 +112,7 @@
|
|||||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||||
<PackageVersion Include="WinUIEx" Version="2.8.0" />
|
<PackageVersion Include="WinUIEx" Version="2.8.0" />
|
||||||
|
<PackageVersion Include="WmiLight" Version="6.14.0" />
|
||||||
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
||||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||||
|
|||||||
@@ -517,6 +517,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\m
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerDisplay", "PowerDisplay", "{B5E6F789-0123-4567-8901-23456789ABCD}"
|
||||||
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilePreviewCommon", "src\common\FilePreviewCommon\FilePreviewCommon.csproj", "{9EBAA524-0EDA-470B-95D4-39383285CBB2}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilePreviewCommon", "src\common\FilePreviewCommon\FilePreviewCommon.csproj", "{9EBAA524-0EDA-470B-95D4-39383285CBB2}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.PowerToys", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.csproj", "{500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.PowerToys", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.csproj", "{500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}"
|
||||||
@@ -563,6 +565,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerDisplay", "src\modules\powerdisplay\PowerDisplay\PowerDisplay.csproj", "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}"
|
||||||
|
EndProject
|
||||||
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerDisplayModuleInterface", "src\modules\powerdisplay\PowerDisplayModuleInterface\PowerDisplayModuleInterface.vcxproj", "{D1234567-8901-2345-6789-ABCDEF012345}"
|
||||||
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}"
|
||||||
@@ -2246,6 +2252,22 @@ Global
|
|||||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.Build.0 = Release|ARM64
|
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.ActiveCfg = Release|x64
|
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.ActiveCfg = Release|x64
|
||||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.Build.0 = Release|x64
|
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.Build.0 = Release|x64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.Build.0 = Release|x64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.Build.0 = Release|x64
|
||||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.Build.0 = Debug|ARM64
|
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.ActiveCfg = Debug|x64
|
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
@@ -3222,6 +3244,9 @@ Global
|
|||||||
{C32D254F-7597-4CBE-BF74-D922D81CDF29} = {9873BA05-4C41-4819-9283-CF45D795431B}
|
{C32D254F-7597-4CBE-BF74-D922D81CDF29} = {9873BA05-4C41-4819-9283-CF45D795431B}
|
||||||
{02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
{02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
||||||
{8E23E173-7127-4A5F-9F93-3049F2B68047} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
{8E23E173-7127-4A5F-9F93-3049F2B68047} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
||||||
|
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF} = {B5E6F789-0123-4567-8901-23456789ABCD}
|
||||||
|
{D1234567-8901-2345-6789-ABCDEF012345} = {B5E6F789-0123-4567-8901-23456789ABCD}
|
||||||
|
{B5E6F789-0123-4567-8901-23456789ABCD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
|
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
|
||||||
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||||
{1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95}
|
{1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95}
|
||||||
|
|||||||
30
installer/PowerToysSetup/PowerDisplay.wxs
Normal file
30
installer/PowerToysSetup/PowerDisplay.wxs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||||
|
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||||
|
|
||||||
|
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||||
|
|
||||||
|
<?define PowerDisplayAssetsFiles=?>
|
||||||
|
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||||
|
|
||||||
|
<Fragment>
|
||||||
|
<!-- Power Display -->
|
||||||
|
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||||
|
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||||
|
</DirectoryRef>
|
||||||
|
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||||
|
<!-- Generated by generateFileComponents.ps1 -->
|
||||||
|
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||||
|
</DirectoryRef>
|
||||||
|
|
||||||
|
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||||
|
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||||
|
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||||
|
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||||
|
</RegistryKey>
|
||||||
|
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||||
|
</Component>
|
||||||
|
</ComponentGroup>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
|
</Wix>
|
||||||
30
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
30
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||||
|
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||||
|
|
||||||
|
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||||
|
|
||||||
|
<?define PowerDisplayAssetsFiles=?>
|
||||||
|
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||||
|
|
||||||
|
<Fragment>
|
||||||
|
<!-- Power Display -->
|
||||||
|
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||||
|
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||||
|
</DirectoryRef>
|
||||||
|
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||||
|
<!-- Generated by generateFileComponents.ps1 -->
|
||||||
|
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||||
|
</DirectoryRef>
|
||||||
|
|
||||||
|
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||||
|
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||||
|
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||||
|
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||||
|
</RegistryKey>
|
||||||
|
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||||
|
</Component>
|
||||||
|
</ComponentGroup>
|
||||||
|
|
||||||
|
</Fragment>
|
||||||
|
</Wix>
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||||
|
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||||
<ComponentGroupRef Id="RunComponentGroup" />
|
<ComponentGroupRef Id="RunComponentGroup" />
|
||||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ namespace Common.UI
|
|||||||
NewPlus,
|
NewPlus,
|
||||||
CmdPal,
|
CmdPal,
|
||||||
ZoomIt,
|
ZoomIt,
|
||||||
|
PowerDisplay,
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||||
@@ -113,6 +114,8 @@ namespace Common.UI
|
|||||||
return "CmdPal";
|
return "CmdPal";
|
||||||
case SettingsWindow.ZoomIt:
|
case SettingsWindow.ZoomIt:
|
||||||
return "ZoomIt";
|
return "ZoomIt";
|
||||||
|
case SettingsWindow.PowerDisplay:
|
||||||
|
return "PowerDisplay";
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ namespace ManagedCommon
|
|||||||
PowerRename,
|
PowerRename,
|
||||||
PowerLauncher,
|
PowerLauncher,
|
||||||
PowerAccent,
|
PowerAccent,
|
||||||
|
PowerDisplay,
|
||||||
RegistryPreview,
|
RegistryPreview,
|
||||||
MeasureTool,
|
MeasureTool,
|
||||||
ShortcutGuide,
|
ShortcutGuide,
|
||||||
|
|||||||
@@ -195,4 +195,12 @@ namespace winrt::PowerToys::Interop::implementation
|
|||||||
{
|
{
|
||||||
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
||||||
}
|
}
|
||||||
|
hstring Constants::ShowPowerDisplayEvent()
|
||||||
|
{
|
||||||
|
return CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT;
|
||||||
|
}
|
||||||
|
hstring Constants::TerminatePowerDisplayEvent()
|
||||||
|
{
|
||||||
|
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ namespace winrt::PowerToys::Interop::implementation
|
|||||||
static hstring WorkspacesHotkeyEvent();
|
static hstring WorkspacesHotkeyEvent();
|
||||||
static hstring PowerToysRunnerTerminateSettingsEvent();
|
static hstring PowerToysRunnerTerminateSettingsEvent();
|
||||||
static hstring ShowCmdPalEvent();
|
static hstring ShowCmdPalEvent();
|
||||||
|
static hstring ShowPowerDisplayEvent();
|
||||||
|
static hstring TerminatePowerDisplayEvent();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ namespace CommonSharedConstants
|
|||||||
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
|
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
|
||||||
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
|
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
|
||||||
|
|
||||||
|
// Path to the events used by PowerDisplay
|
||||||
|
const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
|
||||||
|
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||||
|
|
||||||
// used from quick access window
|
// used from quick access window
|
||||||
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
|
||||||
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
|
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";
|
||||||
|
|||||||
BIN
src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay.ico
Normal file
BIN
src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
// 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.Windows.Input;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Commands
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Basic relay command implementation for parameterless actions
|
||||||
|
/// </summary>
|
||||||
|
public partial class RelayCommand : ICommand
|
||||||
|
{
|
||||||
|
private readonly Action _execute;
|
||||||
|
private readonly Func<bool>? _canExecute;
|
||||||
|
|
||||||
|
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||||
|
{
|
||||||
|
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||||
|
_canExecute = canExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
|
||||||
|
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||||
|
|
||||||
|
public void Execute(object? parameter) => _execute();
|
||||||
|
|
||||||
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic relay command implementation for parameterized actions
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of the command parameter</typeparam>
|
||||||
|
public partial class RelayCommand<T> : ICommand
|
||||||
|
{
|
||||||
|
private readonly Action<T?> _execute;
|
||||||
|
private readonly Func<T?, bool>? _canExecute;
|
||||||
|
|
||||||
|
public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute = null)
|
||||||
|
{
|
||||||
|
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||||
|
_canExecute = canExecute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
|
||||||
|
public bool CanExecute(object? parameter) => _canExecute?.Invoke((T?)parameter) ?? true;
|
||||||
|
|
||||||
|
public void Execute(object? parameter) => _execute((T?)parameter);
|
||||||
|
|
||||||
|
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace PowerDisplay.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Application-wide constants and configuration values
|
||||||
|
/// </summary>
|
||||||
|
public static class AppConstants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// State management configuration
|
||||||
|
/// </summary>
|
||||||
|
public static class State
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interval in milliseconds to check for pending state changes to save
|
||||||
|
/// </summary>
|
||||||
|
public const int SaveIntervalMs = 2000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Name of the state file for monitor parameters
|
||||||
|
/// </summary>
|
||||||
|
public const string StateFileName = "monitor_state.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor parameter defaults and ranges
|
||||||
|
/// </summary>
|
||||||
|
public static class MonitorDefaults
|
||||||
|
{
|
||||||
|
// Brightness
|
||||||
|
public const int MinBrightness = 0;
|
||||||
|
public const int MaxBrightness = 100;
|
||||||
|
public const int DefaultBrightness = 50;
|
||||||
|
|
||||||
|
// Color Temperature (Kelvin)
|
||||||
|
public const int MinColorTemp = 2000; // Warm
|
||||||
|
public const int MaxColorTemp = 10000; // Cool
|
||||||
|
public const int DefaultColorTemp = 6500; // Neutral
|
||||||
|
|
||||||
|
// Contrast
|
||||||
|
public const int MinContrast = 0;
|
||||||
|
public const int MaxContrast = 100;
|
||||||
|
public const int DefaultContrast = 50;
|
||||||
|
|
||||||
|
// Volume
|
||||||
|
public const int MinVolume = 0;
|
||||||
|
public const int MaxVolume = 100;
|
||||||
|
public const int DefaultVolume = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI layout and timing constants
|
||||||
|
/// </summary>
|
||||||
|
public static class UI
|
||||||
|
{
|
||||||
|
// Window dimensions
|
||||||
|
public const int WindowWidth = 640;
|
||||||
|
public const int MaxWindowHeight = 650;
|
||||||
|
public const int WindowRightMargin = 10;
|
||||||
|
|
||||||
|
// Animation and layout update delays (milliseconds)
|
||||||
|
public const int AnimationDelayMs = 100;
|
||||||
|
public const int LayoutUpdateDelayMs = 50;
|
||||||
|
public const int MonitorDiscoveryDelayMs = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Application lifecycle timing constants
|
||||||
|
/// </summary>
|
||||||
|
public static class Lifetime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Normal shutdown timeout in milliseconds
|
||||||
|
/// </summary>
|
||||||
|
public const int NormalShutdownTimeoutMs = 1000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emergency shutdown timeout in milliseconds
|
||||||
|
/// </summary>
|
||||||
|
public const int EmergencyShutdownTimeoutMs = 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 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 Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts boolean values to Visibility
|
||||||
|
/// </summary>
|
||||||
|
public partial class BoolToVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is bool boolValue)
|
||||||
|
{
|
||||||
|
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is Visibility visibility)
|
||||||
|
{
|
||||||
|
return visibility == Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 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 Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts boolean values to their inverse
|
||||||
|
/// </summary>
|
||||||
|
public partial class InverseBoolConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is bool boolValue)
|
||||||
|
{
|
||||||
|
return !boolValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Default to enabled if value is not bool
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is bool boolValue)
|
||||||
|
{
|
||||||
|
return !boolValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// 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 Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts boolean values to Visibility (inverted)
|
||||||
|
/// </summary>
|
||||||
|
public partial class InverseBoolToVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is bool boolValue)
|
||||||
|
{
|
||||||
|
return boolValue ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is Visibility visibility)
|
||||||
|
{
|
||||||
|
return visibility != Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor controller interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IMonitorController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Controller name
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported monitor type
|
||||||
|
/// </summary>
|
||||||
|
MonitorType SupportedType { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the specified monitor can be controlled
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Whether the monitor can be controlled</returns>
|
||||||
|
Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets monitor brightness
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Brightness information</returns>
|
||||||
|
Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets monitor brightness
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="brightness">Brightness value (0-100)</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Operation result</returns>
|
||||||
|
Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discovers supported monitors
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>List of monitors</returns>
|
||||||
|
Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates monitor connection status
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Whether the monitor is connected</returns>
|
||||||
|
Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets monitor contrast
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Contrast information</returns>
|
||||||
|
Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets monitor contrast
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="contrast">Contrast value (0-100)</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Operation result</returns>
|
||||||
|
Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets monitor volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Volume information</returns>
|
||||||
|
Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets monitor volume
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="volume">Volume value (0-100)</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Operation result</returns>
|
||||||
|
Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets monitor color temperature
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Color temperature information</returns>
|
||||||
|
Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets monitor color temperature
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="colorTemperature">Color temperature value (2000-10000K)</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Operation result</returns>
|
||||||
|
Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets monitor capabilities string (DDC/CI)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Capabilities string</returns>
|
||||||
|
Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves current settings to monitor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="monitor">Monitor object</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Operation result</returns>
|
||||||
|
Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases resources
|
||||||
|
/// </summary>
|
||||||
|
void Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMonitorManager interface removed - YAGNI principle
|
||||||
|
// Only one implementation exists (MonitorManager), so interface abstraction is unnecessary
|
||||||
|
// This simplifies the codebase and eliminates maintenance overhead
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor list changed event arguments
|
||||||
|
/// </summary>
|
||||||
|
public class MonitorListChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public IReadOnlyList<Monitor> AddedMonitors { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Monitor> RemovedMonitors { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Monitor> AllMonitors { get; }
|
||||||
|
|
||||||
|
public MonitorListChangedEventArgs(
|
||||||
|
IReadOnlyList<Monitor> addedMonitors,
|
||||||
|
IReadOnlyList<Monitor> removedMonitors,
|
||||||
|
IReadOnlyList<Monitor> allMonitors)
|
||||||
|
{
|
||||||
|
AddedMonitors = addedMonitors;
|
||||||
|
RemovedMonitors = removedMonitors;
|
||||||
|
AllMonitors = allMonitors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// 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 PowerDisplay.Core.Models;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor status changed event arguments
|
||||||
|
/// </summary>
|
||||||
|
public class MonitorStatusChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public Monitor Monitor { get; }
|
||||||
|
|
||||||
|
public int? OldBrightness { get; }
|
||||||
|
|
||||||
|
public int NewBrightness { get; }
|
||||||
|
|
||||||
|
public bool? OldAvailability { get; }
|
||||||
|
|
||||||
|
public bool NewAvailability { get; }
|
||||||
|
|
||||||
|
public string Message { get; }
|
||||||
|
|
||||||
|
public ChangeType Type { get; }
|
||||||
|
|
||||||
|
public enum ChangeType
|
||||||
|
{
|
||||||
|
Brightness,
|
||||||
|
Contrast,
|
||||||
|
Volume,
|
||||||
|
ColorTemperature,
|
||||||
|
Availability,
|
||||||
|
General,
|
||||||
|
}
|
||||||
|
|
||||||
|
public MonitorStatusChangedEventArgs(
|
||||||
|
Monitor monitor,
|
||||||
|
int? oldBrightness,
|
||||||
|
int newBrightness,
|
||||||
|
bool? oldAvailability,
|
||||||
|
bool newAvailability)
|
||||||
|
{
|
||||||
|
Monitor = monitor;
|
||||||
|
OldBrightness = oldBrightness;
|
||||||
|
NewBrightness = newBrightness;
|
||||||
|
OldAvailability = oldAvailability;
|
||||||
|
NewAvailability = newAvailability;
|
||||||
|
Message = $"Brightness changed from {oldBrightness} to {newBrightness}";
|
||||||
|
Type = ChangeType.Brightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MonitorStatusChangedEventArgs(
|
||||||
|
Monitor monitor,
|
||||||
|
string message,
|
||||||
|
ChangeType changeType)
|
||||||
|
{
|
||||||
|
Monitor = monitor;
|
||||||
|
Message = message;
|
||||||
|
Type = changeType;
|
||||||
|
|
||||||
|
// Set defaults for compatibility
|
||||||
|
OldBrightness = null;
|
||||||
|
NewBrightness = monitor.CurrentBrightness;
|
||||||
|
OldAvailability = null;
|
||||||
|
NewAvailability = monitor.IsAvailable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Brightness information structure
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct BrightnessInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current brightness value
|
||||||
|
/// </summary>
|
||||||
|
public int Current { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum brightness value
|
||||||
|
/// </summary>
|
||||||
|
public int Minimum { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum brightness value
|
||||||
|
/// </summary>
|
||||||
|
public int Maximum { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the brightness information is valid
|
||||||
|
/// </summary>
|
||||||
|
public bool IsValid { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timestamp when the brightness information was obtained
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
|
||||||
|
public BrightnessInfo(int current, int minimum, int maximum)
|
||||||
|
{
|
||||||
|
Current = current;
|
||||||
|
Minimum = minimum;
|
||||||
|
Maximum = maximum;
|
||||||
|
IsValid = current >= minimum && current <= maximum && maximum > minimum;
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BrightnessInfo(int current, int maximum)
|
||||||
|
: this(current, 0, maximum)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates invalid brightness information
|
||||||
|
/// </summary>
|
||||||
|
public static BrightnessInfo Invalid => new(-1, -1, -1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts brightness value to percentage (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public int ToPercentage()
|
||||||
|
{
|
||||||
|
if (!IsValid || Maximum == Minimum)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)Math.Round((double)(Current - Minimum) * 100 / (Maximum - Minimum));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates brightness value from percentage
|
||||||
|
/// </summary>
|
||||||
|
public int FromPercentage(int percentage)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
percentage = Math.Clamp(percentage, 0, 100);
|
||||||
|
return Minimum + (int)Math.Round((double)(Maximum - Minimum) * percentage / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return IsValid ? $"{Current}/{Maximum} ({ToPercentage()}%)" : "Invalid";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
249
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
249
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
// 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.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using PowerDisplay.Configuration;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor model that implements property change notification
|
||||||
|
/// </summary>
|
||||||
|
public partial class Monitor : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private int _currentBrightness;
|
||||||
|
private int _currentColorTemperature = AppConstants.MonitorDefaults.DefaultColorTemp;
|
||||||
|
private bool _isAvailable = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique identifier (based on hardware ID)
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hardware ID (EDID format like GSM5C6D)
|
||||||
|
/// </summary>
|
||||||
|
public string HardwareId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display name
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor type
|
||||||
|
/// </summary>
|
||||||
|
public MonitorType Type { get; set; } = MonitorType.Unknown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current brightness (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentBrightness
|
||||||
|
{
|
||||||
|
get => _currentBrightness;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var clamped = Math.Clamp(value, MinBrightness, MaxBrightness);
|
||||||
|
if (_currentBrightness != clamped)
|
||||||
|
{
|
||||||
|
_currentBrightness = clamped;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum brightness value
|
||||||
|
/// </summary>
|
||||||
|
public int MinBrightness { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum brightness value
|
||||||
|
/// </summary>
|
||||||
|
public int MaxBrightness { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current color temperature (2000-10000K)
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentColorTemperature
|
||||||
|
{
|
||||||
|
get => _currentColorTemperature;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var clamped = Math.Clamp(value, MinColorTemperature, MaxColorTemperature);
|
||||||
|
if (_currentColorTemperature != clamped)
|
||||||
|
{
|
||||||
|
_currentColorTemperature = clamped;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum color temperature value
|
||||||
|
/// </summary>
|
||||||
|
public int MinColorTemperature { get; set; } = AppConstants.MonitorDefaults.MinColorTemp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum color temperature value
|
||||||
|
/// </summary>
|
||||||
|
public int MaxColorTemperature { get; set; } = AppConstants.MonitorDefaults.MaxColorTemp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether supports color temperature adjustment
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsColorTemperature { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether supports contrast adjustment
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether supports volume adjustment (for audio-capable monitors)
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
|
||||||
|
|
||||||
|
private int _currentContrast = 50;
|
||||||
|
private int _currentVolume = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current contrast (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentContrast
|
||||||
|
{
|
||||||
|
get => _currentContrast;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var clamped = Math.Clamp(value, MinContrast, MaxContrast);
|
||||||
|
if (_currentContrast != clamped)
|
||||||
|
{
|
||||||
|
_currentContrast = clamped;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum contrast value
|
||||||
|
/// </summary>
|
||||||
|
public int MinContrast { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum contrast value
|
||||||
|
/// </summary>
|
||||||
|
public int MaxContrast { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current volume (0-100)
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentVolume
|
||||||
|
{
|
||||||
|
get => _currentVolume;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var clamped = Math.Clamp(value, MinVolume, MaxVolume);
|
||||||
|
if (_currentVolume != clamped)
|
||||||
|
{
|
||||||
|
_currentVolume = clamped;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum volume value
|
||||||
|
/// </summary>
|
||||||
|
public int MinVolume { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum volume value
|
||||||
|
/// </summary>
|
||||||
|
public int MaxVolume { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether available/online
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAvailable
|
||||||
|
{
|
||||||
|
get => _isAvailable;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isAvailable != value)
|
||||||
|
{
|
||||||
|
_isAvailable = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Physical monitor handle (for DDC/CI)
|
||||||
|
/// </summary>
|
||||||
|
public IntPtr Handle { get; set; } = IntPtr.Zero;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey)
|
||||||
|
/// </summary>
|
||||||
|
public string DeviceKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance name (used by WMI)
|
||||||
|
/// </summary>
|
||||||
|
public string InstanceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manufacturer information
|
||||||
|
/// </summary>
|
||||||
|
public string Manufacturer { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connection type (HDMI, DP, VGA, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public string ConnectionType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Communication method (DDC/CI, WMI, HDR API, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public string CommunicationMethod { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supported control methods
|
||||||
|
/// </summary>
|
||||||
|
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last update time
|
||||||
|
/// </summary>
|
||||||
|
public DateTime LastUpdate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Name} ({Type}) - {CurrentBrightness}%";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update monitor status
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateStatus(int brightness, bool isAvailable = true)
|
||||||
|
{
|
||||||
|
IsAvailable = isAvailable;
|
||||||
|
if (isAvailable)
|
||||||
|
{
|
||||||
|
CurrentBrightness = brightness;
|
||||||
|
LastUpdate = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor control capabilities flags
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum MonitorCapabilities
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports brightness control
|
||||||
|
/// </summary>
|
||||||
|
Brightness = 1 << 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports contrast control
|
||||||
|
/// </summary>
|
||||||
|
Contrast = 1 << 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports DDC/CI protocol
|
||||||
|
/// </summary>
|
||||||
|
DdcCi = 1 << 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports WMI control
|
||||||
|
/// </summary>
|
||||||
|
Wmi = 1 << 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports HDR
|
||||||
|
/// </summary>
|
||||||
|
Hdr = 1 << 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports high-level monitor API
|
||||||
|
/// </summary>
|
||||||
|
HighLevel = 1 << 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Supports volume control
|
||||||
|
/// </summary>
|
||||||
|
Volume = 1 << 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor operation result
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct MonitorOperationResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the operation was successful
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error message
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System error code
|
||||||
|
/// </summary>
|
||||||
|
public int? ErrorCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Operation timestamp
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
|
||||||
|
private MonitorOperationResult(bool isSuccess, string? errorMessage = null, int? errorCode = null)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
ErrorCode = errorCode;
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a successful result
|
||||||
|
/// </summary>
|
||||||
|
public static MonitorOperationResult Success() => new(true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a failed result
|
||||||
|
/// </summary>
|
||||||
|
public static MonitorOperationResult Failure(string errorMessage, int? errorCode = null)
|
||||||
|
=> new(false, errorMessage, errorCode);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return IsSuccess ? "Success" : $"Failed: {ErrorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor type enumeration
|
||||||
|
/// </summary>
|
||||||
|
public enum MonitorType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unknown type
|
||||||
|
/// </summary>
|
||||||
|
Unknown,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal display (laptop screen, controlled via WMI)
|
||||||
|
/// </summary>
|
||||||
|
Internal,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// External display (controlled via DDC/CI)
|
||||||
|
/// </summary>
|
||||||
|
External,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HDR display (controlled via Display Config API)
|
||||||
|
/// </summary>
|
||||||
|
HDR,
|
||||||
|
}
|
||||||
|
}
|
||||||
455
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
455
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Core.Interfaces;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using PowerDisplay.Core.Utils;
|
||||||
|
using PowerDisplay.Native.DDC;
|
||||||
|
using PowerDisplay.Native.WMI;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor manager for unified control of all monitors
|
||||||
|
/// No interface abstraction - KISS principle (only one implementation needed)
|
||||||
|
/// </summary>
|
||||||
|
public partial class MonitorManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<Monitor> _monitors = new();
|
||||||
|
private readonly List<IMonitorController> _controllers = new();
|
||||||
|
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
|
||||||
|
|
||||||
|
public event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
|
||||||
|
|
||||||
|
public MonitorManager()
|
||||||
|
{
|
||||||
|
// Initialize controllers
|
||||||
|
InitializeControllers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize controllers
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeControllers()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// DDC/CI controller (external monitors)
|
||||||
|
_controllers.Add(new DdcCiController());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// WMI controller (internal monitors)
|
||||||
|
// First check if WMI is available
|
||||||
|
if (WmiController.IsWmiAvailable())
|
||||||
|
{
|
||||||
|
_controllers.Add(new WmiController());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInfo("WMI brightness control not available on this system");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discover all monitors
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _discoveryLock.WaitAsync(cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var oldMonitors = _monitors.ToList();
|
||||||
|
var newMonitors = new List<Monitor>();
|
||||||
|
|
||||||
|
// Discover monitors supported by all controllers in parallel
|
||||||
|
var discoveryTasks = _controllers.Select(async controller =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
|
||||||
|
return (Controller: controller, Monitors: monitors.ToList());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// If a controller fails, log the error and return empty list
|
||||||
|
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
|
||||||
|
return (Controller: controller, Monitors: new List<Monitor>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(discoveryTasks);
|
||||||
|
|
||||||
|
// Collect all discovered monitors
|
||||||
|
foreach (var (controller, monitors) in results)
|
||||||
|
{
|
||||||
|
foreach (var monitor in monitors)
|
||||||
|
{
|
||||||
|
// Verify if monitor can be controlled
|
||||||
|
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
||||||
|
{
|
||||||
|
// Get current brightness
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||||
|
if (brightnessInfo.IsValid)
|
||||||
|
{
|
||||||
|
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
|
||||||
|
monitor.MinBrightness = brightnessInfo.Minimum;
|
||||||
|
monitor.MaxBrightness = brightnessInfo.Maximum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// If unable to get brightness, use default values
|
||||||
|
Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
newMonitors.Add(monitor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update monitor list
|
||||||
|
_monitors.Clear();
|
||||||
|
_monitors.AddRange(newMonitors);
|
||||||
|
|
||||||
|
// Trigger change events
|
||||||
|
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
|
||||||
|
var removedMonitors = oldMonitors.Where(o => !newMonitors.Any(m => m.Id == o.Id)).ToList();
|
||||||
|
|
||||||
|
if (addedMonitors.Count > 0 || removedMonitors.Count > 0)
|
||||||
|
{
|
||||||
|
MonitorsChanged?.Invoke(this, new MonitorListChangedEventArgs(
|
||||||
|
addedMonitors.AsReadOnly(),
|
||||||
|
removedMonitors.AsReadOnly(),
|
||||||
|
_monitors.AsReadOnly()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _monitors.AsReadOnly();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_discoveryLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get brightness of the specified monitor
|
||||||
|
/// </summary>
|
||||||
|
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var monitor = GetMonitor(monitorId);
|
||||||
|
if (monitor == null)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var controller = GetControllerForMonitor(monitor);
|
||||||
|
if (controller == null)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.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 BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set brightness of the specified monitor
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"[MonitorManager] SetBrightnessAsync called for {monitorId}, brightness={brightness}");
|
||||||
|
|
||||||
|
var monitor = GetMonitor(monitorId);
|
||||||
|
if (monitor == null)
|
||||||
|
{
|
||||||
|
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
|
||||||
|
return MonitorOperationResult.Failure("Monitor not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug($"[MonitorManager] Monitor found: {monitor.Id}, Type={monitor.Type}, Handle=0x{monitor.Handle:X}, DeviceKey={monitor.DeviceKey}");
|
||||||
|
|
||||||
|
var controller = GetControllerForMonitor(monitor);
|
||||||
|
if (controller == null)
|
||||||
|
{
|
||||||
|
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}");
|
||||||
|
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug($"[MonitorManager] Controller found: {controller.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"[MonitorManager] Calling controller.SetBrightnessAsync for {monitor.Id}");
|
||||||
|
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
|
||||||
|
Logger.LogDebug($"[MonitorManager] controller.SetBrightnessAsync returned: IsSuccess={result.IsSuccess}, ErrorMessage={result.ErrorMessage}");
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
// Update monitor status
|
||||||
|
monitor.UpdateStatus(brightness, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If setting fails, monitor may be unavailable
|
||||||
|
monitor.IsAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
monitor.IsAvailable = false;
|
||||||
|
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set brightness of all monitors
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var tasks = _monitors
|
||||||
|
.Where(m => m.IsAvailable)
|
||||||
|
.Select(async monitor =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set contrast of the specified monitor
|
||||||
|
/// </summary>
|
||||||
|
public Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
|
||||||
|
=> ExecuteMonitorOperationAsync(
|
||||||
|
monitorId,
|
||||||
|
contrast,
|
||||||
|
(ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct),
|
||||||
|
(mon, val) => mon.CurrentContrast = val,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set volume of the specified monitor
|
||||||
|
/// </summary>
|
||||||
|
public Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
|
||||||
|
=> ExecuteMonitorOperationAsync(
|
||||||
|
monitorId,
|
||||||
|
volume,
|
||||||
|
(ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct),
|
||||||
|
(mon, val) => mon.CurrentVolume = val,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor color temperature
|
||||||
|
/// </summary>
|
||||||
|
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var monitor = GetMonitor(monitorId);
|
||||||
|
if (monitor == null)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var controller = GetControllerForMonitor(monitor);
|
||||||
|
if (controller == null)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set monitor color temperature
|
||||||
|
/// </summary>
|
||||||
|
public Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
|
||||||
|
=> ExecuteMonitorOperationAsync(
|
||||||
|
monitorId,
|
||||||
|
colorTemperature,
|
||||||
|
(ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct),
|
||||||
|
(mon, val) => mon.CurrentColorTemperature = val,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize color temperature for a monitor (async operation)
|
||||||
|
/// </summary>
|
||||||
|
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
|
||||||
|
if (tempInfo.IsValid)
|
||||||
|
{
|
||||||
|
var monitor = GetMonitor(monitorId);
|
||||||
|
if (monitor != null)
|
||||||
|
{
|
||||||
|
// Convert VCP value to approximate Kelvin temperature
|
||||||
|
// This is a rough mapping - actual values depend on monitor implementation
|
||||||
|
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
|
||||||
|
monitor.CurrentColorTemperature = kelvin;
|
||||||
|
|
||||||
|
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert VCP value to approximate Kelvin temperature (uses unified converter)
|
||||||
|
/// </summary>
|
||||||
|
private static int ConvertVcpValueToKelvin(int vcpValue, int maxVcpValue)
|
||||||
|
{
|
||||||
|
return ColorTemperatureConverter.VcpToKelvin(vcpValue, maxVcpValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor by ID
|
||||||
|
/// </summary>
|
||||||
|
public Monitor? GetMonitor(string monitorId)
|
||||||
|
{
|
||||||
|
return _monitors.FirstOrDefault(m => m.Id == monitorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get controller for the monitor
|
||||||
|
/// </summary>
|
||||||
|
private IMonitorController? GetControllerForMonitor(Monitor monitor)
|
||||||
|
{
|
||||||
|
return _controllers.FirstOrDefault(c => c.SupportedType == monitor.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic helper to execute monitor operations with common error handling.
|
||||||
|
/// Eliminates code duplication across Set* methods.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<MonitorOperationResult> ExecuteMonitorOperationAsync<T>(
|
||||||
|
string monitorId,
|
||||||
|
T value,
|
||||||
|
Func<IMonitorController, Monitor, T, CancellationToken, Task<MonitorOperationResult>> operation,
|
||||||
|
Action<Monitor, T> onSuccess,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var monitor = GetMonitor(monitorId);
|
||||||
|
if (monitor == null)
|
||||||
|
{
|
||||||
|
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
|
||||||
|
return MonitorOperationResult.Failure("Monitor not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var controller = GetControllerForMonitor(monitor);
|
||||||
|
if (controller == null)
|
||||||
|
{
|
||||||
|
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}");
|
||||||
|
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await operation(controller, monitor, value, cancellationToken);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
onSuccess(monitor, value);
|
||||||
|
monitor.LastUpdate = DateTime.Now;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
monitor.IsAvailable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
monitor.IsAvailable = false;
|
||||||
|
Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}");
|
||||||
|
return MonitorOperationResult.Failure($"Exception: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed && disposing)
|
||||||
|
{
|
||||||
|
_discoveryLock?.Dispose();
|
||||||
|
|
||||||
|
// Release all controllers
|
||||||
|
foreach (var controller in _controllers)
|
||||||
|
{
|
||||||
|
controller?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_controllers.Clear();
|
||||||
|
_monitors.Clear();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Core.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Utility class for converting between Kelvin color temperature and VCP values.
|
||||||
|
/// Centralizes temperature conversion logic to eliminate code duplication (KISS principle).
|
||||||
|
/// </summary>
|
||||||
|
public static class ColorTemperatureConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum color temperature in Kelvin (warm)
|
||||||
|
/// </summary>
|
||||||
|
public const int MinKelvin = 2000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum color temperature in Kelvin (cool)
|
||||||
|
/// </summary>
|
||||||
|
public const int MaxKelvin = 10000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert VCP value to Kelvin temperature
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vcpValue">Current VCP value</param>
|
||||||
|
/// <param name="vcpMax">Maximum VCP value</param>
|
||||||
|
/// <returns>Temperature in Kelvin (2000-10000K)</returns>
|
||||||
|
public static int VcpToKelvin(int vcpValue, int vcpMax)
|
||||||
|
{
|
||||||
|
if (vcpMax <= 0)
|
||||||
|
{
|
||||||
|
return (MinKelvin + MaxKelvin) / 2; // Default to neutral 6000K
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize VCP value to 0-1 range
|
||||||
|
double normalized = Math.Clamp((double)vcpValue / vcpMax, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Map to Kelvin range
|
||||||
|
int kelvin = (int)(MinKelvin + (normalized * (MaxKelvin - MinKelvin)));
|
||||||
|
|
||||||
|
return Math.Clamp(kelvin, MinKelvin, MaxKelvin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert Kelvin temperature to VCP value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="kelvin">Temperature in Kelvin (2000-10000K)</param>
|
||||||
|
/// <param name="vcpMax">Maximum VCP value</param>
|
||||||
|
/// <returns>VCP value (0 to vcpMax)</returns>
|
||||||
|
public static int KelvinToVcp(int kelvin, int vcpMax)
|
||||||
|
{
|
||||||
|
// Clamp input to valid range
|
||||||
|
kelvin = Math.Clamp(kelvin, MinKelvin, MaxKelvin);
|
||||||
|
|
||||||
|
// Normalize kelvin to 0-1 range
|
||||||
|
double normalized = (double)(kelvin - MinKelvin) / (MaxKelvin - MinKelvin);
|
||||||
|
|
||||||
|
// Map to VCP range
|
||||||
|
int vcpValue = (int)(normalized * vcpMax);
|
||||||
|
|
||||||
|
return Math.Clamp(vcpValue, 0, vcpMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if a temperature value is in valid Kelvin range
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsValidKelvin(int kelvin)
|
||||||
|
{
|
||||||
|
return kelvin >= MinKelvin && kelvin <= MaxKelvin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a human-readable description of color temperature
|
||||||
|
/// </summary>
|
||||||
|
public static string GetTemperatureDescription(int kelvin)
|
||||||
|
{
|
||||||
|
return kelvin switch
|
||||||
|
{
|
||||||
|
< 3500 => "Warm",
|
||||||
|
< 5500 => "Neutral",
|
||||||
|
< 7500 => "Cool",
|
||||||
|
_ => "Very Cool",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs
Normal file
9
src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// 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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
// Enable compile-time marshalling for all P/Invoke declarations
|
||||||
|
// This allows LibraryImport to handle array marshalling and achieve 100% coverage
|
||||||
|
[assembly: DisableRuntimeMarshalling]
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Configuration;
|
||||||
|
using PowerDisplay.Serialization;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages monitor parameter state in a separate file from main settings.
|
||||||
|
/// This avoids FileSystemWatcher feedback loops by separating read-only config (settings.json)
|
||||||
|
/// from frequently-updated state (monitor_state.json).
|
||||||
|
/// Simplified to use direct save strategy for reliability and simplicity (KISS principle).
|
||||||
|
/// </summary>
|
||||||
|
public partial class MonitorStateManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _stateFilePath;
|
||||||
|
private readonly Dictionary<string, MonitorState> _states = new();
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor state data (internal tracking, not serialized)
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MonitorState
|
||||||
|
{
|
||||||
|
public int Brightness { get; set; }
|
||||||
|
|
||||||
|
public int ColorTemperature { get; set; }
|
||||||
|
|
||||||
|
public int Contrast { get; set; }
|
||||||
|
|
||||||
|
public int Volume { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public MonitorStateManager()
|
||||||
|
{
|
||||||
|
// Store state file in same location as settings.json but with different name
|
||||||
|
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
|
||||||
|
|
||||||
|
if (!Directory.Exists(powerToysPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(powerToysPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stateFilePath = Path.Combine(powerToysPath, AppConstants.State.StateFileName);
|
||||||
|
|
||||||
|
// Load existing state if available
|
||||||
|
LoadStateFromDisk();
|
||||||
|
|
||||||
|
Logger.LogInfo($"MonitorStateManager initialized with direct-save strategy, state file: {_stateFilePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update monitor parameter and save immediately to disk.
|
||||||
|
/// Uses HardwareId as the stable key.
|
||||||
|
/// Direct-save strategy ensures no data loss and simplifies code (KISS principle).
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateMonitorParameter(string hardwareId, string property, int value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hardwareId))
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Cannot update monitor parameter: HardwareId is empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// Get or create state entry using HardwareId
|
||||||
|
if (!_states.TryGetValue(hardwareId, out var state))
|
||||||
|
{
|
||||||
|
state = new MonitorState();
|
||||||
|
_states[hardwareId] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the specific property
|
||||||
|
switch (property)
|
||||||
|
{
|
||||||
|
case "Brightness":
|
||||||
|
state.Brightness = value;
|
||||||
|
break;
|
||||||
|
case "ColorTemperature":
|
||||||
|
state.ColorTemperature = value;
|
||||||
|
break;
|
||||||
|
case "Contrast":
|
||||||
|
state.Contrast = value;
|
||||||
|
break;
|
||||||
|
case "Volume":
|
||||||
|
state.Volume = value;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Logger.LogWarning($"Unknown property: {property}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save immediately after update - simple and reliable!
|
||||||
|
SaveStateToDisk();
|
||||||
|
|
||||||
|
Logger.LogTrace($"[State] Updated and saved {property}={value} for monitor HardwareId='{hardwareId}'");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get saved parameters for a monitor using HardwareId
|
||||||
|
/// </summary>
|
||||||
|
public (int Brightness, int ColorTemperature, int Contrast, int Volume)? GetMonitorParameters(string hardwareId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hardwareId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_states.TryGetValue(hardwareId, out var state))
|
||||||
|
{
|
||||||
|
return (state.Brightness, state.ColorTemperature, state.Contrast, state.Volume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if state exists for a monitor (by HardwareId)
|
||||||
|
/// </summary>
|
||||||
|
public bool HasMonitorState(string hardwareId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hardwareId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _states.ContainsKey(hardwareId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load state from disk
|
||||||
|
/// </summary>
|
||||||
|
private void LoadStateFromDisk()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_stateFilePath))
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[State] No existing state file found, starting fresh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = File.ReadAllText(_stateFilePath);
|
||||||
|
var stateFile = JsonSerializer.Deserialize(json, AppJsonContext.Default.MonitorStateFile);
|
||||||
|
|
||||||
|
if (stateFile?.Monitors != null)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
foreach (var kvp in stateFile.Monitors)
|
||||||
|
{
|
||||||
|
var monitorKey = kvp.Key; // Should be HardwareId (e.g., "GSM5C6D")
|
||||||
|
var entry = kvp.Value;
|
||||||
|
|
||||||
|
_states[monitorKey] = new MonitorState
|
||||||
|
{
|
||||||
|
Brightness = entry.Brightness,
|
||||||
|
ColorTemperature = entry.ColorTemperature,
|
||||||
|
Contrast = entry.Contrast,
|
||||||
|
Volume = entry.Volume,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo($"[State] Loaded state for {stateFile.Monitors.Count} monitors from {_stateFilePath}");
|
||||||
|
Logger.LogInfo($"[State] Monitor keys in state file: {string.Join(", ", stateFile.Monitors.Keys)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to load monitor state: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save current state to disk immediately.
|
||||||
|
/// Simplified direct-save approach - no timer, no dirty flags, just save!
|
||||||
|
/// </summary>
|
||||||
|
private void SaveStateToDisk()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build state file
|
||||||
|
var stateFile = new MonitorStateFile
|
||||||
|
{
|
||||||
|
LastUpdated = DateTime.Now,
|
||||||
|
};
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
foreach (var kvp in _states)
|
||||||
|
{
|
||||||
|
var monitorId = kvp.Key;
|
||||||
|
var state = kvp.Value;
|
||||||
|
|
||||||
|
stateFile.Monitors[monitorId] = new MonitorStateEntry
|
||||||
|
{
|
||||||
|
Brightness = state.Brightness,
|
||||||
|
ColorTemperature = state.ColorTemperature,
|
||||||
|
Contrast = state.Contrast,
|
||||||
|
Volume = state.Volume,
|
||||||
|
LastUpdated = now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to disk
|
||||||
|
var json = JsonSerializer.Serialize(stateFile, AppJsonContext.Default.MonitorStateFile);
|
||||||
|
File.WriteAllText(_stateFilePath, json);
|
||||||
|
|
||||||
|
Logger.LogDebug($"[State] Saved state for {stateFile.Monitors.Count} monitors");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to save monitor state: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// State is already saved with each update, no need for final flush!
|
||||||
|
Logger.LogInfo("MonitorStateManager disposed");
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// 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 Microsoft.Windows.ApplicationModel.Resources;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Helpers
|
||||||
|
{
|
||||||
|
public static class ResourceLoaderInstance
|
||||||
|
{
|
||||||
|
public static ResourceLoader ResourceLoader { get; private set; }
|
||||||
|
|
||||||
|
static ResourceLoaderInstance()
|
||||||
|
{
|
||||||
|
ResourceLoader = new ResourceLoader("PowerToys.PowerDisplay.pri");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using ManagedCommon;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class to open PowerToys Settings application.
|
||||||
|
/// Simplified version for PowerDisplay module (AOT compatible).
|
||||||
|
/// </summary>
|
||||||
|
internal static class SettingsDeepLink
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Opens PowerToys Settings to PowerDisplay page
|
||||||
|
/// </summary>
|
||||||
|
public static void OpenPowerDisplaySettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// PowerDisplay is a WinUI3 app, PowerToys.exe is in parent directory
|
||||||
|
var directoryPath = Path.Combine(AppContext.BaseDirectory, "..", "PowerToys.exe");
|
||||||
|
|
||||||
|
var startInfo = new ProcessStartInfo(directoryPath)
|
||||||
|
{
|
||||||
|
Arguments = "--open-settings=PowerDisplay",
|
||||||
|
UseShellExecute = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Process.Start(startInfo);
|
||||||
|
Logger.LogInfo("Opened PowerToys Settings to PowerDisplay page");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to open PowerToys Settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/modules/powerdisplay/PowerDisplay/Helpers/SimpleDebouncer.cs
Normal file
106
src/modules/powerdisplay/PowerDisplay/Helpers/SimpleDebouncer.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simple debouncer that delays execution of an action until a quiet period.
|
||||||
|
/// Replaces the complex PropertyUpdateQueue with a much simpler approach (KISS principle).
|
||||||
|
/// </summary>
|
||||||
|
public partial class SimpleDebouncer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly int _delayMs;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SimpleDebouncer"/> class.
|
||||||
|
/// Create a debouncer with specified delay
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delayMs">Delay in milliseconds before executing action</param>
|
||||||
|
public SimpleDebouncer(int delayMs = 300)
|
||||||
|
{
|
||||||
|
_delayMs = delayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debounce an async action. Cancels previous invocation if still pending.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="action">Async action to execute after delay</param>
|
||||||
|
public async void Debounce(Func<Task> action)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationTokenSource cts;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// Cancel previous invocation
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
cts = _cts;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Wait for quiet period
|
||||||
|
await Task.Delay(_delayMs, cts.Token);
|
||||||
|
|
||||||
|
// Execute action if not cancelled
|
||||||
|
if (!cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Expected when debouncing - a newer call cancelled this one
|
||||||
|
Logger.LogTrace("Debounced action cancelled (replaced by newer call)");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Debounced action failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debounce a synchronous action
|
||||||
|
/// </summary>
|
||||||
|
public void Debounce(Action action)
|
||||||
|
{
|
||||||
|
Debounce(() =>
|
||||||
|
{
|
||||||
|
action();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
_cts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
484
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconHelper.cs
Normal file
484
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconHelper.cs
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
// 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.Drawing;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using PowerDisplay.Native;
|
||||||
|
using static PowerDisplay.Native.PInvoke;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// System tray icon helper class
|
||||||
|
/// </summary>
|
||||||
|
public partial class TrayIconHelper : IDisposable
|
||||||
|
{
|
||||||
|
private const uint NifMessage = 0x00000001;
|
||||||
|
private const uint NifIcon = 0x00000002;
|
||||||
|
private const uint NifTip = 0x00000004;
|
||||||
|
private const uint NifInfo = 0x00000010;
|
||||||
|
|
||||||
|
private const uint NimAdd = 0x00000000;
|
||||||
|
private const uint NimModify = 0x00000001;
|
||||||
|
private const uint NimDelete = 0x00000002;
|
||||||
|
|
||||||
|
private const uint WmUser = 0x0400;
|
||||||
|
private const uint WmTrayicon = WmUser + 1;
|
||||||
|
private const uint WmLbuttonup = 0x0202;
|
||||||
|
private const uint WmRbuttonup = 0x0205;
|
||||||
|
private const uint WmCommand = 0x0111;
|
||||||
|
|
||||||
|
private uint _wmTaskbarCreated; // TaskbarCreated message ID
|
||||||
|
|
||||||
|
// Menu item IDs
|
||||||
|
private const int IdShow = 1001;
|
||||||
|
private const int IdExit = 1002;
|
||||||
|
private const int IdRefresh = 1003;
|
||||||
|
private const int IdSettings = 1004;
|
||||||
|
|
||||||
|
private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
private const uint MfString = 0x00000000;
|
||||||
|
private const uint MfSeparator = 0x00000800;
|
||||||
|
private const uint TpmLeftalign = 0x0000;
|
||||||
|
private const uint TpmReturncmd = 0x0100;
|
||||||
|
|
||||||
|
private const int SwHide = 0;
|
||||||
|
private const int SwShow = 5;
|
||||||
|
|
||||||
|
private IntPtr _messageWindowHandle;
|
||||||
|
private NOTIFYICONDATA _notifyIconData;
|
||||||
|
private bool _isDisposed;
|
||||||
|
private WndProc _wndProc;
|
||||||
|
private Window _mainWindow;
|
||||||
|
private Action? _onShowWindow;
|
||||||
|
private Action? _onExitApplication;
|
||||||
|
private Action? _onRefresh;
|
||||||
|
private Action? _onSettings;
|
||||||
|
private bool _isWindowVisible = true;
|
||||||
|
private System.Drawing.Icon? _trayIcon; // Keep icon reference to prevent garbage collection
|
||||||
|
|
||||||
|
public TrayIconHelper(Window mainWindow)
|
||||||
|
{
|
||||||
|
_mainWindow = mainWindow;
|
||||||
|
_wndProc = WindowProc;
|
||||||
|
|
||||||
|
// Register TaskbarCreated message
|
||||||
|
_wmTaskbarCreated = RegisterWindowMessage("TaskbarCreated");
|
||||||
|
Logger.LogInfo($"Registered TaskbarCreated message: {_wmTaskbarCreated}");
|
||||||
|
|
||||||
|
if (!CreateMessageWindow())
|
||||||
|
{
|
||||||
|
Logger.LogError("Failed to create message window");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTrayIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set callback functions
|
||||||
|
/// </summary>
|
||||||
|
public void SetCallbacks(Action onShow, Action onExit, Action? onRefresh = null, Action? onSettings = null)
|
||||||
|
{
|
||||||
|
_onShowWindow = onShow;
|
||||||
|
_onExitApplication = onExit;
|
||||||
|
_onRefresh = onRefresh;
|
||||||
|
_onSettings = onSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create message window - using system predefined Message window class
|
||||||
|
/// </summary>
|
||||||
|
private bool CreateMessageWindow()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Creating message window using system Message class...");
|
||||||
|
|
||||||
|
// Use system predefined "Message" window class, no registration needed
|
||||||
|
// HWND_MESSAGE (-3) creates pure message window, no hInstance needed
|
||||||
|
_messageWindowHandle = CreateWindowEx(
|
||||||
|
0, // dwExStyle
|
||||||
|
"Message", // lpClassName - system predefined message window class
|
||||||
|
string.Empty, // lpWindowName
|
||||||
|
0, // dwStyle
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0, // x, y, width, height
|
||||||
|
new IntPtr(-3), // hWndParent = HWND_MESSAGE (pure message window)
|
||||||
|
IntPtr.Zero, // hMenu
|
||||||
|
IntPtr.Zero, // hInstance - not needed
|
||||||
|
IntPtr.Zero); // lpParam
|
||||||
|
|
||||||
|
if (_messageWindowHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
var error = Marshal.GetLastWin32Error();
|
||||||
|
Logger.LogError($"CreateWindowEx failed with error: {error}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo($"Message window created successfully: {_messageWindowHandle}");
|
||||||
|
|
||||||
|
// Set window procedure to handle our messages
|
||||||
|
SetWindowLongPtr(_messageWindowHandle, -4, Marshal.GetFunctionPointerForDelegate(_wndProc));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"CreateMessageWindow exception: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create tray icon
|
||||||
|
/// </summary>
|
||||||
|
private unsafe void CreateTrayIcon()
|
||||||
|
{
|
||||||
|
if (_messageWindowHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Logger.LogError("Cannot create tray icon: invalid message window handle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try to delete any existing old icon (if any)
|
||||||
|
var tempData = new NOTIFYICONDATA
|
||||||
|
{
|
||||||
|
CbSize = (uint)sizeof(NOTIFYICONDATA),
|
||||||
|
HWnd = _messageWindowHandle,
|
||||||
|
UID = 1,
|
||||||
|
};
|
||||||
|
Shell_NotifyIcon(NimDelete, ref tempData);
|
||||||
|
|
||||||
|
// Get icon handle
|
||||||
|
var iconHandle = GetDefaultIcon();
|
||||||
|
if (iconHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Logger.LogError("Cannot create tray icon: invalid icon handle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_notifyIconData = new NOTIFYICONDATA
|
||||||
|
{
|
||||||
|
CbSize = (uint)sizeof(NOTIFYICONDATA),
|
||||||
|
HWnd = _messageWindowHandle,
|
||||||
|
UID = 1,
|
||||||
|
UFlags = NifMessage | NifIcon | NifTip,
|
||||||
|
UCallbackMessage = WmTrayicon,
|
||||||
|
HIcon = iconHandle,
|
||||||
|
};
|
||||||
|
_notifyIconData.SetTip("Power Display");
|
||||||
|
|
||||||
|
// Retry mechanism: try up to 3 times to create tray icon
|
||||||
|
const int maxRetries = 3;
|
||||||
|
const int retryDelayMs = 500;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Creating tray icon (attempt {attempt}/{maxRetries})...");
|
||||||
|
|
||||||
|
bool result = Shell_NotifyIcon(NimAdd, ref _notifyIconData);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"Tray icon created successfully on attempt {attempt}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastError = Marshal.GetLastWin32Error();
|
||||||
|
Logger.LogWarning($"Failed to create tray icon on attempt {attempt}. Error: {lastError}");
|
||||||
|
|
||||||
|
// Analyze specific error and provide suggestions
|
||||||
|
switch (lastError)
|
||||||
|
{
|
||||||
|
case 0: // ERROR_SUCCESS - may be false success
|
||||||
|
Logger.LogWarning("Shell_NotifyIcon returned false but GetLastWin32Error is 0");
|
||||||
|
break;
|
||||||
|
case 1400: // ERROR_INVALID_WINDOW_HANDLE
|
||||||
|
Logger.LogWarning("Invalid window handle - message window may not be properly created");
|
||||||
|
break;
|
||||||
|
case 1418: // ERROR_THREAD_1_INACTIVE
|
||||||
|
Logger.LogWarning("Thread inactive - may need to wait for Explorer to be ready");
|
||||||
|
break;
|
||||||
|
case 1414: // ERROR_INVALID_ICON_HANDLE
|
||||||
|
Logger.LogWarning("Invalid icon handle - icon may have been garbage collected");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Logger.LogWarning($"Unexpected error code: {lastError}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not the last attempt, wait and retry
|
||||||
|
if (attempt < maxRetries)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Retrying in {retryDelayMs}ms...");
|
||||||
|
System.Threading.Thread.Sleep(retryDelayMs);
|
||||||
|
|
||||||
|
// Re-get icon handle to prevent handle invalidation
|
||||||
|
iconHandle = GetDefaultIcon();
|
||||||
|
_notifyIconData.HIcon = iconHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogError($"Failed to create tray icon after {maxRetries} attempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get default icon
|
||||||
|
/// </summary>
|
||||||
|
private IntPtr GetDefaultIcon()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First release previous icon
|
||||||
|
_trayIcon?.Dispose();
|
||||||
|
_trayIcon = null;
|
||||||
|
|
||||||
|
// Try to load icon from Assets folder in exe directory
|
||||||
|
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
|
||||||
|
if (!string.IsNullOrEmpty(exePath))
|
||||||
|
{
|
||||||
|
var exeDir = System.IO.Path.GetDirectoryName(exePath);
|
||||||
|
if (!string.IsNullOrEmpty(exeDir))
|
||||||
|
{
|
||||||
|
var iconPath = System.IO.Path.Combine(exeDir, "Assets", "PowerDisplay.ico");
|
||||||
|
|
||||||
|
Logger.LogDebug($"Attempting to load icon from: {iconPath}");
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(iconPath))
|
||||||
|
{
|
||||||
|
// Create icon and save as class member to prevent garbage collection
|
||||||
|
_trayIcon = new System.Drawing.Icon(iconPath);
|
||||||
|
Logger.LogInfo($"Successfully loaded custom icon from {iconPath}");
|
||||||
|
return _trayIcon.Handle;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Icon file not found at: {iconPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to load PowerDisplay icon: {ex.Message}");
|
||||||
|
_trayIcon?.Dispose();
|
||||||
|
_trayIcon = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If loading fails, use system default icon
|
||||||
|
var systemIconHandle = LoadIcon(IntPtr.Zero, new IntPtr(32512)); // IDI_APPLICATION
|
||||||
|
Logger.LogInfo($"Using system default icon: {systemIconHandle}");
|
||||||
|
return systemIconHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Window message processing
|
||||||
|
/// </summary>
|
||||||
|
private IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
|
||||||
|
{
|
||||||
|
if (msg == _wmTaskbarCreated)
|
||||||
|
{
|
||||||
|
// Explorer restarted, need to recreate tray icon
|
||||||
|
Logger.LogInfo("TaskbarCreated message received - recreating tray icon");
|
||||||
|
CreateTrayIcon();
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg)
|
||||||
|
{
|
||||||
|
case WmTrayicon:
|
||||||
|
HandleTrayIconMessage(lParam);
|
||||||
|
break;
|
||||||
|
case WmCommand:
|
||||||
|
HandleMenuCommand(wParam);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle tray icon messages
|
||||||
|
/// </summary>
|
||||||
|
private void HandleTrayIconMessage(IntPtr lParam)
|
||||||
|
{
|
||||||
|
switch ((uint)lParam)
|
||||||
|
{
|
||||||
|
case WmLbuttonup:
|
||||||
|
// Left click - show/hide window
|
||||||
|
ToggleWindowVisibility();
|
||||||
|
break;
|
||||||
|
case WmRbuttonup:
|
||||||
|
// Right click - show menu
|
||||||
|
ShowContextMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle window visibility state
|
||||||
|
/// </summary>
|
||||||
|
private void ToggleWindowVisibility()
|
||||||
|
{
|
||||||
|
_isWindowVisible = !_isWindowVisible;
|
||||||
|
if (_isWindowVisible)
|
||||||
|
{
|
||||||
|
_onShowWindow?.Invoke();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Hide window logic will be implemented in MainWindow
|
||||||
|
if (_mainWindow != null)
|
||||||
|
{
|
||||||
|
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow);
|
||||||
|
ShowWindow(hWnd, SwHide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show right-click menu
|
||||||
|
/// </summary>
|
||||||
|
private void ShowContextMenu()
|
||||||
|
{
|
||||||
|
var hMenu = CreatePopupMenu();
|
||||||
|
|
||||||
|
AppendMenu(hMenu, MfString, IdShow, _isWindowVisible ? "Hide Window" : "Show Window");
|
||||||
|
if (_onRefresh != null)
|
||||||
|
{
|
||||||
|
AppendMenu(hMenu, MfString, IdRefresh, "Refresh Monitors");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_onSettings != null)
|
||||||
|
{
|
||||||
|
AppendMenu(hMenu, MfString, IdSettings, "Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendMenu(hMenu, MfSeparator, 0, string.Empty);
|
||||||
|
AppendMenu(hMenu, MfString, IdExit, "Exit");
|
||||||
|
|
||||||
|
GetCursorPos(out POINT pt);
|
||||||
|
SetForegroundWindow(_messageWindowHandle);
|
||||||
|
|
||||||
|
var cmd = TrackPopupMenu(hMenu, TpmLeftalign | TpmReturncmd, pt.X, pt.Y, 0, _messageWindowHandle, IntPtr.Zero);
|
||||||
|
|
||||||
|
if (cmd != 0)
|
||||||
|
{
|
||||||
|
HandleMenuCommand(new IntPtr(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
DestroyMenu(hMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle menu commands
|
||||||
|
/// </summary>
|
||||||
|
private void HandleMenuCommand(IntPtr commandId)
|
||||||
|
{
|
||||||
|
switch (commandId.ToInt32())
|
||||||
|
{
|
||||||
|
case IdShow:
|
||||||
|
ToggleWindowVisibility();
|
||||||
|
break;
|
||||||
|
case IdRefresh:
|
||||||
|
_onRefresh?.Invoke();
|
||||||
|
break;
|
||||||
|
case IdSettings:
|
||||||
|
_onSettings?.Invoke();
|
||||||
|
break;
|
||||||
|
case IdExit:
|
||||||
|
_onExitApplication?.Invoke();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show balloon tip
|
||||||
|
/// </summary>
|
||||||
|
public void ShowBalloonTip(string title, string text, uint timeout = 3000)
|
||||||
|
{
|
||||||
|
_notifyIconData.UFlags |= NifInfo;
|
||||||
|
_notifyIconData.SetInfoTitle(title);
|
||||||
|
_notifyIconData.SetInfo(text);
|
||||||
|
_notifyIconData.UTimeout = timeout;
|
||||||
|
_notifyIconData.DwInfoFlags = 1; // NIIF_INFO
|
||||||
|
|
||||||
|
Shell_NotifyIcon(NimModify, ref _notifyIconData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update tray icon tooltip text
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateTooltip(string tooltip)
|
||||||
|
{
|
||||||
|
_notifyIconData.SetTip(tooltip);
|
||||||
|
Shell_NotifyIcon(NimModify, ref _notifyIconData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recreate tray icon (for failure recovery)
|
||||||
|
/// </summary>
|
||||||
|
public void RecreateTrayIcon()
|
||||||
|
{
|
||||||
|
Logger.LogInfo("Manually recreating tray icon...");
|
||||||
|
CreateTrayIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_isDisposed)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Disposing TrayIconHelper...");
|
||||||
|
|
||||||
|
// Remove tray icon
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Shell_NotifyIcon(NimDelete, ref _notifyIconData);
|
||||||
|
Logger.LogInfo("Tray icon removed successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error removing tray icon: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release icon resources
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_trayIcon?.Dispose();
|
||||||
|
_trayIcon = null;
|
||||||
|
Logger.LogInfo("Icon resources disposed successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error disposing icon: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy message window
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_messageWindowHandle != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
DestroyWindow(_messageWindowHandle);
|
||||||
|
_messageWindowHandle = IntPtr.Zero;
|
||||||
|
Logger.LogInfo("Message window destroyed successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error destroying message window: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
Logger.LogDebug("TrayIconHelper disposed completely");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// 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.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class ensures types used in XAML are preserved during AOT compilation.
|
||||||
|
/// Framework types cannot have attributes added directly to their definitions since they're external types.
|
||||||
|
/// Use DynamicDependency to preserve all members of these WinUI3 framework types.
|
||||||
|
/// </summary>
|
||||||
|
internal static class TypePreservation
|
||||||
|
{
|
||||||
|
// Core WinUI3 Controls used in XAML
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Window))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Application))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Grid))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Border))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ScrollViewer))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.StackPanel))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ItemsControl))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Slider))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.TextBlock))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Button))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIcon))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ProgressRing))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.InfoBar))]
|
||||||
|
|
||||||
|
// Animation and Transform types
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.Storyboard))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.DoubleAnimation))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.CubicEase))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.TranslateTransform))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.TransitionCollection))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.EntranceThemeTransition))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.RepositionThemeTransition))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.EasingFunctionBase))]
|
||||||
|
|
||||||
|
// Template and Resource types
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DataTemplate))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ItemsPanelTemplate))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Style))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIconSource))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.ResourceDictionary))]
|
||||||
|
|
||||||
|
// Text and Document types
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Documents.Run))]
|
||||||
|
|
||||||
|
// Layout types
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.RowDefinition))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ColumnDefinition))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.RowDefinitionCollection))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ColumnDefinitionCollection))]
|
||||||
|
|
||||||
|
// Media types for brushes and visuals
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.SolidColorBrush))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Brush))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Transform))]
|
||||||
|
|
||||||
|
// Core UI element types
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.UIElement))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.FrameworkElement))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DependencyObject))]
|
||||||
|
|
||||||
|
// Thickness and other value types used in XAML (structs, not enums)
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Thickness))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.CornerRadius))]
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.GridLength))]
|
||||||
|
|
||||||
|
// ToolTip service used in buttons
|
||||||
|
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ToolTipService))]
|
||||||
|
|
||||||
|
public static void PreserveTypes()
|
||||||
|
{
|
||||||
|
// This method exists only to hold the DynamicDependency attributes above.
|
||||||
|
// It must be called to ensure the types are not trimmed during AOT compilation.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,637 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Core.Interfaces;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using static PowerDisplay.Native.NativeConstants;
|
||||||
|
using static PowerDisplay.Native.NativeDelegates;
|
||||||
|
using static PowerDisplay.Native.PInvoke;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
|
||||||
|
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
|
||||||
|
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||||
|
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||||
|
using RECT = PowerDisplay.Native.Rect;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native.DDC
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DDC/CI monitor controller for controlling external monitors
|
||||||
|
/// </summary>
|
||||||
|
public partial class DdcCiController : IMonitorController, IDisposable
|
||||||
|
{
|
||||||
|
private readonly PhysicalMonitorHandleManager _handleManager = new();
|
||||||
|
private readonly VcpCodeResolver _vcpResolver = new();
|
||||||
|
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public DdcCiController()
|
||||||
|
{
|
||||||
|
_discoveryHelper = new MonitorDiscoveryHelper(_vcpResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name => "DDC/CI Monitor Controller";
|
||||||
|
|
||||||
|
public MonitorType SupportedType => MonitorType.External;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the specified monitor can be controlled
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (monitor.Type != MonitorType.External)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var physicalHandle = GetPhysicalHandle(monitor);
|
||||||
|
return physicalHandle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(physicalHandle);
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor brightness
|
||||||
|
/// </summary>
|
||||||
|
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var physicalHandle = GetPhysicalHandle(monitor);
|
||||||
|
if (physicalHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try high-level API
|
||||||
|
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint minBrightness, out uint currentBrightness, out uint maxBrightness))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)currentBrightness, (int)minBrightness, (int)maxBrightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different VCP codes
|
||||||
|
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||||
|
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out uint current, out uint max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, 0, (int)max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set monitor brightness
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
brightness = Math.Clamp(brightness, 0, 100);
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var physicalHandle = GetPhysicalHandle(monitor);
|
||||||
|
if (physicalHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("No physical handle found");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentInfo = GetBrightnessInfo(monitor, physicalHandle);
|
||||||
|
if (!currentInfo.IsValid)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("Cannot read current brightness");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint targetValue = (uint)currentInfo.FromPercentage(brightness);
|
||||||
|
|
||||||
|
// First try high-level API
|
||||||
|
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try VCP codes
|
||||||
|
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||||
|
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(physicalHandle, vcpCode.Value, targetValue))
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastError = GetLastError();
|
||||||
|
return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor contrast
|
||||||
|
/// </summary>
|
||||||
|
public Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
=> GetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set monitor contrast
|
||||||
|
/// </summary>
|
||||||
|
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
|
||||||
|
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, 0, 100, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor volume
|
||||||
|
/// </summary>
|
||||||
|
public Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
=> GetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set monitor volume
|
||||||
|
/// </summary>
|
||||||
|
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
|
||||||
|
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, 0, 100, cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor color temperature
|
||||||
|
/// </summary>
|
||||||
|
public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (monitor.Handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different VCP codes for color temperature
|
||||||
|
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
|
||||||
|
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode.Value, out uint current, out uint max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, 0, (int)max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set monitor color temperature
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
colorTemperature = Math.Clamp(colorTemperature, 2000, 10000);
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (monitor.Handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get current color temperature info to understand the range
|
||||||
|
var currentInfo = _vcpResolver.GetCurrentColorTemperature(monitor.Handle);
|
||||||
|
if (!currentInfo.IsValid)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("Cannot read current color temperature");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Kelvin temperature to VCP value
|
||||||
|
uint targetValue = _vcpResolver.ConvertKelvinToVcpValue(colorTemperature, currentInfo);
|
||||||
|
|
||||||
|
// Try to set using the best available VCP code
|
||||||
|
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
|
||||||
|
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode.Value, targetValue))
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"Successfully set color temperature to {colorTemperature}K via DDC/CI (VCP 0x{vcpCode.Value:X2})");
|
||||||
|
return MonitorOperationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastError = GetLastError();
|
||||||
|
return MonitorOperationResult.Failure($"Failed to set color temperature via DDC/CI", (int)lastError);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor capabilities string
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (monitor.Handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (GetCapabilitiesStringLength(monitor.Handle, out uint length) && length > 0)
|
||||||
|
{
|
||||||
|
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length))
|
||||||
|
{
|
||||||
|
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to get capabilities string: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save current settings
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (monitor.Handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (SaveCurrentSettings(monitor.Handle))
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastError = GetLastError();
|
||||||
|
return MonitorOperationResult.Failure($"Failed to save settings", (int)lastError);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Exception saving settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discover supported monitors
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
var monitors = new List<Monitor>();
|
||||||
|
var newHandleMap = new Dictionary<string, IntPtr>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get all display devices with stable device IDs (Twinkle Tray style)
|
||||||
|
var displayDevices = DdcCiNative.GetAllDisplayDevices();
|
||||||
|
Logger.LogInfo($"DDC: Found {displayDevices.Count} display devices via EnumDisplayDevices");
|
||||||
|
|
||||||
|
// Also get hardware info for friendly names
|
||||||
|
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
|
||||||
|
Logger.LogDebug($"DDC: GetAllMonitorDisplayInfo returned {monitorDisplayInfo.Count} items");
|
||||||
|
|
||||||
|
// Enumerate all monitors
|
||||||
|
var monitorHandles = new List<IntPtr>();
|
||||||
|
Logger.LogDebug($"DDC: About to call EnumDisplayMonitors...");
|
||||||
|
|
||||||
|
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"DDC: EnumProc callback - hMonitor=0x{hMonitor:X}");
|
||||||
|
monitorHandles.Add(hMonitor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero);
|
||||||
|
Logger.LogDebug($"DDC: EnumDisplayMonitors returned {enumResult}, found {monitorHandles.Count} monitor handles");
|
||||||
|
|
||||||
|
if (!enumResult)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: EnumDisplayMonitors failed");
|
||||||
|
return monitors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get physical handles for each monitor
|
||||||
|
foreach (var hMonitor in monitorHandles)
|
||||||
|
{
|
||||||
|
var adapterName = _discoveryHelper.GetMonitorDeviceId(hMonitor);
|
||||||
|
if (string.IsNullOrEmpty(adapterName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes Windows returns NULL handles. Implement Twinkle Tray's retry logic.
|
||||||
|
// See: twinkle-tray/src/Monitors.js line 617
|
||||||
|
PHYSICAL_MONITOR[]? physicalMonitors = null;
|
||||||
|
const int maxRetries = 3;
|
||||||
|
const int retryDelayMs = 200;
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||||
|
{
|
||||||
|
if (attempt > 0)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"DDC: Retry attempt {attempt}/{maxRetries - 1} for hMonitor 0x{hMonitor:X} after {retryDelayMs}ms delay");
|
||||||
|
await Task.Delay(retryDelayMs, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalMonitors = _discoveryHelper.GetPhysicalMonitors(hMonitor);
|
||||||
|
|
||||||
|
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||||
|
{
|
||||||
|
if (attempt < maxRetries - 1)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null/empty on attempt {attempt + 1}, will retry");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any handle is NULL (Twinkle Tray checks handleIsValid)
|
||||||
|
bool hasNullHandle = false;
|
||||||
|
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||||
|
{
|
||||||
|
if (physicalMonitors[i].HPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
hasNullHandle = true;
|
||||||
|
Logger.LogWarning($"DDC: Physical monitor [{i}] has NULL handle on attempt {attempt + 1}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNullHandle)
|
||||||
|
{
|
||||||
|
// Success! All handles are valid
|
||||||
|
if (attempt > 0)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"DDC: Successfully obtained valid handles on attempt {attempt + 1}");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (attempt < maxRetries - 1)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: NULL handle detected, will retry (attempt {attempt + 1}/{maxRetries})");
|
||||||
|
physicalMonitors = null; // Reset for retry
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: NULL handle still present after {maxRetries} attempts, continuing anyway");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: Failed to get physical monitors for hMonitor 0x{hMonitor:X} after {maxRetries} attempts");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match physical monitors with DisplayDeviceInfo (Twinkle Tray logic)
|
||||||
|
// For each physical monitor on this adapter, find the corresponding DisplayDeviceInfo
|
||||||
|
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||||
|
{
|
||||||
|
var physicalMonitor = physicalMonitors[i];
|
||||||
|
if (physicalMonitor.HPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find matching DisplayDeviceInfo for this physical monitor
|
||||||
|
DisplayDeviceInfo? matchedDevice = null;
|
||||||
|
int foundCount = 0;
|
||||||
|
|
||||||
|
foreach (var displayDevice in displayDevices)
|
||||||
|
{
|
||||||
|
if (displayDevice.AdapterName == adapterName)
|
||||||
|
{
|
||||||
|
if (foundCount == i)
|
||||||
|
{
|
||||||
|
matchedDevice = displayDevice;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine device key for handle reuse logic
|
||||||
|
string deviceKey = matchedDevice?.DeviceKey ?? $"{adapterName}_{i}";
|
||||||
|
|
||||||
|
// Use HandleManager to reuse or create handle
|
||||||
|
var (handleToUse, reusingOldHandle) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor);
|
||||||
|
|
||||||
|
// Validate DDC/CI connection for the handle we're going to use
|
||||||
|
if (!reusingOldHandle && !DdcCiNative.ValidateDdcCiConnection(handleToUse))
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: New handle 0x{handleToUse:X} failed DDC/CI validation, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update physical monitor handle to use the correct one
|
||||||
|
var monitorToCreate = physicalMonitor;
|
||||||
|
monitorToCreate.HPhysicalMonitor = handleToUse;
|
||||||
|
|
||||||
|
var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice);
|
||||||
|
if (monitor != null)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"DDC: Created monitor {monitor.Id} with handle 0x{monitor.Handle:X} (reused: {reusingOldHandle}), deviceKey: {monitor.DeviceKey}");
|
||||||
|
monitors.Add(monitor);
|
||||||
|
|
||||||
|
// Store in new map for cleanup
|
||||||
|
newHandleMap[monitor.DeviceKey] = handleToUse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handle manager with new mapping
|
||||||
|
_handleManager.UpdateHandleMap(newHandleMap);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"DDC: DiscoverMonitorsAsync returning {monitors.Count} monitors");
|
||||||
|
}
|
||||||
|
|
||||||
|
return monitors;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate monitor connection status
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() => monitor.Handle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(monitor.Handle),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic method to get VCP feature value
|
||||||
|
/// </summary>
|
||||||
|
private async Task<BrightnessInfo> GetVcpFeatureAsync(
|
||||||
|
Monitor monitor,
|
||||||
|
byte vcpCode,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (monitor.Handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode, out uint current, out uint max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, 0, (int)max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generic method to set VCP feature value
|
||||||
|
/// </summary>
|
||||||
|
private async Task<MonitorOperationResult> SetVcpFeatureAsync(
|
||||||
|
Monitor monitor,
|
||||||
|
byte vcpCode,
|
||||||
|
int value,
|
||||||
|
int min = 0,
|
||||||
|
int max = 100,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
value = Math.Clamp(value, min, max);
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (monitor.Handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get current value to determine range
|
||||||
|
var currentInfo = GetVcpFeatureAsync(monitor, vcpCode).Result;
|
||||||
|
if (!currentInfo.IsValid)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Cannot read current value for VCP 0x{vcpCode:X2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
uint targetValue = (uint)currentInfo.FromPercentage(value);
|
||||||
|
|
||||||
|
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode, targetValue))
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastError = GetLastError();
|
||||||
|
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", (int)lastError);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Exception setting VCP 0x{vcpCode:X2}: {ex.Message}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get brightness information (with explicit handle)
|
||||||
|
/// </summary>
|
||||||
|
private BrightnessInfo GetBrightnessInfo(Monitor monitor, IntPtr physicalHandle)
|
||||||
|
{
|
||||||
|
if (physicalHandle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try high-level API
|
||||||
|
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint min, out uint current, out uint max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try VCP codes
|
||||||
|
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||||
|
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out current, out max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, 0, (int)max);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get physical handle for monitor using stable deviceKey
|
||||||
|
/// </summary>
|
||||||
|
private IntPtr GetPhysicalHandle(Monitor monitor)
|
||||||
|
{
|
||||||
|
return _handleManager.GetPhysicalHandle(monitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed && disposing)
|
||||||
|
{
|
||||||
|
_handleManager?.Dispose();
|
||||||
|
_vcpResolver?.ClearCache();
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
499
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
499
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using ManagedCommon;
|
||||||
|
using static PowerDisplay.Native.NativeConstants;
|
||||||
|
using static PowerDisplay.Native.NativeDelegates;
|
||||||
|
using static PowerDisplay.Native.PInvoke;
|
||||||
|
|
||||||
|
// 类型别名,兼容 Windows API 命名约定
|
||||||
|
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
|
||||||
|
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
|
||||||
|
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
|
||||||
|
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Native.DISPLAYCONFIG_PATH_INFO;
|
||||||
|
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Native.DISPLAYCONFIG_TARGET_DEVICE_NAME;
|
||||||
|
using LUID = PowerDisplay.Native.Luid;
|
||||||
|
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||||
|
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||||
|
using RECT = PowerDisplay.Native.Rect;
|
||||||
|
|
||||||
|
#pragma warning disable SA1649 // File name should match first type name - Multiple related types for DDC/CI
|
||||||
|
#pragma warning disable SA1402 // File may only contain a single type - Related DDC/CI types grouped together
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native.DDC
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 显示设备信息类
|
||||||
|
/// </summary>
|
||||||
|
public class DisplayDeviceInfo
|
||||||
|
{
|
||||||
|
public string DeviceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string AdapterName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string DeviceID { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string DeviceKey { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DDC/CI 原生 API 封装
|
||||||
|
/// </summary>
|
||||||
|
public static class DdcCiNative
|
||||||
|
{
|
||||||
|
// Display Configuration 常量
|
||||||
|
public const uint QdcAllPaths = 0x00000001;
|
||||||
|
|
||||||
|
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||||
|
|
||||||
|
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 VCP 功能值的安全包装
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||||
|
/// <param name="vcpCode">VCP 代码</param>
|
||||||
|
/// <param name="currentValue">当前值</param>
|
||||||
|
/// <param name="maxValue">最大值</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
|
||||||
|
{
|
||||||
|
currentValue = 0;
|
||||||
|
maxValue = 0;
|
||||||
|
|
||||||
|
if (hPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return GetVCPFeatureAndVCPFeatureReply(hPhysicalMonitor, vcpCode, IntPtr.Zero, out currentValue, out maxValue);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置 VCP 功能值的安全包装
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||||
|
/// <param name="vcpCode">VCP 代码</param>
|
||||||
|
/// <param name="value">新值</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
|
||||||
|
{
|
||||||
|
if (hPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return SetVCPFeature(hPhysicalMonitor, vcpCode, value);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取高级亮度信息的安全包装
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||||
|
/// <param name="minBrightness">最小亮度</param>
|
||||||
|
/// <param name="currentBrightness">当前亮度</param>
|
||||||
|
/// <param name="maxBrightness">最大亮度</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
|
||||||
|
{
|
||||||
|
minBrightness = 0;
|
||||||
|
currentBrightness = 0;
|
||||||
|
maxBrightness = 0;
|
||||||
|
|
||||||
|
if (hPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return GetMonitorBrightness(hPhysicalMonitor, out minBrightness, out currentBrightness, out maxBrightness);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置高级亮度的安全包装
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||||
|
/// <param name="brightness">亮度值</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public static bool TrySetMonitorBrightness(IntPtr hPhysicalMonitor, uint brightness)
|
||||||
|
{
|
||||||
|
if (hPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return SetMonitorBrightness(hPhysicalMonitor, brightness);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查 DDC/CI 连接的有效性
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||||
|
/// <returns>是否连接有效</returns>
|
||||||
|
public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor)
|
||||||
|
{
|
||||||
|
if (hPhysicalMonitor == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试读取基本的 VCP 代码来验证连接
|
||||||
|
var testCodes = new byte[] { NativeConstants.VcpCodeBrightness, NativeConstants.VcpCodeNewControlValue, NativeConstants.VcpCodeVcpVersion };
|
||||||
|
|
||||||
|
foreach (var code in testCodes)
|
||||||
|
{
|
||||||
|
if (TryGetVCPFeature(hPhysicalMonitor, code, out _, out _))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取显示器友好名称
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="adapterId">适配器 ID</param>
|
||||||
|
/// <param name="targetId">目标 ID</param>
|
||||||
|
/// <returns>显示器友好名称,如果获取失败返回 null</returns>
|
||||||
|
public static unsafe string? GetMonitorFriendlyName(LUID adapterId, uint targetId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||||
|
{
|
||||||
|
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||||
|
{
|
||||||
|
Type = DisplayconfigDeviceInfoGetTargetName,
|
||||||
|
Size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME),
|
||||||
|
AdapterId = adapterId,
|
||||||
|
Id = targetId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = DisplayConfigGetDeviceInfo(ref deviceName);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
return deviceName.GetMonitorFriendlyDeviceName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过枚举显示配置获取所有显示器友好名称
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>设备路径到友好名称的映射</returns>
|
||||||
|
public static Dictionary<string, string> GetAllMonitorFriendlyNames()
|
||||||
|
{
|
||||||
|
var friendlyNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 获取缓冲区大小
|
||||||
|
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return friendlyNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配缓冲区
|
||||||
|
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
|
||||||
|
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
|
||||||
|
|
||||||
|
// 查询显示配置
|
||||||
|
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return friendlyNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每个路径的友好名称
|
||||||
|
for (int i = 0; i < pathCount; i++)
|
||||||
|
{
|
||||||
|
var path = paths[i];
|
||||||
|
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(friendlyName))
|
||||||
|
{
|
||||||
|
// 使用适配器和目标 ID 作为键
|
||||||
|
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
|
||||||
|
friendlyNames[key] = friendlyName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return friendlyNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取显示器的EDID硬件ID信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="adapterId">适配器ID</param>
|
||||||
|
/// <param name="targetId">目标ID</param>
|
||||||
|
/// <returns>硬件ID字符串,格式为制造商代码+产品代码</returns>
|
||||||
|
public static unsafe string? GetMonitorHardwareId(LUID adapterId, uint targetId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||||
|
{
|
||||||
|
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||||
|
{
|
||||||
|
Type = DisplayconfigDeviceInfoGetTargetName,
|
||||||
|
Size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME),
|
||||||
|
AdapterId = adapterId,
|
||||||
|
Id = targetId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = DisplayConfigGetDeviceInfo(ref deviceName);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
// 将制造商ID转换为3字符字符串
|
||||||
|
var manufacturerId = deviceName.EdidManufactureId;
|
||||||
|
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
|
||||||
|
|
||||||
|
// 将产品ID转换为4位十六进制字符串
|
||||||
|
var productCode = deviceName.EdidProductCodeId.ToString("X4", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var hardwareId = $"{manufactureCode}{productCode}";
|
||||||
|
Logger.LogDebug($"GetMonitorHardwareId - ManufacturerId: 0x{manufacturerId:X4}, Code: '{manufactureCode}', ProductCode: '{productCode}', Result: '{hardwareId}'");
|
||||||
|
|
||||||
|
return hardwareId;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError($"GetMonitorHardwareId - DisplayConfigGetDeviceInfo failed with result: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将制造商ID转换为3字符制造商代码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="manufacturerId">制造商ID</param>
|
||||||
|
/// <returns>3字符制造商代码</returns>
|
||||||
|
private static string ConvertManufactureIdToString(ushort manufacturerId)
|
||||||
|
{
|
||||||
|
// EDID制造商ID需要先进行字节序交换
|
||||||
|
manufacturerId = (ushort)(((manufacturerId & 0xff00) >> 8) | ((manufacturerId & 0x00ff) << 8));
|
||||||
|
|
||||||
|
// 提取3个5位字符(每个字符是A-Z,其中A=1, B=2, ..., Z=26)
|
||||||
|
var char1 = (char)('A' - 1 + ((manufacturerId >> 0) & 0x1f));
|
||||||
|
var char2 = (char)('A' - 1 + ((manufacturerId >> 5) & 0x1f));
|
||||||
|
var char3 = (char)('A' - 1 + ((manufacturerId >> 10) & 0x1f));
|
||||||
|
|
||||||
|
// 按正确顺序组合字符
|
||||||
|
return $"{char3}{char2}{char1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有显示器的完整信息,包括友好名称和硬件ID
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>包含显示器信息的字典</returns>
|
||||||
|
public static Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
|
||||||
|
{
|
||||||
|
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 获取缓冲区大小
|
||||||
|
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return monitorInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配缓冲区
|
||||||
|
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
|
||||||
|
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
|
||||||
|
|
||||||
|
// 查询显示配置
|
||||||
|
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
|
||||||
|
if (result != 0)
|
||||||
|
{
|
||||||
|
return monitorInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取每个路径的信息
|
||||||
|
for (int i = 0; i < pathCount; i++)
|
||||||
|
{
|
||||||
|
var path = paths[i];
|
||||||
|
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
|
||||||
|
var hardwareId = GetMonitorHardwareId(path.TargetInfo.AdapterId, path.TargetInfo.Id);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(friendlyName) || !string.IsNullOrEmpty(hardwareId))
|
||||||
|
{
|
||||||
|
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
|
||||||
|
monitorInfo[key] = new MonitorDisplayInfo
|
||||||
|
{
|
||||||
|
FriendlyName = friendlyName ?? string.Empty,
|
||||||
|
HardwareId = hardwareId ?? string.Empty,
|
||||||
|
AdapterId = path.TargetInfo.AdapterId,
|
||||||
|
TargetId = path.TargetInfo.Id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return monitorInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有显示设备信息(使用 EnumDisplayDevices API)
|
||||||
|
/// 与 Twinkle Tray 实现保持一致
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>显示设备信息列表</returns>
|
||||||
|
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
|
||||||
|
{
|
||||||
|
var devices = new List<DisplayDeviceInfo>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 枚举所有适配器
|
||||||
|
uint adapterIndex = 0;
|
||||||
|
var adapter = default(DISPLAY_DEVICE);
|
||||||
|
adapter.Cb = (uint)sizeof(DisplayDevice);
|
||||||
|
|
||||||
|
while (EnumDisplayDevices(null, adapterIndex, ref adapter, EddGetDeviceInterfaceName))
|
||||||
|
{
|
||||||
|
// 跳过镜像驱动程序
|
||||||
|
if ((adapter.StateFlags & DisplayDeviceMirroringDriver) != 0)
|
||||||
|
{
|
||||||
|
adapterIndex++;
|
||||||
|
adapter = default(DISPLAY_DEVICE);
|
||||||
|
adapter.Cb = (uint)sizeof(DisplayDevice);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理已连接到桌面的适配器
|
||||||
|
if ((adapter.StateFlags & DisplayDeviceAttachedToDesktop) != 0)
|
||||||
|
{
|
||||||
|
// 枚举该适配器上的所有显示器
|
||||||
|
uint displayIndex = 0;
|
||||||
|
var display = default(DISPLAY_DEVICE);
|
||||||
|
display.Cb = (uint)sizeof(DisplayDevice);
|
||||||
|
|
||||||
|
string adapterDeviceName = adapter.GetDeviceName();
|
||||||
|
while (EnumDisplayDevices(adapterDeviceName, displayIndex, ref display, EddGetDeviceInterfaceName))
|
||||||
|
{
|
||||||
|
string displayDeviceID = display.GetDeviceID();
|
||||||
|
|
||||||
|
// 只处理活动的显示器
|
||||||
|
if ((display.StateFlags & DisplayDeviceAttachedToDesktop) != 0 &&
|
||||||
|
!string.IsNullOrEmpty(displayDeviceID))
|
||||||
|
{
|
||||||
|
var deviceInfo = new DisplayDeviceInfo
|
||||||
|
{
|
||||||
|
DeviceName = display.GetDeviceName(),
|
||||||
|
AdapterName = adapterDeviceName,
|
||||||
|
DeviceID = displayDeviceID,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取 DeviceKey:移除 GUID 部分(#{...} 及之后的内容)
|
||||||
|
// 例如:\\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID} -> \\?\DISPLAY#GSM5C6D#5&1234&0&UID
|
||||||
|
int guidIndex = deviceInfo.DeviceID.IndexOf("#{", StringComparison.Ordinal);
|
||||||
|
if (guidIndex >= 0)
|
||||||
|
{
|
||||||
|
deviceInfo.DeviceKey = deviceInfo.DeviceID.Substring(0, guidIndex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
deviceInfo.DeviceKey = deviceInfo.DeviceID;
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.Add(deviceInfo);
|
||||||
|
|
||||||
|
Logger.LogDebug($"Found display device - Name: {deviceInfo.DeviceName}, Adapter: {deviceInfo.AdapterName}, DeviceKey: {deviceInfo.DeviceKey}");
|
||||||
|
}
|
||||||
|
|
||||||
|
displayIndex++;
|
||||||
|
display = default(DISPLAY_DEVICE);
|
||||||
|
display.Cb = (uint)sizeof(DisplayDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
adapterIndex++;
|
||||||
|
adapter = default(DISPLAY_DEVICE);
|
||||||
|
adapter.Cb = (uint)sizeof(DisplayDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo($"GetAllDisplayDevices found {devices.Count} display devices");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"GetAllDisplayDevices exception: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示器显示信息结构
|
||||||
|
/// </summary>
|
||||||
|
public struct MonitorDisplayInfo
|
||||||
|
{
|
||||||
|
public string FriendlyName { get; set; }
|
||||||
|
|
||||||
|
public string HardwareId { get; set; }
|
||||||
|
|
||||||
|
public LUID AdapterId { get; set; }
|
||||||
|
|
||||||
|
public uint TargetId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using static PowerDisplay.Native.NativeConstants;
|
||||||
|
using static PowerDisplay.Native.PInvoke;
|
||||||
|
|
||||||
|
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||||
|
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||||
|
using RECT = PowerDisplay.Native.Rect;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native.DDC
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for discovering and creating monitor objects
|
||||||
|
/// </summary>
|
||||||
|
public class MonitorDiscoveryHelper
|
||||||
|
{
|
||||||
|
private readonly VcpCodeResolver _vcpResolver;
|
||||||
|
|
||||||
|
public MonitorDiscoveryHelper(VcpCodeResolver vcpResolver)
|
||||||
|
{
|
||||||
|
_vcpResolver = vcpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor device ID
|
||||||
|
/// </summary>
|
||||||
|
public unsafe string GetMonitorDeviceId(IntPtr hMonitor)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MonitorInfoEx) };
|
||||||
|
if (GetMonitorInfo(hMonitor, ref monitorInfo))
|
||||||
|
{
|
||||||
|
return monitorInfo.GetDeviceName() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Silent failure
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get physical monitors for a logical monitor
|
||||||
|
/// </summary>
|
||||||
|
internal PHYSICAL_MONITOR[]? GetPhysicalMonitors(IntPtr hMonitor)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"GetPhysicalMonitors: hMonitor=0x{hMonitor:X}");
|
||||||
|
|
||||||
|
if (!GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint numMonitors))
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"GetPhysicalMonitors: GetNumberOfPhysicalMonitorsFromHMONITOR failed for 0x{hMonitor:X}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug($"GetPhysicalMonitors: numMonitors={numMonitors}");
|
||||||
|
if (numMonitors == 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"GetPhysicalMonitors: numMonitors is 0");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var physicalMonitors = new PHYSICAL_MONITOR[numMonitors];
|
||||||
|
bool apiResult;
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (PHYSICAL_MONITOR* ptr = physicalMonitors)
|
||||||
|
{
|
||||||
|
apiResult = GetPhysicalMonitorsFromHMONITOR(hMonitor, numMonitors, ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogDebug($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR returned {apiResult}");
|
||||||
|
|
||||||
|
if (!apiResult)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR failed");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log each physical monitor
|
||||||
|
for (int i = 0; i < numMonitors; i++)
|
||||||
|
{
|
||||||
|
string desc = physicalMonitors[i].GetDescription() ?? string.Empty;
|
||||||
|
IntPtr handle = physicalMonitors[i].HPhysicalMonitor;
|
||||||
|
Logger.LogDebug($"GetPhysicalMonitors: [{i}] Handle=0x{handle:X}, Desc='{desc}'");
|
||||||
|
|
||||||
|
if (handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"GetPhysicalMonitors: Monitor [{i}] has NULL handle despite successful API call!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return physicalMonitors;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"GetPhysicalMonitors: Exception: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Monitor object from physical monitor
|
||||||
|
/// </summary>
|
||||||
|
internal Monitor? CreateMonitorFromPhysical(
|
||||||
|
PHYSICAL_MONITOR physicalMonitor,
|
||||||
|
string adapterName,
|
||||||
|
int index,
|
||||||
|
Dictionary<string, MonitorDisplayInfo> monitorDisplayInfo,
|
||||||
|
DisplayDeviceInfo? displayDevice)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get hardware ID and friendly name from the display info
|
||||||
|
string hardwareId = string.Empty;
|
||||||
|
string name = physicalMonitor.GetDescription() ?? string.Empty;
|
||||||
|
|
||||||
|
// Try to find matching monitor info
|
||||||
|
foreach (var kvp in monitorDisplayInfo.Values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(kvp.HardwareId))
|
||||||
|
{
|
||||||
|
hardwareId = kvp.HardwareId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(kvp.FriendlyName) && !kvp.FriendlyName.Contains("Generic"))
|
||||||
|
{
|
||||||
|
name = kvp.FriendlyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use stable device IDs from DisplayDeviceInfo
|
||||||
|
string deviceKey;
|
||||||
|
string monitorId;
|
||||||
|
|
||||||
|
if (displayDevice != null && !string.IsNullOrEmpty(displayDevice.DeviceKey))
|
||||||
|
{
|
||||||
|
// Use stable device key from EnumDisplayDevices
|
||||||
|
deviceKey = displayDevice.DeviceKey;
|
||||||
|
monitorId = $"DDC_{deviceKey.Replace(@"\\?\", string.Empty, StringComparison.Ordinal).Replace("#", "_", StringComparison.Ordinal).Replace("&", "_", StringComparison.Ordinal)}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: create device ID without handle in the key
|
||||||
|
var baseDevice = adapterName.Replace(@"\\.\", string.Empty, StringComparison.Ordinal);
|
||||||
|
deviceKey = $"{baseDevice}_{index}";
|
||||||
|
monitorId = $"DDC_{deviceKey}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no good name, use default value
|
||||||
|
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
|
||||||
|
{
|
||||||
|
name = $"External Display {index + 1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current brightness
|
||||||
|
var brightnessInfo = GetCurrentBrightness(physicalMonitor.HPhysicalMonitor);
|
||||||
|
|
||||||
|
var monitor = new Monitor
|
||||||
|
{
|
||||||
|
Id = monitorId,
|
||||||
|
HardwareId = hardwareId,
|
||||||
|
Name = name.Trim(),
|
||||||
|
Type = MonitorType.External,
|
||||||
|
CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50,
|
||||||
|
MinBrightness = 0,
|
||||||
|
MaxBrightness = 100,
|
||||||
|
IsAvailable = true,
|
||||||
|
Handle = physicalMonitor.HPhysicalMonitor,
|
||||||
|
DeviceKey = deviceKey,
|
||||||
|
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.DdcCi,
|
||||||
|
ConnectionType = "External",
|
||||||
|
CommunicationMethod = "DDC/CI",
|
||||||
|
Manufacturer = ExtractManufacturer(name),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check contrast support
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeContrast, out _, out _))
|
||||||
|
{
|
||||||
|
monitor.Capabilities |= MonitorCapabilities.Contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check color temperature support (suppress logging for discovery)
|
||||||
|
var colorTempVcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitorId, physicalMonitor.HPhysicalMonitor);
|
||||||
|
monitor.SupportsColorTemperature = colorTempVcpCode.HasValue;
|
||||||
|
|
||||||
|
// Check volume support
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeVolume, out _, out _))
|
||||||
|
{
|
||||||
|
monitor.Capabilities |= MonitorCapabilities.Volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check high-level API support
|
||||||
|
if (DdcCiNative.TryGetMonitorBrightness(physicalMonitor.HPhysicalMonitor, out _, out _, out _))
|
||||||
|
{
|
||||||
|
monitor.Capabilities |= MonitorCapabilities.HighLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"DDC: CreateMonitorFromPhysical exception: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current brightness using VCP codes
|
||||||
|
/// </summary>
|
||||||
|
private BrightnessInfo GetCurrentBrightness(IntPtr handle)
|
||||||
|
{
|
||||||
|
// Try high-level API
|
||||||
|
if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try VCP codes
|
||||||
|
byte[] vcpCodes = { VcpCodeBrightness, VcpCodeBacklightControl, VcpCodeBacklightLevelWhite, VcpCodeContrast };
|
||||||
|
foreach (var code in vcpCodes)
|
||||||
|
{
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(handle, code, out current, out max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, 0, (int)max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extract manufacturer from name
|
||||||
|
/// </summary>
|
||||||
|
private string ExtractManufacturer(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common manufacturer prefixes
|
||||||
|
var manufacturers = new[] { "DELL", "HP", "LG", "Samsung", "ASUS", "Acer", "BenQ", "AOC", "ViewSonic" };
|
||||||
|
var upperName = name.ToUpperInvariant();
|
||||||
|
|
||||||
|
foreach (var manufacturer in manufacturers)
|
||||||
|
{
|
||||||
|
if (upperName.Contains(manufacturer))
|
||||||
|
{
|
||||||
|
return manufacturer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return first word as manufacturer
|
||||||
|
var firstWord = name.Split(' ')[0];
|
||||||
|
return firstWord.Length > 2 ? firstWord : "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using static PowerDisplay.Native.PInvoke;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native.DDC
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages physical monitor handles - reuse, cleanup, and validation
|
||||||
|
/// Twinkle Tray style handle management
|
||||||
|
/// </summary>
|
||||||
|
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||||
|
{
|
||||||
|
// Twinkle Tray style mapping: deviceKey -> physical handle
|
||||||
|
private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new();
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get physical handle for monitor using stable deviceKey
|
||||||
|
/// </summary>
|
||||||
|
public IntPtr GetPhysicalHandle(Monitor monitor)
|
||||||
|
{
|
||||||
|
// Primary lookup: use stable deviceKey from EnumDisplayDevices
|
||||||
|
if (!string.IsNullOrEmpty(monitor.DeviceKey) &&
|
||||||
|
_deviceKeyToHandleMap.TryGetValue(monitor.DeviceKey, out var handle))
|
||||||
|
{
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: use direct handle from monitor object
|
||||||
|
if (monitor.Handle != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return monitor.Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to reuse existing handle if valid, otherwise use new handle
|
||||||
|
/// Returns the handle to use and whether it was reused
|
||||||
|
/// </summary>
|
||||||
|
public (IntPtr Handle, bool WasReused) ReuseOrCreateHandle(string deviceKey, IntPtr newHandle)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(deviceKey))
|
||||||
|
{
|
||||||
|
return (newHandle, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to reuse existing handle if it's still valid
|
||||||
|
if (_deviceKeyToHandleMap.TryGetValue(deviceKey, out var existingHandle) &&
|
||||||
|
existingHandle != IntPtr.Zero &&
|
||||||
|
DdcCiNative.ValidateDdcCiConnection(existingHandle))
|
||||||
|
{
|
||||||
|
// Destroy the newly created handle since we're using the old one
|
||||||
|
if (newHandle != existingHandle && newHandle != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
DestroyPhysicalMonitor(newHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (existingHandle, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (newHandle, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the handle mapping with new handles
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
|
||||||
|
{
|
||||||
|
// Clean up unused handles before updating
|
||||||
|
CleanupUnusedHandles(newHandleMap);
|
||||||
|
|
||||||
|
// Update the device key map
|
||||||
|
_deviceKeyToHandleMap.Clear();
|
||||||
|
foreach (var kvp in newHandleMap)
|
||||||
|
{
|
||||||
|
_deviceKeyToHandleMap[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clean up handles that are no longer in use
|
||||||
|
/// </summary>
|
||||||
|
private void CleanupUnusedHandles(Dictionary<string, IntPtr> newHandles)
|
||||||
|
{
|
||||||
|
if (_deviceKeyToHandleMap.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handlesToDestroy = new List<IntPtr>();
|
||||||
|
|
||||||
|
// Find handles that are in old map but not being reused
|
||||||
|
foreach (var oldMapping in _deviceKeyToHandleMap)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
foreach (var newMapping in newHandles)
|
||||||
|
{
|
||||||
|
// If the same handle is being reused, don't destroy it
|
||||||
|
if (oldMapping.Value == newMapping.Value)
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found && oldMapping.Value != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
handlesToDestroy.Add(oldMapping.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy unused handles
|
||||||
|
foreach (var handle in handlesToDestroy)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DestroyPhysicalMonitor(handle);
|
||||||
|
Logger.LogDebug($"DDC: Cleaned up unused handle 0x{handle:X}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"DDC: Failed to destroy handle 0x{handle:X}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release all physical monitor handles
|
||||||
|
foreach (var handle in _deviceKeyToHandleMap.Values)
|
||||||
|
{
|
||||||
|
if (handle != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DestroyPhysicalMonitor(handle);
|
||||||
|
Logger.LogDebug($"Released physical monitor handle 0x{handle:X}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to destroy physical monitor handle 0x{handle:X}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_deviceKeyToHandleMap.Clear();
|
||||||
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using PowerDisplay.Core.Utils;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native.DDC
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves and caches VCP codes for monitor controls
|
||||||
|
/// Handles brightness, color temperature, and other VCP feature codes
|
||||||
|
/// </summary>
|
||||||
|
public class VcpCodeResolver
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, byte> _cachedCodes = new();
|
||||||
|
|
||||||
|
// VCP code priority order (for brightness control)
|
||||||
|
private static readonly byte[] BrightnessVcpCodes =
|
||||||
|
{
|
||||||
|
NativeConstants.VcpCodeBrightness, // 0x10 - Standard brightness
|
||||||
|
NativeConstants.VcpCodeBacklightControl, // 0x13 - Backlight control
|
||||||
|
NativeConstants.VcpCodeBacklightLevelWhite, // 0x6B - White backlight level
|
||||||
|
NativeConstants.VcpCodeContrast, // 0x12 - Contrast as last resort
|
||||||
|
};
|
||||||
|
|
||||||
|
// VCP code priority order (for color temperature control)
|
||||||
|
private static readonly byte[] ColorTemperatureVcpCodes =
|
||||||
|
{
|
||||||
|
NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature
|
||||||
|
NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment
|
||||||
|
NativeConstants.VcpCodeSelectColorPreset, // 0x14 - Color preset selection
|
||||||
|
NativeConstants.VcpCodeGamma, // 0x72 - Gamma correction
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get best VCP code for brightness control
|
||||||
|
/// </summary>
|
||||||
|
public byte? GetBrightnessVcpCode(string monitorId, IntPtr physicalHandle)
|
||||||
|
{
|
||||||
|
// Return cached best code if available
|
||||||
|
if (_cachedCodes.TryGetValue(monitorId, out var cachedCode))
|
||||||
|
{
|
||||||
|
return cachedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first working VCP code (highest priority)
|
||||||
|
foreach (var code in BrightnessVcpCodes)
|
||||||
|
{
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out _, out _))
|
||||||
|
{
|
||||||
|
// Cache and return the best (first working) code
|
||||||
|
_cachedCodes[monitorId] = code;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get best VCP code for color temperature control
|
||||||
|
/// </summary>
|
||||||
|
public byte? GetColorTemperatureVcpCode(string monitorId, IntPtr physicalHandle)
|
||||||
|
{
|
||||||
|
var cacheKey = $"{monitorId}_colortemp";
|
||||||
|
|
||||||
|
// Return cached best code if available
|
||||||
|
if (_cachedCodes.TryGetValue(cacheKey, out var cachedCode))
|
||||||
|
{
|
||||||
|
return cachedCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find first working VCP code (highest priority)
|
||||||
|
foreach (var code in ColorTemperatureVcpCodes)
|
||||||
|
{
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out _, out _))
|
||||||
|
{
|
||||||
|
// Cache and return the best (first working) code
|
||||||
|
_cachedCodes[cacheKey] = code;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert Kelvin temperature to VCP value (uses unified converter)
|
||||||
|
/// </summary>
|
||||||
|
public uint ConvertKelvinToVcpValue(int kelvin, BrightnessInfo vcpRange)
|
||||||
|
{
|
||||||
|
return (uint)ColorTemperatureConverter.KelvinToVcp(kelvin, vcpRange.Maximum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get current color temperature information
|
||||||
|
/// </summary>
|
||||||
|
public BrightnessInfo GetCurrentColorTemperature(IntPtr physicalHandle)
|
||||||
|
{
|
||||||
|
// Try different VCP codes to get color temperature
|
||||||
|
foreach (var code in ColorTemperatureVcpCodes)
|
||||||
|
{
|
||||||
|
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out uint current, out uint max))
|
||||||
|
{
|
||||||
|
return new BrightnessInfo((int)current, 0, (int)max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear all cached codes
|
||||||
|
/// </summary>
|
||||||
|
public void ClearCache()
|
||||||
|
{
|
||||||
|
_cachedCodes.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
304
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
304
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Windows API constant definitions
|
||||||
|
/// </summary>
|
||||||
|
public static class NativeConstants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Brightness (primary usage)
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeBrightness = 0x10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Contrast
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeContrast = 0x12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Backlight control (alternative brightness)
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeBacklightControl = 0x13;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: White backlight level
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeBacklightLevelWhite = 0x6B;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Audio speaker volume
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeVolume = 0x62;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Audio mute
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeMute = 0x8D;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Color temperature request (主要色温控制)
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeColorTemperature = 0x0C;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Color temperature increment (色温增量调节)
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Gamma correction (Gamma调节)
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeGamma = 0x72;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: Select color preset (颜色预设选择)
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeSelectColorPreset = 0x14;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: VCP version
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeVcpVersion = 0xDF;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VCP code: New control value
|
||||||
|
/// </summary>
|
||||||
|
public const byte VcpCodeNewControlValue = 0x02;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display device attached to desktop
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayDeviceAttachedToDesktop = 0x00000001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multi-monitor primary display
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayDeviceMultiDriver = 0x00000002;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Primary device
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayDevicePrimaryDevice = 0x00000004;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirroring driver
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayDeviceMirroringDriver = 0x00000008;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// VGA compatible
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayDeviceVgaCompatible = 0x00000010;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removable device
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayDeviceRemovable = 0x00000020;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get device interface name
|
||||||
|
/// </summary>
|
||||||
|
public const uint EddGetDeviceInterfaceName = 0x00000001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Primary monitor
|
||||||
|
/// </summary>
|
||||||
|
public const uint MonitorinfoFPrimary = 0x00000001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query display config: only active paths
|
||||||
|
/// </summary>
|
||||||
|
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query display config: all paths
|
||||||
|
/// </summary>
|
||||||
|
public const uint QdcAllPaths = 0x00000001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set display config: apply
|
||||||
|
/// </summary>
|
||||||
|
public const uint SdcApply = 0x00000080;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set display config: use supplied display config
|
||||||
|
/// </summary>
|
||||||
|
public const uint SdcUseSuppliedDisplayConfig = 0x00000020;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set display config: save to database
|
||||||
|
/// </summary>
|
||||||
|
public const uint SdcSaveToDatabase = 0x00000200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set display config: topology supplied
|
||||||
|
/// </summary>
|
||||||
|
public const uint SdcTopologySupplied = 0x00000010;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set display config: allow path order changes
|
||||||
|
/// </summary>
|
||||||
|
public const uint SdcAllowPathOrderChanges = 0x00002000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get target name
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigDeviceInfoGetTargetName = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get SDR white level
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigDeviceInfoGetSdrWhiteLevel = 7;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get advanced color information
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigDeviceInfoGetAdvancedColorInfo = 9;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set SDR white level (custom)
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigDeviceInfoSetSdrWhiteLevel = 0xFFFFFFEE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path active
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigPathActive = 0x00000001;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path mode index invalid
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigPathModeIdxInvalid = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// COM initialization: multithreaded
|
||||||
|
/// </summary>
|
||||||
|
public const uint CoinitMultithreaded = 0x0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RPC authentication level: connect
|
||||||
|
/// </summary>
|
||||||
|
public const uint RpcCAuthnLevelConnect = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RPC impersonation level: impersonate
|
||||||
|
/// </summary>
|
||||||
|
public const uint RpcCImpLevelImpersonate = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RPC authentication service: Win NT
|
||||||
|
/// </summary>
|
||||||
|
public const uint RpcCAuthnWinnt = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RPC authorization service: none
|
||||||
|
/// </summary>
|
||||||
|
public const uint RpcCAuthzNone = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RPC authentication level: call
|
||||||
|
/// </summary>
|
||||||
|
public const uint RpcCAuthnLevelCall = 3;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EOAC: none
|
||||||
|
/// </summary>
|
||||||
|
public const uint EoacNone = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WMI flag: forward only
|
||||||
|
/// </summary>
|
||||||
|
public const long WbemFlagForwardOnly = 0x20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WMI flag: return immediately
|
||||||
|
/// </summary>
|
||||||
|
public const long WbemFlagReturnImmediately = 0x10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WMI flag: connect use max wait
|
||||||
|
/// </summary>
|
||||||
|
public const long WbemFlagConnectUseMaxWait = 0x80;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Success
|
||||||
|
/// </summary>
|
||||||
|
public const int ErrorSuccess = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Insufficient buffer
|
||||||
|
/// </summary>
|
||||||
|
public const int ErrorInsufficientBuffer = 122;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid parameter
|
||||||
|
/// </summary>
|
||||||
|
public const int ErrorInvalidParameter = 87;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access denied
|
||||||
|
/// </summary>
|
||||||
|
public const int ErrorAccessDenied = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// General failure
|
||||||
|
/// </summary>
|
||||||
|
public const int ErrorGenFailure = 31;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsupported VCP code
|
||||||
|
/// </summary>
|
||||||
|
public const int ErrorGraphicsDdcciVcpNotSupported = -1071243251;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Infinite wait
|
||||||
|
/// </summary>
|
||||||
|
public const uint Infinite = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User message
|
||||||
|
/// </summary>
|
||||||
|
public const uint WmUser = 0x0400;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output technology: HDMI
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigOutputTechnologyHdmi = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output technology: DVI
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigOutputTechnologyDvi = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output technology: DisplayPort
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigOutputTechnologyDisplayportExternal = 6;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Output technology: internal
|
||||||
|
/// </summary>
|
||||||
|
public const uint DisplayconfigOutputTechnologyInternal = 0x80000000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HDR minimum SDR white level (nits)
|
||||||
|
/// </summary>
|
||||||
|
public const int HdrMinSdrWhiteLevel = 80;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HDR maximum SDR white level (nits)
|
||||||
|
/// </summary>
|
||||||
|
public const int HdrMaxSdrWhiteLevel = 480;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SDR white level conversion factor
|
||||||
|
/// </summary>
|
||||||
|
public const int SdrWhiteLevelFactor = 80;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
// 类型别名,兼容 Windows API 命名约定
|
||||||
|
using RECT = PowerDisplay.Native.Rect;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 委托类型定义
|
||||||
|
/// </summary>
|
||||||
|
public static class NativeDelegates
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 显示器枚举过程委托
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hMonitor">显示器句柄</param>
|
||||||
|
/// <param name="hdcMonitor">显示器 DC</param>
|
||||||
|
/// <param name="lprcMonitor">显示器矩形指针</param>
|
||||||
|
/// <param name="dwData">用户数据</param>
|
||||||
|
/// <returns>继续枚举返回 true</returns>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
|
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 线程启动例程委托
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lpParameter">线程参数</param>
|
||||||
|
/// <returns>线程退出代码</returns>
|
||||||
|
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
|
||||||
|
}
|
||||||
532
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
532
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
#pragma warning disable SA1649 // File name should match first type name - Multiple related P/Invoke structures
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Physical monitor structure for DDC/CI
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
public unsafe struct PhysicalMonitor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Physical monitor handle
|
||||||
|
/// </summary>
|
||||||
|
public IntPtr HPhysicalMonitor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Physical monitor description string - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort SzPhysicalMonitorDescription[128];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get description as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetDescription()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = SzPhysicalMonitorDescription)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rectangle structure
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct Rect
|
||||||
|
{
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
|
||||||
|
public int Width => Right - Left;
|
||||||
|
|
||||||
|
public int Height => Bottom - Top;
|
||||||
|
|
||||||
|
public Rect(int left, int top, int right, int bottom)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Top = top;
|
||||||
|
Right = right;
|
||||||
|
Bottom = bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor information extended structure
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
public unsafe struct MonitorInfoEx
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Structure size
|
||||||
|
/// </summary>
|
||||||
|
public uint CbSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor rectangle area
|
||||||
|
/// </summary>
|
||||||
|
public Rect RcMonitor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Work area rectangle
|
||||||
|
/// </summary>
|
||||||
|
public Rect RcWork;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flags
|
||||||
|
/// </summary>
|
||||||
|
public uint DwFlags;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device name (e.g., "\\.\DISPLAY1") - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort SzDevice[32];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper property to get device name as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetDeviceName()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = SzDevice)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display device structure
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
public unsafe struct DisplayDevice
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Structure size
|
||||||
|
/// </summary>
|
||||||
|
public uint Cb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device name (e.g., "\\.\DISPLAY1\Monitor0") - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort DeviceName[32];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device description string - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort DeviceString[128];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Status flags
|
||||||
|
/// </summary>
|
||||||
|
public uint StateFlags;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Device ID - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort DeviceID[128];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registry device key - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort DeviceKey[128];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get device name as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetDeviceName()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = DeviceName)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get device string as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetDeviceString()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = DeviceString)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get device ID as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetDeviceID()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = DeviceID)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get device key as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetDeviceKey()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = DeviceKey)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LUID (Locally Unique Identifier) structure
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct Luid
|
||||||
|
{
|
||||||
|
public uint LowPart;
|
||||||
|
public int HighPart;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{HighPart:X8}:{LowPart:X8}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration path information
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_PATH_INFO
|
||||||
|
{
|
||||||
|
public DISPLAYCONFIG_PATH_SOURCE_INFO SourceInfo;
|
||||||
|
public DISPLAYCONFIG_PATH_TARGET_INFO TargetInfo;
|
||||||
|
public uint Flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration path source information
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_PATH_SOURCE_INFO
|
||||||
|
{
|
||||||
|
public Luid AdapterId;
|
||||||
|
public uint Id;
|
||||||
|
public uint ModeInfoIdx;
|
||||||
|
public uint StatusFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration path target information
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_PATH_TARGET_INFO
|
||||||
|
{
|
||||||
|
public Luid AdapterId;
|
||||||
|
public uint Id;
|
||||||
|
public uint ModeInfoIdx;
|
||||||
|
public uint OutputTechnology;
|
||||||
|
public uint Rotation;
|
||||||
|
public uint Scaling;
|
||||||
|
public DISPLAYCONFIG_RATIONAL RefreshRate;
|
||||||
|
public uint ScanLineOrdering;
|
||||||
|
public bool TargetAvailable;
|
||||||
|
public uint StatusFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration rational number
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_RATIONAL
|
||||||
|
{
|
||||||
|
public uint Numerator;
|
||||||
|
public uint Denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration mode information
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_MODE_INFO
|
||||||
|
{
|
||||||
|
public uint InfoType;
|
||||||
|
public uint Id;
|
||||||
|
public Luid AdapterId;
|
||||||
|
public DISPLAYCONFIG_MODE_INFO_UNION ModeInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration mode information union
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
public struct DISPLAYCONFIG_MODE_INFO_UNION
|
||||||
|
{
|
||||||
|
[FieldOffset(0)]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Native API structure field")]
|
||||||
|
public DISPLAYCONFIG_TARGET_MODE targetMode;
|
||||||
|
|
||||||
|
[FieldOffset(0)]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Native API structure field")]
|
||||||
|
public DISPLAYCONFIG_SOURCE_MODE sourceMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration target mode
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_TARGET_MODE
|
||||||
|
{
|
||||||
|
public DISPLAYCONFIG_VIDEO_SIGNAL_INFO TargetVideoSignalInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration source mode
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_SOURCE_MODE
|
||||||
|
{
|
||||||
|
public uint Width;
|
||||||
|
public uint Height;
|
||||||
|
public uint PixelFormat;
|
||||||
|
public DISPLAYCONFIG_POINT Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration point
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_POINT
|
||||||
|
{
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration video signal information
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_VIDEO_SIGNAL_INFO
|
||||||
|
{
|
||||||
|
public ulong PixelRate;
|
||||||
|
public DISPLAYCONFIG_RATIONAL HSyncFreq;
|
||||||
|
public DISPLAYCONFIG_RATIONAL VSyncFreq;
|
||||||
|
public DISPLAYCONFIG_2DREGION ActiveSize;
|
||||||
|
public DISPLAYCONFIG_2DREGION TotalSize;
|
||||||
|
public uint VideoStandard;
|
||||||
|
public uint ScanLineOrdering;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration 2D region
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_2DREGION
|
||||||
|
{
|
||||||
|
public uint Cx;
|
||||||
|
public uint Cy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration device information header
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||||
|
{
|
||||||
|
public uint Type;
|
||||||
|
public uint Size;
|
||||||
|
public Luid AdapterId;
|
||||||
|
public uint Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration target device name
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
public unsafe struct DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||||
|
{
|
||||||
|
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||||
|
public uint Flags;
|
||||||
|
public uint OutputTechnology;
|
||||||
|
public ushort EdidManufactureId;
|
||||||
|
public ushort EdidProductCodeId;
|
||||||
|
public uint ConnectorInstance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor friendly name - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort MonitorFriendlyDeviceName[64];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor device path - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort MonitorDevicePath[128];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get monitor friendly name as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetMonitorFriendlyDeviceName()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = MonitorFriendlyDeviceName)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to get monitor device path as string
|
||||||
|
/// </summary>
|
||||||
|
public readonly string GetMonitorDevicePath()
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = MonitorDevicePath)
|
||||||
|
{
|
||||||
|
return new string((char*)ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration SDR white level
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_SDR_WHITE_LEVEL
|
||||||
|
{
|
||||||
|
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||||
|
public uint SDRWhiteLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display configuration advanced color information
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO
|
||||||
|
{
|
||||||
|
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||||
|
public uint AdvancedColorSupported;
|
||||||
|
public uint AdvancedColorEnabled;
|
||||||
|
public uint BitsPerColorChannel;
|
||||||
|
public uint ColorEncoding;
|
||||||
|
public uint FormatSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom structure for setting SDR white level
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct DISPLAYCONFIG_SET_SDR_WHITE_LEVEL
|
||||||
|
{
|
||||||
|
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||||
|
public uint SDRWhiteLevel;
|
||||||
|
public byte FinalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Point structure
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct POINT
|
||||||
|
{
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
|
||||||
|
public POINT(int x, int y)
|
||||||
|
{
|
||||||
|
this.X = x;
|
||||||
|
this.Y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notify icon data structure (for system tray)
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
public unsafe struct NOTIFYICONDATA
|
||||||
|
{
|
||||||
|
public uint CbSize;
|
||||||
|
public IntPtr HWnd;
|
||||||
|
public uint UID;
|
||||||
|
public uint UFlags;
|
||||||
|
public uint UCallbackMessage;
|
||||||
|
public IntPtr HIcon;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tooltip text - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort SzTip[128];
|
||||||
|
|
||||||
|
public uint DwState;
|
||||||
|
public uint DwStateMask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Info balloon text - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort SzInfo[256];
|
||||||
|
|
||||||
|
public uint UTimeout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Info balloon title - fixed buffer for LibraryImport compatibility
|
||||||
|
/// </summary>
|
||||||
|
public fixed ushort SzInfoTitle[64];
|
||||||
|
|
||||||
|
public uint DwInfoFlags;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to set tooltip text
|
||||||
|
/// </summary>
|
||||||
|
public void SetTip(string tip)
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = SzTip)
|
||||||
|
{
|
||||||
|
int length = Math.Min(tip.Length, 127);
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
ptr[i] = tip[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr[length] = 0; // Null terminator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to set info balloon text
|
||||||
|
/// </summary>
|
||||||
|
public void SetInfo(string info)
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = SzInfo)
|
||||||
|
{
|
||||||
|
int length = Math.Min(info.Length, 255);
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
ptr[i] = info[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr[length] = 0; // Null terminator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to set info balloon title
|
||||||
|
/// </summary>
|
||||||
|
public void SetInfoTitle(string title)
|
||||||
|
{
|
||||||
|
fixed (ushort* ptr = SzInfoTitle)
|
||||||
|
{
|
||||||
|
int length = Math.Min(title.Length, 63);
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
ptr[i] = title[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr[length] = 0; // Null terminator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
src/modules/powerdisplay/PowerDisplay/Native/PInvoke.cs
Normal file
275
src/modules/powerdisplay/PowerDisplay/Native/PInvoke.cs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// P/Invoke declarations using LibraryImport source generator
|
||||||
|
/// </summary>
|
||||||
|
internal static partial class PInvoke
|
||||||
|
{
|
||||||
|
// ==================== User32.dll - Window Management ====================
|
||||||
|
// GetWindowLong: On 64-bit use GetWindowLongPtrW, on 32-bit use GetWindowLongW
|
||||||
|
#if WIN64
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||||
|
internal static partial IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
|
||||||
|
#else
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongW")]
|
||||||
|
internal static partial int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// SetWindowLong: On 64-bit use SetWindowLongPtrW, on 32-bit use SetWindowLongW
|
||||||
|
#if WIN64
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||||
|
internal static partial IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
||||||
|
#else
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongW")]
|
||||||
|
internal static partial int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// SetWindowLongPtr: Always uses the Ptr variant (64-bit)
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||||
|
internal static partial IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetWindowPos(
|
||||||
|
IntPtr hWnd,
|
||||||
|
IntPtr hWndInsertAfter,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int cx,
|
||||||
|
int cy,
|
||||||
|
uint uFlags);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetForegroundWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
// ==================== User32.dll - Window Creation and Messaging ====================
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
internal static partial IntPtr CreateWindowEx(
|
||||||
|
uint dwExStyle,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpWindowName,
|
||||||
|
uint dwStyle,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int nWidth,
|
||||||
|
int nHeight,
|
||||||
|
IntPtr hWndParent,
|
||||||
|
IntPtr hMenu,
|
||||||
|
IntPtr hInstance,
|
||||||
|
IntPtr lpParam);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool DestroyWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")]
|
||||||
|
internal static partial IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
internal static partial uint RegisterWindowMessage([MarshalAs(UnmanagedType.LPWStr)] string lpString);
|
||||||
|
|
||||||
|
// ==================== User32.dll - Menu Functions ====================
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial IntPtr CreatePopupMenu();
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "AppendMenuW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool AppendMenu(
|
||||||
|
IntPtr hMenu,
|
||||||
|
uint uFlags,
|
||||||
|
uint uIDNewItem,
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string lpNewItem);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool DestroyMenu(IntPtr hMenu);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial int TrackPopupMenu(
|
||||||
|
IntPtr hMenu,
|
||||||
|
uint uFlags,
|
||||||
|
int x,
|
||||||
|
int y,
|
||||||
|
int nReserved,
|
||||||
|
IntPtr hWnd,
|
||||||
|
IntPtr prcRect);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetCursorPos(out POINT lpPoint);
|
||||||
|
|
||||||
|
// ==================== User32.dll - Display Configuration ====================
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial int GetDisplayConfigBufferSizes(
|
||||||
|
uint flags,
|
||||||
|
out uint numPathArrayElements,
|
||||||
|
out uint numModeInfoArrayElements);
|
||||||
|
|
||||||
|
// With DisableRuntimeMarshalling, LibraryImport can handle struct arrays
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial int QueryDisplayConfig(
|
||||||
|
uint flags,
|
||||||
|
ref uint numPathArrayElements,
|
||||||
|
[Out] DISPLAYCONFIG_PATH_INFO[] pathArray,
|
||||||
|
ref uint numModeInfoArrayElements,
|
||||||
|
[Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray,
|
||||||
|
IntPtr currentTopologyId);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial int DisplayConfigGetDeviceInfo(
|
||||||
|
ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName);
|
||||||
|
|
||||||
|
// ==================== User32.dll - Monitor Enumeration ====================
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool EnumDisplayMonitors(
|
||||||
|
IntPtr hdc,
|
||||||
|
IntPtr lprcClip,
|
||||||
|
NativeDelegates.MonitorEnumProc lpfnEnum,
|
||||||
|
IntPtr dwData);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetMonitorInfo(
|
||||||
|
IntPtr hMonitor,
|
||||||
|
ref MonitorInfoEx lpmi);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll", EntryPoint = "EnumDisplayDevicesW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool EnumDisplayDevices(
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] string? lpDevice,
|
||||||
|
uint iDevNum,
|
||||||
|
ref DisplayDevice lpDisplayDevice,
|
||||||
|
uint dwFlags);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial IntPtr MonitorFromWindow(
|
||||||
|
IntPtr hwnd,
|
||||||
|
uint dwFlags);
|
||||||
|
|
||||||
|
[LibraryImport("user32.dll")]
|
||||||
|
internal static partial IntPtr MonitorFromPoint(
|
||||||
|
POINT pt,
|
||||||
|
uint dwFlags);
|
||||||
|
|
||||||
|
// ==================== Shell32.dll - Tray Icon ====================
|
||||||
|
[LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool Shell_NotifyIcon(
|
||||||
|
uint dwMessage,
|
||||||
|
ref NOTIFYICONDATA lpData);
|
||||||
|
|
||||||
|
// ==================== Dxva2.dll - DDC/CI Monitor Control ====================
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetNumberOfPhysicalMonitorsFromHMONITOR(
|
||||||
|
IntPtr hMonitor,
|
||||||
|
out uint pdwNumberOfPhysicalMonitors);
|
||||||
|
|
||||||
|
// Use unsafe pointer to avoid ArraySubType limitation
|
||||||
|
[LibraryImport("Dxva2.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static unsafe partial bool GetPhysicalMonitorsFromHMONITOR(
|
||||||
|
IntPtr hMonitor,
|
||||||
|
uint dwPhysicalMonitorArraySize,
|
||||||
|
PhysicalMonitor* pPhysicalMonitorArray);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool DestroyPhysicalMonitor(IntPtr hMonitor);
|
||||||
|
|
||||||
|
// Use unsafe pointer to avoid LPArray limitation
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static unsafe partial bool DestroyPhysicalMonitors(
|
||||||
|
uint dwPhysicalMonitorArraySize,
|
||||||
|
PhysicalMonitor* pPhysicalMonitorArray);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetVCPFeatureAndVCPFeatureReply(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
byte bVCPCode,
|
||||||
|
IntPtr pvct,
|
||||||
|
out uint pdwCurrentValue,
|
||||||
|
out uint pdwMaximumValue);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetVCPFeature(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
byte bVCPCode,
|
||||||
|
uint dwNewValue);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SaveCurrentSettings(IntPtr hPhysicalMonitor);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetCapabilitiesStringLength(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
out uint pdwCapabilitiesStringLengthInCharacters);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool CapabilitiesRequestAndCapabilitiesReply(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
IntPtr pszASCIICapabilitiesString,
|
||||||
|
uint dwCapabilitiesStringLengthInCharacters);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetMonitorBrightness(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
out uint pdwMinimumBrightness,
|
||||||
|
out uint pdwCurrentBrightness,
|
||||||
|
out uint pdwMaximumBrightness);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetMonitorBrightness(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
uint dwNewBrightness);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetMonitorContrast(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
out uint pdwMinimumContrast,
|
||||||
|
out uint pdwCurrentContrast,
|
||||||
|
out uint pdwMaximumContrast);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool SetMonitorContrast(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
uint dwNewContrast);
|
||||||
|
|
||||||
|
[LibraryImport("Dxva2.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetMonitorCapabilities(
|
||||||
|
IntPtr hPhysicalMonitor,
|
||||||
|
out uint pdwMonitorCapabilities,
|
||||||
|
out uint pdwSupportedColorTemperatures);
|
||||||
|
|
||||||
|
// ==================== Kernel32.dll ====================
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
internal static partial uint GetLastError();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using PowerDisplay.Core.Interfaces;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using WmiLight;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native.WMI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WMI monitor controller for controlling internal laptop displays.
|
||||||
|
/// Rewritten to use WmiLight library for Native AOT compatibility.
|
||||||
|
/// </summary>
|
||||||
|
public partial class WmiController : IMonitorController, IDisposable
|
||||||
|
{
|
||||||
|
private const string WmiNamespace = @"root\WMI";
|
||||||
|
private const string BrightnessQueryClass = "WmiMonitorBrightness";
|
||||||
|
private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
|
||||||
|
private const string MonitorIdClass = "WmiMonitorID";
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public string Name => "WMI Monitor Controller (WmiLight)";
|
||||||
|
|
||||||
|
public MonitorType SupportedType => MonitorType.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the specified monitor can be controlled
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (monitor.Type != MonitorType.Internal)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
var query = $"SELECT * FROM {BrightnessQueryClass}";
|
||||||
|
var results = connection.CreateQuery(query).ToList();
|
||||||
|
return results.Count > 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"WMI CanControlMonitor check failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get monitor brightness
|
||||||
|
/// </summary>
|
||||||
|
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
var query = $"SELECT CurrentBrightness FROM {BrightnessQueryClass}";
|
||||||
|
var results = connection.CreateQuery(query);
|
||||||
|
|
||||||
|
foreach (var obj in results)
|
||||||
|
{
|
||||||
|
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
|
||||||
|
return new BrightnessInfo(currentBrightness, 0, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WmiException ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"WMI GetBrightness failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"WMI GetBrightness failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return BrightnessInfo.Invalid;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set monitor brightness
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Validate brightness range
|
||||||
|
brightness = Math.Clamp(brightness, 0, 100);
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
var query = $"SELECT * FROM {BrightnessMethodClass}";
|
||||||
|
var results = connection.CreateQuery(query);
|
||||||
|
|
||||||
|
foreach (var obj in results)
|
||||||
|
{
|
||||||
|
// Call WmiSetBrightness method
|
||||||
|
// Parameters: Timeout (uint32), Brightness (byte)
|
||||||
|
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
|
||||||
|
using (WmiMethodParameters inParams = method.CreateInParameters())
|
||||||
|
{
|
||||||
|
inParams.SetPropertyValue("Timeout", 0u);
|
||||||
|
inParams.SetPropertyValue("Brightness", (byte)brightness);
|
||||||
|
|
||||||
|
uint result = obj.ExecuteMethod<uint>(
|
||||||
|
method,
|
||||||
|
inParams,
|
||||||
|
out WmiMethodParameters outParams);
|
||||||
|
|
||||||
|
// Check return value (0 indicates success)
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Success();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MonitorOperationResult.Failure("No WMI brightness methods found");
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
|
||||||
|
}
|
||||||
|
catch (WmiException ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"WMI error: {ex.Message}", ex.HResult);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return MonitorOperationResult.Failure($"Unexpected error: {ex.Message}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discover supported monitors
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var monitors = new List<Monitor>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
|
||||||
|
// First check if WMI brightness support is available
|
||||||
|
var brightnessQuery = $"SELECT * FROM {BrightnessQueryClass}";
|
||||||
|
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
|
||||||
|
|
||||||
|
if (brightnessResults.Count == 0)
|
||||||
|
{
|
||||||
|
return monitors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get monitor information
|
||||||
|
var idQuery = $"SELECT * FROM {MonitorIdClass}";
|
||||||
|
var idResults = connection.CreateQuery(idQuery).ToList();
|
||||||
|
|
||||||
|
var monitorInfos = new Dictionary<string, (string Name, string InstanceName)>();
|
||||||
|
|
||||||
|
foreach (var obj in idResults)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
|
||||||
|
var userFriendlyName = GetUserFriendlyName(obj) ?? "Internal Display";
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(instanceName))
|
||||||
|
{
|
||||||
|
monitorInfos[instanceName] = (userFriendlyName, instanceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Skip problematic entries
|
||||||
|
Logger.LogDebug($"Failed to parse WMI monitor info: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create monitor objects for each supported brightness instance
|
||||||
|
foreach (var obj in brightnessResults)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
|
||||||
|
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
|
||||||
|
|
||||||
|
var name = "Internal Display";
|
||||||
|
if (monitorInfos.TryGetValue(instanceName, out var info))
|
||||||
|
{
|
||||||
|
name = info.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
var monitor = new Monitor
|
||||||
|
{
|
||||||
|
Id = $"WMI_{instanceName}",
|
||||||
|
Name = name,
|
||||||
|
Type = MonitorType.Internal,
|
||||||
|
CurrentBrightness = currentBrightness,
|
||||||
|
MinBrightness = 0,
|
||||||
|
MaxBrightness = 100,
|
||||||
|
IsAvailable = true,
|
||||||
|
InstanceName = instanceName,
|
||||||
|
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
|
||||||
|
ConnectionType = "Internal",
|
||||||
|
CommunicationMethod = "WMI",
|
||||||
|
Manufacturer = "Internal",
|
||||||
|
SupportsColorTemperature = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
monitors.Add(monitor);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Skip problematic monitors
|
||||||
|
Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (WmiException ex)
|
||||||
|
{
|
||||||
|
// Return empty list instead of throwing exception
|
||||||
|
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return monitors;
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate monitor connection status
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await Task.Run(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to read current brightness to validate connection
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
var query = $"SELECT CurrentBrightness FROM {BrightnessQueryClass} WHERE InstanceName='{monitor.InstanceName}'";
|
||||||
|
var results = connection.CreateQuery(query).ToList();
|
||||||
|
return results.Count > 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"WMI ValidateConnection failed for {monitor.InstanceName}: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get user-friendly name from WMI object
|
||||||
|
/// </summary>
|
||||||
|
private static string? GetUserFriendlyName(WmiObject monitorObject)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// WmiLight returns arrays as object arrays
|
||||||
|
var userFriendlyNameObj = monitorObject.GetPropertyValue<object>("UserFriendlyName");
|
||||||
|
|
||||||
|
if (userFriendlyNameObj is ushort[] userFriendlyName && userFriendlyName.Length > 0)
|
||||||
|
{
|
||||||
|
// Convert UINT16 array to string
|
||||||
|
var chars = userFriendlyName
|
||||||
|
.Where(c => c != 0)
|
||||||
|
.Select(c => (char)c)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (chars.Length > 0)
|
||||||
|
{
|
||||||
|
return new string(chars).Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Ignore conversion errors
|
||||||
|
Logger.LogDebug($"Failed to parse UserFriendlyName: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check WMI service availability
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsWmiAvailable()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
var query = $"SELECT * FROM {BrightnessQueryClass}";
|
||||||
|
var results = connection.CreateQuery(query).ToList();
|
||||||
|
return results.Count > 0;
|
||||||
|
}
|
||||||
|
catch (WmiException ex) when (ex.HResult == 0x1068)
|
||||||
|
{
|
||||||
|
// Expected on systems without WMI brightness support (desktops, some laptops)
|
||||||
|
Logger.LogInfo("WMI brightness control not supported on this system (expected for desktops)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Unexpected error during WMI check
|
||||||
|
Logger.LogDebug($"WMI availability check failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if administrator privileges are required
|
||||||
|
/// </summary>
|
||||||
|
public static bool RequiresElevation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new WmiConnection(WmiNamespace);
|
||||||
|
var query = $"SELECT * FROM {BrightnessMethodClass}";
|
||||||
|
var results = connection.CreateQuery(query).ToList();
|
||||||
|
|
||||||
|
foreach (var obj in results)
|
||||||
|
{
|
||||||
|
// Try to call method to check permissions
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
|
||||||
|
using (WmiMethodParameters inParams = method.CreateInParameters())
|
||||||
|
{
|
||||||
|
inParams.SetPropertyValue("Timeout", 0u);
|
||||||
|
inParams.SetPropertyValue("Brightness", (byte)50);
|
||||||
|
obj.ExecuteMethod<uint>(method, inParams, out WmiMethodParameters outParams);
|
||||||
|
return false; // If successful, no elevation required
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return true; // Administrator privileges required
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Other errors may not be permission issues
|
||||||
|
Logger.LogDebug($"WMI RequiresElevation check error: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Cannot determine, assume privileges required
|
||||||
|
Logger.LogWarning($"WMI RequiresElevation check failed: {ex.Message}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extended features not supported by WMI
|
||||||
|
public Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(BrightnessInfo.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(MonitorOperationResult.Failure("Contrast control not supported via WMI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(BrightnessInfo.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(MonitorOperationResult.Failure("Volume control not supported via WMI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(BrightnessInfo.Invalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(MonitorOperationResult.Failure("Color temperature control not supported via WMI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(MonitorOperationResult.Failure("Save settings not supported via WMI"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed && disposing)
|
||||||
|
{
|
||||||
|
// WmiLight objects are automatically cleaned up, no specific cleanup needed here
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
170
src/modules/powerdisplay/PowerDisplay/Native/WindowHelper.cs
Normal file
170
src/modules/powerdisplay/PowerDisplay/Native/WindowHelper.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// 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 static PowerDisplay.Native.PInvoke;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Native
|
||||||
|
{
|
||||||
|
internal static class WindowHelper
|
||||||
|
{
|
||||||
|
// Window Styles
|
||||||
|
private const int GwlStyle = -16;
|
||||||
|
private const int WsCaption = 0x00C00000;
|
||||||
|
private const int WsThickframe = 0x00040000;
|
||||||
|
private const int WsMinimizebox = 0x00020000;
|
||||||
|
private const int WsMaximizebox = 0x00010000;
|
||||||
|
private const int WsSysmenu = 0x00080000;
|
||||||
|
|
||||||
|
// Extended Window Styles
|
||||||
|
private const int GwlExstyle = -20;
|
||||||
|
private const int WsExDlgmodalframe = 0x00000001;
|
||||||
|
private const int WsExWindowedge = 0x00000100;
|
||||||
|
private const int WsExClientedge = 0x00000200;
|
||||||
|
private const int WsExStaticedge = 0x00020000;
|
||||||
|
private const int WsExToolwindow = 0x00000080;
|
||||||
|
|
||||||
|
// Window Messages
|
||||||
|
private const int WmNclbuttondown = 0x00A1;
|
||||||
|
private const int WmSyscommand = 0x0112;
|
||||||
|
private const int ScMove = 0xF010;
|
||||||
|
|
||||||
|
private const uint SwpNosize = 0x0001;
|
||||||
|
private const uint SwpNomove = 0x0002;
|
||||||
|
private const uint SwpFramechanged = 0x0020;
|
||||||
|
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
|
||||||
|
private static readonly IntPtr HwndNotopmost = new IntPtr(-2);
|
||||||
|
|
||||||
|
// ShowWindow commands
|
||||||
|
private const int SwHide = 0;
|
||||||
|
private const int SwShow = 5;
|
||||||
|
private const int SwMinimize = 6;
|
||||||
|
private const int SwRestore = 9;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 禁用窗口的拖动和缩放功能
|
||||||
|
/// </summary>
|
||||||
|
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
// 获取当前窗口样式
|
||||||
|
#if WIN64
|
||||||
|
int style = (int)GetWindowLong(hWnd, GwlStyle);
|
||||||
|
#else
|
||||||
|
int style = GetWindowLong(hWnd, GwlStyle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 移除可调整大小的边框、标题栏和系统菜单
|
||||||
|
style &= ~WsThickframe;
|
||||||
|
style &= ~WsMaximizebox;
|
||||||
|
style &= ~WsMinimizebox;
|
||||||
|
style &= ~WsCaption; // 移除整个标题栏
|
||||||
|
style &= ~WsSysmenu; // 移除系统菜单
|
||||||
|
|
||||||
|
// 设置新的窗口样式
|
||||||
|
#if WIN64
|
||||||
|
_ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style));
|
||||||
|
#else
|
||||||
|
_ = SetWindowLong(hWnd, GwlStyle, style);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 获取扩展样式并移除相关边框
|
||||||
|
#if WIN64
|
||||||
|
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||||
|
#else
|
||||||
|
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||||
|
#endif
|
||||||
|
exStyle &= ~WsExDlgmodalframe;
|
||||||
|
exStyle &= ~WsExWindowedge;
|
||||||
|
exStyle &= ~WsExClientedge;
|
||||||
|
exStyle &= ~WsExStaticedge;
|
||||||
|
#if WIN64
|
||||||
|
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
|
||||||
|
#else
|
||||||
|
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 刷新窗口框架
|
||||||
|
SetWindowPos(
|
||||||
|
hWnd,
|
||||||
|
IntPtr.Zero,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SwpNomove | SwpNosize | SwpFramechanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置窗口是否置顶
|
||||||
|
/// </summary>
|
||||||
|
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
|
||||||
|
{
|
||||||
|
SetWindowPos(
|
||||||
|
hWnd,
|
||||||
|
topmost ? HwndTopmost : HwndNotopmost,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SwpNomove | SwpNosize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示或隐藏窗口
|
||||||
|
/// </summary>
|
||||||
|
public static void ShowWindow(IntPtr hWnd, bool show)
|
||||||
|
{
|
||||||
|
PInvoke.ShowWindow(hWnd, show ? SwShow : SwHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最小化窗口
|
||||||
|
/// </summary>
|
||||||
|
public static void MinimizeWindow(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
PInvoke.ShowWindow(hWnd, SwMinimize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 恢复窗口
|
||||||
|
/// </summary>
|
||||||
|
public static void RestoreWindow(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
PInvoke.ShowWindow(hWnd, SwRestore);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置窗口不在任务栏显示
|
||||||
|
/// </summary>
|
||||||
|
public static void HideFromTaskbar(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
// 获取当前扩展样式
|
||||||
|
#if WIN64
|
||||||
|
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||||
|
#else
|
||||||
|
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 添加 WS_EX_TOOLWINDOW 样式,这会让窗口不在任务栏显示
|
||||||
|
exStyle |= WsExToolwindow;
|
||||||
|
|
||||||
|
// 设置新的扩展样式
|
||||||
|
#if WIN64
|
||||||
|
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
|
||||||
|
#else
|
||||||
|
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 刷新窗口框架
|
||||||
|
SetWindowPos(
|
||||||
|
hWnd,
|
||||||
|
IntPtr.Zero,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SwpNomove | SwpNosize | SwpFramechanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
86
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<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.SelfContained.props" />
|
||||||
|
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<RootNamespace>PowerDisplay</RootNamespace>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<Platforms>x64;ARM64</Platforms>
|
||||||
|
<UseWinUI>true</UseWinUI>
|
||||||
|
<EnableMsixTooling>true</EnableMsixTooling>
|
||||||
|
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||||
|
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||||
|
<AssemblyName>PowerToys.PowerDisplay</AssemblyName>
|
||||||
|
<ApplicationIcon>Assets\PowerDisplay.ico</ApplicationIcon>
|
||||||
|
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||||
|
<ProjectPriFileName>PowerToys.PowerDisplay.pri</ProjectPriFileName>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<!-- Disable XAML-generated Main method, use custom Program.cs -->
|
||||||
|
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- Native AOT Configuration -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<PublishSingleFile>false</PublishSingleFile>
|
||||||
|
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
<TrimmerSingleWarn>false</TrimmerSingleWarn>
|
||||||
|
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Remove="PowerDisplayXAML\App.xaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ApplicationDefinition Include="PowerDisplayXAML\App.xaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="PowerDisplayXAML\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||||
|
<PackageReference Include="WmiLight" />
|
||||||
|
<PackageReference Include="System.Collections.Immutable" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||||
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
|
</ItemGroup>
|
||||||
|
<!--
|
||||||
|
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||||
|
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||||
|
package has not yet been restored.
|
||||||
|
-->
|
||||||
|
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||||
|
<ProjectCapability Include="Msix" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally -->
|
||||||
|
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<!-- Copy Assets folder to output directory -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\**\*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<!--
|
||||||
|
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||||
|
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||||
|
the Windows App SDK Nuget package has not yet been restored.
|
||||||
|
-->
|
||||||
|
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||||
|
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Application x:Class="PowerDisplay.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="using:PowerDisplay.Converters"
|
||||||
|
xmlns:toolkit="using:CommunityToolkit.WinUI">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- WinUI 3 System Resources -->
|
||||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<!-- Converters -->
|
||||||
|
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||||
|
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
|
||||||
|
<converters:InverseBoolConverter x:Key="InverseBoolConverter"/>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,540 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.PowerToys.Telemetry;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Microsoft.Windows.AppLifecycle;
|
||||||
|
using PowerDisplay.Helpers;
|
||||||
|
using PowerDisplay.Serialization;
|
||||||
|
|
||||||
|
namespace PowerDisplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PowerDisplay application main class
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private Window? _mainWindow;
|
||||||
|
private int _powerToysRunnerPid;
|
||||||
|
private string _pipeUuid = string.Empty;
|
||||||
|
private static Mutex? _mutex;
|
||||||
|
|
||||||
|
// Bidirectional named pipes for IPC
|
||||||
|
private static System.IO.Pipes.NamedPipeClientStream? _readPipe; // Read from ModuleInterface (OUT pipe)
|
||||||
|
private static System.IO.Pipes.NamedPipeClientStream? _writePipe; // Write to ModuleInterface (IN pipe)
|
||||||
|
private static Thread? _messageReceiverThread;
|
||||||
|
private static bool _stopReceiver;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends IPC message to Settings UI via ModuleInterface
|
||||||
|
/// </summary>
|
||||||
|
public static void SendIPCMessage(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_writePipe != null && _writePipe.IsConnected)
|
||||||
|
{
|
||||||
|
var writer = new System.IO.StreamWriter(_writePipe) { AutoFlush = true };
|
||||||
|
writer.WriteLine(message);
|
||||||
|
Logger.LogTrace($"Sent IPC message: {message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Cannot send IPC message: pipe not connected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to send IPC message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public App(int runnerPid, string pipeUuid)
|
||||||
|
{
|
||||||
|
_powerToysRunnerPid = runnerPid;
|
||||||
|
_pipeUuid = pipeUuid;
|
||||||
|
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// Ensure types used in XAML are preserved for AOT compilation
|
||||||
|
TypePreservation.PreserveTypes();
|
||||||
|
|
||||||
|
// Initialize Logger
|
||||||
|
Logger.InitializeLogger("\\PowerDisplay\\Logs");
|
||||||
|
|
||||||
|
// Initialize PowerToys telemetry
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Telemetry errors should not crash the app
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize language settings
|
||||||
|
string appLanguage = LanguageHelper.LoadLanguage();
|
||||||
|
if (!string.IsNullOrEmpty(appLanguage))
|
||||||
|
{
|
||||||
|
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unhandled exceptions
|
||||||
|
this.UnhandledException += OnUnhandledException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle unhandled exceptions
|
||||||
|
/// </summary>
|
||||||
|
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
// Try to display error information
|
||||||
|
ShowStartupError(e.Exception);
|
||||||
|
|
||||||
|
// Mark exception as handled to prevent app crash
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when the application is launched
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Launch arguments</param>
|
||||||
|
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use Mutex to ensure only one PowerDisplay instance is running
|
||||||
|
_mutex = new Mutex(true, "PowerDisplay", out bool isNewInstance);
|
||||||
|
|
||||||
|
if (!isNewInstance)
|
||||||
|
{
|
||||||
|
// PowerDisplay is already running, exit current instance
|
||||||
|
Logger.LogInfo("PowerDisplay is already running. Exiting duplicate instance.");
|
||||||
|
Environment.Exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure Mutex is released when app exits
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (_, _) => _mutex?.ReleaseMutex();
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
var cmdArgs = Environment.GetCommandLineArgs();
|
||||||
|
if (cmdArgs?.Length > 1)
|
||||||
|
{
|
||||||
|
// Support two formats: direct PID or --pid PID
|
||||||
|
int pidValue = -1;
|
||||||
|
|
||||||
|
// Check if using --pid format
|
||||||
|
for (int i = 1; i < cmdArgs.Length - 1; i++)
|
||||||
|
{
|
||||||
|
if (cmdArgs[i] == "--pid" && int.TryParse(cmdArgs[i + 1], out pidValue))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not --pid format, try parsing last argument (compatible with old format)
|
||||||
|
if (pidValue == -1 && cmdArgs.Length > 1)
|
||||||
|
{
|
||||||
|
_ = int.TryParse(cmdArgs[cmdArgs.Length - 1], out pidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pidValue > 0)
|
||||||
|
{
|
||||||
|
_powerToysRunnerPid = pidValue;
|
||||||
|
|
||||||
|
// Started from PowerToys Runner
|
||||||
|
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
|
||||||
|
|
||||||
|
// Monitor parent process
|
||||||
|
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
|
||||||
|
{
|
||||||
|
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
|
||||||
|
ForceExit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Standalone mode
|
||||||
|
Logger.LogInfo("PowerDisplay started detached from PowerToys Runner.");
|
||||||
|
_powerToysRunnerPid = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize IPC in background (non-blocking)
|
||||||
|
// Only connect pipes when launched from PowerToys (not standalone)
|
||||||
|
if (!string.IsNullOrEmpty(_pipeUuid) && _powerToysRunnerPid != -1)
|
||||||
|
{
|
||||||
|
// Async pipe connection in background - don't block UI thread
|
||||||
|
_ = Task.Run(() => InitializeBidirectionalPipes(_pipeUuid));
|
||||||
|
Logger.LogInfo("Starting IPC pipe connection in background");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInfo("Running in standalone mode, IPC disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create main window but don't activate, window will auto-hide after initialization
|
||||||
|
_mainWindow = new MainWindow();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ShowStartupError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize bidirectional named pipes for IPC with ModuleInterface
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeBidirectionalPipes(string pipeUuid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Pipe names based on UUID from ModuleInterface
|
||||||
|
string pipeNameIn = $"powertoys_powerdisplay_{pipeUuid}_in"; // Write to this (ModuleInterface reads)
|
||||||
|
string pipeNameOut = $"powertoys_powerdisplay_{pipeUuid}_out"; // Read from this (ModuleInterface writes)
|
||||||
|
|
||||||
|
Logger.LogInfo($"Connecting to pipes: IN={pipeNameIn}, OUT={pipeNameOut}");
|
||||||
|
|
||||||
|
// Connect to write pipe (IN pipe from ModuleInterface perspective)
|
||||||
|
_writePipe = new System.IO.Pipes.NamedPipeClientStream(
|
||||||
|
".",
|
||||||
|
pipeNameIn,
|
||||||
|
System.IO.Pipes.PipeDirection.Out);
|
||||||
|
_writePipe.Connect(2000); // 2 second timeout (reduced from 5s, we're in background thread)
|
||||||
|
|
||||||
|
// Connect to read pipe (OUT pipe from ModuleInterface perspective)
|
||||||
|
_readPipe = new System.IO.Pipes.NamedPipeClientStream(
|
||||||
|
".",
|
||||||
|
pipeNameOut,
|
||||||
|
System.IO.Pipes.PipeDirection.In);
|
||||||
|
_readPipe.Connect(2000); // 2 second timeout (reduced from 5s)
|
||||||
|
|
||||||
|
Logger.LogInfo("Successfully connected to bidirectional pipes");
|
||||||
|
|
||||||
|
// Start message receiver thread
|
||||||
|
_stopReceiver = false;
|
||||||
|
_messageReceiverThread = new Thread(MessageReceiverThreadProc)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "PowerDisplay IPC Receiver",
|
||||||
|
};
|
||||||
|
_messageReceiverThread.Start();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to initialize bidirectional pipes: {ex.Message}. App will continue in standalone mode.");
|
||||||
|
|
||||||
|
// Clean up on failure
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_writePipe?.Dispose();
|
||||||
|
_readPipe?.Dispose();
|
||||||
|
_writePipe = null;
|
||||||
|
_readPipe = null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message receiver thread procedure
|
||||||
|
/// </summary>
|
||||||
|
private static void MessageReceiverThreadProc()
|
||||||
|
{
|
||||||
|
Logger.LogInfo("Message receiver thread started");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_readPipe == null || !_readPipe.IsConnected)
|
||||||
|
{
|
||||||
|
Logger.LogError("Read pipe is not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = new System.IO.StreamReader(_readPipe);
|
||||||
|
|
||||||
|
while (!_stopReceiver && _readPipe.IsConnected)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? message = reader.ReadLine();
|
||||||
|
if (message != null)
|
||||||
|
{
|
||||||
|
OnIPCMessageReceived(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.IO.IOException)
|
||||||
|
{
|
||||||
|
// Pipe disconnected
|
||||||
|
Logger.LogWarning("Pipe disconnected");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error reading from pipe: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Message receiver thread error: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo("Message receiver thread exiting");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle IPC messages received from ModuleInterface/Settings UI
|
||||||
|
/// </summary>
|
||||||
|
private static void OnIPCMessageReceived(string message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"Received IPC message: {message}");
|
||||||
|
|
||||||
|
// Parse JSON message and handle commands (using source-generated context for AOT)
|
||||||
|
// Expected format: {"action": "command_name", ...}
|
||||||
|
var ipcMessage = System.Text.Json.JsonSerializer.Deserialize(message, AppJsonContext.Default.IPCMessageAction);
|
||||||
|
if (ipcMessage?.Action != null)
|
||||||
|
{
|
||||||
|
string action = ipcMessage.Action;
|
||||||
|
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case "show_window":
|
||||||
|
Logger.LogInfo("Received show_window command");
|
||||||
|
|
||||||
|
// TODO: Implement window show logic
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "toggle_window":
|
||||||
|
Logger.LogInfo("Received toggle_window command");
|
||||||
|
|
||||||
|
// TODO: Implement window toggle logic
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "refresh_monitors":
|
||||||
|
Logger.LogInfo("Received refresh_monitors command");
|
||||||
|
|
||||||
|
// TODO: Implement monitor refresh logic
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "settings_updated":
|
||||||
|
Logger.LogInfo("Received settings_updated command");
|
||||||
|
|
||||||
|
// TODO: Implement settings update logic
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "terminate":
|
||||||
|
Logger.LogInfo("Received terminate command");
|
||||||
|
|
||||||
|
// TODO: Implement graceful shutdown
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Logger.LogWarning($"Unknown action received: {action}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error processing IPC message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show startup error
|
||||||
|
/// </summary>
|
||||||
|
private void ShowStartupError(Exception ex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogError($"PowerDisplay startup failed: {ex.Message}");
|
||||||
|
|
||||||
|
var errorWindow = new Window { Title = "PowerDisplay - Startup Error" };
|
||||||
|
var panel = new StackPanel { Margin = new Thickness(20), Spacing = 16 };
|
||||||
|
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "PowerDisplay Startup Failed",
|
||||||
|
FontSize = 20,
|
||||||
|
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"Error: {ex.Message}",
|
||||||
|
FontSize = 14,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"Details:\n{ex}",
|
||||||
|
FontSize = 12,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
Foreground = new SolidColorBrush(Microsoft.UI.Colors.Gray),
|
||||||
|
Margin = new Thickness(0, 10, 0, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
var closeButton = new Button
|
||||||
|
{
|
||||||
|
Content = "Close",
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
Margin = new Thickness(0, 10, 0, 0),
|
||||||
|
};
|
||||||
|
closeButton.Click += (_, _) => errorWindow.Close();
|
||||||
|
panel.Children.Add(closeButton);
|
||||||
|
|
||||||
|
errorWindow.Content = new ScrollViewer
|
||||||
|
{
|
||||||
|
Content = panel,
|
||||||
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||||
|
MaxHeight = 600,
|
||||||
|
MaxWidth = 800,
|
||||||
|
};
|
||||||
|
|
||||||
|
errorWindow.Activate();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the main window instance
|
||||||
|
/// </summary>
|
||||||
|
public Window? MainWindow => _mainWindow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if running standalone (not launched from PowerToys Runner)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRunningDetachedFromPowerToys()
|
||||||
|
{
|
||||||
|
return _powerToysRunnerPid == -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quick cleanup when application exits
|
||||||
|
/// </summary>
|
||||||
|
public void Shutdown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Start timeout mechanism, ensure exit within 1 second
|
||||||
|
var timeoutTimer = new System.Threading.Timer(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Shutdown timeout reached, forcing exit");
|
||||||
|
Environment.Exit(0);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
1000,
|
||||||
|
System.Threading.Timeout.Infinite);
|
||||||
|
|
||||||
|
// Immediately notify MainWindow that program is exiting, enable fast shutdown mode
|
||||||
|
if (_mainWindow is MainWindow mainWindow)
|
||||||
|
{
|
||||||
|
mainWindow.SetExiting();
|
||||||
|
mainWindow.FastShutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
_mainWindow = null;
|
||||||
|
|
||||||
|
// Clean up IPC pipes
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stopReceiver = true;
|
||||||
|
_messageReceiverThread?.Join(1000); // Wait max 1 second
|
||||||
|
|
||||||
|
_readPipe?.Close();
|
||||||
|
_readPipe?.Dispose();
|
||||||
|
_readPipe = null;
|
||||||
|
|
||||||
|
_writePipe?.Close();
|
||||||
|
_writePipe?.Dispose();
|
||||||
|
_writePipe = null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore IPC cleanup errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately release Mutex
|
||||||
|
_mutex?.ReleaseMutex();
|
||||||
|
_mutex?.Dispose();
|
||||||
|
_mutex = null;
|
||||||
|
|
||||||
|
// Cancel timeout timer
|
||||||
|
timeoutTimer?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore cleanup errors, ensure exit
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force exit application, ensure complete termination
|
||||||
|
/// </summary>
|
||||||
|
private void ForceExit()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Immediately start timeout mechanism, must exit within 500ms
|
||||||
|
var emergencyTimer = new System.Threading.Timer(
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Emergency exit timeout reached, terminating process");
|
||||||
|
Environment.Exit(0);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
500,
|
||||||
|
System.Threading.Timeout.Infinite);
|
||||||
|
|
||||||
|
PerformForceExit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If all other methods fail, immediately force exit process
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform fast exit operation
|
||||||
|
/// </summary>
|
||||||
|
private void PerformForceExit()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Fast shutdown
|
||||||
|
Shutdown();
|
||||||
|
|
||||||
|
// Immediately exit
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ensure exit
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Window x:Class="PowerDisplay.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:PowerDisplay.ViewModels"
|
||||||
|
xmlns:converters="using:PowerDisplay.Converters"
|
||||||
|
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||||
|
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||||
|
xmlns:animations="using:CommunityToolkit.WinUI.Animations">
|
||||||
|
<Grid x:Name="RootGrid">
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<TranslateTransform x:Name="RootGridTransform" X="0" />
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
|
||||||
|
<Grid.Resources>
|
||||||
|
<!-- Enhanced Slide-in animation storyboard -->
|
||||||
|
<Storyboard x:Key="SlideInStoryboard">
|
||||||
|
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
|
||||||
|
Storyboard.TargetProperty="X"
|
||||||
|
From="300"
|
||||||
|
To="0"
|
||||||
|
Duration="0:0:0.4">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<CubicEase EasingMode="EaseOut" />
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="RootGrid"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
From="0"
|
||||||
|
To="1"
|
||||||
|
Duration="0:0:0.3" />
|
||||||
|
<DoubleAnimation Storyboard.TargetName="MainContainer"
|
||||||
|
Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
|
||||||
|
From="20"
|
||||||
|
To="0"
|
||||||
|
Duration="0:0:0.5">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<CubicEase EasingMode="EaseOut" />
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
</Storyboard>
|
||||||
|
|
||||||
|
<!-- Enhanced Slide-out animation storyboard -->
|
||||||
|
<Storyboard x:Key="SlideOutStoryboard">
|
||||||
|
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
|
||||||
|
Storyboard.TargetProperty="X"
|
||||||
|
From="0"
|
||||||
|
To="300"
|
||||||
|
Duration="0:0:0.3">
|
||||||
|
<DoubleAnimation.EasingFunction>
|
||||||
|
<CubicEase EasingMode="EaseIn" />
|
||||||
|
</DoubleAnimation.EasingFunction>
|
||||||
|
</DoubleAnimation>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="RootGrid"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
From="1"
|
||||||
|
To="0"
|
||||||
|
Duration="0:0:0.2" />
|
||||||
|
</Storyboard>
|
||||||
|
</Grid.Resources>
|
||||||
|
|
||||||
|
<!-- Main Container with modern design -->
|
||||||
|
<Border x:Name="MainContainer"
|
||||||
|
CornerRadius="8"
|
||||||
|
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
Margin="0"
|
||||||
|
MaxWidth="640"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Top">
|
||||||
|
<Border.RenderTransform>
|
||||||
|
<TranslateTransform Y="0" />
|
||||||
|
</Border.RenderTransform>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<ScrollViewer Grid.Row="0"
|
||||||
|
ZoomMode="Disabled"
|
||||||
|
HorizontalScrollMode="Disabled"
|
||||||
|
HorizontalScrollBarVisibility="Disabled"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
MaxHeight="420"
|
||||||
|
Padding="8">
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
|
||||||
|
<!-- Loading State with modern progress -->
|
||||||
|
<StackPanel Orientation="Vertical"
|
||||||
|
Spacing="16"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.IsScanning), Mode=OneWay}">
|
||||||
|
<ProgressRing IsActive="True"
|
||||||
|
Width="32"
|
||||||
|
Height="32"
|
||||||
|
Foreground="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||||
|
<TextBlock x:Name="ScanningMonitorsTextBlock"
|
||||||
|
FontSize="12"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
TextAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- No Monitors State with InfoBar -->
|
||||||
|
<InfoBar x:Name="NoMonitorsInfoBar"
|
||||||
|
IsOpen="{x:Bind ViewModel.ShowNoMonitorsMessage, Mode=OneWay}"
|
||||||
|
Severity="Informational"
|
||||||
|
IsClosable="False"
|
||||||
|
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}">
|
||||||
|
<InfoBar.IconSource>
|
||||||
|
<FontIconSource Glyph="" />
|
||||||
|
</InfoBar.IconSource>
|
||||||
|
<TextBlock x:Name="NoMonitorsTextBlock" />
|
||||||
|
</InfoBar>
|
||||||
|
|
||||||
|
<!-- Monitors List with modern card design -->
|
||||||
|
<ItemsControl ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
|
||||||
|
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.HasMonitors), Mode=OneWay}"
|
||||||
|
HorizontalAlignment="Stretch">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical"
|
||||||
|
Spacing="0">
|
||||||
|
<StackPanel.ChildrenTransitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<EntranceThemeTransition FromVerticalOffset="20" />
|
||||||
|
<RepositionThemeTransition IsStaggeringEnabled="True" />
|
||||||
|
</TransitionCollection>
|
||||||
|
</StackPanel.ChildrenTransitions>
|
||||||
|
</StackPanel>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:MonitorViewModel">
|
||||||
|
<StackPanel Spacing="2" HorizontalAlignment="Stretch">
|
||||||
|
<StackPanel.ChildrenTransitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<EntranceThemeTransition FromVerticalOffset="8" />
|
||||||
|
<RepositionThemeTransition />
|
||||||
|
</TransitionCollection>
|
||||||
|
</StackPanel.ChildrenTransitions>
|
||||||
|
|
||||||
|
<!-- Monitor Name with Icon -->
|
||||||
|
<StackPanel Orientation="Horizontal"
|
||||||
|
Spacing="12"
|
||||||
|
Padding="12,8">
|
||||||
|
<FontIcon Glyph="{x:Bind MonitorIconGlyph, Mode=OneWay}"
|
||||||
|
FontSize="20"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
<TextBlock Text="{x:Bind Name, Mode=OneWay}"
|
||||||
|
FontSize="20"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Brightness Control -->
|
||||||
|
<Grid Height="40" HorizontalAlignment="Stretch">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
<ColumnDefinition Width="7*"/>
|
||||||
|
<ColumnDefinition Width="2*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<FontIcon Grid.Column="0"
|
||||||
|
Glyph=""
|
||||||
|
FontSize="14"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
|
||||||
|
<Slider Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinHeight="32"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,2"
|
||||||
|
Minimum="{x:Bind MinBrightness, Mode=OneWay}"
|
||||||
|
Maximum="{x:Bind MaxBrightness, Mode=OneWay}"
|
||||||
|
Value="{x:Bind Brightness, Mode=OneWay}"
|
||||||
|
ValueChanged="Slider_ValueChanged"
|
||||||
|
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||||
|
Tag="Brightness"
|
||||||
|
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Text="{x:Bind Brightness, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Color Temperature Control -->
|
||||||
|
<Grid Height="40"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Visibility="{x:Bind ConvertBoolToVisibility(ShowColorTemperature), Mode=OneWay}">
|
||||||
|
<Grid.Transitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||||
|
<RepositionThemeTransition />
|
||||||
|
</TransitionCollection>
|
||||||
|
</Grid.Transitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
<ColumnDefinition Width="7*"/>
|
||||||
|
<ColumnDefinition Width="2*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<FontIcon Grid.Column="0"
|
||||||
|
Glyph=""
|
||||||
|
FontSize="14"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
|
||||||
|
<Slider Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinHeight="32"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,2"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="100"
|
||||||
|
Value="{x:Bind ColorTemperaturePercent, Mode=OneWay}"
|
||||||
|
ValueChanged="Slider_ValueChanged"
|
||||||
|
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||||
|
Tag="ColorTemperature"
|
||||||
|
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Run Text="{x:Bind ColorTemperature, Mode=OneWay}" />
|
||||||
|
<Run Text="K" FontSize="9" />
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Contrast Control -->
|
||||||
|
<Grid Height="40"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Visibility="{x:Bind ConvertBoolToVisibility(ShowContrast), Mode=OneWay}">
|
||||||
|
<Grid.Transitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||||
|
<RepositionThemeTransition />
|
||||||
|
</TransitionCollection>
|
||||||
|
</Grid.Transitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
<ColumnDefinition Width="7*"/>
|
||||||
|
<ColumnDefinition Width="2*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<FontIcon Grid.Column="0"
|
||||||
|
Glyph=""
|
||||||
|
FontSize="14"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
|
||||||
|
<Slider Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinHeight="32"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,2"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="100"
|
||||||
|
Value="{x:Bind ContrastPercent, Mode=OneWay}"
|
||||||
|
ValueChanged="Slider_ValueChanged"
|
||||||
|
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||||
|
Tag="Contrast"
|
||||||
|
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Run Text="{x:Bind Contrast, Mode=OneWay}" />
|
||||||
|
<Run Text="%" FontSize="9" />
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Volume Control -->
|
||||||
|
<Grid Height="40"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Visibility="{x:Bind ConvertBoolToVisibility(ShowVolume), Mode=OneWay}">
|
||||||
|
<Grid.Transitions>
|
||||||
|
<TransitionCollection>
|
||||||
|
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||||
|
<RepositionThemeTransition />
|
||||||
|
</TransitionCollection>
|
||||||
|
</Grid.Transitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="1*"/>
|
||||||
|
<ColumnDefinition Width="7*"/>
|
||||||
|
<ColumnDefinition Width="2*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<FontIcon Grid.Column="0"
|
||||||
|
Glyph=""
|
||||||
|
FontSize="14"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
|
||||||
|
<Slider Grid.Column="1"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
MinHeight="32"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0,2"
|
||||||
|
Minimum="{x:Bind MinVolume, Mode=OneWay}"
|
||||||
|
Maximum="{x:Bind MaxVolume, Mode=OneWay}"
|
||||||
|
Value="{x:Bind Volume, Mode=OneWay}"
|
||||||
|
ValueChanged="Slider_ValueChanged"
|
||||||
|
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||||
|
Tag="Volume"
|
||||||
|
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Run Text="{x:Bind Volume, Mode=OneWay}" />
|
||||||
|
<Run Text="%" FontSize="9" />
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Status Bar with modern design -->
|
||||||
|
<Grid Grid.Row="1"
|
||||||
|
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="0,1,0,0"
|
||||||
|
Padding="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Status Information -->
|
||||||
|
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="AdjustBrightnessTextBlock"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="SemiBold" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<StackPanel Grid.Column="1"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="4"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
<Button x:Name="LinkButton"
|
||||||
|
Width="40"
|
||||||
|
Height="40"
|
||||||
|
CornerRadius="4"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
ToolTipService.ToolTip="Sync all monitors">
|
||||||
|
<FontIcon Glyph="" FontSize="16" />
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="DisableButton"
|
||||||
|
Width="40"
|
||||||
|
Height="40"
|
||||||
|
CornerRadius="4"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
ToolTipService.ToolTip="Toggle control">
|
||||||
|
<FontIcon Glyph="" FontSize="16" />
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="RefreshButton"
|
||||||
|
Width="40"
|
||||||
|
Height="40"
|
||||||
|
CornerRadius="4"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
ToolTipService.ToolTip="Refresh monitors">
|
||||||
|
<FontIcon Glyph="" FontSize="16" />
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,793 @@
|
|||||||
|
// 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.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
|
using Microsoft.UI;
|
||||||
|
using Microsoft.UI.Composition;
|
||||||
|
using Microsoft.UI.Windowing;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Microsoft.UI.Xaml.Media.Animation;
|
||||||
|
using PowerDisplay.Core;
|
||||||
|
using PowerDisplay.Core.Interfaces;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using PowerDisplay.Helpers;
|
||||||
|
using PowerDisplay.Native;
|
||||||
|
using PowerDisplay.ViewModels;
|
||||||
|
using Windows.Graphics;
|
||||||
|
using WinRT.Interop;
|
||||||
|
using static PowerDisplay.Native.PInvoke;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
namespace PowerDisplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PowerDisplay main window
|
||||||
|
/// </summary>
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||||
|
public sealed partial class MainWindow : Window, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
|
||||||
|
private MainViewModel _viewModel = null!;
|
||||||
|
private TrayIconHelper _trayIcon = null!;
|
||||||
|
private AppWindow _appWindow = null!;
|
||||||
|
private bool _isExiting;
|
||||||
|
|
||||||
|
// Expose ViewModel as property for x:Bind
|
||||||
|
public MainViewModel ViewModel => _viewModel;
|
||||||
|
|
||||||
|
// Conversion functions for x:Bind (AOT-compatible alternative to converters)
|
||||||
|
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// Lightweight initialization - no heavy operations in constructor
|
||||||
|
// Setup window properties
|
||||||
|
SetupWindow();
|
||||||
|
|
||||||
|
// Initialize tray icon
|
||||||
|
InitializeTrayIcon();
|
||||||
|
|
||||||
|
// Initialize UI text
|
||||||
|
InitializeUIText();
|
||||||
|
|
||||||
|
// Clean up resources on window close
|
||||||
|
this.Closed += OnWindowClosed;
|
||||||
|
|
||||||
|
// Delay ViewModel creation until first activation (async)
|
||||||
|
this.Activated += OnFirstActivated;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"MainWindow initialization failed: {ex.Message}");
|
||||||
|
ShowError($"Unable to start main window: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _hasInitialized;
|
||||||
|
|
||||||
|
private async void OnFirstActivated(object sender, WindowActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
// Only initialize once on first activation
|
||||||
|
if (_hasInitialized)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasInitialized = true;
|
||||||
|
this.Activated -= OnFirstActivated; // Unsubscribe after first run
|
||||||
|
|
||||||
|
// Create and initialize ViewModel asynchronously
|
||||||
|
// This will trigger Loading UI (IsScanning) during monitor discovery
|
||||||
|
_viewModel = new MainViewModel();
|
||||||
|
RootGrid.DataContext = _viewModel;
|
||||||
|
|
||||||
|
// Notify bindings that ViewModel is now available (for x:Bind)
|
||||||
|
Bindings.Update();
|
||||||
|
|
||||||
|
// Initialize ViewModel event handlers
|
||||||
|
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
|
||||||
|
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
|
||||||
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
|
||||||
|
// Bind button events
|
||||||
|
LinkButton.Click += OnLinkClick;
|
||||||
|
DisableButton.Click += OnDisableClick;
|
||||||
|
RefreshButton.Click += OnRefreshClick;
|
||||||
|
|
||||||
|
// Start async initialization (monitor scanning happens here)
|
||||||
|
await InitializeAsync();
|
||||||
|
|
||||||
|
// Hide window after initialization completes
|
||||||
|
HideWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// No delays! Direct async operation
|
||||||
|
await _viewModel.RefreshMonitorsAsync();
|
||||||
|
await _viewModel.ReloadMonitorSettingsAsync();
|
||||||
|
|
||||||
|
// Adjust window size after data is loaded (event-driven)
|
||||||
|
AdjustWindowSizeToContent();
|
||||||
|
}
|
||||||
|
catch (WmiLight.WmiException ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"WMI access failed: {ex.Message}");
|
||||||
|
ShowError("Unable to access internal display control, administrator privileges may be required.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Initialization failed: {ex.Message}");
|
||||||
|
ShowError($"Initialization failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeUIText()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||||
|
|
||||||
|
// Set text block content
|
||||||
|
ScanningMonitorsTextBlock.Text = loader.GetString("ScanningMonitorsText");
|
||||||
|
NoMonitorsTextBlock.Text = loader.GetString("NoMonitorsText");
|
||||||
|
AdjustBrightnessTextBlock.Text = loader.GetString("AdjustBrightnessText");
|
||||||
|
|
||||||
|
// Set button tooltips
|
||||||
|
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, loader.GetString("SyncAllMonitorsTooltip"));
|
||||||
|
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, loader.GetString("ToggleControlTooltip"));
|
||||||
|
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, loader.GetString("RefreshTooltip"));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Use English defaults if resource loading fails
|
||||||
|
Logger.LogWarning($"Failed to load localized strings: {ex.Message}");
|
||||||
|
ScanningMonitorsTextBlock.Text = "Scanning monitors...";
|
||||||
|
NoMonitorsTextBlock.Text = "No monitors detected";
|
||||||
|
AdjustBrightnessTextBlock.Text = "PowerDisplay";
|
||||||
|
|
||||||
|
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, "Synchronize all monitors to the same brightness");
|
||||||
|
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, "Enable or disable brightness control");
|
||||||
|
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, "Rescan connected monitors");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowError(string message)
|
||||||
|
{
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.StatusText = $"Error: {message}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error (ViewModel not yet initialized): {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||||
|
{
|
||||||
|
// Allow window to close if program is exiting
|
||||||
|
if (_isExiting)
|
||||||
|
{
|
||||||
|
// Clean up event subscriptions
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||||
|
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||||
|
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Handled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only user operation (although we hide close button), just hide window
|
||||||
|
args.Handled = true; // Prevent window closing
|
||||||
|
HideWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeTrayIcon()
|
||||||
|
{
|
||||||
|
_trayIcon = new TrayIconHelper(this);
|
||||||
|
_trayIcon.SetCallbacks(
|
||||||
|
onShow: ShowWindow,
|
||||||
|
onExit: ExitApplication,
|
||||||
|
onRefresh: () => _viewModel?.RefreshCommand?.Execute(null),
|
||||||
|
onSettings: OpenSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenSettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Open PowerToys Settings to PowerDisplay page
|
||||||
|
PowerDisplay.Helpers.SettingsDeepLink.OpenPowerDisplaySettings();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to open settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowWindow()
|
||||||
|
{
|
||||||
|
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||||
|
|
||||||
|
// Adjust window size before showing
|
||||||
|
AdjustWindowSizeToContent();
|
||||||
|
|
||||||
|
// Reposition to bottom right (set position before showing)
|
||||||
|
if (_appWindow != null)
|
||||||
|
{
|
||||||
|
PositionWindowAtBottomRight(_appWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial state for animation
|
||||||
|
RootGrid.Opacity = 0;
|
||||||
|
|
||||||
|
// Show window
|
||||||
|
WindowHelper.ShowWindow(hWnd, true);
|
||||||
|
|
||||||
|
// Bring window to foreground
|
||||||
|
PInvoke.SetForegroundWindow(hWnd);
|
||||||
|
|
||||||
|
// Use storyboard animation for window entrance
|
||||||
|
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
|
||||||
|
{
|
||||||
|
var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard;
|
||||||
|
slideInStoryboard?.Begin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideWindow()
|
||||||
|
{
|
||||||
|
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||||
|
|
||||||
|
// Use storyboard animation for window exit
|
||||||
|
if (RootGrid.Resources.ContainsKey("SlideOutStoryboard"))
|
||||||
|
{
|
||||||
|
var slideOutStoryboard = RootGrid.Resources["SlideOutStoryboard"] as Storyboard;
|
||||||
|
if (slideOutStoryboard != null)
|
||||||
|
{
|
||||||
|
slideOutStoryboard.Completed += (s, e) =>
|
||||||
|
{
|
||||||
|
// Hide window after animation completes
|
||||||
|
WindowHelper.ShowWindow(hWnd, false);
|
||||||
|
};
|
||||||
|
slideOutStoryboard.Begin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: hide immediately if animation not found
|
||||||
|
WindowHelper.ShowWindow(hWnd, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnUIRefreshRequested(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("UI refresh requested due to settings change");
|
||||||
|
await _viewModel.ReloadMonitorSettingsAsync();
|
||||||
|
|
||||||
|
// Adjust window size after settings are reloaded (no delay needed!)
|
||||||
|
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMonitorsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// Adjust window size when monitors collection changes (event-driven!)
|
||||||
|
// The UI binding will update first, then we adjust size
|
||||||
|
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||||
|
{
|
||||||
|
AdjustWindowSizeToContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// Adjust window size when relevant properties change (event-driven!)
|
||||||
|
if (e.PropertyName == nameof(_viewModel.IsScanning) ||
|
||||||
|
e.PropertyName == nameof(_viewModel.HasMonitors) ||
|
||||||
|
e.PropertyName == nameof(_viewModel.ShowNoMonitorsMessage))
|
||||||
|
{
|
||||||
|
// Use Low priority to ensure UI bindings update first
|
||||||
|
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||||
|
{
|
||||||
|
AdjustWindowSizeToContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set exit flag to allow window to close normally
|
||||||
|
/// </summary>
|
||||||
|
public void SetExiting()
|
||||||
|
{
|
||||||
|
_isExiting = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速关闭窗口,跳过动画和复杂清理
|
||||||
|
/// </summary>
|
||||||
|
public void FastShutdown()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isExiting = true;
|
||||||
|
|
||||||
|
// 立即释放托盘图标
|
||||||
|
_trayIcon?.Dispose();
|
||||||
|
|
||||||
|
// 快速清理 ViewModel
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
// 取消事件订阅
|
||||||
|
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||||
|
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||||
|
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
|
|
||||||
|
// 立即释放
|
||||||
|
_viewModel.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接关闭窗口,不等待动画
|
||||||
|
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||||
|
WindowHelper.ShowWindow(hWnd, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 忽略清理错误,确保能够关闭
|
||||||
|
Logger.LogWarning($"FastShutdown error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExitApplication()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 使用快速关闭
|
||||||
|
FastShutdown();
|
||||||
|
|
||||||
|
// 直接调用应用程序快速退出
|
||||||
|
if (Application.Current is App app)
|
||||||
|
{
|
||||||
|
app.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保立即退出
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 确保能够退出
|
||||||
|
Logger.LogError($"ExitApplication error: {ex.Message}");
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnRefreshClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add button press animation
|
||||||
|
if (sender is Button button)
|
||||||
|
{
|
||||||
|
await AnimateButtonPress(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh monitor list
|
||||||
|
if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
|
||||||
|
{
|
||||||
|
_viewModel.RefreshCommand.Execute(null);
|
||||||
|
|
||||||
|
// Window size will be adjusted automatically by OnMonitorsCollectionChanged event!
|
||||||
|
// No delay needed - event-driven design
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"OnRefreshClick failed: {ex}");
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.StatusText = "Refresh failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnLinkClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add button press animation
|
||||||
|
if (sender is Button button)
|
||||||
|
{
|
||||||
|
await AnimateButtonPress(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link all monitor brightness (synchronized adjustment)
|
||||||
|
if (_viewModel != null && _viewModel.Monitors.Count > 0)
|
||||||
|
{
|
||||||
|
// Get first monitor brightness as reference
|
||||||
|
var baseBrightness = _viewModel.Monitors.First().Brightness;
|
||||||
|
_ = _viewModel.SetAllBrightnessAsync(baseBrightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"OnLinkClick failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnDisableClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add button press animation
|
||||||
|
if (sender is Button button)
|
||||||
|
{
|
||||||
|
await AnimateButtonPress(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable/enable all monitor controls
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
foreach (var monitor in _viewModel.Monitors)
|
||||||
|
{
|
||||||
|
monitor.IsAvailable = !monitor.IsAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewModel.StatusText = _viewModel.Monitors.Any(m => m.IsAvailable)
|
||||||
|
? "Display control enabled"
|
||||||
|
: "Display control disabled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"OnDisableClick failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get internal monitor name, consistent with SettingsManager logic
|
||||||
|
/// </summary>
|
||||||
|
private async void OnTestClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ContentDialog? dlg = null;
|
||||||
|
Core.MonitorManager? manager = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Test monitor discovery functionality
|
||||||
|
dlg = new ContentDialog
|
||||||
|
{
|
||||||
|
Title = "Monitor Detection Test",
|
||||||
|
Content = "Starting monitor detection...",
|
||||||
|
CloseButtonText = "Close",
|
||||||
|
XamlRoot = this.Content.XamlRoot,
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = dlg.ShowAsync();
|
||||||
|
|
||||||
|
manager = new Core.MonitorManager();
|
||||||
|
var monitors = await manager.DiscoverMonitorsAsync(default(System.Threading.CancellationToken));
|
||||||
|
|
||||||
|
string message = $"Found {monitors.Count} monitors:\n\n";
|
||||||
|
foreach (var monitor in monitors)
|
||||||
|
{
|
||||||
|
message += $"• {monitor.Name}\n";
|
||||||
|
message += $" Type: {monitor.Type}\n";
|
||||||
|
message += $" Brightness: {monitor.CurrentBrightness}%\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitors.Count == 0)
|
||||||
|
{
|
||||||
|
message = "No monitors found.\n\n";
|
||||||
|
message += "Possible reasons:\n";
|
||||||
|
message += "• DDC/CI not supported\n";
|
||||||
|
message += "• Driver issues\n";
|
||||||
|
message += "• Permission issues\n";
|
||||||
|
message += "• Cable doesn't support DDC/CI";
|
||||||
|
}
|
||||||
|
|
||||||
|
dlg.Content = message;
|
||||||
|
|
||||||
|
// Don't dispose manager, use existing manager
|
||||||
|
// Initialize ViewModel and bind to root Grid refresh
|
||||||
|
if (monitors.Count > 0)
|
||||||
|
{
|
||||||
|
// Use existing refresh command
|
||||||
|
await _viewModel.RefreshMonitorsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"OnTestClick failed: {ex}");
|
||||||
|
if (dlg != null)
|
||||||
|
{
|
||||||
|
dlg.Content = $"Error: {ex.Message}\n\nType: {ex.GetType().Name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
manager?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupWindow()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get window handle
|
||||||
|
var hWnd = WindowNative.GetWindowHandle(this);
|
||||||
|
var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||||||
|
_appWindow = AppWindow.GetFromWindowId(windowId);
|
||||||
|
|
||||||
|
if (_appWindow != null)
|
||||||
|
{
|
||||||
|
// Set initial window size - will be adjusted later based on content
|
||||||
|
_appWindow.Resize(new SizeInt32 { Width = 640, Height = 480 });
|
||||||
|
|
||||||
|
// Position window at bottom right corner
|
||||||
|
PositionWindowAtBottomRight(_appWindow);
|
||||||
|
|
||||||
|
// Set window icon and title bar
|
||||||
|
_appWindow.Title = "PowerDisplay";
|
||||||
|
|
||||||
|
// Remove title bar and system buttons
|
||||||
|
var presenter = _appWindow.Presenter as OverlappedPresenter;
|
||||||
|
if (presenter != null)
|
||||||
|
{
|
||||||
|
// Disable resizing
|
||||||
|
presenter.IsResizable = false;
|
||||||
|
|
||||||
|
// Disable maximize button
|
||||||
|
presenter.IsMaximizable = false;
|
||||||
|
|
||||||
|
// Disable minimize button
|
||||||
|
presenter.IsMinimizable = false;
|
||||||
|
|
||||||
|
// Set borderless mode
|
||||||
|
presenter.SetBorderAndTitleBar(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom title bar - completely remove all buttons
|
||||||
|
var titleBar = _appWindow.TitleBar;
|
||||||
|
if (titleBar != null)
|
||||||
|
{
|
||||||
|
// Extend content into title bar area
|
||||||
|
titleBar.ExtendsContentIntoTitleBar = true;
|
||||||
|
|
||||||
|
// Completely remove title bar height
|
||||||
|
titleBar.PreferredHeightOption = Microsoft.UI.Windowing.TitleBarHeightOption.Collapsed;
|
||||||
|
|
||||||
|
// Set all button colors to transparent
|
||||||
|
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Disable title bar interaction area
|
||||||
|
titleBar.SetDragRectangles(Array.Empty<Windows.Graphics.RectInt32>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set modern Mica Alt backdrop for Windows 11
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Use Mica Alt for a more modern appearance
|
||||||
|
if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
|
||||||
|
{
|
||||||
|
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback to basic backdrop for older systems
|
||||||
|
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.DesktopAcrylicBackdrop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback: use solid color background
|
||||||
|
this.SystemBackdrop = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Win32 API to further disable window moving
|
||||||
|
WindowHelper.DisableWindowMovingAndResizing(hWnd);
|
||||||
|
|
||||||
|
// Hide window from taskbar
|
||||||
|
WindowHelper.HideFromTaskbar(hWnd);
|
||||||
|
|
||||||
|
// Optional: set window topmost
|
||||||
|
// WindowHelper.SetWindowTopmost(hWnd, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Ignore window setup errors
|
||||||
|
Logger.LogWarning($"Window setup error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdjustWindowSizeToContent()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_appWindow == null || RootGrid == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force layout update to ensure proper measurement
|
||||||
|
RootGrid.UpdateLayout();
|
||||||
|
|
||||||
|
// Get precise content height
|
||||||
|
var availableWidth = 640.0;
|
||||||
|
var contentHeight = GetContentHeight(availableWidth);
|
||||||
|
|
||||||
|
// Account for display scaling
|
||||||
|
var scale = RootGrid.XamlRoot?.RasterizationScale ?? 1.0;
|
||||||
|
var scaledHeight = (int)Math.Ceiling(contentHeight * scale);
|
||||||
|
|
||||||
|
// Only set maximum height for scrollable content
|
||||||
|
scaledHeight = Math.Min(scaledHeight, 650);
|
||||||
|
|
||||||
|
// Check if resize is needed
|
||||||
|
var currentSize = _appWindow.Size;
|
||||||
|
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"Adjusting window height from {currentSize.Height} to {scaledHeight} (content: {contentHeight})");
|
||||||
|
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
|
||||||
|
|
||||||
|
// Update clip region to match new window size
|
||||||
|
UpdateClipRegion(640, scaledHeight / scale);
|
||||||
|
|
||||||
|
// Reposition to maintain bottom-right position
|
||||||
|
PositionWindowAtBottomRight(_appWindow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error adjusting window size: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetContentHeight(double availableWidth)
|
||||||
|
{
|
||||||
|
// Try to measure MainContainer directly for precise content size
|
||||||
|
if (RootGrid.FindName("MainContainer") is Border mainContainer)
|
||||||
|
{
|
||||||
|
mainContainer.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||||
|
return mainContainer.DesiredSize.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Measure the root grid
|
||||||
|
RootGrid.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||||
|
return RootGrid.DesiredSize.Height + 4; // Small padding for fallback method
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateClipRegion(double width, double height)
|
||||||
|
{
|
||||||
|
// Clip region removed to allow automatic sizing
|
||||||
|
// No longer needed as we removed the fixed clip from RootGrid
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PositionWindowAtBottomRight(AppWindow appWindow)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get display area
|
||||||
|
var displayArea = DisplayArea.GetFromWindowId(appWindow.Id, DisplayAreaFallback.Nearest);
|
||||||
|
if (displayArea != null)
|
||||||
|
{
|
||||||
|
var workArea = displayArea.WorkArea;
|
||||||
|
var windowSize = appWindow.Size;
|
||||||
|
|
||||||
|
// Calculate bottom-right position, close to taskbar
|
||||||
|
// WorkArea already excludes taskbar area, so use WorkArea bottom directly
|
||||||
|
int rightMargin = 10; // Small margin from right edge
|
||||||
|
int x = workArea.Width - windowSize.Width - rightMargin;
|
||||||
|
int y = workArea.Height - windowSize.Height; // Close to taskbar top, no gap
|
||||||
|
|
||||||
|
// Move window to bottom right
|
||||||
|
appWindow.Move(new PointInt32 { X = x, Y = y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Ignore errors when positioning window
|
||||||
|
Logger.LogDebug($"Failed to position window: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Animates button press for modern interaction feedback
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="button">The button to animate</param>
|
||||||
|
private async Task AnimateButtonPress(Button button)
|
||||||
|
{
|
||||||
|
// Button animation disabled to avoid compilation errors
|
||||||
|
// Using default button visual states instead
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slider ValueChanged event handler - does nothing during drag
|
||||||
|
/// This allows the slider UI to update smoothly without triggering hardware operations
|
||||||
|
/// </summary>
|
||||||
|
private void Slider_ValueChanged(object sender, Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// During drag, this event fires 60-120 times per second
|
||||||
|
// We intentionally do nothing here to keep UI smooth
|
||||||
|
// The actual ViewModel update happens in PointerCaptureLost after drag completes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
Logger.LogDebug($"[UI] Brightness drag completed: {finalValue}");
|
||||||
|
break;
|
||||||
|
case "ColorTemperature":
|
||||||
|
monitorVm.ColorTemperaturePercent = finalValue;
|
||||||
|
Logger.LogDebug($"[UI] ColorTemperature drag completed: {finalValue}%");
|
||||||
|
break;
|
||||||
|
case "Contrast":
|
||||||
|
monitorVm.ContrastPercent = finalValue;
|
||||||
|
Logger.LogDebug($"[UI] Contrast drag completed: {finalValue}%");
|
||||||
|
break;
|
||||||
|
case "Volume":
|
||||||
|
monitorVm.Volume = finalValue;
|
||||||
|
Logger.LogDebug($"[UI] Volume drag completed: {finalValue}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_viewModel?.Dispose();
|
||||||
|
_trayIcon?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/modules/powerdisplay/PowerDisplay/Program.cs
Normal file
59
src/modules/powerdisplay/PowerDisplay/Program.cs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.Windows.AppLifecycle;
|
||||||
|
|
||||||
|
namespace PowerDisplay
|
||||||
|
{
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Logger.InitializeLogger("\\PowerDisplay\\Logs");
|
||||||
|
|
||||||
|
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||||
|
|
||||||
|
// Parse command line arguments: args[0] = runner_pid, args[1] = pipe_uuid
|
||||||
|
int runnerPid = -1;
|
||||||
|
string pipeUuid = string.Empty;
|
||||||
|
|
||||||
|
if (args.Length >= 2)
|
||||||
|
{
|
||||||
|
if (int.TryParse(args[0], out int parsedPid))
|
||||||
|
{
|
||||||
|
runnerPid = parsedPid;
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeUuid = args[1];
|
||||||
|
Logger.LogInfo($"PowerDisplay started with runner_pid={runnerPid}, pipe_uuid={pipeUuid}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("PowerDisplay started without command line arguments");
|
||||||
|
Logger.LogWarning($"PowerDisplay started with insufficient arguments (expected 2, got {args.Length}). Running in standalone mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance");
|
||||||
|
|
||||||
|
if (instanceKey.IsCurrent)
|
||||||
|
{
|
||||||
|
Microsoft.UI.Xaml.Application.Start((p) =>
|
||||||
|
{
|
||||||
|
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||||
|
SynchronizationContext.SetSynchronizationContext(context);
|
||||||
|
_ = new App(runnerPid, pipeUuid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Another instance of PowerDisplay is running. Exiting.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
|
|
||||||
|
#pragma warning disable SA1402 // File may only contain a single type - Related JSON serialization types grouped together
|
||||||
|
|
||||||
|
namespace PowerDisplay.Serialization
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON source generation context for AOT compatibility.
|
||||||
|
/// Eliminates reflection-based JSON serialization.
|
||||||
|
/// </summary>
|
||||||
|
[JsonSerializable(typeof(PowerDisplayMonitorsIPCResponse))]
|
||||||
|
[JsonSerializable(typeof(MonitorInfoData))]
|
||||||
|
[JsonSerializable(typeof(IPCMessageAction))]
|
||||||
|
[JsonSerializable(typeof(MonitorStateFile))]
|
||||||
|
[JsonSerializable(typeof(MonitorStateEntry))]
|
||||||
|
[JsonSourceGenerationOptions(
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
|
||||||
|
internal sealed partial class AppJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IPC message wrapper for parsing action-based messages.
|
||||||
|
/// Used in App.xaml.cs for dynamic IPC command handling.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class IPCMessageAction
|
||||||
|
{
|
||||||
|
[JsonPropertyName("action")]
|
||||||
|
public string? Action { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor state file structure for JSON persistence.
|
||||||
|
/// Made internal (from private) to support source generation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class MonitorStateFile
|
||||||
|
{
|
||||||
|
[JsonPropertyName("monitors")]
|
||||||
|
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonPropertyName("lastUpdated")]
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Individual monitor state entry.
|
||||||
|
/// Made internal (from private) to support source generation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class MonitorStateEntry
|
||||||
|
{
|
||||||
|
[JsonPropertyName("brightness")]
|
||||||
|
public int Brightness { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("colorTemperature")]
|
||||||
|
public int ColorTemperature { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("contrast")]
|
||||||
|
public int Contrast { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("volume")]
|
||||||
|
public int Volume { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("lastUpdated")]
|
||||||
|
public DateTime LastUpdated { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="ScanningMonitorsText" xml:space="preserve">
|
||||||
|
<value>Scanning monitors...</value>
|
||||||
|
</data>
|
||||||
|
<data name="NoMonitorsText" xml:space="preserve">
|
||||||
|
<value>No monitors detected</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdjustBrightnessText" xml:space="preserve">
|
||||||
|
<value>PowerDisplay</value>
|
||||||
|
</data>
|
||||||
|
<data name="SyncAllMonitorsTooltip" xml:space="preserve">
|
||||||
|
<value>Synchronize all monitors to the same brightness</value>
|
||||||
|
</data>
|
||||||
|
<data name="ToggleControlTooltip" xml:space="preserve">
|
||||||
|
<value>Enable or disable brightness control</value>
|
||||||
|
</data>
|
||||||
|
<data name="RefreshTooltip" xml:space="preserve">
|
||||||
|
<value>Rescan connected monitors</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// 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.Diagnostics.Tracing;
|
||||||
|
using Microsoft.PowerToys.Telemetry;
|
||||||
|
using Microsoft.PowerToys.Telemetry.Events;
|
||||||
|
|
||||||
|
namespace PowerDisplay.Telemetry.Events
|
||||||
|
{
|
||||||
|
[EventData]
|
||||||
|
public class PowerDisplayStartEvent : EventBase, IEvent
|
||||||
|
{
|
||||||
|
public new string EventName => "PowerDisplay_Start";
|
||||||
|
|
||||||
|
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,792 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.PowerToys.Settings.UI.Library;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using PowerDisplay.Commands;
|
||||||
|
using PowerDisplay.Core;
|
||||||
|
using PowerDisplay.Core.Interfaces;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using PowerDisplay.Helpers;
|
||||||
|
using PowerDisplay.Serialization;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
namespace PowerDisplay.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Main ViewModel for the PowerDisplay application
|
||||||
|
/// </summary>
|
||||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||||
|
public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||||
|
{
|
||||||
|
private readonly MonitorManager _monitorManager;
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private readonly ISettingsUtils _settingsUtils;
|
||||||
|
private readonly MonitorStateManager _stateManager;
|
||||||
|
private FileSystemWatcher? _settingsWatcher;
|
||||||
|
|
||||||
|
private ObservableCollection<MonitorViewModel> _monitors;
|
||||||
|
private string _statusText;
|
||||||
|
private bool _isScanning;
|
||||||
|
private bool _isInitialized;
|
||||||
|
private bool _isLoading;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event triggered when UI refresh is requested due to settings changes
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler? UIRefreshRequested;
|
||||||
|
|
||||||
|
public MainViewModel()
|
||||||
|
{
|
||||||
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_monitors = new ObservableCollection<MonitorViewModel>();
|
||||||
|
_statusText = "Initializing...";
|
||||||
|
_isScanning = true;
|
||||||
|
|
||||||
|
// Initialize settings utils
|
||||||
|
_settingsUtils = new SettingsUtils();
|
||||||
|
_stateManager = new MonitorStateManager();
|
||||||
|
|
||||||
|
// Initialize the monitor manager
|
||||||
|
_monitorManager = new MonitorManager();
|
||||||
|
|
||||||
|
// Subscribe to events
|
||||||
|
_monitorManager.MonitorsChanged += OnMonitorsChanged;
|
||||||
|
|
||||||
|
// Setup settings file monitoring
|
||||||
|
SetupSettingsFileWatcher();
|
||||||
|
|
||||||
|
// Start initial discovery
|
||||||
|
_ = InitializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<MonitorViewModel> Monitors
|
||||||
|
{
|
||||||
|
get => _monitors;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_monitors = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string StatusText
|
||||||
|
{
|
||||||
|
get => _statusText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_statusText = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsScanning
|
||||||
|
{
|
||||||
|
get => _isScanning;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isScanning = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(HasMonitors));
|
||||||
|
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
|
||||||
|
OnPropertyChanged(nameof(IsInteractionEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasMonitors => !IsScanning && Monitors.Count > 0;
|
||||||
|
|
||||||
|
public bool ShowNoMonitorsMessage => !IsScanning && Monitors.Count == 0;
|
||||||
|
|
||||||
|
public bool IsInitialized
|
||||||
|
{
|
||||||
|
get => _isInitialized;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_isInitialized = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsLoading
|
||||||
|
{
|
||||||
|
get => _isLoading;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_isLoading = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(IsInteractionEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether gets whether user interaction is enabled (not loading or scanning)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsInteractionEnabled => !IsLoading && !IsScanning;
|
||||||
|
|
||||||
|
public ICommand RefreshCommand => new RelayCommand(async () => await RefreshMonitorsAsync());
|
||||||
|
|
||||||
|
public ICommand SetAllBrightnessCommand => new RelayCommand<int?>(async (brightness) =>
|
||||||
|
{
|
||||||
|
if (brightness.HasValue)
|
||||||
|
{
|
||||||
|
await SetAllBrightnessAsync(brightness.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StatusText = "Scanning monitors...";
|
||||||
|
IsScanning = true;
|
||||||
|
|
||||||
|
// Discover monitors
|
||||||
|
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
// Update UI on the dispatcher thread
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
UpdateMonitorList(monitors);
|
||||||
|
IsScanning = false;
|
||||||
|
IsInitialized = true;
|
||||||
|
|
||||||
|
if (monitors.Count > 0)
|
||||||
|
{
|
||||||
|
StatusText = $"Found {monitors.Count} monitors";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StatusText = "No controllable monitors found";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
StatusText = $"Scan failed: {ex.Message}";
|
||||||
|
IsScanning = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshMonitorsAsync()
|
||||||
|
{
|
||||||
|
if (IsScanning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StatusText = "Refreshing monitor list...";
|
||||||
|
IsScanning = true;
|
||||||
|
|
||||||
|
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
UpdateMonitorList(monitors);
|
||||||
|
IsScanning = false;
|
||||||
|
StatusText = $"Found {monitors.Count} monitors";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
StatusText = $"Refresh failed: {ex.Message}";
|
||||||
|
IsScanning = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMonitorList(IReadOnlyList<Monitor> monitors)
|
||||||
|
{
|
||||||
|
Monitors.Clear();
|
||||||
|
|
||||||
|
var colorTempTasks = new List<Task>();
|
||||||
|
foreach (var monitor in monitors)
|
||||||
|
{
|
||||||
|
var vm = new MonitorViewModel(monitor, _monitorManager, this);
|
||||||
|
Monitors.Add(vm);
|
||||||
|
|
||||||
|
// Asynchronously initialize color temperature for DDC/CI monitors
|
||||||
|
if (monitor.SupportsColorTemperature && monitor.Type == MonitorType.External)
|
||||||
|
{
|
||||||
|
var task = InitializeColorTemperatureSafeAsync(monitor.Id, vm);
|
||||||
|
colorTempTasks.Add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(HasMonitors));
|
||||||
|
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
|
||||||
|
|
||||||
|
// Send monitor information to Settings UI via IPC
|
||||||
|
SendMonitorInfoToSettingsUI();
|
||||||
|
|
||||||
|
// Restore saved settings if enabled (async, don't block)
|
||||||
|
// Pass color temperature initialization tasks so we can wait for them if needed
|
||||||
|
_ = ReloadMonitorSettingsAsync(colorTempTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAllBrightnessAsync(int brightness)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
StatusText = $"Setting all monitors brightness to {brightness}%...";
|
||||||
|
await _monitorManager.SetAllBrightnessAsync(brightness, _cancellationTokenSource.Token);
|
||||||
|
StatusText = $"All monitors brightness set to {brightness}%";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"Failed to set brightness: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMonitorsChanged(object? sender, MonitorListChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
// Handle monitors being added or removed
|
||||||
|
if (e.AddedMonitors.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var monitor in e.AddedMonitors)
|
||||||
|
{
|
||||||
|
var existingVm = GetMonitorViewModel(monitor.Id);
|
||||||
|
if (existingVm == null)
|
||||||
|
{
|
||||||
|
var vm = new MonitorViewModel(monitor, _monitorManager, this);
|
||||||
|
Monitors.Add(vm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.RemovedMonitors.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var monitor in e.RemovedMonitors)
|
||||||
|
{
|
||||||
|
var vm = GetMonitorViewModel(monitor.Id);
|
||||||
|
if (vm != null)
|
||||||
|
{
|
||||||
|
Monitors.Remove(vm);
|
||||||
|
vm.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusText = $"Monitor list updated ({Monitors.Count} total)";
|
||||||
|
|
||||||
|
// Send updated monitor list to Settings UI via IPC
|
||||||
|
SendMonitorInfoToSettingsUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private MonitorViewModel? GetMonitorViewModel(string monitorId)
|
||||||
|
{
|
||||||
|
foreach (var vm in Monitors)
|
||||||
|
{
|
||||||
|
if (vm.Id == monitorId)
|
||||||
|
{
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Setup settings file watcher
|
||||||
|
/// </summary>
|
||||||
|
private void SetupSettingsFileWatcher()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settingsPath = _settingsUtils.GetSettingsFilePath("PowerDisplay");
|
||||||
|
var directory = Path.GetDirectoryName(settingsPath);
|
||||||
|
var fileName = Path.GetFileName(settingsPath);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
_settingsWatcher = new FileSystemWatcher(directory, fileName)
|
||||||
|
{
|
||||||
|
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
|
||||||
|
EnableRaisingEvents = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
_settingsWatcher.Changed += OnSettingsFileChanged;
|
||||||
|
_settingsWatcher.Created += OnSettingsFileChanged;
|
||||||
|
|
||||||
|
Logger.LogInfo($"Settings file watcher setup for: {settingsPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to setup settings file watcher: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle settings file changes - only monitors UI configuration changes from Settings UI
|
||||||
|
/// (monitor_state.json is managed separately and doesn't trigger this)
|
||||||
|
/// </summary>
|
||||||
|
private void OnSettingsFileChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"Settings file changed by Settings UI: {e.FullPath}");
|
||||||
|
|
||||||
|
// Add small delay to ensure file write completion
|
||||||
|
Task.Delay(200).ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Read updated settings
|
||||||
|
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||||
|
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
// Update feature visibility for each monitor (UI configuration only)
|
||||||
|
foreach (var monitorVm in Monitors)
|
||||||
|
{
|
||||||
|
// Use HardwareId for lookup (unified identification)
|
||||||
|
Logger.LogInfo($"[Settings Update] Looking for monitor settings with Hardware ID: '{monitorVm.HardwareId}'");
|
||||||
|
|
||||||
|
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
|
||||||
|
m.HardwareId == monitorVm.HardwareId);
|
||||||
|
|
||||||
|
if (monitorSettings != null)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"[Settings Update] Found monitor settings for Hardware ID '{monitorVm.HardwareId}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}");
|
||||||
|
|
||||||
|
// Update visibility flags based on Settings UI toggles
|
||||||
|
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
|
||||||
|
monitorVm.ShowContrast = monitorSettings.EnableContrast;
|
||||||
|
monitorVm.ShowVolume = monitorSettings.EnableVolume;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"[Settings Update] No monitor settings found for Hardware ID '{monitorVm.HardwareId}'");
|
||||||
|
Logger.LogInfo($"[Settings Update] Available monitors in settings:");
|
||||||
|
foreach (var availableMonitor in settings.Properties.Monitors)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($" - Hardware: '{availableMonitor.HardwareId}', Name: '{availableMonitor.Name}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger UI refresh for configuration changes
|
||||||
|
UIRefreshRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.LogInfo($"Settings UI configuration reloaded, monitor count: {settings.Properties.Monitors.Count}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to reload settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Error handling settings file change: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Safe wrapper for initializing color temperature asynchronously
|
||||||
|
/// </summary>
|
||||||
|
private async Task InitializeColorTemperatureSafeAsync(string monitorId, MonitorViewModel vm)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _monitorManager.InitializeColorTemperatureAsync(monitorId);
|
||||||
|
|
||||||
|
// Update UI on dispatcher thread - get the monitor from manager
|
||||||
|
var monitor = _monitorManager.GetMonitor(monitorId);
|
||||||
|
if (monitor != null)
|
||||||
|
{
|
||||||
|
_dispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
// Update color temperature without triggering hardware write
|
||||||
|
vm.UpdatePropertySilently(nameof(vm.ColorTemperature), monitor.CurrentColorTemperature);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// INotifyPropertyChanged
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reload monitor settings from configuration
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="colorTempInitTasks">Optional tasks for color temperature initialization to wait for</param>
|
||||||
|
public async Task ReloadMonitorSettingsAsync(List<Task>? colorTempInitTasks = null)
|
||||||
|
{
|
||||||
|
// Prevent duplicate calls
|
||||||
|
if (IsLoading)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[Startup] ReloadMonitorSettingsAsync already in progress, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Set loading state to block UI interactions
|
||||||
|
IsLoading = true;
|
||||||
|
StatusText = "Loading settings...";
|
||||||
|
|
||||||
|
// Read current settings
|
||||||
|
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||||
|
|
||||||
|
if (settings.Properties.RestoreSettingsOnStartup)
|
||||||
|
{
|
||||||
|
// Restore saved settings from configuration file
|
||||||
|
Logger.LogInfo("[Startup] RestoreSettingsOnStartup enabled - applying saved settings");
|
||||||
|
|
||||||
|
foreach (var monitorVm in Monitors)
|
||||||
|
{
|
||||||
|
var hardwareId = monitorVm.HardwareId;
|
||||||
|
Logger.LogInfo($"[Startup] Processing monitor: '{monitorVm.Name}', HardwareId: '{hardwareId}'");
|
||||||
|
|
||||||
|
// Find and apply corresponding saved settings from state file using stable HardwareId
|
||||||
|
var savedState = _stateManager.GetMonitorParameters(hardwareId);
|
||||||
|
if (savedState.HasValue)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"[Startup] Restoring state for HardwareId '{hardwareId}': Brightness={savedState.Value.Brightness}, ColorTemp={savedState.Value.ColorTemperature}");
|
||||||
|
|
||||||
|
// Validate and apply saved values (skip invalid values)
|
||||||
|
// Use UpdatePropertySilently to avoid triggering hardware updates during initialization
|
||||||
|
if (savedState.Value.Brightness >= monitorVm.MinBrightness && savedState.Value.Brightness <= monitorVm.MaxBrightness)
|
||||||
|
{
|
||||||
|
monitorVm.UpdatePropertySilently(nameof(monitorVm.Brightness), savedState.Value.Brightness);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color temperature must be valid and within range
|
||||||
|
if (savedState.Value.ColorTemperature > 0 &&
|
||||||
|
savedState.Value.ColorTemperature >= monitorVm.MinColorTemperature &&
|
||||||
|
savedState.Value.ColorTemperature <= monitorVm.MaxColorTemperature)
|
||||||
|
{
|
||||||
|
monitorVm.UpdatePropertySilently(nameof(monitorVm.ColorTemperature), savedState.Value.ColorTemperature);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"[Startup] Invalid color temperature value {savedState.Value.ColorTemperature} for HardwareId '{hardwareId}', skipping");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrast validation - only apply if hardware supports it
|
||||||
|
if (monitorVm.ShowContrast &&
|
||||||
|
savedState.Value.Contrast >= monitorVm.MinContrast &&
|
||||||
|
savedState.Value.Contrast <= monitorVm.MaxContrast)
|
||||||
|
{
|
||||||
|
monitorVm.UpdatePropertySilently(nameof(monitorVm.Contrast), savedState.Value.Contrast);
|
||||||
|
}
|
||||||
|
else if (!monitorVm.ShowContrast)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"[Startup] Contrast not supported on HardwareId '{hardwareId}', skipping");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volume validation - only apply if hardware supports it
|
||||||
|
if (monitorVm.ShowVolume &&
|
||||||
|
savedState.Value.Volume >= monitorVm.MinVolume &&
|
||||||
|
savedState.Value.Volume <= monitorVm.MaxVolume)
|
||||||
|
{
|
||||||
|
monitorVm.UpdatePropertySilently(nameof(monitorVm.Volume), savedState.Value.Volume);
|
||||||
|
}
|
||||||
|
else if (!monitorVm.ShowVolume)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"[Startup] Volume not supported on HardwareId '{hardwareId}', skipping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"[Startup] No saved state for HardwareId '{hardwareId}' - keeping current hardware values");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply feature visibility settings using HardwareId
|
||||||
|
ApplyFeatureVisibility(monitorVm, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusText = "Saved settings restored successfully";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Save current hardware values to configuration file
|
||||||
|
Logger.LogInfo("[Startup] RestoreSettingsOnStartup disabled - saving current hardware values");
|
||||||
|
|
||||||
|
// Wait for color temperature initialization to complete (if any)
|
||||||
|
if (colorTempInitTasks != null && colorTempInitTasks.Count > 0)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[Startup] Waiting for color temperature initialization to complete...");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.WhenAll(colorTempInitTasks);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"[Startup] Some color temperature initializations failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var monitorVm in Monitors)
|
||||||
|
{
|
||||||
|
// Save current hardware values to settings
|
||||||
|
SaveMonitorSettingDirect(monitorVm.HardwareId, "Brightness", monitorVm.Brightness);
|
||||||
|
SaveMonitorSettingDirect(monitorVm.HardwareId, "ColorTemperature", monitorVm.ColorTemperature);
|
||||||
|
SaveMonitorSettingDirect(monitorVm.HardwareId, "Contrast", monitorVm.Contrast);
|
||||||
|
SaveMonitorSettingDirect(monitorVm.HardwareId, "Volume", monitorVm.Volume);
|
||||||
|
|
||||||
|
Logger.LogInfo($"[Startup] Saved current values for Hardware ID '{monitorVm.HardwareId}': Brightness={monitorVm.Brightness}, ColorTemp={monitorVm.ColorTemperature}");
|
||||||
|
|
||||||
|
// Apply feature visibility settings
|
||||||
|
ApplyFeatureVisibility(monitorVm, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to flush - MonitorStateManager now saves directly!
|
||||||
|
StatusText = "Current monitor values saved to state file";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to reload/save settings: {ex.Message}");
|
||||||
|
StatusText = $"Failed to process settings: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Clear loading state to enable UI interactions
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply feature visibility settings to a monitor ViewModel
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
|
||||||
|
{
|
||||||
|
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
|
||||||
|
m.HardwareId == monitorVm.HardwareId);
|
||||||
|
|
||||||
|
if (monitorSettings != null)
|
||||||
|
{
|
||||||
|
Logger.LogInfo($"[Startup] Applying feature visibility for Hardware ID '{monitorVm.HardwareId}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}");
|
||||||
|
|
||||||
|
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
|
||||||
|
monitorVm.ShowContrast = monitorSettings.EnableContrast;
|
||||||
|
monitorVm.ShowVolume = monitorSettings.EnableVolume;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"[Startup] No feature settings found for Hardware ID '{monitorVm.HardwareId}' - using defaults");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safe save method that can be called from background threads.
|
||||||
|
/// Does not access UI collections or update UI properties.
|
||||||
|
/// </summary>
|
||||||
|
public void SaveMonitorSettingDirect(string hardwareId, string property, int value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// This is thread-safe - _stateManager has internal locking
|
||||||
|
// No UI thread operations, no ObservableCollection access
|
||||||
|
_stateManager.UpdateMonitorParameter(hardwareId, property, value);
|
||||||
|
|
||||||
|
Logger.LogTrace($"[State] Queued setting change for HardwareId '{hardwareId}': {property}={value}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Only log, don't update UI from background thread
|
||||||
|
Logger.LogError($"Failed to queue setting save for HardwareId '{hardwareId}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset a monitor to default values
|
||||||
|
/// </summary>
|
||||||
|
public void ResetMonitor(string monitorId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var monitorVm = GetMonitorViewModel(monitorId);
|
||||||
|
if (monitorVm != null)
|
||||||
|
{
|
||||||
|
// Apply default values
|
||||||
|
monitorVm.Brightness = 30;
|
||||||
|
monitorVm.ColorTemperature = 6500;
|
||||||
|
monitorVm.Contrast = 50;
|
||||||
|
monitorVm.Volume = 50;
|
||||||
|
|
||||||
|
StatusText = $"Monitor {monitorVm.Name} reset to default values";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusText = $"Failed to reset monitor: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send monitor information to Settings UI via IPC (using standard Model)
|
||||||
|
/// </summary>
|
||||||
|
private void SendMonitorInfoToSettingsUI()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Monitors.Count == 0)
|
||||||
|
{
|
||||||
|
Logger.LogInfo("[IPC] No monitors to send to Settings UI");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build monitor data list
|
||||||
|
var monitorsData = new List<MonitorInfoData>();
|
||||||
|
|
||||||
|
foreach (var vm in Monitors)
|
||||||
|
{
|
||||||
|
var monitorData = new MonitorInfoData
|
||||||
|
{
|
||||||
|
Name = vm.Name,
|
||||||
|
InternalName = vm.Id,
|
||||||
|
HardwareId = vm.HardwareId,
|
||||||
|
CommunicationMethod = GetCommunicationMethodString(vm.Type),
|
||||||
|
MonitorType = vm.Type.ToString(),
|
||||||
|
CurrentBrightness = vm.Brightness,
|
||||||
|
ColorTemperature = vm.ColorTemperature,
|
||||||
|
};
|
||||||
|
|
||||||
|
monitorsData.Add(monitorData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use standard IPC Response Model with JsonSerializer (source-generated for AOT)
|
||||||
|
var response = new PowerDisplayMonitorsIPCResponse(monitorsData);
|
||||||
|
string jsonMessage = System.Text.Json.JsonSerializer.Serialize(response, AppJsonContext.Default.PowerDisplayMonitorsIPCResponse);
|
||||||
|
|
||||||
|
// Send to Settings UI via IPC
|
||||||
|
App.SendIPCMessage(jsonMessage);
|
||||||
|
|
||||||
|
Logger.LogInfo($"[IPC] Sent {Monitors.Count} monitors to Settings UI");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"[IPC] Failed to send monitor info: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get communication method string based on monitor type
|
||||||
|
/// </summary>
|
||||||
|
private string GetCommunicationMethodString(MonitorType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
MonitorType.External => "DDC/CI",
|
||||||
|
MonitorType.Internal => "WMI",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDisposable
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Cancel all async operations first
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
// Stop file monitoring immediately
|
||||||
|
_settingsWatcher?.Dispose();
|
||||||
|
_settingsWatcher = null;
|
||||||
|
|
||||||
|
// No need to flush state - MonitorStateManager now saves directly on each update!
|
||||||
|
// State is already persisted, no pending changes to wait for.
|
||||||
|
|
||||||
|
// Quick cleanup of monitor view models
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var vm in Monitors)
|
||||||
|
{
|
||||||
|
vm?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitors.Clear();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* Ignore cleanup errors */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release monitor manager
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_monitorManager?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* Ignore cleanup errors */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release state manager
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stateManager?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* Ignore cleanup errors */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally release cancellation token
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* Ignore cleanup errors */
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ensure Dispose doesn't throw exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
// 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.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using ManagedCommon;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using PowerDisplay.Commands;
|
||||||
|
using PowerDisplay.Core;
|
||||||
|
using PowerDisplay.Core.Models;
|
||||||
|
using PowerDisplay.Helpers;
|
||||||
|
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||||
|
|
||||||
|
namespace PowerDisplay.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel for individual monitor
|
||||||
|
/// </summary>
|
||||||
|
public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||||
|
{
|
||||||
|
private readonly Monitor _monitor;
|
||||||
|
private readonly MonitorManager _monitorManager;
|
||||||
|
private readonly MainViewModel? _mainViewModel;
|
||||||
|
|
||||||
|
// Simple debouncers for each property (KISS principle - simpler than complex queue)
|
||||||
|
private readonly SimpleDebouncer _brightnessDebouncer = new(300);
|
||||||
|
private readonly SimpleDebouncer _colorTempDebouncer = new(300);
|
||||||
|
private readonly SimpleDebouncer _contrastDebouncer = new(300);
|
||||||
|
private readonly SimpleDebouncer _volumeDebouncer = new(300);
|
||||||
|
|
||||||
|
private int _brightness;
|
||||||
|
private int _colorTemperature;
|
||||||
|
private int _contrast;
|
||||||
|
private int _volume;
|
||||||
|
private bool _isAvailable;
|
||||||
|
|
||||||
|
// Visibility settings (controlled by Settings UI)
|
||||||
|
private bool _showColorTemperature;
|
||||||
|
private bool _showContrast;
|
||||||
|
private bool _showVolume;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a property value directly without triggering hardware updates.
|
||||||
|
/// Used during initialization to update UI from saved state.
|
||||||
|
/// </summary>
|
||||||
|
internal void UpdatePropertySilently(string propertyName, int value)
|
||||||
|
{
|
||||||
|
switch (propertyName)
|
||||||
|
{
|
||||||
|
case nameof(ColorTemperature):
|
||||||
|
_colorTemperature = value;
|
||||||
|
OnPropertyChanged(nameof(ColorTemperature));
|
||||||
|
OnPropertyChanged(nameof(ColorTemperaturePercent));
|
||||||
|
break;
|
||||||
|
case nameof(Brightness):
|
||||||
|
_brightness = value;
|
||||||
|
OnPropertyChanged(nameof(Brightness));
|
||||||
|
break;
|
||||||
|
case nameof(Contrast):
|
||||||
|
_contrast = value;
|
||||||
|
OnPropertyChanged(nameof(Contrast));
|
||||||
|
OnPropertyChanged(nameof(ContrastPercent));
|
||||||
|
break;
|
||||||
|
case nameof(Volume):
|
||||||
|
_volume = value;
|
||||||
|
OnPropertyChanged(nameof(Volume));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion function for x:Bind (AOT-compatible alternative to converters)
|
||||||
|
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
// Property to access IsInteractionEnabled from parent ViewModel
|
||||||
|
public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
|
||||||
|
|
||||||
|
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
|
||||||
|
{
|
||||||
|
_monitor = monitor;
|
||||||
|
_monitorManager = monitorManager;
|
||||||
|
_mainViewModel = mainViewModel;
|
||||||
|
|
||||||
|
// Subscribe to MainViewModel property changes to update IsInteractionEnabled
|
||||||
|
if (_mainViewModel != null)
|
||||||
|
{
|
||||||
|
_mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Show properties based on hardware capabilities
|
||||||
|
_showColorTemperature = monitor.SupportsColorTemperature; // Only show for DDC/CI monitors that support it
|
||||||
|
_showContrast = monitor.SupportsContrast;
|
||||||
|
_showVolume = monitor.SupportsVolume;
|
||||||
|
|
||||||
|
// Try to get current color temperature via DDC/CI, use default if failed
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// For DDC/CI monitors that support color temperature, use 6500K as default
|
||||||
|
// The actual temperature will be loaded asynchronously after construction
|
||||||
|
if (monitor.SupportsColorTemperature)
|
||||||
|
{
|
||||||
|
_colorTemperature = 6500; // Default neutral temperature for DDC monitors
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_colorTemperature = 6500; // Default for unsupported monitors
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.CurrentColorTemperature = _colorTemperature;
|
||||||
|
Logger.LogDebug($"Initialized {monitor.Id} with default color temperature {_colorTemperature}K");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to initialize color temperature for {monitor.Id}: {ex.Message}");
|
||||||
|
_colorTemperature = 6500; // Default neutral temperature
|
||||||
|
monitor.CurrentColorTemperature = 6500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize basic properties from monitor
|
||||||
|
_brightness = monitor.CurrentBrightness;
|
||||||
|
_contrast = monitor.CurrentContrast;
|
||||||
|
_volume = monitor.CurrentVolume;
|
||||||
|
_isAvailable = monitor.IsAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Id => _monitor.Id;
|
||||||
|
|
||||||
|
public string HardwareId => _monitor.HardwareId;
|
||||||
|
|
||||||
|
public string Name => _monitor.Name;
|
||||||
|
|
||||||
|
public string Manufacturer => _monitor.Manufacturer;
|
||||||
|
|
||||||
|
public MonitorType Type => _monitor.Type;
|
||||||
|
|
||||||
|
public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the icon glyph based on monitor type
|
||||||
|
/// </summary>
|
||||||
|
public string MonitorIconGlyph => Type == MonitorType.Internal ? "\uEA37" : "\uE7F4";
|
||||||
|
|
||||||
|
// Monitor property ranges
|
||||||
|
public int MinBrightness => _monitor.MinBrightness;
|
||||||
|
|
||||||
|
public int MaxBrightness => _monitor.MaxBrightness;
|
||||||
|
|
||||||
|
public int MinColorTemperature => _monitor.MinColorTemperature;
|
||||||
|
|
||||||
|
public int MaxColorTemperature => _monitor.MaxColorTemperature;
|
||||||
|
|
||||||
|
public int MinContrast => _monitor.MinContrast;
|
||||||
|
|
||||||
|
public int MaxContrast => _monitor.MaxContrast;
|
||||||
|
|
||||||
|
public int MinVolume => _monitor.MinVolume;
|
||||||
|
|
||||||
|
public int MaxVolume => _monitor.MaxVolume;
|
||||||
|
|
||||||
|
// Advanced control display logic
|
||||||
|
public bool HasAdvancedControls => ShowColorTemperature || ShowContrast || ShowVolume;
|
||||||
|
|
||||||
|
public bool ShowColorTemperature
|
||||||
|
{
|
||||||
|
get => _showColorTemperature;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_showColorTemperature != value)
|
||||||
|
{
|
||||||
|
_showColorTemperature = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(HasAdvancedControls));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 int Brightness
|
||||||
|
{
|
||||||
|
get => _brightness;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_brightness != value)
|
||||||
|
{
|
||||||
|
// Update UI state immediately - keep slider smooth
|
||||||
|
_brightness = value;
|
||||||
|
OnPropertyChanged(); // UI responds immediately
|
||||||
|
|
||||||
|
// Debounce hardware update - much simpler than complex queue!
|
||||||
|
var capturedValue = value; // Capture value for async closure
|
||||||
|
_brightnessDebouncer.Debounce(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _monitorManager.SetBrightnessAsync(Id, capturedValue);
|
||||||
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Brightness", capturedValue);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to set brightness for {Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ColorTemperature
|
||||||
|
{
|
||||||
|
get => _colorTemperature;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_colorTemperature != value)
|
||||||
|
{
|
||||||
|
_colorTemperature = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
// Debounce hardware update - simple and clean!
|
||||||
|
var capturedValue = value;
|
||||||
|
_colorTempDebouncer.Debounce(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _monitorManager.SetColorTemperatureAsync(Id, capturedValue);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
_monitor.CurrentColorTemperature = capturedValue;
|
||||||
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "ColorTemperature", capturedValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogError($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to set color temperature for {Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Contrast
|
||||||
|
{
|
||||||
|
get => _contrast;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_contrast != value)
|
||||||
|
{
|
||||||
|
_contrast = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
// Debounce hardware update
|
||||||
|
var capturedValue = value;
|
||||||
|
_contrastDebouncer.Debounce(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _monitorManager.SetContrastAsync(Id, capturedValue);
|
||||||
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Contrast", capturedValue);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to set contrast for {Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Volume
|
||||||
|
{
|
||||||
|
get => _volume;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_volume != value)
|
||||||
|
{
|
||||||
|
_volume = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
|
||||||
|
// Debounce hardware update
|
||||||
|
var capturedValue = value;
|
||||||
|
_volumeDebouncer.Debounce(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _monitorManager.SetVolumeAsync(Id, capturedValue);
|
||||||
|
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Volume", capturedValue);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"Failed to set volume for {Id}: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAvailable
|
||||||
|
{
|
||||||
|
get => _isAvailable;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_isAvailable = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand SetBrightnessCommand => new RelayCommand<int?>((brightness) =>
|
||||||
|
{
|
||||||
|
if (brightness.HasValue)
|
||||||
|
{
|
||||||
|
Brightness = brightness.Value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public ICommand SetColorTemperatureCommand => new RelayCommand<int?>((temperature) =>
|
||||||
|
{
|
||||||
|
if (temperature.HasValue && _monitor.SupportsColorTemperature)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"[{Id}] Color temperature command: {temperature.Value}K (DDC/CI)");
|
||||||
|
ColorTemperature = temperature.Value;
|
||||||
|
}
|
||||||
|
else if (temperature.HasValue && !_monitor.SupportsColorTemperature)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"[{Id}] Color temperature not supported on this monitor");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public ICommand SetContrastCommand => new RelayCommand<int?>((contrast) =>
|
||||||
|
{
|
||||||
|
if (contrast.HasValue)
|
||||||
|
{
|
||||||
|
Contrast = contrast.Value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public ICommand SetVolumeCommand => new RelayCommand<int?>((volume) =>
|
||||||
|
{
|
||||||
|
if (volume.HasValue)
|
||||||
|
{
|
||||||
|
Volume = volume.Value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Percentage-based properties for uniform slider behavior
|
||||||
|
public int ColorTemperaturePercent
|
||||||
|
{
|
||||||
|
get => MapToPercent(_colorTemperature, MinColorTemperature, MaxColorTemperature);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var actualValue = MapFromPercent(value, MinColorTemperature, MaxColorTemperature);
|
||||||
|
ColorTemperature = actualValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ContrastPercent
|
||||||
|
{
|
||||||
|
get => MapToPercent(_contrast, MinContrast, MaxContrast);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var actualValue = MapFromPercent(value, MinContrast, MaxContrast);
|
||||||
|
Contrast = actualValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping functions for percentage conversion
|
||||||
|
private int MapToPercent(int value, int min, int max)
|
||||||
|
{
|
||||||
|
if (max <= min)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)Math.Round((value - min) * 100.0 / (max - min));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int MapFromPercent(int percent, int min, int max)
|
||||||
|
{
|
||||||
|
if (max <= min)
|
||||||
|
{
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
percent = Math.Clamp(percent, 0, 100);
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Notify percentage properties when actual values change
|
||||||
|
if (propertyName == nameof(ColorTemperature))
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ColorTemperaturePercent)));
|
||||||
|
}
|
||||||
|
else if (propertyName == nameof(Contrast))
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(IsInteractionEnabled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Unsubscribe from MainViewModel events
|
||||||
|
if (_mainViewModel != null)
|
||||||
|
{
|
||||||
|
_mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose all debouncers
|
||||||
|
_brightnessDebouncer?.Dispose();
|
||||||
|
_colorTempDebouncer?.Dispose();
|
||||||
|
_contrastDebouncer?.Dispose();
|
||||||
|
_volumeDebouncer?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace PowerDisplay.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the current state of a ViewModel
|
||||||
|
/// </summary>
|
||||||
|
public enum ViewModelState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initial state - ViewModel is being initialized
|
||||||
|
/// </summary>
|
||||||
|
Initializing,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loading state - data is being reloaded or refreshed
|
||||||
|
/// </summary>
|
||||||
|
Loading,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ready state - ViewModel is ready for user interaction
|
||||||
|
/// </summary>
|
||||||
|
Ready,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Error state - ViewModel encountered an error
|
||||||
|
/// </summary>
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/modules/powerdisplay/PowerDisplay/app.manifest
Normal file
32
src/modules/powerdisplay/PowerDisplay/app.manifest
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="PowerDisplay.app"/>
|
||||||
|
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
<!-- Windows 11 -->
|
||||||
|
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<!-- The combination of below two tags have the following effect:
|
||||||
|
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||||
|
2) System < Windows 10 Anniversary Update
|
||||||
|
-->
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace PowerDisplayConstants
|
||||||
|
{
|
||||||
|
// Name of the powertoy module.
|
||||||
|
inline const std::wstring ModuleKey = L"PowerDisplay";
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Microsoft Visual C++ generated resource script.
|
||||||
|
//
|
||||||
|
#include <windows.h>
|
||||||
|
#include "resource.h"
|
||||||
|
#include "../../../common/version/version.h"
|
||||||
|
|
||||||
|
#define APSTUDIO_READONLY_SYMBOLS
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 2 resource.
|
||||||
|
//
|
||||||
|
#include "winres.h"
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
1 VERSIONINFO
|
||||||
|
FILEVERSION FILE_VERSION
|
||||||
|
PRODUCTVERSION PRODUCT_VERSION
|
||||||
|
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS VS_FF_DEBUG
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS VOS_NT_WINDOWS32
|
||||||
|
FILETYPE VFT_DLL
|
||||||
|
FILESUBTYPE VFT2_UNKNOWN
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", COMPANY_NAME
|
||||||
|
VALUE "FileDescription", FILE_DESCRIPTION
|
||||||
|
VALUE "FileVersion", FILE_VERSION_STRING
|
||||||
|
VALUE "InternalName", INTERNAL_NAME
|
||||||
|
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||||
|
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||||
|
VALUE "ProductName", PRODUCT_NAME
|
||||||
|
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
#pragma code_page(1252)
|
||||||
|
|
||||||
|
#ifdef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// TEXTINCLUDE
|
||||||
|
//
|
||||||
|
|
||||||
|
1 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"resource.h\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
2 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"#include ""winres.h""\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
3 TEXTINCLUDE
|
||||||
|
BEGIN
|
||||||
|
"\r\n"
|
||||||
|
"\0"
|
||||||
|
END
|
||||||
|
|
||||||
|
#endif // APSTUDIO_INVOKED
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef APSTUDIO_INVOKED
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Generated from the TEXTINCLUDE 3 resource.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
#endif // not APSTUDIO_INVOKED
|
||||||
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||||
|
<ItemGroup Label="ProjectConfigurations">
|
||||||
|
<ProjectConfiguration Include="Debug|ARM64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>ARM64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|ARM64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>ARM64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Debug|x64">
|
||||||
|
<Configuration>Debug</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
<ProjectConfiguration Include="Release|x64">
|
||||||
|
<Configuration>Release</Configuration>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
</ProjectConfiguration>
|
||||||
|
</ItemGroup>
|
||||||
|
<PropertyGroup Label="Globals">
|
||||||
|
<VCProjectVersion>16.0</VCProjectVersion>
|
||||||
|
<ProjectGuid>{D1234567-8901-2345-6789-ABCDEF012345}</ProjectGuid>
|
||||||
|
<Keyword>Win32Proj</Keyword>
|
||||||
|
<RootNamespace>PowerDisplayModuleInterface</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||||
|
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||||
|
<UseDebugLibraries>true</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||||
|
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||||
|
<UseDebugLibraries>false</UseDebugLibraries>
|
||||||
|
<PlatformToolset>v143</PlatformToolset>
|
||||||
|
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||||
|
<CharacterSet>Unicode</CharacterSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||||
|
<ImportGroup Label="ExtensionSettings">
|
||||||
|
</ImportGroup>
|
||||||
|
<ImportGroup Label="Shared">
|
||||||
|
</ImportGroup>
|
||||||
|
<ImportGroup>
|
||||||
|
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||||
|
</ImportGroup>
|
||||||
|
<PropertyGroup Label="UserMacros" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\modules\PowerDisplay\</OutDir>
|
||||||
|
<IntDir>$(Platform)\$(Configuration)\PowerDisplayModuleInterface\</IntDir>
|
||||||
|
<TargetName>PowerToys.PowerDisplayModuleInterface</TargetName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level3</WarningLevel>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<PreprocessorDefinitions>_DEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Windows</SubSystem>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<EnableUAC>false</EnableUAC>
|
||||||
|
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||||
|
<ClCompile>
|
||||||
|
<WarningLevel>Level3</WarningLevel>
|
||||||
|
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||||
|
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||||
|
<SDLCheck>true</SDLCheck>
|
||||||
|
<PreprocessorDefinitions>NDEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||||
|
<ConformanceMode>true</ConformanceMode>
|
||||||
|
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||||
|
</ClCompile>
|
||||||
|
<Link>
|
||||||
|
<SubSystem>Windows</SubSystem>
|
||||||
|
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||||
|
<OptimizeReferences>true</OptimizeReferences>
|
||||||
|
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||||
|
<EnableUAC>false</EnableUAC>
|
||||||
|
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||||
|
</Link>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="Constants.h" />
|
||||||
|
<ClInclude Include="pch.h" />
|
||||||
|
<ClInclude Include="PowerDisplayProcessManager.h" />
|
||||||
|
<ClInclude Include="resource.h" />
|
||||||
|
<ClInclude Include="trace.h" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="dllmain.cpp" />
|
||||||
|
<ClCompile Include="pch.cpp">
|
||||||
|
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="PowerDisplayProcessManager.cpp" />
|
||||||
|
<ClCompile Include="trace.cpp" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||||
|
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||||
|
</ProjectReference>
|
||||||
|
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||||
|
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ResourceCompile Include="PowerDisplayModuleInterface.rc" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||||
|
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||||
|
<ImportGroup Label="ExtensionTargets">
|
||||||
|
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||||
|
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||||
|
</ImportGroup>
|
||||||
|
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||||
|
<PropertyGroup>
|
||||||
|
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||||
|
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||||
|
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||||
|
</Target>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<ItemGroup>
|
||||||
|
<Filter Include="Source Files">
|
||||||
|
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||||
|
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||||
|
</Filter>
|
||||||
|
<Filter Include="Header Files">
|
||||||
|
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||||
|
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||||
|
</Filter>
|
||||||
|
<Filter Include="Resource Files">
|
||||||
|
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||||
|
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||||
|
</Filter>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClInclude Include="pch.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="Constants.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="Trace.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="resource.h">
|
||||||
|
<Filter>Header Files</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ClCompile Include="dllmain.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="pch.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="Trace.cpp">
|
||||||
|
<Filter>Source Files</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ResourceCompile Include="RegistryPreviewExt.rc">
|
||||||
|
<Filter>Resource Files</Filter>
|
||||||
|
</ResourceCompile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#include "pch.h"
|
||||||
|
#include "PowerDisplayProcessManager.h"
|
||||||
|
|
||||||
|
#include <common/logger/logger.h>
|
||||||
|
#include <common/utils/winapi_error.h>
|
||||||
|
#include <common/interop/shared_constants.h>
|
||||||
|
#include <atlstr.h>
|
||||||
|
#include <format>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a pipe name with UUID suffix
|
||||||
|
/// </summary>
|
||||||
|
std::optional<std::wstring> get_pipe_uuid()
|
||||||
|
{
|
||||||
|
UUID temp_uuid;
|
||||||
|
wchar_t* uuid_chars = nullptr;
|
||||||
|
|
||||||
|
if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
|
||||||
|
{
|
||||||
|
const auto val = get_last_error_message(GetLastError());
|
||||||
|
Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
else if (UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) != RPC_S_OK)
|
||||||
|
{
|
||||||
|
const auto val = get_last_error_message(GetLastError());
|
||||||
|
Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto uuid_str = std::wstring(uuid_chars);
|
||||||
|
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
|
||||||
|
|
||||||
|
return uuid_str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PowerDisplayProcessManager::~PowerDisplayProcessManager()
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerDisplayProcessManager::start()
|
||||||
|
{
|
||||||
|
m_enabled = true;
|
||||||
|
submit_task([this]() { refresh(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerDisplayProcessManager::stop()
|
||||||
|
{
|
||||||
|
m_enabled = false;
|
||||||
|
submit_task([this]() { refresh(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerDisplayProcessManager::send_message_to_powerdisplay(const std::wstring& message)
|
||||||
|
{
|
||||||
|
submit_task([this, message]() {
|
||||||
|
if (m_write_pipe)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const auto formatted = std::format(L"{}\r\n", message);
|
||||||
|
const CString msg(formatted.c_str());
|
||||||
|
const DWORD bytes_to_write = static_cast<DWORD>(msg.GetLength() * sizeof(TCHAR));
|
||||||
|
|
||||||
|
DWORD bytes_written = 0;
|
||||||
|
if (FAILED(m_write_pipe->Write(msg.GetString(), bytes_to_write, &bytes_written)))
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to write message to PowerDisplay pipe");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::trace(L"Sent message to PowerDisplay: {}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
Logger::error(L"Exception while sending message to PowerDisplay");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::warn(L"Cannot send message to PowerDisplay: pipe not connected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerDisplayProcessManager::submit_task(std::function<void()> task)
|
||||||
|
{
|
||||||
|
m_thread_executor.submit(OnThreadExecutor::task_t{ task });
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PowerDisplayProcessManager::is_process_running() const
|
||||||
|
{
|
||||||
|
return m_hProcess != nullptr && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerDisplayProcessManager::terminate_process()
|
||||||
|
{
|
||||||
|
// Close pipe
|
||||||
|
m_write_pipe.reset();
|
||||||
|
|
||||||
|
// Terminate process
|
||||||
|
if (m_hProcess != nullptr)
|
||||||
|
{
|
||||||
|
TerminateProcess(m_hProcess, 1);
|
||||||
|
CloseHandle(m_hProcess);
|
||||||
|
m_hProcess = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::trace(L"PowerDisplay process terminated");
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_uuid)
|
||||||
|
{
|
||||||
|
const unsigned long powertoys_pid = GetCurrentProcessId();
|
||||||
|
|
||||||
|
// Pass both runner PID and pipe UUID to PowerDisplay.exe
|
||||||
|
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_uuid);
|
||||||
|
|
||||||
|
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||||
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
||||||
|
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
|
||||||
|
sei.nShow = SW_SHOWNORMAL;
|
||||||
|
sei.lpParameters = executable_args.data();
|
||||||
|
|
||||||
|
if (ShellExecuteExW(&sei))
|
||||||
|
{
|
||||||
|
Logger::trace(L"Successfully started PowerDisplay process with UUID: {}", pipe_uuid);
|
||||||
|
terminate_process(); // Clean up old process if any
|
||||||
|
m_hProcess = sei.hProcess;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::error(L"PowerDisplay process failed to start. {}", get_last_error_or_default(GetLastError()));
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT PowerDisplayProcessManager::start_command_pipe(const std::wstring& pipe_uuid)
|
||||||
|
{
|
||||||
|
const constexpr DWORD BUFSIZE = 4096 * 4;
|
||||||
|
|
||||||
|
// Create pipe for writing to PowerDisplay (OUT)
|
||||||
|
m_pipe_name_out = std::format(L"\\\\.\\pipe\\powertoys_powerdisplay_{}_out", pipe_uuid);
|
||||||
|
|
||||||
|
HANDLE hWritePipe = CreateNamedPipe(
|
||||||
|
m_pipe_name_out.c_str(),
|
||||||
|
PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
|
||||||
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||||
|
1, // max instances
|
||||||
|
BUFSIZE, // out buffer size
|
||||||
|
0, // in buffer size (not used for outbound)
|
||||||
|
0, // client timeout
|
||||||
|
NULL // default security
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hWritePipe == NULL || hWritePipe == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
Logger::error(L"Error creating write pipe for PowerDisplay");
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create overlapped event for waiting for client to connect
|
||||||
|
OVERLAPPED write_overlapped = { 0 };
|
||||||
|
write_overlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||||
|
|
||||||
|
if (!write_overlapped.hEvent)
|
||||||
|
{
|
||||||
|
Logger::error(L"Error creating overlapped event for PowerDisplay pipe");
|
||||||
|
CloseHandle(hWritePipe);
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect write pipe
|
||||||
|
if (!ConnectNamedPipe(hWritePipe, &write_overlapped))
|
||||||
|
{
|
||||||
|
const auto lastError = GetLastError();
|
||||||
|
if (lastError != ERROR_IO_PENDING && lastError != ERROR_PIPE_CONNECTED)
|
||||||
|
{
|
||||||
|
Logger::error(L"Error connecting to write pipe");
|
||||||
|
CloseHandle(write_overlapped.hEvent);
|
||||||
|
CloseHandle(hWritePipe);
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for pipe to connect (with timeout)
|
||||||
|
const constexpr DWORD client_timeout_millis = 5000;
|
||||||
|
DWORD wait_result = WaitForSingleObject(write_overlapped.hEvent, client_timeout_millis);
|
||||||
|
|
||||||
|
if (wait_result == WAIT_OBJECT_0)
|
||||||
|
{
|
||||||
|
// Pipe connected successfully
|
||||||
|
m_write_pipe = std::make_unique<CAtlFile>(hWritePipe);
|
||||||
|
CloseHandle(write_overlapped.hEvent);
|
||||||
|
|
||||||
|
Logger::trace(L"PowerDisplay command pipe connected successfully");
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::error(L"Timeout waiting for PowerDisplay to connect to command pipe");
|
||||||
|
CloseHandle(write_overlapped.hEvent);
|
||||||
|
CloseHandle(hWritePipe);
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerDisplayProcessManager::refresh()
|
||||||
|
{
|
||||||
|
if (m_enabled == is_process_running())
|
||||||
|
{
|
||||||
|
// Already in correct state
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_enabled)
|
||||||
|
{
|
||||||
|
// Start PowerDisplay process
|
||||||
|
Logger::trace(L"Starting PowerDisplay process");
|
||||||
|
|
||||||
|
const auto pipe_uuid = get_pipe_uuid();
|
||||||
|
if (!pipe_uuid)
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to generate pipe UUID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_command_pipe(pipe_uuid.value()) != S_OK)
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to initialize command pipe");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start_process(pipe_uuid.value()) != S_OK)
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to start PowerDisplay process, cleaning up pipes");
|
||||||
|
terminate_process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Stop PowerDisplay process
|
||||||
|
Logger::trace(L"Stopping PowerDisplay process");
|
||||||
|
|
||||||
|
// Send terminate message
|
||||||
|
send_message_to_powerdisplay(L"{\"action\":\"terminate\"}");
|
||||||
|
|
||||||
|
// Wait for graceful exit
|
||||||
|
if (m_hProcess != nullptr)
|
||||||
|
{
|
||||||
|
WaitForSingleObject(m_hProcess, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_process_running())
|
||||||
|
{
|
||||||
|
Logger::warn(L"PowerDisplay process failed to gracefully exit, terminating");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::trace(L"PowerDisplay process successfully exited");
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate_process();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <atlfile.h>
|
||||||
|
#include <common/utils/OnThreadExecutor.h>
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages PowerDisplay.exe process lifecycle and bidirectional IPC communication
|
||||||
|
/// </summary>
|
||||||
|
class PowerDisplayProcessManager
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
HANDLE m_hProcess = nullptr;
|
||||||
|
std::unique_ptr<CAtlFile> m_write_pipe; // Write to PowerDisplay (OUT)
|
||||||
|
OnThreadExecutor m_thread_executor;
|
||||||
|
bool m_enabled = false;
|
||||||
|
|
||||||
|
// Pipe name for this session
|
||||||
|
std::wstring m_pipe_name_out;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PowerDisplayProcessManager() = default;
|
||||||
|
~PowerDisplayProcessManager();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start PowerDisplay.exe process
|
||||||
|
/// </summary>
|
||||||
|
void start();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop PowerDisplay.exe process
|
||||||
|
/// </summary>
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send message to PowerDisplay.exe
|
||||||
|
/// </summary>
|
||||||
|
void send_message_to_powerdisplay(const std::wstring& message);
|
||||||
|
|
||||||
|
private:
|
||||||
|
/// <summary>
|
||||||
|
/// Submit task to thread executor
|
||||||
|
/// </summary>
|
||||||
|
void submit_task(std::function<void()> task);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if PowerDisplay.exe is running
|
||||||
|
/// </summary>
|
||||||
|
bool is_process_running() const;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Terminate PowerDisplay.exe process
|
||||||
|
/// </summary>
|
||||||
|
void terminate_process();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start PowerDisplay.exe with command line arguments
|
||||||
|
/// </summary>
|
||||||
|
HRESULT start_process(const std::wstring& pipe_uuid);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create named pipe for sending commands to PowerDisplay
|
||||||
|
/// </summary>
|
||||||
|
HRESULT start_command_pipe(const std::wstring& pipe_uuid);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh - start or stop process based on m_enabled state
|
||||||
|
/// </summary>
|
||||||
|
void refresh();
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
#include "trace.h"
|
||||||
|
|
||||||
|
#include <common/Telemetry/TraceBase.h>
|
||||||
|
|
||||||
|
TRACELOGGING_DEFINE_PROVIDER(
|
||||||
|
g_hProvider,
|
||||||
|
"Microsoft.PowerToys",
|
||||||
|
// {38e8889b-9731-53f5-e901-e8a7c1753074}
|
||||||
|
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
|
||||||
|
TraceLoggingOptionProjectTelemetry());
|
||||||
|
|
||||||
|
// Log if the user has enabled or disabled the app
|
||||||
|
void Trace::EnablePowerDisplay(_In_ bool enabled) noexcept
|
||||||
|
{
|
||||||
|
TraceLoggingWriteWrapper(
|
||||||
|
g_hProvider,
|
||||||
|
"PowerDisplay_EnablePowerDisplay",
|
||||||
|
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||||
|
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
|
||||||
|
TraceLoggingBoolean(enabled, "Enabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log that the user tried to activate the app
|
||||||
|
void Trace::ActivatePowerDisplay() noexcept
|
||||||
|
{
|
||||||
|
TraceLoggingWriteWrapper(
|
||||||
|
g_hProvider,
|
||||||
|
"PowerDisplay_Activate",
|
||||||
|
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||||
|
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||||
|
}
|
||||||
13
src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h
Normal file
13
src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <common/Telemetry/TraceBase.h>
|
||||||
|
|
||||||
|
class Trace : public telemetry::TraceBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Log if the user has enabled or disabled the app
|
||||||
|
static void EnablePowerDisplay(const bool enabled) noexcept;
|
||||||
|
|
||||||
|
// Log that the user tried to activate the app
|
||||||
|
static void ActivatePowerDisplay() noexcept;
|
||||||
|
};
|
||||||
245
src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
Normal file
245
src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
// dllmain.cpp : Defines the entry point for the DLL Application.
|
||||||
|
#include "pch.h"
|
||||||
|
#include <interface/powertoy_module_interface.h>
|
||||||
|
#include <common/SettingsAPI/settings_objects.h>
|
||||||
|
#include "trace.h"
|
||||||
|
#include <common/interop/shared_constants.h>
|
||||||
|
#include <common/utils/string_utils.h>
|
||||||
|
#include <common/utils/winapi_error.h>
|
||||||
|
#include <common/utils/logger_helper.h>
|
||||||
|
#include <common/utils/resources.h>
|
||||||
|
|
||||||
|
#include "resource.h"
|
||||||
|
#include "Constants.h"
|
||||||
|
#include "PowerDisplayProcessManager.h"
|
||||||
|
|
||||||
|
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||||
|
|
||||||
|
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||||
|
{
|
||||||
|
switch (ul_reason_for_call)
|
||||||
|
{
|
||||||
|
case DLL_PROCESS_ATTACH:
|
||||||
|
Trace::RegisterProvider();
|
||||||
|
break;
|
||||||
|
case DLL_THREAD_ATTACH:
|
||||||
|
case DLL_THREAD_DETACH:
|
||||||
|
break;
|
||||||
|
case DLL_PROCESS_DETACH:
|
||||||
|
Trace::UnregisterProvider();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const static wchar_t* MODULE_NAME = L"PowerDisplay";
|
||||||
|
const static wchar_t* MODULE_DESC = L"A utility to manage display brightness and color temperature across multiple monitors.";
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
|
||||||
|
const wchar_t JSON_KEY_ENABLED[] = L"enabled";
|
||||||
|
const wchar_t JSON_KEY_HOTKEY_ENABLED[] = L"hotkey_enabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
class PowerDisplayModule : public PowertoyModuleIface
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
bool m_enabled = false;
|
||||||
|
bool m_hotkey_enabled = false;
|
||||||
|
|
||||||
|
// Process manager for handling PowerDisplay.exe lifecycle and IPC
|
||||||
|
PowerDisplayProcessManager m_process_manager;
|
||||||
|
|
||||||
|
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
|
||||||
|
{
|
||||||
|
auto settingsObject = settings.get_raw_json();
|
||||||
|
if (settingsObject.GetView().Size())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
|
||||||
|
{
|
||||||
|
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||||
|
m_hotkey_enabled = properties.GetNamedBoolean(JSON_KEY_HOTKEY_ENABLED, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::info("Properties object not found in settings, using defaults");
|
||||||
|
m_hotkey_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
Logger::info("Failed to parse hotkey settings, using defaults");
|
||||||
|
m_hotkey_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::info("Power Display settings are empty");
|
||||||
|
m_hotkey_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_settings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PowerToysSettings::PowerToyValues settings =
|
||||||
|
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
|
||||||
|
|
||||||
|
parse_hotkey_settings(settings);
|
||||||
|
}
|
||||||
|
catch (std::exception&)
|
||||||
|
{
|
||||||
|
Logger::error("Invalid json when trying to load the Power Display settings json from file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
PowerDisplayModule()
|
||||||
|
{
|
||||||
|
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "PowerDisplay");
|
||||||
|
Logger::info("Power Display object is constructing");
|
||||||
|
|
||||||
|
init_settings();
|
||||||
|
|
||||||
|
// Note: PowerDisplay.exe will send messages directly to runner via named pipes
|
||||||
|
// The runner's message_receiver_thread will handle routing to Settings UI
|
||||||
|
// No need to set a callback here - the process manager just manages lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
|
~PowerDisplayModule()
|
||||||
|
{
|
||||||
|
if (m_enabled)
|
||||||
|
{
|
||||||
|
m_process_manager.stop();
|
||||||
|
}
|
||||||
|
m_enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void destroy() override
|
||||||
|
{
|
||||||
|
Logger::trace("PowerDisplay::destroy()");
|
||||||
|
if (m_enabled)
|
||||||
|
{
|
||||||
|
m_process_manager.stop();
|
||||||
|
}
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual const wchar_t* get_name() override
|
||||||
|
{
|
||||||
|
return MODULE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual const wchar_t* get_key() override
|
||||||
|
{
|
||||||
|
return MODULE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
||||||
|
{
|
||||||
|
return powertoys_gpo::gpo_rule_configured_not_configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||||
|
{
|
||||||
|
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||||
|
|
||||||
|
PowerToysSettings::Settings settings(hinstance, get_name());
|
||||||
|
settings.set_description(MODULE_DESC);
|
||||||
|
|
||||||
|
return settings.serialize_to_buffer(buffer, buffer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void call_custom_action(const wchar_t* action) override
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PowerToysSettings::CustomActionObject action_object =
|
||||||
|
PowerToysSettings::CustomActionObject::from_json_string(action);
|
||||||
|
|
||||||
|
if (action_object.get_name() == L"Launch")
|
||||||
|
{
|
||||||
|
Logger::trace(L"Launch action received, sending show_window command");
|
||||||
|
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"show_window\"}");
|
||||||
|
Trace::ActivatePowerDisplay();
|
||||||
|
}
|
||||||
|
else if (action_object.get_name() == L"RefreshMonitors")
|
||||||
|
{
|
||||||
|
Logger::trace(L"RefreshMonitors action received");
|
||||||
|
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"refresh_monitors\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::exception&)
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to parse action. {}", action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void set_config(const wchar_t* config) override
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PowerToysSettings::PowerToyValues values =
|
||||||
|
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
|
||||||
|
|
||||||
|
parse_hotkey_settings(values);
|
||||||
|
values.save_to_settings_file();
|
||||||
|
|
||||||
|
// Notify PowerDisplay.exe that settings have been updated
|
||||||
|
auto message = std::format(L"{{\"action\":\"settings_updated\",\"config\":{}}}", config);
|
||||||
|
m_process_manager.send_message_to_powerdisplay(message);
|
||||||
|
}
|
||||||
|
catch (std::exception&)
|
||||||
|
{
|
||||||
|
Logger::error(L"Invalid json when trying to parse Power Display settings json.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void enable() override
|
||||||
|
{
|
||||||
|
m_enabled = true;
|
||||||
|
Trace::EnablePowerDisplay(true);
|
||||||
|
|
||||||
|
Logger::trace(L"PowerDisplay enabled, starting process manager");
|
||||||
|
m_process_manager.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void disable() override
|
||||||
|
{
|
||||||
|
if (m_enabled)
|
||||||
|
{
|
||||||
|
Logger::trace(L"Disabling Power Display...");
|
||||||
|
m_process_manager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_enabled = false;
|
||||||
|
Trace::EnablePowerDisplay(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool is_enabled() override
|
||||||
|
{
|
||||||
|
return m_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool on_hotkey(size_t /*hotkeyId*/) override
|
||||||
|
{
|
||||||
|
if (m_enabled)
|
||||||
|
{
|
||||||
|
Logger::trace(L"Power Display hotkey pressed");
|
||||||
|
// Send toggle window command
|
||||||
|
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"toggle_window\"}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||||
|
{
|
||||||
|
return new PowerDisplayModule();
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||||
|
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||||
|
</packages>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "pch.h"
|
||||||
15
src/modules/powerdisplay/PowerDisplayModuleInterface/pch.h
Normal file
15
src/modules/powerdisplay/PowerDisplayModuleInterface/pch.h
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <windows.h>
|
||||||
|
//#include <winrt/Windows.Foundation.h>
|
||||||
|
#include <strsafe.h>
|
||||||
|
#include <hIdUsage.h>
|
||||||
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <winrt/Windows.Foundation.Collections.h>
|
||||||
|
//#include <Shlwapi.h>
|
||||||
|
#include <common/SettingsAPI/settings_helpers.h>
|
||||||
|
#include <common/logger/logger.h>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//{{NO_DEPENDENCIES}}
|
||||||
|
// Microsoft Visual C++ generated include file.
|
||||||
|
// Used by PowerDisplayExt.rc
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// Non-localizable
|
||||||
|
|
||||||
|
#define FILE_DESCRIPTION "PowerToys PowerDisplay Module"
|
||||||
|
#define INTERNAL_NAME "PowerToys.PowerDisplay"
|
||||||
|
#define ORIGINAL_FILENAME "PowerToys.PowerDisplay.dll"
|
||||||
|
|
||||||
|
// Non-localizable
|
||||||
|
//////////////////////////////
|
||||||
@@ -178,6 +178,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
|||||||
L"PowerToys.CmdPalModuleInterface.dll",
|
L"PowerToys.CmdPalModuleInterface.dll",
|
||||||
L"PowerToys.ZoomItModuleInterface.dll",
|
L"PowerToys.ZoomItModuleInterface.dll",
|
||||||
L"PowerToys.LightSwitchModuleInterface.dll",
|
L"PowerToys.LightSwitchModuleInterface.dll",
|
||||||
|
L"PowerToys.PowerDisplayExt.dll",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto moduleSubdir : knownModules)
|
for (auto moduleSubdir : knownModules)
|
||||||
|
|||||||
@@ -323,6 +323,48 @@ void dispatch_received_json(const std::wstring& json_to_parse)
|
|||||||
Logger::error(L"Failed to process get all hotkey conflicts request");
|
Logger::error(L"Failed to process get all hotkey conflicts request");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (name == L"powerdisplay_response")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Forward PowerDisplay response messages to Settings UI
|
||||||
|
// PowerDisplay sends monitor information via IPC
|
||||||
|
std::unique_lock lock{ ipc_mutex };
|
||||||
|
if (current_settings_ipc)
|
||||||
|
{
|
||||||
|
current_settings_ipc->send(value.Stringify().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to forward PowerDisplay response to Settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (name == L"powerdisplay_command")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Forward command from Settings UI to PowerDisplay module
|
||||||
|
Logger::trace(L"Received command from Settings UI to PowerDisplay");
|
||||||
|
|
||||||
|
// Find PowerDisplay module and send the command
|
||||||
|
auto moduleIt = modules().find(L"PowerDisplay");
|
||||||
|
if (moduleIt != modules().end())
|
||||||
|
{
|
||||||
|
// Use call_custom_action to send the command
|
||||||
|
// The command should contain an action field
|
||||||
|
moduleIt->second->call_custom_action(value.Stringify().c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::warn(L"PowerDisplay module not found, cannot send command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
Logger::error(L"Failed to forward command to PowerDisplay");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -809,6 +851,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
|
|||||||
return "CmdPal";
|
return "CmdPal";
|
||||||
case ESettingsWindowNames::ZoomIt:
|
case ESettingsWindowNames::ZoomIt:
|
||||||
return "ZoomIt";
|
return "ZoomIt";
|
||||||
|
case ESettingsWindowNames::PowerDisplay:
|
||||||
|
return "PowerDisplay";
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast<int>(value));
|
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast<int>(value));
|
||||||
@@ -948,6 +992,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
|
|||||||
{
|
{
|
||||||
return ESettingsWindowNames::ZoomIt;
|
return ESettingsWindowNames::ZoomIt;
|
||||||
}
|
}
|
||||||
|
else if (value == "PowerDisplay")
|
||||||
|
{
|
||||||
|
return ESettingsWindowNames::PowerDisplay;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));
|
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));
|
||||||
@@ -956,3 +1004,29 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
|
|||||||
|
|
||||||
return ESettingsWindowNames::Dashboard;
|
return ESettingsWindowNames::Dashboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global function for PowerDisplay module to send messages to Settings UI
|
||||||
|
void send_powerdisplay_message_to_settings_ui(const wchar_t* message)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger::trace(L"Sending PowerDisplay message to Settings UI");
|
||||||
|
|
||||||
|
std::unique_lock lock{ ipc_mutex };
|
||||||
|
if (current_settings_ipc)
|
||||||
|
{
|
||||||
|
// Wrap the message in powerdisplay_response format
|
||||||
|
json::JsonObject wrapper;
|
||||||
|
wrapper.SetNamedValue(L"powerdisplay_response", json::JsonValue::Parse(message));
|
||||||
|
current_settings_ipc->send(wrapper.Stringify().c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::warn(L"current_settings_ipc is null, cannot send to Settings UI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception&)
|
||||||
|
{
|
||||||
|
Logger::error(L"Exception while sending PowerDisplay message to Settings UI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ enum class ESettingsWindowNames
|
|||||||
NewPlus,
|
NewPlus,
|
||||||
CmdPal,
|
CmdPal,
|
||||||
ZoomIt,
|
ZoomIt,
|
||||||
|
PowerDisplay,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
|
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
|
||||||
@@ -47,3 +48,6 @@ void close_settings_window();
|
|||||||
void open_oobe_window();
|
void open_oobe_window();
|
||||||
void open_scoobe_window();
|
void open_scoobe_window();
|
||||||
void open_flyout();
|
void open_flyout();
|
||||||
|
|
||||||
|
// PowerDisplay IPC support
|
||||||
|
void send_powerdisplay_message_to_settings_ui(const wchar_t* message);
|
||||||
|
|||||||
@@ -58,6 +58,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> JsonSerializer.Serialize(this);
|
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.AdvancedPasteProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,33 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for all PowerToys module settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para><strong>IMPORTANT for Native AOT compatibility:</strong></para>
|
||||||
|
/// <para>When creating a new class that inherits from <see cref="BasePTModuleSettings"/>,
|
||||||
|
/// you MUST register it in <see cref="SettingsSerializationContext"/> by adding a
|
||||||
|
/// <c>[JsonSerializable(typeof(YourNewSettingsClass))]</c> attribute.</para>
|
||||||
|
/// <para>Failure to register the type will cause <see cref="ToJsonString"/> to throw
|
||||||
|
/// <see cref="InvalidOperationException"/> at runtime.</para>
|
||||||
|
/// <para>See <see cref="SettingsSerializationContext"/> for registration instructions.</para>
|
||||||
|
/// </remarks>
|
||||||
public abstract class BasePTModuleSettings
|
public abstract class BasePTModuleSettings
|
||||||
{
|
{
|
||||||
|
// Cached JsonSerializerOptions for Native AOT compatibility
|
||||||
|
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
TypeInfoResolver = SettingsSerializationContext.Default,
|
||||||
|
};
|
||||||
|
|
||||||
// Gets or sets name of the powertoy module.
|
// Gets or sets name of the powertoy module.
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
@@ -17,11 +37,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonPropertyName("version")]
|
[JsonPropertyName("version")]
|
||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
|
|
||||||
// converts the current to a json string.
|
/// <summary>
|
||||||
|
/// Converts the current settings object to a JSON string.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>JSON string representation of this settings object.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">
|
||||||
|
/// Thrown when the runtime type is not registered in <see cref="SettingsSerializationContext"/>.
|
||||||
|
/// All derived types must be registered with <c>[JsonSerializable(typeof(YourType))]</c> attribute.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method uses Native AOT-compatible JSON serialization. The runtime type must be
|
||||||
|
/// registered in <see cref="SettingsSerializationContext"/> for serialization to work.
|
||||||
|
/// </remarks>
|
||||||
public virtual string ToJsonString()
|
public virtual string ToJsonString()
|
||||||
{
|
{
|
||||||
// By default JsonSerializer will only serialize the properties in the base class. This can be avoided by passing the object type (more details at https://stackoverflow.com/a/62498888)
|
// By default JsonSerializer will only serialize the properties in the base class. This can be avoided by passing the object type (more details at https://stackoverflow.com/a/62498888)
|
||||||
return JsonSerializer.Serialize(this, GetType());
|
var runtimeType = GetType();
|
||||||
|
|
||||||
|
// For Native AOT compatibility, get JsonTypeInfo from the TypeInfoResolver
|
||||||
|
var typeInfo = _jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(runtimeType, _jsonSerializerOptions);
|
||||||
|
|
||||||
|
if (typeInfo == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Type {runtimeType.FullName} is not registered in SettingsSerializationContext. Please add it to the [JsonSerializable] attributes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use AOT-friendly serialization
|
||||||
|
return JsonSerializer.Serialize(this, typeInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.BoolProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryToCmdRepresentable(out string result)
|
public bool TryToCmdRepresentable(out string result)
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
{
|
{
|
||||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var boolProperty = JsonSerializer.Deserialize<BoolProperty>(ref reader, options);
|
var boolProperty = JsonSerializer.Deserialize(ref reader, SettingsSerializationContext.Default.BoolProperty);
|
||||||
return boolProperty.Value;
|
return boolProperty.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var boolProperty = new BoolProperty(value);
|
var boolProperty = new BoolProperty(value);
|
||||||
JsonSerializer.Serialize(writer, boolProperty, options);
|
JsonSerializer.Serialize(writer, boolProperty, SettingsSerializationContext.Default.BoolProperty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
|
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
|
||||||
{
|
{
|
||||||
Hotkey = JsonSerializer.Deserialize<HotkeySettings>(hotkeyElement.GetRawText());
|
Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText(), SettingsSerializationContext.Default.HotkeySettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
|||||||
@@ -87,6 +87,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
public bool ShowColorName { get; set; }
|
public bool ShowColorName { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> JsonSerializer.Serialize(this);
|
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
public bool ShowColorName { get; set; }
|
public bool ShowColorName { get; set; }
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> JsonSerializer.Serialize(this);
|
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerPropertiesVersion1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
// Returns a JSON version of the class settings configuration class.
|
// Returns a JSON version of the class settings configuration class.
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.DoubleProperty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -530,6 +530,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool powerDisplay;
|
||||||
|
|
||||||
|
[JsonPropertyName("PowerDisplay")]
|
||||||
|
public bool PowerDisplay
|
||||||
|
{
|
||||||
|
get => powerDisplay;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (powerDisplay != value)
|
||||||
|
{
|
||||||
|
LogTelemetryEvent(value);
|
||||||
|
powerDisplay = value;
|
||||||
|
NotifyChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void NotifyChange()
|
private void NotifyChange()
|
||||||
{
|
{
|
||||||
notifyEnabledChangedAction?.Invoke();
|
notifyEnabledChangedAction?.Invoke();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public string ToJsonString()
|
public string ToJsonString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithLocalProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function is required to implement the ISettingsConfig interface and obtain the settings configurations.
|
// This function is required to implement the ISettingsConfig interface and obtain the settings configurations.
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonPropertyName("bool_show_extended_menu")]
|
[JsonPropertyName("bool_show_extended_menu")]
|
||||||
public BoolProperty ExtendedContextMenuOnly { get; set; }
|
public BoolProperty ExtendedContextMenuOnly { get; set; }
|
||||||
|
|
||||||
public override string ToString() => JsonSerializer.Serialize(this);
|
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
// converts the current to a json string.
|
// converts the current to a json string.
|
||||||
public string ToJsonString()
|
public string ToJsonString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string DefaultPowertoysVersion()
|
private static string DefaultPowertoysVersion()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettingsCustomAction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
|||||||
{
|
{
|
||||||
public struct SunTimes
|
public struct SunTimes
|
||||||
{
|
{
|
||||||
public int SunriseHour;
|
public int SunriseHour { get; set; }
|
||||||
public int SunriseMinute;
|
|
||||||
public int SunsetHour;
|
|
||||||
public int SunsetMinute;
|
|
||||||
public string Text;
|
|
||||||
|
|
||||||
public bool HasSunrise;
|
public int SunriseMinute { get; set; }
|
||||||
public bool HasSunset;
|
|
||||||
|
public int SunsetHour { get; set; }
|
||||||
|
|
||||||
|
public int SunsetMinute { get; set; }
|
||||||
|
|
||||||
|
public string Text { get; set; }
|
||||||
|
|
||||||
|
public bool HasSunrise { get; set; }
|
||||||
|
|
||||||
|
public bool HasSunset { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public override string ToJsonString()
|
public override string ToJsonString()
|
||||||
{
|
{
|
||||||
var options = _serializerOptions;
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerSettings);
|
||||||
return JsonSerializer.Serialize(this, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetModuleName()
|
public string GetModuleName()
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
// Returns a JSON version of the class settings configuration class.
|
// Returns a JSON version of the class settings configuration class.
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.IntProperty);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator IntProperty(int v)
|
public static implicit operator IntProperty(int v)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public string ToJsonString()
|
public string ToJsonString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.KeyboardManagerProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetModuleName()
|
public string GetModuleName()
|
||||||
|
|||||||
@@ -46,6 +46,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public IntProperty DefaultMeasureStyle { get; set; }
|
public IntProperty DefaultMeasureStyle { get; set; }
|
||||||
|
|
||||||
public override string ToString() => JsonSerializer.Serialize(this);
|
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.MeasureToolProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
201
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
201
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// 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 Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||||
|
{
|
||||||
|
public class MonitorInfo : Observable
|
||||||
|
{
|
||||||
|
private string _name = string.Empty;
|
||||||
|
private string _internalName = string.Empty;
|
||||||
|
private string _hardwareId = string.Empty;
|
||||||
|
private string _communicationMethod = string.Empty;
|
||||||
|
private string _monitorType = string.Empty;
|
||||||
|
private int _currentBrightness;
|
||||||
|
private int _colorTemperature = 6500;
|
||||||
|
private bool _isHidden;
|
||||||
|
private bool _enableColorTemperature;
|
||||||
|
private bool _enableContrast;
|
||||||
|
private bool _enableVolume;
|
||||||
|
|
||||||
|
public MonitorInfo()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MonitorInfo(string name, string internalName, string communicationMethod)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
InternalName = internalName;
|
||||||
|
CommunicationMethod = communicationMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, string monitorType, int currentBrightness, int colorTemperature)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
InternalName = internalName;
|
||||||
|
HardwareId = hardwareId;
|
||||||
|
CommunicationMethod = communicationMethod;
|
||||||
|
MonitorType = monitorType;
|
||||||
|
CurrentBrightness = currentBrightness;
|
||||||
|
ColorTemperature = colorTemperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_name != value)
|
||||||
|
{
|
||||||
|
_name = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("internalName")]
|
||||||
|
public string InternalName
|
||||||
|
{
|
||||||
|
get => _internalName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_internalName != value)
|
||||||
|
{
|
||||||
|
_internalName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("communicationMethod")]
|
||||||
|
public string CommunicationMethod
|
||||||
|
{
|
||||||
|
get => _communicationMethod;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_communicationMethod != value)
|
||||||
|
{
|
||||||
|
_communicationMethod = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("hardwareId")]
|
||||||
|
public string HardwareId
|
||||||
|
{
|
||||||
|
get => _hardwareId;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_hardwareId != value)
|
||||||
|
{
|
||||||
|
_hardwareId = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("monitorType")]
|
||||||
|
public string MonitorType
|
||||||
|
{
|
||||||
|
get => _monitorType;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_monitorType != value)
|
||||||
|
{
|
||||||
|
_monitorType = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("currentBrightness")]
|
||||||
|
public int CurrentBrightness
|
||||||
|
{
|
||||||
|
get => _currentBrightness;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_currentBrightness != value)
|
||||||
|
{
|
||||||
|
_currentBrightness = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("colorTemperature")]
|
||||||
|
public int ColorTemperature
|
||||||
|
{
|
||||||
|
get => _colorTemperature;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_colorTemperature != value)
|
||||||
|
{
|
||||||
|
_colorTemperature = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("isHidden")]
|
||||||
|
public bool IsHidden
|
||||||
|
{
|
||||||
|
get => _isHidden;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_isHidden != value)
|
||||||
|
{
|
||||||
|
_isHidden = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("enableColorTemperature")]
|
||||||
|
public bool EnableColorTemperature
|
||||||
|
{
|
||||||
|
get => _enableColorTemperature;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_enableColorTemperature != value)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[MonitorInfo] EnableColorTemperature changing from {_enableColorTemperature} to {value} for monitor {Name}");
|
||||||
|
_enableColorTemperature = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("enableContrast")]
|
||||||
|
public bool EnableContrast
|
||||||
|
{
|
||||||
|
get => _enableContrast;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_enableContrast != value)
|
||||||
|
{
|
||||||
|
_enableContrast = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("enableVolume")]
|
||||||
|
public bool EnableVolume
|
||||||
|
{
|
||||||
|
get => _enableVolume;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_enableVolume != value)
|
||||||
|
{
|
||||||
|
_enableVolume = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/settings-ui/Settings.UI.Library/MonitorInfoData.cs
Normal file
35
src/settings-ui/Settings.UI.Library/MonitorInfoData.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitor information data for IPC
|
||||||
|
/// </summary>
|
||||||
|
public class MonitorInfoData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("internalName")]
|
||||||
|
public string InternalName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("hardwareId")]
|
||||||
|
public string HardwareId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("communicationMethod")]
|
||||||
|
public string CommunicationMethod { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("monitorType")]
|
||||||
|
public string MonitorType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("currentBrightness")]
|
||||||
|
public int CurrentBrightness { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("colorTemperature")]
|
||||||
|
public int ColorTemperature { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,8 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
public struct ConnectionRequest
|
public struct ConnectionRequest
|
||||||
#pragma warning restore SA1649 // File name should match first type name
|
#pragma warning restore SA1649 // File name should match first type name
|
||||||
{
|
{
|
||||||
public string PCName;
|
public string PCName { get; set; }
|
||||||
public string SecurityKey;
|
|
||||||
|
public string SecurityKey { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct NewKeyGenerationRequest
|
public struct NewKeyGenerationRequest
|
||||||
|
|||||||
@@ -33,6 +33,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonPropertyName("ReplaceVariables")]
|
[JsonPropertyName("ReplaceVariables")]
|
||||||
public BoolProperty ReplaceVariables { get; set; }
|
public BoolProperty ReplaceVariables { get; set; }
|
||||||
|
|
||||||
public override string ToString() => JsonSerializer.Serialize(this);
|
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.NewPlusProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingGeneralSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingLanguageSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace Settings.UI.Library
|
|||||||
|
|
||||||
public string ToJsonString()
|
public string ToJsonString()
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(this);
|
return JsonSerializer.Serialize(this, Microsoft.PowerToys.Settings.UI.Library.SettingsSerializationContext.Default.PeekPreviewSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetModuleName()
|
public string GetModuleName()
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
public BoolProperty EnableSpaceToActivate { get; set; }
|
public BoolProperty EnableSpaceToActivate { get; set; }
|
||||||
|
|
||||||
public override string ToString() => JsonSerializer.Serialize(this);
|
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PeekProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Message for PowerDisplay module actions
|
||||||
|
/// </summary>
|
||||||
|
public class PowerDisplayActionMessage
|
||||||
|
{
|
||||||
|
[JsonPropertyName("action")]
|
||||||
|
public ActionData Action { get; set; }
|
||||||
|
|
||||||
|
public class ActionData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("PowerDisplay")]
|
||||||
|
public PowerDisplayAction PowerDisplay { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PowerDisplayAction
|
||||||
|
{
|
||||||
|
[JsonPropertyName("action_name")]
|
||||||
|
public string ActionName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("value")]
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user