mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-06 12:27:01 +01:00
Compare commits
35 Commits
dev/vanzue
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c23cbd448 | ||
|
|
2992907142 | ||
|
|
a7e006332a | ||
|
|
d85d109c78 | ||
|
|
fe5edd9c5d | ||
|
|
8806e4ef2e | ||
|
|
b4ccac5ec2 | ||
|
|
b381472bf7 | ||
|
|
925c97a7f9 | ||
|
|
336234d05b | ||
|
|
8aec939c9d | ||
|
|
d64bb78727 | ||
|
|
b8abff02ac | ||
|
|
fc54172e13 | ||
|
|
a48e999963 | ||
|
|
ad83b5e67f | ||
|
|
f10c9f49e9 | ||
|
|
3f84ccc603 | ||
|
|
55cd6c95b8 | ||
|
|
15e6a762d3 | ||
|
|
f05740b0cb | ||
|
|
5f97f7f222 | ||
|
|
94bc13e703 | ||
|
|
e645a19629 | ||
|
|
4c799b61fc | ||
|
|
83410f1bc8 | ||
|
|
33d5ff26c6 | ||
|
|
d822745c98 | ||
|
|
0b7109dee4 | ||
|
|
ac9fd27095 | ||
|
|
753fecbe9f | ||
|
|
c24b5d97c5 | ||
|
|
5d63ca7a9c | ||
|
|
e90c4273f7 | ||
|
|
e2774eff2d |
@@ -85,6 +85,7 @@
|
||||
<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. -->
|
||||
<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.ComponentModel.Composition" 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="UTF.Unknown" Version="2.6.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="WyHash" Version="1.0.5" />
|
||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||
|
||||
32
NOTICE.md
32
NOTICE.md
@@ -1537,4 +1537,34 @@ SOFTWARE.
|
||||
- UTF.Unknown
|
||||
- WinUIEx
|
||||
- WPF-UI
|
||||
- WyHash
|
||||
- WyHash
|
||||
|
||||
## Utility: PowerDisplay
|
||||
|
||||
### Twinkle Tray
|
||||
|
||||
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
|
||||
|
||||
**Source**: https://github.com/xanderfrangos/twinkle-tray
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright © 2020 Xander Frangos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -517,6 +517,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\m
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}"
|
||||
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}"
|
||||
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}"
|
||||
@@ -563,6 +565,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}"
|
||||
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}"
|
||||
EndProject
|
||||
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|x64.ActiveCfg = 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.Build.0 = Debug|ARM64
|
||||
{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}
|
||||
{02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
||||
{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}
|
||||
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{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="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -113,6 +114,8 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
@@ -195,4 +195,12 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
||||
}
|
||||
hstring Constants::RefreshPowerDisplayMonitorsEvent()
|
||||
{
|
||||
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
|
||||
}
|
||||
hstring Constants::ApplyProfilePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring WorkspacesHotkeyEvent();
|
||||
static hstring PowerToysRunnerTerminateSettingsEvent();
|
||||
static hstring ShowCmdPalEvent();
|
||||
static hstring RefreshPowerDisplayMonitorsEvent();
|
||||
static hstring ApplyProfilePowerDisplayEvent();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ namespace PowerToys
|
||||
static String WorkspacesHotkeyEvent();
|
||||
static String PowerToysRunnerTerminateSettingsEvent();
|
||||
static String ShowCmdPalEvent();
|
||||
static String RefreshPowerDisplayMonitorsEvent();
|
||||
static String ApplyProfilePowerDisplayEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,15 @@ namespace CommonSharedConstants
|
||||
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";
|
||||
|
||||
// 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 TOGGLE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
|
||||
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
|
||||
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
|
||||
const wchar_t APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
|
||||
const wchar_t APPLY_PROFILE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
|
||||
|
||||
// used from quick access window
|
||||
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";
|
||||
|
||||
@@ -196,6 +196,44 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
if (settings.changeApps && isAppsCurrentlyLight)
|
||||
SetAppsTheme(false);
|
||||
}
|
||||
|
||||
// Notify PowerDisplay about theme change if any profile is enabled
|
||||
bool shouldNotify = false;
|
||||
|
||||
if (isLightActive && settings.enableLightModeProfile && !settings.lightModeProfile.empty())
|
||||
{
|
||||
shouldNotify = true;
|
||||
}
|
||||
else if (!isLightActive && settings.enableDarkModeProfile && !settings.darkModeProfile.empty())
|
||||
{
|
||||
shouldNotify = true;
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Signal PowerDisplay to check LightSwitch settings and apply appropriate profile
|
||||
// PowerDisplay will read LightSwitch settings to determine which profile to apply
|
||||
Logger::info(L"[LightSwitch] Notifying PowerDisplay about theme change (isLight: {})", isLightActive);
|
||||
|
||||
HANDLE hThemeChangedEvent = CreateEventW(nullptr, FALSE, FALSE, L"Local\\PowerToys_LightSwitch_ThemeChanged");
|
||||
if (hThemeChangedEvent)
|
||||
{
|
||||
SetEvent(hThemeChangedEvent);
|
||||
CloseHandle(hThemeChangedEvent);
|
||||
Logger::info(L"[LightSwitch] Theme change event signaled");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"[LightSwitch] Failed to create theme change event");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"[LightSwitch] Failed to notify PowerDisplay");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- At service start: immediately honor the schedule ---
|
||||
@@ -278,12 +316,12 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
|
||||
break;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
|
||||
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,46 @@ void LightSwitchSettings::LoadSettings()
|
||||
NotifyObservers(SettingId::ChangeApps);
|
||||
}
|
||||
}
|
||||
|
||||
// EnableDarkModeProfile
|
||||
if (const auto jsonVal = values.get_bool_value(L"enableDarkModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.enableDarkModeProfile != val)
|
||||
{
|
||||
m_settings.enableDarkModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// EnableLightModeProfile
|
||||
if (const auto jsonVal = values.get_bool_value(L"enableLightModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.enableLightModeProfile != val)
|
||||
{
|
||||
m_settings.enableLightModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// DarkModeProfile
|
||||
if (const auto jsonVal = values.get_string_value(L"darkModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.darkModeProfile != val)
|
||||
{
|
||||
m_settings.darkModeProfile = val;
|
||||
}
|
||||
}
|
||||
|
||||
// LightModeProfile
|
||||
if (const auto jsonVal = values.get_string_value(L"lightModeProfile"))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.lightModeProfile != val)
|
||||
{
|
||||
m_settings.lightModeProfile = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
||||
@@ -56,6 +56,11 @@ struct LightSwitchConfig
|
||||
|
||||
bool changeSystem = false;
|
||||
bool changeApps = false;
|
||||
|
||||
bool enableDarkModeProfile = false;
|
||||
bool enableLightModeProfile = false;
|
||||
std::wstring darkModeProfile = L"";
|
||||
std::wstring lightModeProfile = L"";
|
||||
};
|
||||
|
||||
class LightSwitchSettings
|
||||
|
||||
109
src/modules/powerdisplay/PowerDisplay/Commands/RelayCommand.cs
Normal file
109
src/modules/powerdisplay/PowerDisplay/Commands/RelayCommand.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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;
|
||||
using ManagedCommon;
|
||||
|
||||
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)
|
||||
{
|
||||
if (_canExecute == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _canExecute.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"CanExecute failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
try
|
||||
{
|
||||
_execute();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Command execution failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (_canExecute == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _canExecute.Invoke((T?)parameter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"CanExecute<T> failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
try
|
||||
{
|
||||
_execute((T?)parameter);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Command<T> execution failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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;
|
||||
|
||||
// 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 = 362;
|
||||
public const int MaxWindowHeight = 650;
|
||||
public const int WindowRightMargin = 12;
|
||||
|
||||
// Animation and layout update delays (milliseconds)
|
||||
public const int LayoutUpdateDelayMs = 50;
|
||||
public const int MonitorDiscoveryDelayMs = 200;
|
||||
|
||||
/// <summary>
|
||||
/// Debounce delay for slider controls in milliseconds
|
||||
/// </summary>
|
||||
public const int SliderDebounceDelayMs = 300;
|
||||
}
|
||||
|
||||
/// <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,140 @@
|
||||
// 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>
|
||||
/// 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
258
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
258
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
// 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;
|
||||
using PowerDisplay.Core.Utils;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor model that implements property change notification
|
||||
/// </summary>
|
||||
public partial class Monitor : INotifyPropertyChanged
|
||||
{
|
||||
private int _currentBrightness;
|
||||
private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
|
||||
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>
|
||||
/// 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 VCP preset value (from VCP code 0x14).
|
||||
/// This stores the raw VCP value (e.g., 0x05 for 6500K), not Kelvin temperature.
|
||||
/// Use ColorTemperaturePresetName to get human-readable name.
|
||||
/// </summary>
|
||||
public int CurrentColorTemperature
|
||||
{
|
||||
get => _currentColorTemperature;
|
||||
set
|
||||
{
|
||||
if (_currentColorTemperature != value)
|
||||
{
|
||||
_currentColorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable color temperature preset name (e.g., "6500K (0x05)", "sRGB (0x01)")
|
||||
/// </summary>
|
||||
public string ColorTemperaturePresetName =>
|
||||
VcpValueNames.GetFormattedName(0x14, CurrentColorTemperature);
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports color temperature adjustment via VCP 0x14
|
||||
/// </summary>
|
||||
public bool SupportsColorTemperature { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Capabilities detection status: "available", "unavailable", or "unknown"
|
||||
/// </summary>
|
||||
public string CapabilitiesStatus { get; set; } = "unknown";
|
||||
|
||||
/// <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
|
||||
/// </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>
|
||||
/// Raw DDC/CI capabilities string (MCCS format)
|
||||
/// </summary>
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parsed VCP capabilities information
|
||||
/// </summary>
|
||||
public VcpCapabilities? VcpCapabilitiesInfo { get; set; }
|
||||
|
||||
/// <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} ({CommunicationMethod}) - {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,134 @@
|
||||
// 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;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// DDC/CI VCP capabilities information
|
||||
/// </summary>
|
||||
public class VcpCapabilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Raw capabilities string (MCCS format)
|
||||
/// </summary>
|
||||
public string Raw { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor model name from capabilities
|
||||
/// </summary>
|
||||
public string? Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Monitor type from capabilities (e.g., "LCD")
|
||||
/// </summary>
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// MCCS protocol version
|
||||
/// </summary>
|
||||
public string? Protocol { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported command codes
|
||||
/// </summary>
|
||||
public List<byte> SupportedCommands { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Supported VCP codes with their information
|
||||
/// </summary>
|
||||
public Dictionary<byte, VcpCodeInfo> SupportedVcpCodes { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific VCP code is supported
|
||||
/// </summary>
|
||||
public bool SupportsVcpCode(byte code) => SupportedVcpCodes.ContainsKey(code);
|
||||
|
||||
/// <summary>
|
||||
/// Get VCP code information
|
||||
/// </summary>
|
||||
public VcpCodeInfo? GetVcpCodeInfo(byte code)
|
||||
{
|
||||
return SupportedVcpCodes.TryGetValue(code, out var info) ? info : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a VCP code supports discrete values
|
||||
/// </summary>
|
||||
public bool HasDiscreteValues(byte code)
|
||||
{
|
||||
var info = GetVcpCodeInfo(code);
|
||||
return info?.HasDiscreteValues ?? false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get supported values for a VCP code
|
||||
/// </summary>
|
||||
public IReadOnlyList<int>? GetSupportedValues(byte code)
|
||||
{
|
||||
return GetVcpCodeInfo(code)?.SupportedValues;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an empty capabilities object
|
||||
/// </summary>
|
||||
public static VcpCapabilities Empty => new();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Model: {Model}, VCP Codes: {SupportedVcpCodes.Count}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about a single VCP code
|
||||
/// </summary>
|
||||
public readonly struct VcpCodeInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code (e.g., 0x10 for brightness)
|
||||
/// </summary>
|
||||
public byte Code { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable name of the VCP code
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported discrete values (empty if continuous range)
|
||||
/// </summary>
|
||||
public IReadOnlyList<int> SupportedValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this VCP code has discrete values
|
||||
/// </summary>
|
||||
public bool HasDiscreteValues => SupportedValues.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this VCP code supports a continuous range
|
||||
/// </summary>
|
||||
public bool IsContinuous => SupportedValues.Count == 0;
|
||||
|
||||
public VcpCodeInfo(byte code, string name, IReadOnlyList<int>? supportedValues = null)
|
||||
{
|
||||
Code = code;
|
||||
Name = name;
|
||||
SupportedValues = supportedValues ?? Array.Empty<int>();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (HasDiscreteValues)
|
||||
{
|
||||
return $"0x{Code:X2} ({Name}): {string.Join(", ", SupportedValues)}";
|
||||
}
|
||||
|
||||
return $"0x{Code:X2} ({Name}): Continuous";
|
||||
}
|
||||
}
|
||||
}
|
||||
527
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
527
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
@@ -0,0 +1,527 @@
|
||||
// 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;
|
||||
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.LogWarning("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
|
||||
var allMonitors = new List<Monitor>();
|
||||
|
||||
foreach (var (controller, monitors) in results)
|
||||
{
|
||||
// Initialize monitors in parallel
|
||||
var initTasks = monitors.Select(async monitor =>
|
||||
{
|
||||
// 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}");
|
||||
}
|
||||
|
||||
// Get capabilities for DDC/CI monitors
|
||||
// Check by CommunicationMethod instead of Type
|
||||
if (monitor.CommunicationMethod?.Contains("DDC", StringComparison.OrdinalIgnoreCase) == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Getting capabilities for monitor {monitor.Id}");
|
||||
var capsString = await controller.GetCapabilitiesStringAsync(monitor, cancellationToken);
|
||||
|
||||
if (!string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
monitor.CapabilitiesRaw = capsString;
|
||||
|
||||
// Parse capabilities
|
||||
monitor.VcpCapabilitiesInfo = Utils.VcpCapabilitiesParser.Parse(capsString);
|
||||
|
||||
Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes");
|
||||
|
||||
// Update capability flags based on parsed VCP codes
|
||||
if (monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0)
|
||||
{
|
||||
UpdateMonitorCapabilitiesFromVcp(monitor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Got empty capabilities string for monitor {monitor.Id}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to get capabilities for monitor {monitor.Id}: {ex.Message}");
|
||||
|
||||
// Continue without capabilities - not critical
|
||||
}
|
||||
}
|
||||
|
||||
return monitor;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
var initializedMonitors = await Task.WhenAll(initTasks);
|
||||
var validMonitors = initializedMonitors.Where(m => m != null).Cast<Monitor>();
|
||||
newMonitors.AddRange(validMonitors);
|
||||
}
|
||||
|
||||
// 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 = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
||||
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)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
Logger.LogError($"Monitor not found: {monitorId}");
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
||||
if (controller == null)
|
||||
{
|
||||
Logger.LogError($"No controller available for monitor {monitorId}");
|
||||
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
|
||||
|
||||
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 = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
||||
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)
|
||||
{
|
||||
// Store raw VCP 0x14 preset value (e.g., 0x05 for 6500K)
|
||||
// No Kelvin conversion - we use discrete presets
|
||||
monitor.CurrentColorTemperature = tempInfo.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 async Task<IMonitorController?> GetControllerForMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// WMI monitors use WmiController, DDC/CI monitors use DdcCiController
|
||||
foreach (var controller in _controllers)
|
||||
{
|
||||
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
||||
{
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <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 = await GetControllerForMonitorAsync(monitor, cancellationToken);
|
||||
if (controller == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor capability flags based on parsed VCP capabilities
|
||||
/// </summary>
|
||||
private void UpdateMonitorCapabilitiesFromVcp(Monitor monitor)
|
||||
{
|
||||
var vcpCaps = monitor.VcpCapabilitiesInfo;
|
||||
if (vcpCaps == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Contrast support (VCP 0x12)
|
||||
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeContrast))
|
||||
{
|
||||
monitor.Capabilities |= MonitorCapabilities.Contrast;
|
||||
Logger.LogDebug($"[{monitor.Id}] Contrast support detected via VCP 0x12");
|
||||
}
|
||||
|
||||
// Check for Volume support (VCP 0x62)
|
||||
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeVolume))
|
||||
{
|
||||
monitor.Capabilities |= MonitorCapabilities.Volume;
|
||||
Logger.LogDebug($"[{monitor.Id}] Volume support detected via VCP 0x62");
|
||||
}
|
||||
|
||||
// Check for Color Temperature support (VCP 0x14)
|
||||
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeSelectColorPreset))
|
||||
{
|
||||
monitor.SupportsColorTemperature = true;
|
||||
Logger.LogDebug($"[{monitor.Id}] Color temperature support detected via VCP 0x14");
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[{monitor.Id}] Capabilities updated: Contrast={monitor.SupportsContrast}, Volume={monitor.SupportsVolume}, ColorTemp={monitor.SupportsColorTemperature}");
|
||||
}
|
||||
|
||||
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,315 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Models;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Parser for DDC/CI MCCS capabilities strings
|
||||
/// </summary>
|
||||
public static class VcpCapabilitiesParser
|
||||
{
|
||||
private static readonly char[] SpaceSeparator = new[] { ' ' };
|
||||
private static readonly char[] ValueSeparators = new[] { ' ', '(', ')' };
|
||||
|
||||
/// <summary>
|
||||
/// Parse a capabilities string into structured VcpCapabilities
|
||||
/// </summary>
|
||||
/// <param name="capabilitiesString">Raw MCCS capabilities string</param>
|
||||
/// <returns>Parsed capabilities object, or Empty if parsing fails</returns>
|
||||
public static VcpCapabilities Parse(string? capabilitiesString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(capabilitiesString))
|
||||
{
|
||||
return VcpCapabilities.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var capabilities = new VcpCapabilities
|
||||
{
|
||||
Raw = capabilitiesString,
|
||||
};
|
||||
|
||||
// Extract model, type, protocol
|
||||
capabilities.Model = ExtractValue(capabilitiesString, "model");
|
||||
capabilities.Type = ExtractValue(capabilitiesString, "type");
|
||||
capabilities.Protocol = ExtractValue(capabilitiesString, "prot");
|
||||
|
||||
// Extract supported commands
|
||||
capabilities.SupportedCommands = ParseCommandList(capabilitiesString);
|
||||
|
||||
// Extract and parse VCP codes
|
||||
capabilities.SupportedVcpCodes = ParseVcpCodes(capabilitiesString);
|
||||
|
||||
Logger.LogInfo($"Parsed capabilities: Model={capabilities.Model}, VCP Codes={capabilities.SupportedVcpCodes.Count}");
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to parse capabilities string: {ex.Message}");
|
||||
return VcpCapabilities.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract a simple value from capabilities string
|
||||
/// Example: "model(PD3220U)" -> "PD3220U"
|
||||
/// </summary>
|
||||
private static string? ExtractValue(string capabilities, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pattern = $@"{key}\(([^)]+)\)";
|
||||
var match = Regex.Match(capabilities, pattern, RegexOptions.IgnoreCase);
|
||||
return match.Success ? match.Groups[1].Value : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse command list from capabilities string
|
||||
/// Example: "cmds(01 02 03 07 0C)" -> [0x01, 0x02, 0x03, 0x07, 0x0C]
|
||||
/// </summary>
|
||||
private static List<byte> ParseCommandList(string capabilities)
|
||||
{
|
||||
var commands = new List<byte>();
|
||||
|
||||
try
|
||||
{
|
||||
var match = Regex.Match(capabilities, @"cmds\(([^)]+)\)", RegexOptions.IgnoreCase);
|
||||
if (match.Success)
|
||||
{
|
||||
var cmdString = match.Groups[1].Value;
|
||||
var cmdTokens = cmdString.Split(SpaceSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var token in cmdTokens)
|
||||
{
|
||||
if (byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cmd))
|
||||
{
|
||||
commands.Add(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to parse command list: {ex.Message}");
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP codes section from capabilities string
|
||||
/// </summary>
|
||||
private static Dictionary<byte, VcpCodeInfo> ParseVcpCodes(string capabilities)
|
||||
{
|
||||
var vcpCodes = new Dictionary<byte, VcpCodeInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
// Find the "vcp(" section
|
||||
var vcpStart = capabilities.IndexOf("vcp(", StringComparison.OrdinalIgnoreCase);
|
||||
if (vcpStart < 0)
|
||||
{
|
||||
Logger.LogWarning("No 'vcp(' section found in capabilities string");
|
||||
return vcpCodes;
|
||||
}
|
||||
|
||||
// Extract the complete VCP section by matching parentheses
|
||||
var vcpSection = ExtractVcpSection(capabilities, vcpStart + 4); // Skip "vcp("
|
||||
if (string.IsNullOrEmpty(vcpSection))
|
||||
{
|
||||
return vcpCodes;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Extracted VCP section: {vcpSection.Substring(0, Math.Min(100, vcpSection.Length))}...");
|
||||
|
||||
// Parse VCP codes from the section
|
||||
ParseVcpCodesFromSection(vcpSection, vcpCodes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to parse VCP codes: {ex.Message}");
|
||||
}
|
||||
|
||||
return vcpCodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract VCP section by matching parentheses
|
||||
/// </summary>
|
||||
private static string ExtractVcpSection(string capabilities, int startIndex)
|
||||
{
|
||||
var depth = 1;
|
||||
var result = string.Empty;
|
||||
|
||||
for (int i = startIndex; i < capabilities.Length && depth > 0; i++)
|
||||
{
|
||||
var ch = capabilities[i];
|
||||
|
||||
if (ch == '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (ch == ')')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result += ch;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP codes from the extracted VCP section
|
||||
/// </summary>
|
||||
private static void ParseVcpCodesFromSection(string vcpSection, Dictionary<byte, VcpCodeInfo> vcpCodes)
|
||||
{
|
||||
var i = 0;
|
||||
|
||||
while (i < vcpSection.Length)
|
||||
{
|
||||
// Skip whitespace
|
||||
while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i]))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= vcpSection.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Read VCP code (2 hex digits)
|
||||
if (i + 1 < vcpSection.Length &&
|
||||
IsHexDigit(vcpSection[i]) &&
|
||||
IsHexDigit(vcpSection[i + 1]))
|
||||
{
|
||||
var codeStr = vcpSection.Substring(i, 2);
|
||||
if (byte.TryParse(codeStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var code))
|
||||
{
|
||||
i += 2;
|
||||
|
||||
// Check if there are supported values (followed by '(')
|
||||
while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i]))
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
var supportedValues = new List<int>();
|
||||
|
||||
if (i < vcpSection.Length && vcpSection[i] == '(')
|
||||
{
|
||||
// Extract supported values
|
||||
i++; // Skip '('
|
||||
var valuesSection = ExtractVcpValuesSection(vcpSection, i);
|
||||
i += valuesSection.Length + 1; // +1 for closing ')'
|
||||
|
||||
// Parse values
|
||||
ParseVcpValues(valuesSection, supportedValues);
|
||||
}
|
||||
|
||||
// Get VCP code name
|
||||
var name = VcpCodeNames.GetName(code);
|
||||
|
||||
// Store VCP code info
|
||||
vcpCodes[code] = new VcpCodeInfo(code, name, supportedValues);
|
||||
|
||||
Logger.LogDebug($"Parsed VCP code: 0x{code:X2} ({name}), Values: {supportedValues.Count}");
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract VCP values section by matching parentheses
|
||||
/// </summary>
|
||||
private static string ExtractVcpValuesSection(string section, int startIndex)
|
||||
{
|
||||
var depth = 1;
|
||||
var result = string.Empty;
|
||||
|
||||
for (int i = startIndex; i < section.Length && depth > 0; i++)
|
||||
{
|
||||
var ch = section[i];
|
||||
|
||||
if (ch == '(')
|
||||
{
|
||||
depth++;
|
||||
result += ch;
|
||||
}
|
||||
else if (ch == ')')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
result += ch;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse VCP values from the values section
|
||||
/// </summary>
|
||||
private static void ParseVcpValues(string valuesSection, List<int> supportedValues)
|
||||
{
|
||||
var tokens = valuesSection.Split(ValueSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
// Try to parse as hex
|
||||
if (int.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
supportedValues.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a character is a hex digit
|
||||
/// </summary>
|
||||
private static bool IsHexDigit(char c)
|
||||
{
|
||||
return (c >= '0' && c <= '9') ||
|
||||
(c >= 'A' && c <= 'F') ||
|
||||
(c >= 'a' && c <= 'f');
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs
Normal file
239
src/modules/powerdisplay/PowerDisplay/Core/Utils/VcpCodeNames.cs
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code to friendly name mapping based on MCCS v2.2a specification
|
||||
/// </summary>
|
||||
public static class VcpCodeNames
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code to name mapping
|
||||
/// </summary>
|
||||
private static readonly Dictionary<byte, string> CodeNames = new()
|
||||
{
|
||||
// Control codes
|
||||
{ 0x01, "Degauss" },
|
||||
{ 0x02, "New Control Value" },
|
||||
{ 0x03, "Soft Controls" },
|
||||
|
||||
// Geometry codes
|
||||
{ 0x04, "Restore Factory Defaults" },
|
||||
{ 0x05, "Restore Brightness and Contrast" },
|
||||
{ 0x06, "Restore Factory Geometry" },
|
||||
{ 0x08, "Restore Color Defaults" },
|
||||
{ 0x0A, "Restore Factory TV Defaults" },
|
||||
|
||||
// Color temperature codes
|
||||
{ 0x0B, "Color Temperature Increment" },
|
||||
{ 0x0C, "Color Temperature Request" },
|
||||
{ 0x0E, "Clock" },
|
||||
{ 0x0F, "Color Saturation" },
|
||||
|
||||
// Image adjustment codes
|
||||
{ 0x10, "Brightness" },
|
||||
{ 0x11, "Flesh Tone Enhancement" },
|
||||
{ 0x12, "Contrast" },
|
||||
{ 0x13, "Backlight Control" },
|
||||
{ 0x14, "Select Color Preset" },
|
||||
{ 0x16, "Video Gain: Red" },
|
||||
{ 0x17, "User Color Vision Compensation" },
|
||||
{ 0x18, "Video Gain: Green" },
|
||||
{ 0x1A, "Video Gain: Blue" },
|
||||
{ 0x1C, "Focus" },
|
||||
{ 0x1E, "Auto Setup" },
|
||||
{ 0x1F, "Auto Color Setup" },
|
||||
|
||||
// Geometry codes
|
||||
{ 0x20, "Horizontal Position" },
|
||||
{ 0x22, "Horizontal Size" },
|
||||
{ 0x24, "Horizontal Pincushion" },
|
||||
{ 0x26, "Horizontal Pincushion Balance" },
|
||||
{ 0x28, "Horizontal Convergence R/B" },
|
||||
{ 0x29, "Horizontal Convergence M/G" },
|
||||
{ 0x2A, "Horizontal Linearity" },
|
||||
{ 0x2C, "Horizontal Linearity Balance" },
|
||||
{ 0x30, "Vertical Position" },
|
||||
{ 0x32, "Vertical Size" },
|
||||
{ 0x34, "Vertical Pincushion" },
|
||||
{ 0x36, "Vertical Pincushion Balance" },
|
||||
{ 0x38, "Vertical Convergence R/B" },
|
||||
{ 0x39, "Vertical Convergence M/G" },
|
||||
{ 0x3A, "Vertical Linearity" },
|
||||
{ 0x3C, "Vertical Linearity Balance" },
|
||||
{ 0x3E, "Clock Phase" },
|
||||
|
||||
// Miscellaneous codes
|
||||
{ 0x40, "Horizontal Parallelogram" },
|
||||
{ 0x41, "Vertical Parallelogram" },
|
||||
{ 0x42, "Horizontal Keystone" },
|
||||
{ 0x43, "Vertical Keystone" },
|
||||
{ 0x44, "Rotation" },
|
||||
{ 0x46, "Top Corner Flare" },
|
||||
{ 0x48, "Top Corner Hook" },
|
||||
{ 0x4A, "Bottom Corner Flare" },
|
||||
{ 0x4C, "Bottom Corner Hook" },
|
||||
|
||||
// Advanced codes
|
||||
{ 0x52, "Active Control" },
|
||||
{ 0x54, "Performance Preservation" },
|
||||
{ 0x56, "Horizontal Moire" },
|
||||
{ 0x58, "Vertical Moire" },
|
||||
{ 0x59, "6 Axis Saturation: Red" },
|
||||
{ 0x5A, "6 Axis Saturation: Yellow" },
|
||||
{ 0x5B, "6 Axis Saturation: Green" },
|
||||
{ 0x5C, "6 Axis Saturation: Cyan" },
|
||||
{ 0x5D, "6 Axis Saturation: Blue" },
|
||||
{ 0x5E, "6 Axis Saturation: Magenta" },
|
||||
|
||||
// Input source codes
|
||||
{ 0x60, "Input Source" },
|
||||
{ 0x62, "Audio Speaker Volume" },
|
||||
{ 0x63, "Speaker Select" },
|
||||
{ 0x64, "Audio: Microphone Volume" },
|
||||
{ 0x66, "Ambient Light Sensor" },
|
||||
{ 0x6B, "Backlight Level: White" },
|
||||
{ 0x6C, "Video Black Level: Red" },
|
||||
{ 0x6D, "Backlight Level: Red" },
|
||||
{ 0x6E, "Video Black Level: Green" },
|
||||
{ 0x6F, "Backlight Level: Green" },
|
||||
{ 0x70, "Video Black Level: Blue" },
|
||||
{ 0x71, "Backlight Level: Blue" },
|
||||
{ 0x72, "Gamma" },
|
||||
{ 0x73, "LUT Size" },
|
||||
{ 0x74, "Single Point LUT Operation" },
|
||||
{ 0x75, "Block LUT Operation" },
|
||||
|
||||
// Color calibration codes
|
||||
{ 0x86, "Display Scaling" },
|
||||
{ 0x87, "Sharpness" },
|
||||
{ 0x88, "Velocity Scan Modulation" },
|
||||
{ 0x8A, "Color Saturation" },
|
||||
{ 0x8C, "TV Sharpness" },
|
||||
{ 0x8D, "Audio Mute/Screen Blank" },
|
||||
{ 0x8E, "TV Contrast" },
|
||||
{ 0x8F, "Audio Treble" },
|
||||
{ 0x90, "Hue" },
|
||||
{ 0x91, "Audio Bass" },
|
||||
{ 0x92, "TV Black Level/Luminance" },
|
||||
{ 0x93, "Audio Balance L/R" },
|
||||
{ 0x94, "Audio Processor Mode" },
|
||||
{ 0x95, "Window Position(TL_X)" },
|
||||
{ 0x96, "Window Position(TL_Y)" },
|
||||
{ 0x97, "Window Position(BR_X)" },
|
||||
{ 0x98, "Window Position(BR_Y)" },
|
||||
{ 0x99, "Window Background" },
|
||||
{ 0x9A, "6 Axis Hue Control: Red" },
|
||||
{ 0x9B, "6 Axis Hue Control: Yellow" },
|
||||
{ 0x9C, "6 Axis Hue Control: Green" },
|
||||
{ 0x9D, "6 Axis Hue Control: Cyan" },
|
||||
{ 0x9E, "6 Axis Hue Control: Blue" },
|
||||
{ 0x9F, "6 Axis Hue Control: Magenta" },
|
||||
|
||||
// Window control codes
|
||||
{ 0xA0, "Auto Setup On/Off" },
|
||||
{ 0xA2, "Auto Color Setup On/Off" },
|
||||
{ 0xA4, "Window Mask Control" },
|
||||
{ 0xA5, "Window Select" },
|
||||
{ 0xA6, "Window Size" },
|
||||
{ 0xA7, "Window Transparency" },
|
||||
{ 0xAA, "Screen Orientation" },
|
||||
{ 0xAC, "Horizontal Frequency" },
|
||||
{ 0xAE, "Vertical Frequency" },
|
||||
|
||||
// Misc advanced codes
|
||||
{ 0xB0, "Settings" },
|
||||
{ 0xB2, "Flat Panel Sub-Pixel Layout" },
|
||||
{ 0xB4, "Source Timing Mode" },
|
||||
{ 0xB6, "Display Technology Type" },
|
||||
{ 0xB7, "Monitor Status" },
|
||||
{ 0xB8, "Packet Count" },
|
||||
{ 0xB9, "Monitor X Origin" },
|
||||
{ 0xBA, "Monitor Y Origin" },
|
||||
{ 0xBB, "Header Error Count" },
|
||||
{ 0xBC, "Body CRC Error Count" },
|
||||
{ 0xBD, "Client ID" },
|
||||
{ 0xBE, "Link Control" },
|
||||
|
||||
// Display controller codes
|
||||
{ 0xC0, "Display Usage Time" },
|
||||
{ 0xC2, "Display Firmware Level" },
|
||||
{ 0xC4, "Display Descriptor Length" },
|
||||
{ 0xC5, "Transmit Display Descriptor" },
|
||||
{ 0xC6, "Enable Display of 'Display Descriptor'" },
|
||||
{ 0xC8, "Display Controller Type" },
|
||||
{ 0xC9, "Display Firmware Level" },
|
||||
{ 0xCA, "OSD" },
|
||||
{ 0xCC, "OSD Language" },
|
||||
{ 0xD0, "Output Select" },
|
||||
{ 0xD2, "Asset Tag" },
|
||||
{ 0xD4, "Stereo Video Mode" },
|
||||
{ 0xD6, "Power Mode" },
|
||||
{ 0xD7, "Auxiliary Power Output" },
|
||||
{ 0xD8, "Scan Mode" },
|
||||
{ 0xD9, "Image Mode" },
|
||||
{ 0xDA, "On Screen Display" },
|
||||
{ 0xDC, "Display Application" },
|
||||
{ 0xDE, "Scratch Pad" },
|
||||
|
||||
// Information codes
|
||||
{ 0xDF, "VCP Version" },
|
||||
{ 0xE0, "Manufacturer Specific" },
|
||||
{ 0xE1, "Manufacturer Specific" },
|
||||
{ 0xE2, "Manufacturer Specific" },
|
||||
{ 0xE3, "Manufacturer Specific" },
|
||||
{ 0xE4, "Manufacturer Specific" },
|
||||
{ 0xE5, "Manufacturer Specific" },
|
||||
{ 0xE6, "Manufacturer Specific" },
|
||||
{ 0xE7, "Manufacturer Specific" },
|
||||
{ 0xE8, "Manufacturer Specific" },
|
||||
{ 0xE9, "Manufacturer Specific" },
|
||||
{ 0xEA, "Manufacturer Specific" },
|
||||
{ 0xEB, "Manufacturer Specific" },
|
||||
{ 0xEC, "Manufacturer Specific" },
|
||||
{ 0xED, "Manufacturer Specific" },
|
||||
{ 0xEE, "Manufacturer Specific" },
|
||||
{ 0xEF, "Manufacturer Specific" },
|
||||
{ 0xF0, "Manufacturer Specific" },
|
||||
{ 0xF1, "Manufacturer Specific" },
|
||||
{ 0xF2, "Manufacturer Specific" },
|
||||
{ 0xF3, "Manufacturer Specific" },
|
||||
{ 0xF4, "Manufacturer Specific" },
|
||||
{ 0xF5, "Manufacturer Specific" },
|
||||
{ 0xF6, "Manufacturer Specific" },
|
||||
{ 0xF7, "Manufacturer Specific" },
|
||||
{ 0xF8, "Manufacturer Specific" },
|
||||
{ 0xF9, "Manufacturer Specific" },
|
||||
{ 0xFA, "Manufacturer Specific" },
|
||||
{ 0xFB, "Manufacturer Specific" },
|
||||
{ 0xFC, "Manufacturer Specific" },
|
||||
{ 0xFD, "Manufacturer Specific" },
|
||||
{ 0xFE, "Manufacturer Specific" },
|
||||
{ 0xFF, "Manufacturer Specific" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the friendly name for a VCP code
|
||||
/// </summary>
|
||||
/// <param name="code">VCP code (e.g., 0x10)</param>
|
||||
/// <returns>Friendly name, or hex representation if unknown</returns>
|
||||
public static string GetName(byte code)
|
||||
{
|
||||
return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a VCP code has a known name
|
||||
/// </summary>
|
||||
public static bool HasName(byte code) => CodeNames.ContainsKey(code);
|
||||
|
||||
/// <summary>
|
||||
/// Get all known VCP codes
|
||||
/// </summary>
|
||||
public static IEnumerable<byte> GetAllKnownCodes() => CodeNames.Keys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides human-readable names for VCP code values based on MCCS standard
|
||||
/// </summary>
|
||||
public static class VcpValueNames
|
||||
{
|
||||
// Dictionary<VcpCode, Dictionary<Value, Name>>
|
||||
private static readonly Dictionary<byte, Dictionary<int, string>> ValueNames = new()
|
||||
{
|
||||
// 0x14: Select Color Preset
|
||||
[0x14] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "sRGB",
|
||||
[0x02] = "Display Native",
|
||||
[0x03] = "4000K",
|
||||
[0x04] = "5000K",
|
||||
[0x05] = "6500K",
|
||||
[0x06] = "7500K",
|
||||
[0x08] = "9300K",
|
||||
[0x09] = "10000K",
|
||||
[0x0A] = "11500K",
|
||||
[0x0B] = "User 1",
|
||||
[0x0C] = "User 2",
|
||||
[0x0D] = "User 3",
|
||||
},
|
||||
|
||||
// 0x60: Input Source
|
||||
[0x60] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "VGA-1",
|
||||
[0x02] = "VGA-2",
|
||||
[0x03] = "DVI-1",
|
||||
[0x04] = "DVI-2",
|
||||
[0x05] = "Composite Video 1",
|
||||
[0x06] = "Composite Video 2",
|
||||
[0x07] = "S-Video-1",
|
||||
[0x08] = "S-Video-2",
|
||||
[0x09] = "Tuner-1",
|
||||
[0x0A] = "Tuner-2",
|
||||
[0x0B] = "Tuner-3",
|
||||
[0x0C] = "Component Video 1",
|
||||
[0x0D] = "Component Video 2",
|
||||
[0x0E] = "Component Video 3",
|
||||
[0x0F] = "DisplayPort-1",
|
||||
[0x10] = "DisplayPort-2",
|
||||
[0x11] = "HDMI-1",
|
||||
[0x12] = "HDMI-2",
|
||||
[0x1B] = "USB-C",
|
||||
},
|
||||
|
||||
// 0xD6: Power Mode
|
||||
[0xD6] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "On",
|
||||
[0x02] = "Standby",
|
||||
[0x03] = "Suspend",
|
||||
[0x04] = "Off (DPM)",
|
||||
[0x05] = "Off (Hard)",
|
||||
},
|
||||
|
||||
// 0x8D: Audio Mute
|
||||
[0x8D] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "Muted",
|
||||
[0x02] = "Unmuted",
|
||||
},
|
||||
|
||||
// 0xDC: Display Application
|
||||
[0xDC] = new Dictionary<int, string>
|
||||
{
|
||||
[0x00] = "Standard/Default",
|
||||
[0x01] = "Productivity",
|
||||
[0x02] = "Mixed",
|
||||
[0x03] = "Movie",
|
||||
[0x04] = "User Defined",
|
||||
[0x05] = "Games",
|
||||
[0x06] = "Sports",
|
||||
[0x07] = "Professional (calibration)",
|
||||
[0x08] = "Standard/Default with intermediate power consumption",
|
||||
[0x09] = "Standard/Default with low power consumption",
|
||||
[0x0A] = "Demonstration",
|
||||
[0xF0] = "Dynamic Contrast",
|
||||
},
|
||||
|
||||
// 0xCC: OSD Language
|
||||
[0xCC] = new Dictionary<int, string>
|
||||
{
|
||||
[0x01] = "Chinese (traditional, Hantai)",
|
||||
[0x02] = "English",
|
||||
[0x03] = "French",
|
||||
[0x04] = "German",
|
||||
[0x05] = "Italian",
|
||||
[0x06] = "Japanese",
|
||||
[0x07] = "Korean",
|
||||
[0x08] = "Portuguese (Portugal)",
|
||||
[0x09] = "Russian",
|
||||
[0x0A] = "Spanish",
|
||||
[0x0B] = "Swedish",
|
||||
[0x0C] = "Turkish",
|
||||
[0x0D] = "Chinese (simplified, Kantai)",
|
||||
[0x0E] = "Portuguese (Brazil)",
|
||||
[0x0F] = "Arabic",
|
||||
[0x10] = "Bulgarian",
|
||||
[0x11] = "Croatian",
|
||||
[0x12] = "Czech",
|
||||
[0x13] = "Danish",
|
||||
[0x14] = "Dutch",
|
||||
[0x15] = "Estonian",
|
||||
[0x16] = "Finnish",
|
||||
[0x17] = "Greek",
|
||||
[0x18] = "Hebrew",
|
||||
[0x19] = "Hindi",
|
||||
[0x1A] = "Hungarian",
|
||||
[0x1B] = "Latvian",
|
||||
[0x1C] = "Lithuanian",
|
||||
[0x1D] = "Norwegian",
|
||||
[0x1E] = "Polish",
|
||||
[0x1F] = "Romanian",
|
||||
[0x20] = "Serbian",
|
||||
[0x21] = "Slovak",
|
||||
[0x22] = "Slovenian",
|
||||
[0x23] = "Thai",
|
||||
[0x24] = "Ukrainian",
|
||||
[0x25] = "Vietnamese",
|
||||
},
|
||||
|
||||
// 0x62: Audio Speaker Volume
|
||||
[0x62] = new Dictionary<int, string>
|
||||
{
|
||||
[0x00] = "Mute",
|
||||
|
||||
// Other values are continuous
|
||||
},
|
||||
|
||||
// 0xDB: Image Mode (Dell monitors)
|
||||
[0xDB] = new Dictionary<int, string>
|
||||
{
|
||||
[0x00] = "Standard",
|
||||
[0x01] = "Multimedia",
|
||||
[0x02] = "Movie",
|
||||
[0x03] = "Game",
|
||||
[0x04] = "Sports",
|
||||
[0x05] = "Color Temperature",
|
||||
[0x06] = "Custom Color",
|
||||
[0x07] = "ComfortView",
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get human-readable name for a VCP value
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
|
||||
/// <param name="value">Value to translate</param>
|
||||
/// <returns>Name string like "sRGB" or null if unknown</returns>
|
||||
public static string? GetName(byte vcpCode, int value)
|
||||
{
|
||||
if (ValueNames.TryGetValue(vcpCode, out var codeValues))
|
||||
{
|
||||
if (codeValues.TryGetValue(value, out var name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get formatted display name for a VCP value (with hex value in parentheses)
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
|
||||
/// <param name="value">Value to translate</param>
|
||||
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
|
||||
public static string GetFormattedName(byte vcpCode, int value)
|
||||
{
|
||||
var name = GetName(vcpCode, value);
|
||||
if (name != null)
|
||||
{
|
||||
return $"{name} (0x{value:X2})";
|
||||
}
|
||||
|
||||
return $"0x{value:X2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a VCP code has value name mappings
|
||||
/// </summary>
|
||||
/// <param name="vcpCode">VCP code to check</param>
|
||||
/// <returns>True if value names are available</returns>
|
||||
public static bool HasValueNames(byte vcpCode) => ValueNames.ContainsKey(vcpCode);
|
||||
}
|
||||
}
|
||||
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,378 @@
|
||||
// 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 readonly Timer _saveTimer;
|
||||
|
||||
private bool _disposed;
|
||||
private bool _isDirty;
|
||||
private const int SaveDebounceMs = 2000; // Save 2 seconds after last update
|
||||
|
||||
/// <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 string? CapabilitiesRaw { 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);
|
||||
|
||||
// Initialize debounce timer (disabled initially)
|
||||
_saveTimer = new Timer(OnSaveTimerElapsed, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
// Load existing state if available
|
||||
LoadStateFromDisk();
|
||||
|
||||
Logger.LogInfo($"MonitorStateManager initialized with debounced-save strategy (debounce: {SaveDebounceMs}ms), state file: {_stateFilePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timer callback to save state when dirty
|
||||
/// </summary>
|
||||
private async void OnSaveTimerElapsed(object? state)
|
||||
{
|
||||
bool shouldSave = false;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_isDirty && !_disposed)
|
||||
{
|
||||
shouldSave = true;
|
||||
_isDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSave)
|
||||
{
|
||||
await SaveStateToDiskAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor parameter and schedule debounced save to disk.
|
||||
/// Uses HardwareId as the stable key.
|
||||
/// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
|
||||
/// </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;
|
||||
}
|
||||
|
||||
// Mark dirty and schedule debounced save
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
// Reset timer to debounce rapid updates (e.g., during slider drag)
|
||||
_saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor capabilities and schedule save.
|
||||
/// Capabilities are saved separately to avoid frequent writes.
|
||||
/// </summary>
|
||||
public void UpdateMonitorCapabilities(string hardwareId, string? capabilitiesRaw)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
Logger.LogWarning($"Cannot update capabilities: HardwareId is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Get or create state entry
|
||||
if (!_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
state = new MonitorState();
|
||||
_states[hardwareId] = state;
|
||||
}
|
||||
|
||||
// Update capabilities
|
||||
state.CapabilitiesRaw = capabilitiesRaw;
|
||||
|
||||
// Mark dirty and schedule save
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
// Schedule save
|
||||
_saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
|
||||
|
||||
Logger.LogInfo($"[State] Updated capabilities for monitor HardwareId='{hardwareId}' (length: {capabilitiesRaw?.Length ?? 0})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to update monitor capabilities: {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>
|
||||
/// Get saved capabilities for a monitor using HardwareId
|
||||
/// </summary>
|
||||
public string? GetMonitorCapabilities(string hardwareId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
return state.CapabilitiesRaw;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
CapabilitiesRaw = entry.CapabilitiesRaw,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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 (async).
|
||||
/// Called by timer after debounce period or on dispose to flush pending changes.
|
||||
/// </summary>
|
||||
private async Task SaveStateToDiskAsync()
|
||||
{
|
||||
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,
|
||||
CapabilitiesRaw = state.CapabilitiesRaw,
|
||||
LastUpdated = now,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Write to disk asynchronously
|
||||
var json = JsonSerializer.Serialize(stateFile, AppJsonContext.Default.MonitorStateFile);
|
||||
await File.WriteAllTextAsync(_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;
|
||||
}
|
||||
|
||||
// Stop the timer first
|
||||
_saveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
bool wasDirty = false;
|
||||
lock (_lock)
|
||||
{
|
||||
wasDirty = _isDirty;
|
||||
_disposed = true;
|
||||
_isDirty = false;
|
||||
}
|
||||
|
||||
// Flush any pending changes before disposing
|
||||
if (wasDirty)
|
||||
{
|
||||
Logger.LogInfo("Flushing pending state changes before dispose");
|
||||
SaveStateToDiskAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_saveTimer?.Dispose();
|
||||
|
||||
Logger.LogInfo("MonitorStateManager disposed");
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 Microsoft.UI.Dispatching;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for waiting on Windows Named Events (Awake pattern)
|
||||
/// Based on Peek.UI implementation
|
||||
/// </summary>
|
||||
public static class NativeEventWaiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Wait for a Windows Event in a background thread and invoke callback on UI thread when signaled
|
||||
/// </summary>
|
||||
/// <param name="eventName">Name of the Windows Event to wait for</param>
|
||||
/// <param name="callback">Callback to invoke when event is signaled</param>
|
||||
/// <param name="cancellationToken">Token to cancel the wait loop</param>
|
||||
public static void WaitForEventLoop(string eventName, Action callback, CancellationToken cancellationToken)
|
||||
{
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
var t = new Thread(() =>
|
||||
{
|
||||
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (eventHandle.WaitOne(500))
|
||||
{
|
||||
dispatcherQueue.TryEnqueue(() => callback());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
t.IsBackground = true;
|
||||
t.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/modules/powerdisplay/PowerDisplay/Helpers/ProfileManager.cs
Normal file
159
src/modules/powerdisplay/PowerDisplay/Helpers/ProfileManager.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
// 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.Linq;
|
||||
using System.Text.Json;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using PowerDisplay.Configuration;
|
||||
using PowerDisplay.Serialization;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages PowerDisplay profiles storage and retrieval
|
||||
/// </summary>
|
||||
public class ProfileManager
|
||||
{
|
||||
private readonly string _profilesFilePath;
|
||||
private readonly object _lock = new object();
|
||||
private PowerDisplayProfiles? _cachedProfiles;
|
||||
|
||||
public ProfileManager()
|
||||
{
|
||||
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
|
||||
|
||||
if (!Directory.Exists(powerToysPath))
|
||||
{
|
||||
Directory.CreateDirectory(powerToysPath);
|
||||
}
|
||||
|
||||
_profilesFilePath = Path.Combine(powerToysPath, "profiles.json");
|
||||
|
||||
Logger.LogInfo($"ProfileManager initialized, profiles file: {_profilesFilePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads profiles from disk
|
||||
/// </summary>
|
||||
public PowerDisplayProfiles LoadProfiles()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_profilesFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(_profilesFilePath);
|
||||
var profiles = JsonSerializer.Deserialize(json, AppJsonContext.Default.PowerDisplayProfiles);
|
||||
|
||||
if (profiles != null)
|
||||
{
|
||||
// Clean up any legacy Custom profiles
|
||||
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_cachedProfiles = profiles;
|
||||
Logger.LogInfo($"Loaded {profiles.Profiles.Count} profiles");
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInfo("No profiles file found, creating default");
|
||||
_cachedProfiles = new PowerDisplayProfiles();
|
||||
return _cachedProfiles;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load profiles: {ex.Message}");
|
||||
_cachedProfiles = new PowerDisplayProfiles();
|
||||
return _cachedProfiles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves profiles to disk
|
||||
/// </summary>
|
||||
public void SaveProfiles(PowerDisplayProfiles profiles)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Clean up any Custom profiles before saving
|
||||
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
profiles.LastUpdated = DateTime.UtcNow;
|
||||
var json = JsonSerializer.Serialize(profiles, AppJsonContext.Default.PowerDisplayProfiles);
|
||||
File.WriteAllText(_profilesFilePath, json);
|
||||
_cachedProfiles = profiles;
|
||||
|
||||
Logger.LogInfo($"Saved {profiles.Profiles.Count} profiles");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save profiles: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a profile
|
||||
/// </summary>
|
||||
public void AddOrUpdateProfile(PowerDisplayProfile profile)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (profile == null || !profile.IsValid())
|
||||
{
|
||||
Logger.LogWarning("Cannot add invalid profile");
|
||||
return;
|
||||
}
|
||||
|
||||
var profiles = LoadProfiles();
|
||||
profiles.SetProfile(profile);
|
||||
SaveProfiles(profiles);
|
||||
|
||||
Logger.LogInfo($"Profile '{profile.Name}' added/updated with {profile.MonitorSettings.Count} monitors");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a profile by name
|
||||
/// </summary>
|
||||
public bool RemoveProfile(string profileName)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var profiles = LoadProfiles();
|
||||
bool removed = profiles.RemoveProfile(profileName);
|
||||
|
||||
if (removed)
|
||||
{
|
||||
SaveProfiles(profiles);
|
||||
Logger.LogInfo($"Profile '{profileName}' removed");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Profile '{profileName}' not found or cannot be removed");
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all profiles
|
||||
/// </summary>
|
||||
public List<PowerDisplayProfile> GetAllProfiles()
|
||||
{
|
||||
var profiles = LoadProfiles();
|
||||
return profiles.Profiles.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
src/modules/powerdisplay/PowerDisplay/Helpers/SimpleDebouncer.cs
Normal file
127
src/modules/powerdisplay/PowerDisplay/Helpers/SimpleDebouncer.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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 void Debounce(Func<Task> action)
|
||||
{
|
||||
_ = DebounceAsync(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debounce a synchronous action
|
||||
/// </summary>
|
||||
public void Debounce(Action action)
|
||||
{
|
||||
_ = DebounceAsync(() =>
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task DebounceAsync(Func<Task> action)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationTokenSource cts;
|
||||
CancellationTokenSource? oldCts = null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Store old CTS to dispose later
|
||||
oldCts = _cts;
|
||||
|
||||
// Create new CTS
|
||||
_cts = new CancellationTokenSource();
|
||||
cts = _cts;
|
||||
}
|
||||
|
||||
// Dispose old CTS outside the lock to avoid blocking
|
||||
if (oldCts != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
oldCts.Cancel();
|
||||
oldCts.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore disposal errors
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Wait for quiet period
|
||||
await Task.Delay(_delayMs, cts.Token).ConfigureAwait(false);
|
||||
|
||||
// Execute action if not cancelled
|
||||
if (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await action().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when debouncing - a newer call cancelled this one
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Debounced action failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_disposed = true;
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,718 @@
|
||||
// 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.Helpers;
|
||||
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 MonitorDiscoveryHelper _discoveryHelper;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public DdcCiController()
|
||||
{
|
||||
_discoveryHelper = new MonitorDiscoveryHelper();
|
||||
}
|
||||
|
||||
public string Name => "DDC/CI Monitor Controller";
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
var physicalHandle = GetPhysicalHandle(monitor);
|
||||
return physicalHandle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(physicalHandle);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor brightness using VCP code 0x10
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
var physicalHandle = GetPhysicalHandle(monitor);
|
||||
if (physicalHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Invalid physical handle");
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// First try high-level API
|
||||
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint minBrightness, out uint currentBrightness, out uint maxBrightness))
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Brightness via high-level API: {currentBrightness}/{maxBrightness}");
|
||||
return new BrightnessInfo((int)currentBrightness, (int)minBrightness, (int)maxBrightness);
|
||||
}
|
||||
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out uint current, out uint max))
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Brightness via 0x10: {current}/{max}");
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
Logger.LogWarning($"[{monitor.Id}] Failed to read brightness");
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor brightness using VCP code 0x10
|
||||
/// </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)
|
||||
{
|
||||
Logger.LogWarning($"[{monitor.Id}] Cannot read current brightness");
|
||||
return MonitorOperationResult.Failure("Cannot read current brightness");
|
||||
}
|
||||
|
||||
uint targetValue = (uint)currentInfo.FromPercentage(brightness);
|
||||
|
||||
// First try high-level API
|
||||
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
|
||||
{
|
||||
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via high-level API");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TrySetVCPFeature(physicalHandle, VcpCodeBrightness, targetValue))
|
||||
{
|
||||
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via 0x10");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
Logger.LogError($"[{monitor.Id}] Failed to set brightness, error: {lastError}");
|
||||
return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{monitor.Id}] Exception setting brightness: {ex.Message}");
|
||||
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 using VCP code 0x14 (Select Color Preset)
|
||||
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogDebug($"[{monitor.Id}] Invalid handle for color temperature read");
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// Try VCP code 0x14 (Select Color Preset)
|
||||
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, out uint current, out uint max))
|
||||
{
|
||||
var presetName = VcpValueNames.GetFormattedName(0x14, (int)current);
|
||||
Logger.LogInfo($"[{monitor.Id}] Color temperature via 0x14: {presetName}");
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
Logger.LogWarning($"[{monitor.Id}] Failed to read color temperature (0x14 not supported)");
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor to control</param>
|
||||
/// <param name="colorTemperature">VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
public async Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Validate value is in supported list if capabilities available
|
||||
var capabilities = monitor.VcpCapabilitiesInfo;
|
||||
if (capabilities != null && capabilities.SupportsVcpCode(0x14))
|
||||
{
|
||||
var supportedValues = capabilities.GetSupportedValues(0x14);
|
||||
if (supportedValues?.Count > 0 && !supportedValues.Contains(colorTemperature))
|
||||
{
|
||||
var supportedList = string.Join(", ", supportedValues.Select(v => $"0x{v:X2}"));
|
||||
Logger.LogWarning($"[{monitor.Id}] Color preset 0x{colorTemperature:X2} not in supported list: [{supportedList}]");
|
||||
return MonitorOperationResult.Failure($"Color preset 0x{colorTemperature:X2} not supported by monitor");
|
||||
}
|
||||
}
|
||||
|
||||
// Set VCP 0x14 value
|
||||
var presetName = VcpValueNames.GetFormattedName(0x14, colorTemperature);
|
||||
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, (uint)colorTemperature))
|
||||
{
|
||||
Logger.LogInfo($"[{monitor.Id}] Set color temperature to {presetName} via 0x14");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
Logger.LogError($"[{monitor.Id}] Failed to set color temperature, error: {lastError}");
|
||||
return MonitorOperationResult.Failure($"Failed to set color temperature via DDC/CI", (int)lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{monitor.Id}] Exception setting color temperature: {ex.Message}");
|
||||
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor capabilities string with retry logic
|
||||
/// </summary>
|
||||
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Step 1: Get capabilities string length (retry up to 3 times)
|
||||
uint length = 0;
|
||||
const int lengthMaxRetries = 3;
|
||||
for (int i = 0; i < lengthMaxRetries; i++)
|
||||
{
|
||||
if (GetCapabilitiesStringLength(monitor.Handle, out length) && length > 0)
|
||||
{
|
||||
Logger.LogDebug($"Got capabilities length: {length} (attempt {i + 1})");
|
||||
break;
|
||||
}
|
||||
|
||||
if (i < lengthMaxRetries - 1)
|
||||
{
|
||||
Thread.Sleep(100); // 100ms delay between retries
|
||||
}
|
||||
}
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
Logger.LogWarning("Failed to get capabilities string length after retries");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Step 2: Get actual capabilities string (retry up to 5 times)
|
||||
const int capsMaxRetries = 5;
|
||||
for (int i = 0; i < capsMaxRetries; i++)
|
||||
{
|
||||
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
|
||||
try
|
||||
{
|
||||
if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length))
|
||||
{
|
||||
var capsString = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(capsString))
|
||||
{
|
||||
Logger.LogInfo($"Got capabilities string (length: {capsString.Length}, attempt: {i + 1})");
|
||||
return capsString;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
|
||||
if (i < capsMaxRetries - 1)
|
||||
{
|
||||
Thread.Sleep(100); // 100ms delay between retries
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogWarning("Failed to get capabilities string after retries");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Exception getting 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
|
||||
var displayDevices = DdcCiNative.GetAllDisplayDevices();
|
||||
|
||||
// Also get hardware info for friendly names
|
||||
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
|
||||
|
||||
// Enumerate all monitors
|
||||
var monitorHandles = new List<IntPtr>();
|
||||
|
||||
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
|
||||
{
|
||||
monitorHandles.Add(hMonitor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Get physical monitors with retry logic for NULL handle workaround
|
||||
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
|
||||
|
||||
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Failed to get physical monitors for hMonitor 0x{hMonitor:X} after retries");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match physical monitors with DisplayDeviceInfo
|
||||
// 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)
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
|
||||
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>
|
||||
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles
|
||||
/// </summary>
|
||||
/// <param name="hMonitor">Handle to the monitor</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Array of physical monitors, or null if failed after retries</returns>
|
||||
private async Task<PHYSICAL_MONITOR[]?> GetPhysicalMonitorsWithRetryAsync(
|
||||
IntPtr hMonitor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 200;
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
if (attempt > 0)
|
||||
{
|
||||
await Task.Delay(retryDelayMs, cancellationToken);
|
||||
}
|
||||
|
||||
var monitors = _discoveryHelper.GetPhysicalMonitors(hMonitor);
|
||||
|
||||
var validationResult = ValidatePhysicalMonitors(monitors, attempt, maxRetries);
|
||||
|
||||
if (validationResult.IsValid)
|
||||
{
|
||||
return monitors;
|
||||
}
|
||||
|
||||
if (validationResult.ShouldRetry)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Last attempt failed, return what we have
|
||||
return monitors;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate physical monitors array for null handles
|
||||
/// </summary>
|
||||
/// <returns>Tuple indicating if valid and if should retry</returns>
|
||||
private (bool IsValid, bool ShouldRetry) ValidatePhysicalMonitors(
|
||||
PHYSICAL_MONITOR[]? monitors,
|
||||
int attempt,
|
||||
int maxRetries)
|
||||
{
|
||||
if (monitors == null || monitors.Length == 0)
|
||||
{
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null/empty on attempt {attempt + 1}, will retry");
|
||||
}
|
||||
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
bool hasNullHandle = HasAnyNullHandles(monitors, out int nullIndex);
|
||||
|
||||
if (!hasNullHandle)
|
||||
{
|
||||
return (true, false); // Valid, don't retry
|
||||
}
|
||||
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Physical monitor [{nullIndex}] has NULL handle on attempt {attempt + 1}, will retry");
|
||||
return (false, true); // Invalid, should retry
|
||||
}
|
||||
|
||||
Logger.LogWarning($"DDC: NULL handle still present after {maxRetries} attempts, continuing anyway");
|
||||
return (false, false); // Invalid but no more retries
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if any physical monitor has a NULL handle
|
||||
/// </summary>
|
||||
/// <param name="monitors">Array of physical monitors to check</param>
|
||||
/// <param name="nullIndex">Output index of first NULL handle found, or -1 if none</param>
|
||||
/// <returns>True if any NULL handle found</returns>
|
||||
private bool HasAnyNullHandles(PHYSICAL_MONITOR[] monitors, out int nullIndex)
|
||||
{
|
||||
for (int i = 0; i < monitors.Length; i++)
|
||||
{
|
||||
if (monitors[i].HPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
nullIndex = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
nullIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <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(
|
||||
async () =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get current value to determine range
|
||||
var currentInfo = await GetVcpFeatureAsync(monitor, vcpCode);
|
||||
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 using VCP code 0x10 only
|
||||
/// </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 code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, 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();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
498
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
498
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
@@ -0,0 +1,498 @@
|
||||
// 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;
|
||||
|
||||
// Type aliases for Windows API naming conventions compatibility
|
||||
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>
|
||||
/// Display device information class
|
||||
/// </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 native API wrapper
|
||||
/// </summary>
|
||||
public static class DdcCiNative
|
||||
{
|
||||
// Display Configuration constants
|
||||
public const uint QdcAllPaths = 0x00000001;
|
||||
|
||||
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||
|
||||
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
|
||||
|
||||
// Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Safe wrapper for getting VCP feature value
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
|
||||
/// <param name="vcpCode">VCP code</param>
|
||||
/// <param name="currentValue">Current value</param>
|
||||
/// <param name="maxValue">Maximum value</param>
|
||||
/// <returns>True if successful</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>
|
||||
/// Safe wrapper for setting VCP feature value
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
|
||||
/// <param name="vcpCode">VCP code</param>
|
||||
/// <param name="value">New value</param>
|
||||
/// <returns>True if successful</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>
|
||||
/// Safe wrapper for getting advanced brightness information
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
|
||||
/// <param name="minBrightness">Minimum brightness</param>
|
||||
/// <param name="currentBrightness">Current brightness</param>
|
||||
/// <param name="maxBrightness">Maximum brightness</param>
|
||||
/// <returns>True if successful</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>
|
||||
/// Get all display device information using EnumDisplayDevices API
|
||||
/// </summary>
|
||||
/// <returns>List of display device information</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,248 @@
|
||||
// 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
|
||||
{
|
||||
public MonitorDiscoveryHelper()
|
||||
{
|
||||
}
|
||||
|
||||
/// <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(),
|
||||
CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
IsAvailable = true,
|
||||
Handle = physicalMonitor.HPhysicalMonitor,
|
||||
DeviceKey = deviceKey,
|
||||
Capabilities = MonitorCapabilities.DdcCi,
|
||||
ConnectionType = "External",
|
||||
CommunicationMethod = "DDC/CI",
|
||||
Manufacturer = ExtractManufacturer(name),
|
||||
CapabilitiesStatus = "unknown",
|
||||
};
|
||||
|
||||
// Note: Feature detection (brightness, contrast, color temp, volume) is now done
|
||||
// in MonitorManager after capabilities string is retrieved and parsed.
|
||||
// This ensures we rely on capabilities data rather than trial-and-error probing.
|
||||
return monitor;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"DDC: CreateMonitorFromPhysical exception: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current brightness using VCP code 0x10 only
|
||||
/// </summary>
|
||||
private BrightnessInfo GetCurrentBrightness(IntPtr handle)
|
||||
{
|
||||
// Try high-level API first
|
||||
if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||
}
|
||||
|
||||
// Try VCP code 0x10 (standard brightness)
|
||||
if (DdcCiNative.TryGetVCPFeature(handle, VcpCodeBrightness, 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,173 @@
|
||||
// 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
|
||||
/// </summary>
|
||||
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||
{
|
||||
// Mapping: deviceKey -> physical handle
|
||||
private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new();
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Get physical handle for monitor using stable deviceKey
|
||||
/// </summary>
|
||||
public IntPtr GetPhysicalHandle(Monitor monitor)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
291
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
291
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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 (0x10)
|
||||
/// Standard VESA MCCS brightness control.
|
||||
/// This is the ONLY brightness code used by PowerDisplay.
|
||||
/// </summary>
|
||||
public const byte VcpCodeBrightness = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Contrast (0x12)
|
||||
/// Standard VESA MCCS contrast control.
|
||||
/// </summary>
|
||||
public const byte VcpCodeContrast = 0x12;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio Speaker Volume (0x62)
|
||||
/// Standard VESA MCCS volume control for monitors with built-in speakers.
|
||||
/// </summary>
|
||||
public const byte VcpCodeVolume = 0x62;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio mute (0x8D)
|
||||
/// </summary>
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Gamma correction (0x72)
|
||||
/// </summary>
|
||||
public const byte VcpCodeGamma = 0x72;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Select Color Preset (0x14)
|
||||
/// Standard VESA MCCS color temperature preset selection.
|
||||
/// Supports discrete values like: 0x01=sRGB, 0x04=5000K, 0x05=6500K, 0x08=9300K.
|
||||
/// This is the standard method for color temperature control.
|
||||
/// </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;
|
||||
|
||||
// Type aliases for Windows API naming conventions compatibility
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
namespace PowerDisplay.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Native delegate type definitions
|
||||
/// </summary>
|
||||
public static class NativeDelegates
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor enumeration procedure delegate
|
||||
/// </summary>
|
||||
/// <param name="hMonitor">Monitor handle</param>
|
||||
/// <param name="hdcMonitor">Monitor device context</param>
|
||||
/// <param name="lprcMonitor">Pointer to monitor rectangle</param>
|
||||
/// <param name="dwData">User data</param>
|
||||
/// <returns>True to continue enumeration</returns>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||
|
||||
/// <summary>
|
||||
/// Thread start routine delegate
|
||||
/// </summary>
|
||||
/// <param name="lpParameter">Thread parameter</param>
|
||||
/// <returns>Thread exit code</returns>
|
||||
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
|
||||
}
|
||||
445
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
445
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
@@ -0,0 +1,445 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
272
src/modules/powerdisplay/PowerDisplay/Native/PInvoke.cs
Normal file
272
src/modules/powerdisplay/PowerDisplay/Native/PInvoke.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
// 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);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool IsWindowVisible(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);
|
||||
|
||||
// ==================== 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,448 @@
|
||||
// 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)";
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (monitor.CommunicationMethod != "WMI")
|
||||
{
|
||||
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,
|
||||
|
||||
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>
|
||||
/// Disable window moving and resizing functionality
|
||||
/// </summary>
|
||||
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
|
||||
{
|
||||
// Get current window style
|
||||
#if WIN64
|
||||
int style = (int)GetWindowLong(hWnd, GwlStyle);
|
||||
#else
|
||||
int style = GetWindowLong(hWnd, GwlStyle);
|
||||
#endif
|
||||
|
||||
// Remove resizable borders, title bar, and system menu
|
||||
style &= ~WsThickframe;
|
||||
style &= ~WsMaximizebox;
|
||||
style &= ~WsMinimizebox;
|
||||
style &= ~WsCaption; // Remove entire title bar
|
||||
style &= ~WsSysmenu; // Remove system menu
|
||||
|
||||
// Set new window style
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlStyle, style);
|
||||
#endif
|
||||
|
||||
// Get extended style and remove related borders
|
||||
#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
|
||||
|
||||
// Refresh window frame
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize | SwpFramechanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set whether window is topmost
|
||||
/// </summary>
|
||||
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
|
||||
{
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
topmost ? HwndTopmost : HwndNotopmost,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show or hide window
|
||||
/// </summary>
|
||||
public static void ShowWindow(IntPtr hWnd, bool show)
|
||||
{
|
||||
PInvoke.ShowWindow(hWnd, show ? SwShow : SwHide);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimize window
|
||||
/// </summary>
|
||||
public static void MinimizeWindow(IntPtr hWnd)
|
||||
{
|
||||
PInvoke.ShowWindow(hWnd, SwMinimize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore window
|
||||
/// </summary>
|
||||
public static void RestoreWindow(IntPtr hWnd)
|
||||
{
|
||||
PInvoke.ShowWindow(hWnd, SwRestore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hide window from taskbar
|
||||
/// </summary>
|
||||
public static void HideFromTaskbar(IntPtr hWnd)
|
||||
{
|
||||
// Get current extended style
|
||||
#if WIN64
|
||||
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||
#else
|
||||
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||
#endif
|
||||
|
||||
// Add WS_EX_TOOLWINDOW style to hide window from taskbar
|
||||
exStyle |= WsExToolwindow;
|
||||
|
||||
// Set new extended style
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
#endif
|
||||
|
||||
// Refresh window frame
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize | SwpFramechanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
87
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
@@ -0,0 +1,87 @@
|
||||
<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>
|
||||
<!-- 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="WinUIEx" />
|
||||
<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="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<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,15 @@
|
||||
<?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:toolkit="using:CommunityToolkit.WinUI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- WinUI 3 System Resources -->
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,334 @@
|
||||
// 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;
|
||||
using PowerToys.Interop;
|
||||
|
||||
namespace PowerDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// PowerDisplay application main class
|
||||
/// </summary>
|
||||
#pragma warning disable CA1001 // CancellationTokenSource is disposed in Shutdown/ForceExit methods
|
||||
public partial class App : Application
|
||||
#pragma warning restore CA1001
|
||||
{
|
||||
// Windows Event names (from shared_constants.h)
|
||||
private const string ShowPowerDisplayEvent = "Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
|
||||
private const string TogglePowerDisplayEvent = "Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
|
||||
private const string TerminatePowerDisplayEvent = "Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||
private const string RefreshMonitorsEvent = "Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
|
||||
private const string SettingsUpdatedEvent = "Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
|
||||
private const string ApplyColorTemperatureEvent = "Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
|
||||
private const string ApplyProfileEvent = "Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
|
||||
|
||||
private Window? _mainWindow;
|
||||
private int _powerToysRunnerPid;
|
||||
|
||||
public App(int runnerPid)
|
||||
{
|
||||
_powerToysRunnerPid = runnerPid;
|
||||
|
||||
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
|
||||
{
|
||||
// Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs
|
||||
// PID is already parsed in Program.cs and passed to constructor
|
||||
|
||||
// Set up Windows Events monitoring (Awake pattern)
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
ShowPowerDisplayEvent,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("[EVENT] Show event received");
|
||||
Logger.LogInfo($"[EVENT] _mainWindow is null: {_mainWindow == null}");
|
||||
Logger.LogInfo($"[EVENT] _mainWindow type: {_mainWindow?.GetType().Name}");
|
||||
Logger.LogInfo($"[EVENT] Current thread ID: {Environment.CurrentManagedThreadId}");
|
||||
|
||||
// Direct call - NativeEventWaiter already marshalled to UI thread
|
||||
// No need for double DispatcherQueue.TryEnqueue
|
||||
if (_mainWindow is MainWindow mainWindow)
|
||||
{
|
||||
Logger.LogInfo("[EVENT] Calling ShowWindow directly");
|
||||
mainWindow.ShowWindow();
|
||||
Logger.LogInfo("[EVENT] ShowWindow returned");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"[EVENT] _mainWindow type mismatch, actual type: {_mainWindow?.GetType().Name}");
|
||||
}
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
TogglePowerDisplayEvent,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("[EVENT] Toggle event received");
|
||||
if (_mainWindow is MainWindow mainWindow)
|
||||
{
|
||||
Logger.LogInfo("[EVENT] Calling ToggleWindow");
|
||||
mainWindow.ToggleWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"[EVENT] _mainWindow type mismatch for toggle");
|
||||
}
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
TerminatePowerDisplayEvent,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("Received terminate event - exiting immediately");
|
||||
Environment.Exit(0);
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
// Note: PowerDisplay.exe should NOT listen to RefreshMonitorsEvent
|
||||
// That event is sent BY PowerDisplay TO Settings UI for one-way notification
|
||||
// Listening to our own event would create an infinite refresh loop
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
SettingsUpdatedEvent,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("Received settings updated event");
|
||||
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
|
||||
{
|
||||
mainWindow.ViewModel.ApplySettingsFromUI();
|
||||
}
|
||||
});
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
ApplyColorTemperatureEvent,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("Received apply color temperature event");
|
||||
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
|
||||
{
|
||||
mainWindow.ViewModel.ApplyColorTemperatureFromSettings();
|
||||
}
|
||||
});
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
NativeEventWaiter.WaitForEventLoop(
|
||||
ApplyProfileEvent,
|
||||
() =>
|
||||
{
|
||||
Logger.LogInfo("Received apply profile event");
|
||||
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
|
||||
{
|
||||
mainWindow.ViewModel.ApplyProfileFromSettings();
|
||||
}
|
||||
});
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
// Monitor Runner process (backup exit mechanism)
|
||||
if (_powerToysRunnerPid > 0)
|
||||
{
|
||||
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
|
||||
|
||||
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
|
||||
Environment.Exit(0);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("PowerDisplay started in standalone mode");
|
||||
}
|
||||
|
||||
// Create main window
|
||||
_mainWindow = new MainWindow();
|
||||
|
||||
// Window visibility depends on launch mode
|
||||
bool isStandaloneMode = _powerToysRunnerPid <= 0;
|
||||
|
||||
if (isStandaloneMode)
|
||||
{
|
||||
// Standalone mode - activate and show window immediately
|
||||
_mainWindow.Activate();
|
||||
Logger.LogInfo("Window activated (standalone mode)");
|
||||
}
|
||||
else
|
||||
{
|
||||
// PowerToys mode - window remains hidden until show event received
|
||||
Logger.LogInfo("Window created, waiting for show event (PowerToys mode)");
|
||||
|
||||
// Start background initialization to scan monitors even when hidden
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Give window a moment to finish construction
|
||||
await Task.Delay(500);
|
||||
|
||||
// Trigger initialization on UI thread
|
||||
_mainWindow?.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
if (_mainWindow is MainWindow mainWindow)
|
||||
{
|
||||
await mainWindow.EnsureInitializedAsync();
|
||||
Logger.LogInfo("Background initialization completed");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowStartupError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Shutdown application (Awake pattern - simple and clean)
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
Logger.LogInfo("PowerDisplay shutting down");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
<winuiex:WindowEx
|
||||
x:Class="PowerDisplay.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:vm="using:PowerDisplay.ViewModels"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
IsAlwaysOnTop="True"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
IsShownInSwitchers="False"
|
||||
IsTitleBarVisible="False">
|
||||
<winuiex:WindowEx.Backdrop>
|
||||
<winuiex:AcrylicSystemBackdrop
|
||||
DarkFallbackColor="#1c1c1c"
|
||||
DarkLuminosityOpacity="0.96"
|
||||
DarkTintColor="#202020"
|
||||
DarkTintOpacity="0.5"
|
||||
LightFallbackColor="#EEEEEE"
|
||||
LightLuminosityOpacity="0.90"
|
||||
LightTintColor="#F3F3F3"
|
||||
LightTintOpacity="0" />
|
||||
</winuiex:WindowEx.Backdrop>
|
||||
|
||||
<Grid x:Name="RootGrid">
|
||||
<Grid.Resources>
|
||||
<Style
|
||||
x:Key="FlyoutButtonStyle"
|
||||
BasedOn="{StaticResource SubtleButtonStyle}"
|
||||
TargetType="Button">
|
||||
<Setter Property="Padding" Value="6" />
|
||||
<Setter Property="Width" Value="32" />
|
||||
<Setter Property="Height" Value="32" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
<Border x:Name="MainContainer">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="48" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Main Container with modern design -->
|
||||
<Grid
|
||||
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<StackPanel
|
||||
Margin="0,16,0,16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="16"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.IsScanning), Mode=OneWay}">
|
||||
<ProgressRing
|
||||
Width="24"
|
||||
Height="24"
|
||||
Foreground="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
IsActive="True" />
|
||||
<TextBlock
|
||||
x:Name="ScanningMonitorsTextBlock"
|
||||
x:Uid="ScanningMonitorsText"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- No Monitors State with InfoBar -->
|
||||
<InfoBar
|
||||
x:Name="NoMonitorsInfoBar"
|
||||
x:Uid="NoMonitorsText"
|
||||
IconSource="{ui:FontIconSource Glyph=}"
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind ViewModel.ShowNoMonitorsMessage, Mode=OneWay}"
|
||||
Severity="Informational" />
|
||||
|
||||
<!-- Content Area -->
|
||||
<ScrollViewer
|
||||
Padding="16,16,16,16"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
HorizontalScrollMode="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
ZoomMode="Disabled">
|
||||
<!-- Monitors List with modern card design -->
|
||||
<ItemsRepeater
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.HasMonitors), Mode=OneWay}">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Orientation="Vertical" Spacing="32" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MonitorViewModel">
|
||||
<StackPanel HorizontalAlignment="Stretch" Spacing="4">
|
||||
<!-- Monitor Name with Icon -->
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<!-- Spacing -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
x:Uid="MonitorTooltip"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="14"
|
||||
Glyph="{x:Bind MonitorIconGlyph, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<!-- Brightness Control -->
|
||||
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<!-- Spacing -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
x:Uid="BrightnessTooltip"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
<Slider
|
||||
x:Uid="BrightnessAutomation"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
|
||||
Maximum="{x:Bind MaxBrightness, Mode=OneWay}"
|
||||
Minimum="{x:Bind MinBrightness, Mode=OneWay}"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="Brightness"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
Value="{x:Bind Brightness, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Visibility="{x:Bind ConvertBoolToVisibility(ShowContrast), Mode=OneWay}" -->
|
||||
|
||||
<!-- Contrast Control -->
|
||||
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<!-- Spacing -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon
|
||||
x:Uid="ContrastTooltip"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
|
||||
<Slider
|
||||
x:Uid="ContrastAutomation"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="Contrast"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
Value="{x:Bind ContrastPercent, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Visibility="{x:Bind ConvertBoolToVisibility(ShowVolume), Mode=OneWay}"> -->
|
||||
<!-- Volume Control -->
|
||||
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="16" />
|
||||
<ColumnDefinition Width="16" />
|
||||
<!-- Spacing -->
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon
|
||||
x:Uid="VolumeTooltip"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="14"
|
||||
Glyph="" />
|
||||
<Slider
|
||||
x:Uid="VolumeAutomation"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
|
||||
Maximum="{x:Bind MaxVolume, Mode=OneWay}"
|
||||
Minimum="{x:Bind MinVolume, Mode=OneWay}"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="Volume"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
Value="{x:Bind Volume, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
<!-- Status Bar with modern design -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Margin="0,0,12,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Button
|
||||
x:Name="LinkButton"
|
||||
x:Uid="SyncAllMonitorsTooltip"
|
||||
Click="OnLinkClick"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
<Button
|
||||
x:Name="DisableButton"
|
||||
x:Uid="DisableControlTooltip"
|
||||
Click="OnDisableClick"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
<Button
|
||||
x:Name="RefreshButton"
|
||||
x:Uid="RefreshTooltip"
|
||||
Click="OnRefreshClick"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
<Button
|
||||
x:Name="SettingsBtn"
|
||||
x:Uid="SettingsTooltip"
|
||||
Padding="6"
|
||||
Click="OnSettingsClick"
|
||||
Style="{StaticResource FlyoutButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="SettingsTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
<AnimatedIcon x:Name="SearchAnimatedIcon">
|
||||
<AnimatedIcon.Source>
|
||||
<animatedVisuals:AnimatedSettingsVisualSource />
|
||||
</AnimatedIcon.Source>
|
||||
<AnimatedIcon.FallbackIconSource>
|
||||
<SymbolIconSource Symbol="Setting" />
|
||||
</AnimatedIcon.FallbackIconSource>
|
||||
</AnimatedIcon>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
@@ -0,0 +1,654 @@
|
||||
// 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.Configuration;
|
||||
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 WinUIEx;
|
||||
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 : WindowEx, IDisposable
|
||||
{
|
||||
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
|
||||
private MainViewModel _viewModel = 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();
|
||||
|
||||
// 1. Configure window immediately (synchronous, no data dependency)
|
||||
ConfigureWindow();
|
||||
|
||||
// 2. Create ViewModel immediately (lightweight object, no scanning yet)
|
||||
_viewModel = new MainViewModel();
|
||||
RootGrid.DataContext = _viewModel;
|
||||
Bindings.Update();
|
||||
|
||||
// 3. Register event handlers
|
||||
RegisterEventHandlers();
|
||||
|
||||
// 4. Start background initialization (don't wait)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await InitializeAsync();
|
||||
_hasInitialized = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Background initialization failed: {ex.Message}");
|
||||
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"MainWindow initialization failed: {ex.Message}");
|
||||
ShowError($"Unable to start main window: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register all event handlers for window and ViewModel
|
||||
/// </summary>
|
||||
private void RegisterEventHandlers()
|
||||
{
|
||||
// Window events
|
||||
this.Closed += OnWindowClosed;
|
||||
this.Activated += OnWindowActivated;
|
||||
|
||||
// ViewModel events
|
||||
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private bool _hasInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the window is properly initialized with ViewModel and data
|
||||
/// Can be called from external code (e.g., App startup) to pre-initialize in background
|
||||
/// </summary>
|
||||
public async Task EnsureInitializedAsync()
|
||||
{
|
||||
if (_hasInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for background initialization to complete
|
||||
// This is a no-op if initialization already completed
|
||||
await InitializeAsync();
|
||||
_hasInitialized = true;
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Perform monitor scanning (which internally calls ReloadMonitorSettingsAsync)
|
||||
await _viewModel.RefreshMonitorsAsync();
|
||||
|
||||
// Adjust window size after data is loaded (must run on UI thread)
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
}
|
||||
catch (WmiLight.WmiException ex)
|
||||
{
|
||||
Logger.LogError($"WMI access failed: {ex.Message}");
|
||||
DispatcherQueue.TryEnqueue(() => ShowError("Unable to access internal display control, administrator privileges may be required."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Initialization failed: {ex.Message}");
|
||||
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.StatusText = $"Error: {message}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Error (ViewModel not yet initialized): {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// Auto-hide window when it loses focus (deactivated)
|
||||
if (args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
HideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public void ShowWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
// If not initialized, log warning but continue showing
|
||||
if (!_hasInitialized)
|
||||
{
|
||||
Logger.LogWarning("Window not fully initialized yet, showing anyway");
|
||||
}
|
||||
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
AdjustWindowSizeToContent();
|
||||
|
||||
if (_appWindow != null)
|
||||
{
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("AppWindow is null, skipping window repositioning");
|
||||
}
|
||||
|
||||
this.Activate();
|
||||
WindowHelper.ShowWindow(hWnd, true);
|
||||
WindowHelpers.BringToForeground(hWnd);
|
||||
|
||||
bool isVisible = IsWindowVisible();
|
||||
if (!isVisible)
|
||||
{
|
||||
Logger.LogError("Window not visible after show attempt, forcing visibility");
|
||||
this.Activate();
|
||||
WindowHelpers.BringToForeground(hWnd);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to show window: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void HideWindow()
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
// Fallback: hide immediately if animation not found
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if window is currently visible
|
||||
/// </summary>
|
||||
/// <returns>True if window is visible, false otherwise</returns>
|
||||
public bool IsWindowVisible()
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
return PInvoke.IsWindowVisible(hWnd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle window visibility (show if hidden, hide if visible)
|
||||
/// </summary>
|
||||
public void ToggleWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
bool isVisible = IsWindowVisible();
|
||||
|
||||
if (isVisible)
|
||||
{
|
||||
HideWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowWindow();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to toggle window: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIRefreshRequested(object? sender, EventArgs e)
|
||||
{
|
||||
// Adjust window size when UI configuration changes (feature visibility toggles)
|
||||
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>
|
||||
/// Fast shutdown: skip animations and complex cleanup
|
||||
/// </summary>
|
||||
public void FastShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isExiting = true;
|
||||
|
||||
// Quick cleanup of ViewModel
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// Unsubscribe from events
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
|
||||
// Dispose immediately
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
|
||||
// Close window directly without animations
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore cleanup errors to ensure shutdown
|
||||
Logger.LogWarning($"FastShutdown error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApplication()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use fast shutdown
|
||||
FastShutdown();
|
||||
|
||||
// Call application shutdown directly
|
||||
if (Application.Current is App app)
|
||||
{
|
||||
app.Shutdown();
|
||||
}
|
||||
|
||||
// Ensure immediate exit
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ensure exit even on error
|
||||
Logger.LogError($"ExitApplication error: {ex.Message}");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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 void OnLinkClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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 void OnDisableClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 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}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettingsClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// TO DO: Open PowerDisplay settings screen
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configure window properties (synchronous, no data dependency)
|
||||
/// </summary>
|
||||
private void ConfigureWindow()
|
||||
{
|
||||
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 = AppConstants.UI.WindowWidth, 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>());
|
||||
}
|
||||
|
||||
// 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 configuration 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 = (double)AppConstants.UI.WindowWidth;
|
||||
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, AppConstants.UI.MaxWindowHeight);
|
||||
|
||||
// Check if resize is needed
|
||||
var currentSize = _appWindow.Size;
|
||||
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
|
||||
{
|
||||
_appWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = scaledHeight });
|
||||
|
||||
// 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 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 = AppConstants.UI.WindowRightMargin; // 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)
|
||||
{
|
||||
// Ignore errors when positioning window
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
break;
|
||||
|
||||
// ColorTemperature case removed - now controlled via Settings UI
|
||||
case "Contrast":
|
||||
monitorVm.ContrastPercent = finalValue;
|
||||
break;
|
||||
case "Volume":
|
||||
monitorVm.Volume = finalValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_viewModel?.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 (Awake pattern)
|
||||
int runnerPid = -1;
|
||||
|
||||
if (args.Length >= 1)
|
||||
{
|
||||
if (int.TryParse(args[0], out int parsedPid))
|
||||
{
|
||||
runnerPid = parsedPid;
|
||||
Logger.LogInfo($"PowerDisplay started with runner_pid={runnerPid}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Failed to parse PID from args[0]: {args[0]}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("PowerDisplay started without runner PID. 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);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Another instance of PowerDisplay is running. Exiting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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(MonitorInfoData))]
|
||||
[JsonSerializable(typeof(IPCMessageAction))]
|
||||
[JsonSerializable(typeof(MonitorStateFile))]
|
||||
[JsonSerializable(typeof(MonitorStateEntry))]
|
||||
[JsonSerializable(typeof(PowerDisplaySettings))]
|
||||
[JsonSerializable(typeof(ColorTemperatureOperation))]
|
||||
[JsonSerializable(typeof(ProfileOperation))]
|
||||
[JsonSerializable(typeof(PowerDisplayProfiles))]
|
||||
[JsonSerializable(typeof(PowerDisplayProfile))]
|
||||
[JsonSerializable(typeof(ProfileMonitorSetting))]
|
||||
|
||||
// MonitorInfo and related types (Settings.UI.Library)
|
||||
[JsonSerializable(typeof(MonitorInfo))]
|
||||
[JsonSerializable(typeof(VcpCodeDisplayInfo))]
|
||||
[JsonSerializable(typeof(VcpValueInfo))]
|
||||
|
||||
// Generic collection types
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(List<MonitorInfo>))]
|
||||
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
|
||||
[JsonSerializable(typeof(List<VcpValueInfo>))]
|
||||
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
|
||||
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
|
||||
|
||||
[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("capabilitiesRaw")]
|
||||
public string? CapabilitiesRaw { get; set; }
|
||||
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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.Text" xml:space="preserve">
|
||||
<value>Scanning monitors..</value>
|
||||
</data>
|
||||
<data name="NoMonitorsText.Message" xml:space="preserve">
|
||||
<value>No monitors detected</value>
|
||||
</data>
|
||||
<data name="SyncAllMonitorsTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Synchronize all monitors to the same brightness</value>
|
||||
</data>
|
||||
<data name="DisableControlTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Enable or disable brightness control</value>
|
||||
</data>
|
||||
<data name="RefreshTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Rescan connected monitors</value>
|
||||
</data>
|
||||
<data name="SettingsTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="NoMonitorsText.Message" xml:space="preserve">
|
||||
<value>No monitors detected</value>
|
||||
</data>
|
||||
<data name="MonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Monitor</value>
|
||||
</data>
|
||||
<data name="BrightnessTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Brightness</value>
|
||||
</data>
|
||||
<data name="ContrastTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Contrast</value>
|
||||
</data>
|
||||
<data name="VolumeTooltip.ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Volume</value>
|
||||
</data>
|
||||
<data name="VolumeAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Volume</value>
|
||||
</data>
|
||||
<data name="ContrastAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Contrast</value>
|
||||
</data>
|
||||
<data name="BrightnessAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Brightness</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;
|
||||
}
|
||||
}
|
||||
1406
src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
Normal file
1406
src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,548 @@
|
||||
// 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.Configuration;
|
||||
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(AppConstants.UI.SliderDebounceDelayMs);
|
||||
private readonly SimpleDebouncer _contrastDebouncer = new(AppConstants.UI.SliderDebounceDelayMs);
|
||||
private readonly SimpleDebouncer _volumeDebouncer = new(AppConstants.UI.SliderDebounceDelayMs);
|
||||
|
||||
private int _brightness;
|
||||
private int _contrast;
|
||||
private int _volume;
|
||||
private bool _isAvailable;
|
||||
|
||||
// Visibility settings (controlled by Settings UI)
|
||||
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(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;
|
||||
case nameof(ColorTemperature):
|
||||
// Update underlying monitor model
|
||||
_monitor.CurrentColorTemperature = value;
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply brightness with hardware update and state persistence.
|
||||
/// Can be called from Flyout UI (with debounce) or Settings UI/IPC (immediate).
|
||||
/// </summary>
|
||||
/// <param name="brightness">Brightness value (0-100)</param>
|
||||
/// <param name="immediate">If true, applies immediately; if false, debounces for smooth slider</param>
|
||||
/// <param name="fromProfile">If true, skip profile change detection (avoid recursion)</param>
|
||||
public async Task SetBrightnessAsync(int brightness, bool immediate = false, bool fromProfile = false)
|
||||
{
|
||||
brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness);
|
||||
|
||||
// Update UI state immediately for smooth response
|
||||
if (_brightness != brightness)
|
||||
{
|
||||
_brightness = brightness;
|
||||
OnPropertyChanged(nameof(Brightness));
|
||||
}
|
||||
|
||||
// Apply to hardware (with or without debounce)
|
||||
if (immediate)
|
||||
{
|
||||
await ApplyBrightnessToHardwareAsync(brightness, fromProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Debounce for slider smoothness (always from user interaction, not from profile)
|
||||
var capturedValue = brightness;
|
||||
_brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue, fromUserInteraction: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply contrast with hardware update and state persistence.
|
||||
/// </summary>
|
||||
public async Task SetContrastAsync(int contrast, bool immediate = false, bool fromProfile = false)
|
||||
{
|
||||
contrast = Math.Clamp(contrast, MinContrast, MaxContrast);
|
||||
|
||||
if (_contrast != contrast)
|
||||
{
|
||||
_contrast = contrast;
|
||||
OnPropertyChanged(nameof(Contrast));
|
||||
OnPropertyChanged(nameof(ContrastPercent));
|
||||
}
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
await ApplyContrastToHardwareAsync(contrast, fromProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
var capturedValue = contrast;
|
||||
_contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue, fromUserInteraction: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply volume with hardware update and state persistence.
|
||||
/// </summary>
|
||||
public async Task SetVolumeAsync(int volume, bool immediate = false, bool fromProfile = false)
|
||||
{
|
||||
volume = Math.Clamp(volume, MinVolume, MaxVolume);
|
||||
|
||||
if (_volume != volume)
|
||||
{
|
||||
_volume = volume;
|
||||
OnPropertyChanged(nameof(Volume));
|
||||
}
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
await ApplyVolumeToHardwareAsync(volume, fromProfile);
|
||||
}
|
||||
else
|
||||
{
|
||||
var capturedValue = volume;
|
||||
_volumeDebouncer.Debounce(async () => await ApplyVolumeToHardwareAsync(capturedValue, fromUserInteraction: true));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unified method to apply color temperature with hardware update and state persistence.
|
||||
/// Always immediate (no debouncing for discrete preset values).
|
||||
/// </summary>
|
||||
public async Task SetColorTemperatureAsync(int colorTemperature, bool fromProfile = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[{HardwareId}] Setting color temperature to 0x{colorTemperature:X2}");
|
||||
|
||||
var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_monitor.CurrentColorTemperature = colorTemperature;
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
OnPropertyChanged(nameof(ColorTemperaturePresetName));
|
||||
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
|
||||
|
||||
// Trigger profile change detection if from user interaction
|
||||
if (!fromProfile)
|
||||
{
|
||||
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[{HardwareId}] Color temperature applied successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set color temperature: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method - applies brightness to hardware and persists state.
|
||||
/// Unified logic for all sources (Flyout, Settings, etc.).
|
||||
/// </summary>
|
||||
private async Task ApplyBrightnessToHardwareAsync(int brightness, bool fromUserInteraction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"[{HardwareId}] Applying brightness: {brightness}%");
|
||||
|
||||
var result = await _monitorManager.SetBrightnessAsync(Id, brightness);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Brightness), brightness);
|
||||
|
||||
// Trigger profile change detection if from user interaction
|
||||
if (fromUserInteraction)
|
||||
{
|
||||
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Brightness), brightness);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set brightness: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting brightness: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method - applies contrast to hardware and persists state.
|
||||
/// </summary>
|
||||
private async Task ApplyContrastToHardwareAsync(int contrast, bool fromUserInteraction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"[{HardwareId}] Applying contrast: {contrast}%");
|
||||
|
||||
var result = await _monitorManager.SetContrastAsync(Id, contrast);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Contrast), contrast);
|
||||
|
||||
// Trigger profile change detection if from user interaction
|
||||
if (fromUserInteraction)
|
||||
{
|
||||
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Contrast), contrast);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set contrast: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting contrast: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method - applies volume to hardware and persists state.
|
||||
/// </summary>
|
||||
private async Task ApplyVolumeToHardwareAsync(int volume, bool fromUserInteraction = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"[{HardwareId}] Applying volume: {volume}%");
|
||||
|
||||
var result = await _monitorManager.SetVolumeAsync(Id, volume);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Volume), volume);
|
||||
|
||||
// Trigger profile change detection if from user interaction
|
||||
if (fromUserInteraction)
|
||||
{
|
||||
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Volume), volume);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[{HardwareId}] Failed to set volume: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{HardwareId}] Exception setting volume: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
_showContrast = monitor.SupportsContrast;
|
||||
_showVolume = monitor.SupportsVolume;
|
||||
|
||||
// Color temperature initialization removed - now controlled via Settings UI
|
||||
// The Monitor.CurrentColorTemperature stores VCP 0x14 preset value (e.g., 0x05 for 6500K)
|
||||
// and will be initialized by MonitorManager based on capabilities
|
||||
|
||||
// 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 InternalName => _monitor.Id;
|
||||
|
||||
public string Name => _monitor.Name;
|
||||
|
||||
public string Manufacturer => _monitor.Manufacturer;
|
||||
|
||||
public string CommunicationMethod => _monitor.CommunicationMethod;
|
||||
|
||||
public bool IsInternal => _monitor.CommunicationMethod == "WMI";
|
||||
|
||||
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
|
||||
|
||||
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon glyph based on communication method
|
||||
/// WMI monitors (laptop internal displays) use laptop icon, others use external monitor icon
|
||||
/// </summary>
|
||||
public string MonitorIconGlyph => _monitor.CommunicationMethod?.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true
|
||||
? "\uE7F8" // Laptop icon for WMI
|
||||
: "\uE7F4"; // External monitor icon for DDC/CI and others
|
||||
|
||||
// Monitor property ranges
|
||||
public int MinBrightness => _monitor.MinBrightness;
|
||||
|
||||
public int MaxBrightness => _monitor.MaxBrightness;
|
||||
|
||||
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 => ShowContrast || ShowVolume;
|
||||
|
||||
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)
|
||||
{
|
||||
// Use unified method with debouncing for smooth slider
|
||||
_ = SetBrightnessAsync(value, immediate: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets color temperature VCP preset value (from VCP code 0x14).
|
||||
/// Read-only in flyout UI - controlled via Settings UI.
|
||||
/// Returns the raw VCP value (e.g., 0x05 for 6500K).
|
||||
/// </summary>
|
||||
public int ColorTemperature => _monitor.CurrentColorTemperature;
|
||||
|
||||
/// <summary>
|
||||
/// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
|
||||
/// </summary>
|
||||
public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
|
||||
|
||||
public int Contrast
|
||||
{
|
||||
get => _contrast;
|
||||
set
|
||||
{
|
||||
if (_contrast != value)
|
||||
{
|
||||
// Use unified method with debouncing
|
||||
_ = SetContrastAsync(value, immediate: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (_volume != value)
|
||||
{
|
||||
// Use unified method with debouncing
|
||||
_ = SetVolumeAsync(value, immediate: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAvailable
|
||||
{
|
||||
get => _isAvailable;
|
||||
set
|
||||
{
|
||||
_isAvailable = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SetBrightnessCommand => new RelayCommand<int?>((brightness) =>
|
||||
{
|
||||
if (brightness.HasValue)
|
||||
{
|
||||
Brightness = brightness.Value;
|
||||
}
|
||||
});
|
||||
|
||||
// SetColorTemperatureCommand removed - now controlled via Settings UI
|
||||
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
|
||||
// ColorTemperaturePercent removed - now controlled via Settings UI
|
||||
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(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();
|
||||
_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,131 @@
|
||||
<?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)\</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="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="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,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;
|
||||
};
|
||||
472
src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
Normal file
472
src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
Normal file
@@ -0,0 +1,472 @@
|
||||
// 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"
|
||||
|
||||
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";
|
||||
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
|
||||
const wchar_t JSON_KEY_WIN[] = L"win";
|
||||
const wchar_t JSON_KEY_ALT[] = L"alt";
|
||||
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
|
||||
const wchar_t JSON_KEY_SHIFT[] = L"shift";
|
||||
const wchar_t JSON_KEY_CODE[] = L"code";
|
||||
}
|
||||
|
||||
class PowerDisplayModule : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
bool m_hotkey_enabled = false;
|
||||
Hotkey m_activation_hotkey = { .win = true, .ctrl = false, .shift = false, .alt = true, .key = 'M' };
|
||||
|
||||
// Windows Events for IPC (persistent handles - ColorPicker pattern)
|
||||
HANDLE m_hProcess = nullptr;
|
||||
HANDLE m_hInvokeEvent = nullptr;
|
||||
HANDLE m_hToggleEvent = nullptr;
|
||||
HANDLE m_hTerminateEvent = nullptr;
|
||||
HANDLE m_hRefreshEvent = nullptr;
|
||||
HANDLE m_hSettingsUpdatedEvent = nullptr;
|
||||
HANDLE m_hApplyColorTemperatureEvent = nullptr;
|
||||
HANDLE m_hApplyProfileEvent = nullptr;
|
||||
|
||||
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 parse_activation_hotkey(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);
|
||||
if (properties.HasKey(JSON_KEY_ACTIVATION_SHORTCUT))
|
||||
{
|
||||
auto jsonHotkeyObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
|
||||
m_activation_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
|
||||
m_activation_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
|
||||
m_activation_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
m_activation_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
m_activation_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
m_activation_hotkey.isShown = true;
|
||||
Logger::trace(L"Parsed activation hotkey: Win={} Ctrl={} Alt={} Shift={} Key={}",
|
||||
m_activation_hotkey.win, m_activation_hotkey.ctrl, m_activation_hotkey.alt,
|
||||
m_activation_hotkey.shift, m_activation_hotkey.key);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info("ActivationShortcut not found in settings, using default Win+Alt+M");
|
||||
m_activation_hotkey.isShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error("Failed to parse PowerDisplay activation shortcut, using default Win+Alt+M");
|
||||
m_activation_hotkey.isShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void init_settings()
|
||||
{
|
||||
try
|
||||
{
|
||||
PowerToysSettings::PowerToyValues settings =
|
||||
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
|
||||
|
||||
parse_hotkey_settings(settings);
|
||||
parse_activation_hotkey(settings);
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error("Invalid json when trying to load the Power Display settings json from file.");
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to check if PowerDisplay.exe process is still running
|
||||
bool is_process_running()
|
||||
{
|
||||
if (m_hProcess == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
// Helper method to launch PowerDisplay.exe process
|
||||
void launch_process()
|
||||
{
|
||||
Logger::trace(L"Starting PowerDisplay process");
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
std::wstring executable_args = std::to_wstring(powertoys_pid);
|
||||
|
||||
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");
|
||||
m_hProcess = sei.hProcess;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"PowerDisplay process failed to start. {}",
|
||||
get_last_error_or_default(GetLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
PowerDisplayModule()
|
||||
{
|
||||
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "PowerDisplay");
|
||||
Logger::info("Power Display object is constructing");
|
||||
|
||||
init_settings();
|
||||
|
||||
// Create all Windows Events (persistent handles - ColorPicker pattern)
|
||||
m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT);
|
||||
m_hToggleEvent = CreateDefaultEvent(CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT);
|
||||
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT);
|
||||
m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT);
|
||||
m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT);
|
||||
m_hApplyColorTemperatureEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT);
|
||||
m_hApplyProfileEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT);
|
||||
|
||||
if (!m_hInvokeEvent || !m_hToggleEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent || !m_hApplyColorTemperatureEvent || !m_hApplyProfileEvent)
|
||||
{
|
||||
Logger::error(L"Failed to create one or more event handles");
|
||||
}
|
||||
}
|
||||
|
||||
~PowerDisplayModule()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
disable();
|
||||
}
|
||||
|
||||
// Clean up all event handles
|
||||
if (m_hInvokeEvent)
|
||||
{
|
||||
CloseHandle(m_hInvokeEvent);
|
||||
m_hInvokeEvent = nullptr;
|
||||
}
|
||||
if (m_hToggleEvent)
|
||||
{
|
||||
CloseHandle(m_hToggleEvent);
|
||||
m_hToggleEvent = nullptr;
|
||||
}
|
||||
if (m_hTerminateEvent)
|
||||
{
|
||||
CloseHandle(m_hTerminateEvent);
|
||||
m_hTerminateEvent = nullptr;
|
||||
}
|
||||
if (m_hRefreshEvent)
|
||||
{
|
||||
CloseHandle(m_hRefreshEvent);
|
||||
m_hRefreshEvent = nullptr;
|
||||
}
|
||||
if (m_hSettingsUpdatedEvent)
|
||||
{
|
||||
CloseHandle(m_hSettingsUpdatedEvent);
|
||||
m_hSettingsUpdatedEvent = nullptr;
|
||||
}
|
||||
if (m_hApplyColorTemperatureEvent)
|
||||
{
|
||||
CloseHandle(m_hApplyColorTemperatureEvent);
|
||||
m_hApplyColorTemperatureEvent = nullptr;
|
||||
}
|
||||
if (m_hApplyProfileEvent)
|
||||
{
|
||||
CloseHandle(m_hApplyProfileEvent);
|
||||
m_hApplyProfileEvent = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
virtual void destroy() override
|
||||
{
|
||||
Logger::trace("PowerDisplay::destroy()");
|
||||
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");
|
||||
|
||||
// ColorPicker pattern: check if process is running, re-launch if needed
|
||||
if (!is_process_running())
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process not running, re-launching");
|
||||
launch_process();
|
||||
}
|
||||
|
||||
if (m_hToggleEvent)
|
||||
{
|
||||
Logger::trace(L"Signaling toggle event");
|
||||
SetEvent(m_hToggleEvent);
|
||||
}
|
||||
Trace::ActivatePowerDisplay();
|
||||
}
|
||||
else if (action_object.get_name() == L"RefreshMonitors")
|
||||
{
|
||||
Logger::trace(L"RefreshMonitors action received, signaling refresh event");
|
||||
if (m_hRefreshEvent)
|
||||
{
|
||||
SetEvent(m_hRefreshEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Refresh event handle is null");
|
||||
}
|
||||
}
|
||||
else if (action_object.get_name() == L"ApplyColorTemperature")
|
||||
{
|
||||
Logger::trace(L"ApplyColorTemperature action received");
|
||||
if (m_hApplyColorTemperatureEvent)
|
||||
{
|
||||
Logger::trace(L"Signaling apply color temperature event");
|
||||
SetEvent(m_hApplyColorTemperatureEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Apply color temperature event handle is null");
|
||||
}
|
||||
}
|
||||
else if (action_object.get_name() == L"ApplyProfile")
|
||||
{
|
||||
Logger::trace(L"ApplyProfile action received");
|
||||
if (m_hApplyProfileEvent)
|
||||
{
|
||||
Logger::trace(L"Signaling apply profile event");
|
||||
SetEvent(m_hApplyProfileEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Apply profile event handle is null");
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
parse_activation_hotkey(values);
|
||||
|
||||
// Signal settings updated event
|
||||
if (m_hSettingsUpdatedEvent)
|
||||
{
|
||||
Logger::trace(L"Signaling settings updated event");
|
||||
SetEvent(m_hSettingsUpdatedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Settings updated event handle is null");
|
||||
}
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error(L"Invalid json when trying to parse Power Display settings json.");
|
||||
}
|
||||
}
|
||||
|
||||
virtual void enable() override
|
||||
{
|
||||
Logger::trace(L"PowerDisplay::enable()");
|
||||
m_enabled = true;
|
||||
Trace::EnablePowerDisplay(true);
|
||||
|
||||
// Launch PowerDisplay.exe with PID only (Awake pattern)
|
||||
launch_process();
|
||||
}
|
||||
|
||||
virtual void disable() override
|
||||
{
|
||||
Logger::trace(L"PowerDisplay::disable()");
|
||||
|
||||
if (m_enabled)
|
||||
{
|
||||
// Reset invoke event to prevent accidental activation during shutdown
|
||||
if (m_hInvokeEvent)
|
||||
{
|
||||
ResetEvent(m_hInvokeEvent);
|
||||
}
|
||||
|
||||
// Signal terminate event
|
||||
if (m_hTerminateEvent)
|
||||
{
|
||||
Logger::trace(L"Signaling PowerDisplay to exit");
|
||||
SetEvent(m_hTerminateEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Terminate event handle is null");
|
||||
}
|
||||
|
||||
// Close process handle (don't wait, don't force terminate - Awake pattern)
|
||||
if (m_hProcess)
|
||||
{
|
||||
CloseHandle(m_hProcess);
|
||||
m_hProcess = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
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 && m_hToggleEvent)
|
||||
{
|
||||
Logger::trace(L"Power Display hotkey pressed");
|
||||
|
||||
// ColorPicker pattern: check if process is running, re-launch if needed
|
||||
if (!is_process_running())
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process not running, re-launching");
|
||||
launch_process();
|
||||
}
|
||||
|
||||
Logger::trace(L"Signaling toggle event");
|
||||
SetEvent(m_hToggleEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
{
|
||||
if (m_activation_hotkey.key != 0)
|
||||
{
|
||||
if (hotkeys && buffer_size >= 1)
|
||||
{
|
||||
hotkeys[0] = m_activation_hotkey;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
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.ZoomItModuleInterface.dll",
|
||||
L"PowerToys.LightSwitchModuleInterface.dll",
|
||||
L"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
};
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -809,6 +851,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
|
||||
return "CmdPal";
|
||||
case ESettingsWindowNames::ZoomIt:
|
||||
return "ZoomIt";
|
||||
case ESettingsWindowNames::PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
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;
|
||||
}
|
||||
else if (value == "PowerDisplay")
|
||||
{
|
||||
return ESettingsWindowNames::PowerDisplay;
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// 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,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
};
|
||||
|
||||
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
|
||||
@@ -47,3 +48,6 @@ void close_settings_window();
|
||||
void open_oobe_window();
|
||||
void open_scoobe_window();
|
||||
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 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.
|
||||
// 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.Serialization;
|
||||
|
||||
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
|
||||
{
|
||||
// Cached JsonSerializerOptions for Native AOT compatibility
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
TypeInfoResolver = SettingsSerializationContext.Default,
|
||||
};
|
||||
|
||||
// Gets or sets name of the powertoy module.
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
@@ -17,11 +37,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("version")]
|
||||
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()
|
||||
{
|
||||
// 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()
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.BoolProperty);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var boolProperty = JsonSerializer.Deserialize<BoolProperty>(ref reader, options);
|
||||
var boolProperty = JsonSerializer.Deserialize(ref reader, SettingsSerializationContext.Default.BoolProperty);
|
||||
return boolProperty.Value;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
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))
|
||||
{
|
||||
Hotkey = JsonSerializer.Deserialize<HotkeySettings>(hotkeyElement.GetRawText());
|
||||
Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText(), SettingsSerializationContext.Default.HotkeySettings);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
@@ -87,6 +87,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public bool ShowColorName { get; set; }
|
||||
|
||||
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 override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerPropertiesVersion1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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>
|
||||
/// Represents a pending color temperature change operation
|
||||
/// </summary>
|
||||
public class ColorTemperatureOperation
|
||||
{
|
||||
[JsonPropertyName("monitor_id")]
|
||||
public string MonitorId { get; set; }
|
||||
|
||||
[JsonPropertyName("color_temperature")]
|
||||
public int ColorTemperature { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// Returns a JSON version of the class settings configuration class.
|
||||
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()
|
||||
{
|
||||
notifyEnabledChangedAction?.Invoke();
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
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.
|
||||
|
||||
@@ -17,6 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("bool_show_extended_menu")]
|
||||
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.
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettings);
|
||||
}
|
||||
|
||||
private static string DefaultPowertoysVersion()
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
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 int SunriseHour;
|
||||
public int SunriseMinute;
|
||||
public int SunsetHour;
|
||||
public int SunsetMinute;
|
||||
public string Text;
|
||||
public int SunriseHour { get; set; }
|
||||
|
||||
public bool HasSunrise;
|
||||
public bool HasSunset;
|
||||
public int SunriseMinute { get; set; }
|
||||
|
||||
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()
|
||||
{
|
||||
var options = _serializerOptions;
|
||||
return JsonSerializer.Serialize(this, options);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerSettings);
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// Returns a JSON version of the class settings configuration class.
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.IntProperty);
|
||||
}
|
||||
|
||||
public static implicit operator IntProperty(int v)
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.KeyboardManagerProfile);
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
|
||||
@@ -17,6 +17,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public const string DefaultLatitude = "0.0";
|
||||
public const string DefaultLongitude = "0.0";
|
||||
public const string DefaultScheduleMode = "FixedHours";
|
||||
public const bool DefaultEnableDarkModeProfile = false;
|
||||
public const bool DefaultEnableLightModeProfile = false;
|
||||
public const string DefaultDarkModeProfile = "";
|
||||
public const string DefaultLightModeProfile = "";
|
||||
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
|
||||
|
||||
public LightSwitchProperties()
|
||||
@@ -31,6 +35,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
SunsetOffset = new IntProperty(DefaultSunsetOffset);
|
||||
ScheduleMode = new StringProperty(DefaultScheduleMode);
|
||||
ToggleThemeHotkey = new KeyboardKeysProperty(DefaultToggleThemeHotkey);
|
||||
EnableDarkModeProfile = new BoolProperty(DefaultEnableDarkModeProfile);
|
||||
EnableLightModeProfile = new BoolProperty(DefaultEnableLightModeProfile);
|
||||
DarkModeProfile = new StringProperty(DefaultDarkModeProfile);
|
||||
LightModeProfile = new StringProperty(DefaultLightModeProfile);
|
||||
}
|
||||
|
||||
[JsonPropertyName("changeSystem")]
|
||||
@@ -62,5 +70,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("toggle-theme-hotkey")]
|
||||
public KeyboardKeysProperty ToggleThemeHotkey { get; set; }
|
||||
|
||||
[JsonPropertyName("enableDarkModeProfile")]
|
||||
public BoolProperty EnableDarkModeProfile { get; set; }
|
||||
|
||||
[JsonPropertyName("enableLightModeProfile")]
|
||||
public BoolProperty EnableLightModeProfile { get; set; }
|
||||
|
||||
[JsonPropertyName("darkModeProfile")]
|
||||
public StringProperty DarkModeProfile { get; set; }
|
||||
|
||||
[JsonPropertyName("lightModeProfile")]
|
||||
public StringProperty LightModeProfile { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@ namespace Settings.UI.Library
|
||||
Latitude = new StringProperty(Properties.Latitude.Value),
|
||||
Longitude = new StringProperty(Properties.Longitude.Value),
|
||||
ToggleThemeHotkey = new KeyboardKeysProperty(Properties.ToggleThemeHotkey.Value),
|
||||
EnableDarkModeProfile = new BoolProperty(Properties.EnableDarkModeProfile.Value),
|
||||
EnableLightModeProfile = new BoolProperty(Properties.EnableLightModeProfile.Value),
|
||||
DarkModeProfile = new StringProperty(Properties.DarkModeProfile.Value),
|
||||
LightModeProfile = new StringProperty(Properties.LightModeProfile.Value),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public IntProperty DefaultMeasureStyle { get; set; }
|
||||
|
||||
public override string ToString() => JsonSerializer.Serialize(this);
|
||||
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.MeasureToolProperties);
|
||||
}
|
||||
}
|
||||
|
||||
626
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
626
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
@@ -0,0 +1,626 @@
|
||||
// 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.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using ManagedCommon;
|
||||
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 int _currentBrightness;
|
||||
private int _colorTemperature = 6500;
|
||||
private bool _isHidden;
|
||||
private bool _enableContrast;
|
||||
private bool _enableVolume;
|
||||
private string _capabilitiesRaw = string.Empty;
|
||||
private List<string> _vcpCodes = new List<string>();
|
||||
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
|
||||
|
||||
// Feature support status (determined from capabilities)
|
||||
private bool _supportsBrightness = true; // Brightness always shown even if unsupported
|
||||
private bool _supportsContrast;
|
||||
private bool _supportsColorTemperature;
|
||||
private bool _supportsVolume;
|
||||
private string _capabilitiesStatus = "unknown"; // "available", "unavailable", or "unknown"
|
||||
|
||||
// Cached color temperature presets (computed from VcpCodesFormatted)
|
||||
private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
|
||||
|
||||
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, int currentBrightness, int colorTemperature)
|
||||
{
|
||||
Name = name;
|
||||
InternalName = internalName;
|
||||
HardwareId = hardwareId;
|
||||
CommunicationMethod = communicationMethod;
|
||||
CurrentBrightness = currentBrightness;
|
||||
ColorTemperature = colorTemperature;
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (_name != value)
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string MonitorIconGlyph => CommunicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true
|
||||
? "\uE7F8" // Laptop icon for WMI
|
||||
: "\uE7F4"; // External monitor icon for DDC/CI and others
|
||||
|
||||
[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("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();
|
||||
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("isHidden")]
|
||||
public bool IsHidden
|
||||
{
|
||||
get => _isHidden;
|
||||
set
|
||||
{
|
||||
if (_isHidden != value)
|
||||
{
|
||||
_isHidden = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("capabilitiesRaw")]
|
||||
public string CapabilitiesRaw
|
||||
{
|
||||
get => _capabilitiesRaw;
|
||||
set
|
||||
{
|
||||
if (_capabilitiesRaw != value)
|
||||
{
|
||||
_capabilitiesRaw = value ?? string.Empty;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasCapabilities));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("vcpCodes")]
|
||||
public List<string> VcpCodes
|
||||
{
|
||||
get => _vcpCodes;
|
||||
set
|
||||
{
|
||||
if (_vcpCodes != value)
|
||||
{
|
||||
_vcpCodes = value ?? new List<string>();
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(VcpCodesSummary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("vcpCodesFormatted")]
|
||||
public List<VcpCodeDisplayInfo> VcpCodesFormatted
|
||||
{
|
||||
get => _vcpCodesFormatted;
|
||||
set
|
||||
{
|
||||
if (_vcpCodesFormatted != value)
|
||||
{
|
||||
_vcpCodesFormatted = value ?? new List<VcpCodeDisplayInfo>();
|
||||
_availableColorPresetsCache = null; // Clear cache when VCP codes change
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(AvailableColorPresets));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string VcpCodesSummary
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_vcpCodes == null || _vcpCodes.Count == 0)
|
||||
{
|
||||
return "No VCP codes detected";
|
||||
}
|
||||
|
||||
var count = _vcpCodes.Count;
|
||||
var preview = string.Join(", ", _vcpCodes.Take(10));
|
||||
return count > 10
|
||||
? $"{count} VCP codes: {preview}..."
|
||||
: $"{count} VCP codes: {preview}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("supportsBrightness")]
|
||||
public bool SupportsBrightness
|
||||
{
|
||||
get => _supportsBrightness;
|
||||
set
|
||||
{
|
||||
if (_supportsBrightness != value)
|
||||
{
|
||||
_supportsBrightness = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("supportsContrast")]
|
||||
public bool SupportsContrast
|
||||
{
|
||||
get => _supportsContrast;
|
||||
set
|
||||
{
|
||||
if (_supportsContrast != value)
|
||||
{
|
||||
_supportsContrast = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("supportsColorTemperature")]
|
||||
public bool SupportsColorTemperature
|
||||
{
|
||||
get => _supportsColorTemperature;
|
||||
set
|
||||
{
|
||||
if (_supportsColorTemperature != value)
|
||||
{
|
||||
_supportsColorTemperature = value;
|
||||
_availableColorPresetsCache = null; // Clear cache when support status changes
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(AvailableColorPresets)); // Refresh computed property
|
||||
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Refresh display list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("supportsVolume")]
|
||||
public bool SupportsVolume
|
||||
{
|
||||
get => _supportsVolume;
|
||||
set
|
||||
{
|
||||
if (_supportsVolume != value)
|
||||
{
|
||||
_supportsVolume = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("capabilitiesStatus")]
|
||||
public string CapabilitiesStatus
|
||||
{
|
||||
get => _capabilitiesStatus;
|
||||
set
|
||||
{
|
||||
if (_capabilitiesStatus != value)
|
||||
{
|
||||
_capabilitiesStatus = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ShowCapabilitiesWarning));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Available color temperature presets computed from VcpCodesFormatted (VCP code 0x14).
|
||||
/// This is a computed property that parses the VCP capabilities data on-demand.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<ColorPresetItem> AvailableColorPresets
|
||||
{
|
||||
get
|
||||
{
|
||||
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] GET called for monitor '{_name}'");
|
||||
|
||||
// Return cached value if available
|
||||
if (_availableColorPresetsCache != null)
|
||||
{
|
||||
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] Cache HIT - returning {_availableColorPresetsCache.Count} items");
|
||||
return _availableColorPresetsCache;
|
||||
}
|
||||
|
||||
Logger.LogInfo("[MonitorInfo.AvailableColorPresets] Cache MISS - computing from VcpCodesFormatted");
|
||||
|
||||
// Compute from VcpCodesFormatted
|
||||
_availableColorPresetsCache = ComputeAvailableColorPresets();
|
||||
|
||||
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] Computed {_availableColorPresetsCache.Count} items");
|
||||
return _availableColorPresetsCache;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute available color presets from VcpCodesFormatted (VCP code 0x14)
|
||||
/// </summary>
|
||||
private ObservableCollection<ColorPresetItem> ComputeAvailableColorPresets()
|
||||
{
|
||||
Logger.LogInfo($"[ComputeAvailableColorPresets] START for monitor '{_name}'");
|
||||
Logger.LogInfo($" - SupportsColorTemperature: {_supportsColorTemperature}");
|
||||
Logger.LogInfo($" - VcpCodesFormatted: {(_vcpCodesFormatted == null ? "NULL" : $"{_vcpCodesFormatted.Count} items")}");
|
||||
|
||||
// Check if color temperature is supported
|
||||
if (!_supportsColorTemperature || _vcpCodesFormatted == null)
|
||||
{
|
||||
Logger.LogWarning($"[ComputeAvailableColorPresets] Color temperature not supported or no VCP codes - returning empty");
|
||||
return new ObservableCollection<ColorPresetItem>();
|
||||
}
|
||||
|
||||
// Find VCP code 0x14 (Color Temperature / Select Color Preset)
|
||||
var colorTempVcp = _vcpCodesFormatted.FirstOrDefault(v =>
|
||||
{
|
||||
if (int.TryParse(v.Code?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int code))
|
||||
{
|
||||
return code == 0x14;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
Logger.LogInfo($"[ComputeAvailableColorPresets] VCP 0x14 found: {colorTempVcp != null}");
|
||||
if (colorTempVcp != null)
|
||||
{
|
||||
Logger.LogInfo($" - ValueList: {(colorTempVcp.ValueList == null ? "NULL" : $"{colorTempVcp.ValueList.Count} items")}");
|
||||
}
|
||||
|
||||
// No VCP 0x14 or no values
|
||||
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
|
||||
{
|
||||
Logger.LogWarning($"[ComputeAvailableColorPresets] No VCP 0x14 or empty ValueList - returning empty");
|
||||
return new ObservableCollection<ColorPresetItem>();
|
||||
}
|
||||
|
||||
// Build preset list from supported values
|
||||
var presetList = new List<ColorPresetItem>();
|
||||
foreach (var valueInfo in colorTempVcp.ValueList)
|
||||
{
|
||||
if (int.TryParse(valueInfo.Value?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int vcpValue))
|
||||
{
|
||||
var displayName = FormatColorTemperatureDisplayName(valueInfo.Name, vcpValue);
|
||||
presetList.Add(new ColorPresetItem(vcpValue, displayName));
|
||||
Logger.LogDebug($"[ComputeAvailableColorPresets] Added: {displayName}");
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by VCP value for consistent ordering
|
||||
presetList = presetList.OrderBy(p => p.VcpValue).ToList();
|
||||
|
||||
Logger.LogInfo($"[ComputeAvailableColorPresets] COMPLETE - returning {presetList.Count} items");
|
||||
Logger.LogInfo($"[ComputeAvailableColorPresets] Current ColorTemperature value: {_colorTemperature}");
|
||||
|
||||
return new ObservableCollection<ColorPresetItem>(presetList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format color temperature display name
|
||||
/// </summary>
|
||||
private string FormatColorTemperatureDisplayName(string name, int vcpValue)
|
||||
{
|
||||
var hexValue = $"0x{vcpValue:X2}";
|
||||
|
||||
// Check if name is undefined (null or empty)
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return $"Manufacturer Defined ({hexValue})";
|
||||
}
|
||||
|
||||
// For predefined names, append the hex value in parentheses
|
||||
return $"{name} ({hexValue})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color presets for display in ComboBox, includes current value if not in preset list
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
var presets = AvailableColorPresets;
|
||||
if (presets == null || presets.Count == 0)
|
||||
{
|
||||
return new ObservableCollection<ColorPresetItem>();
|
||||
}
|
||||
|
||||
// Check if current value is in the preset list
|
||||
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperature);
|
||||
|
||||
if (currentValueInList)
|
||||
{
|
||||
// Current value is in the list, return as-is
|
||||
return presets;
|
||||
}
|
||||
|
||||
// Current value is not in the preset list - add it at the beginning
|
||||
var displayList = new List<ColorPresetItem>();
|
||||
|
||||
// Add current value with "Custom" indicator
|
||||
var currentValueName = GetColorTemperatureName(_colorTemperature);
|
||||
var displayName = string.IsNullOrEmpty(currentValueName)
|
||||
? $"Custom (0x{_colorTemperature:X2})"
|
||||
: $"{currentValueName} (0x{_colorTemperature:X2}) - Custom";
|
||||
|
||||
displayList.Add(new ColorPresetItem(_colorTemperature, displayName));
|
||||
|
||||
// Add all supported presets
|
||||
displayList.AddRange(presets);
|
||||
|
||||
return new ObservableCollection<ColorPresetItem>(displayList);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the name for a color temperature value from standard VCP naming
|
||||
/// </summary>
|
||||
private string GetColorTemperatureName(int vcpValue)
|
||||
{
|
||||
return vcpValue switch
|
||||
{
|
||||
0x04 => "5000K",
|
||||
0x05 => "6500K",
|
||||
0x06 => "7500K",
|
||||
0x08 => "9300K",
|
||||
0x09 => "10000K",
|
||||
0x0A => "11500K",
|
||||
0x0B => "User 1",
|
||||
0x0C => "User 2",
|
||||
0x0D => "User 3",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasColorPresets => AvailableColorPresets != null && AvailableColorPresets.Count > 0;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
|
||||
|
||||
[JsonIgnore]
|
||||
public bool ShowCapabilitiesWarning => _capabilitiesStatus == "unavailable";
|
||||
|
||||
[JsonIgnore]
|
||||
public string BrightnessTooltip => _supportsBrightness ? string.Empty : "Brightness control not supported by this monitor";
|
||||
|
||||
[JsonIgnore]
|
||||
public string ContrastTooltip => _supportsContrast ? string.Empty : "Contrast control not supported by this monitor";
|
||||
|
||||
/// <summary>
|
||||
/// Generate formatted text of all VCP codes for clipboard
|
||||
/// </summary>
|
||||
public string GetVcpCodesAsText()
|
||||
{
|
||||
if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0)
|
||||
{
|
||||
return "No VCP codes detected";
|
||||
}
|
||||
|
||||
var lines = new List<string>();
|
||||
lines.Add($"VCP Capabilities for {_name}");
|
||||
lines.Add($"Monitor: {_name}");
|
||||
lines.Add($"Hardware ID: {_hardwareId}");
|
||||
lines.Add(string.Empty);
|
||||
lines.Add("Detected VCP Codes:");
|
||||
lines.Add(new string('-', 50));
|
||||
|
||||
foreach (var vcp in _vcpCodesFormatted)
|
||||
{
|
||||
lines.Add(string.Empty);
|
||||
lines.Add(vcp.Title);
|
||||
if (vcp.HasValues)
|
||||
{
|
||||
lines.Add($" {vcp.Values}");
|
||||
}
|
||||
}
|
||||
|
||||
lines.Add(string.Empty);
|
||||
lines.Add(new string('-', 50));
|
||||
lines.Add($"Total: {_vcpCodesFormatted.Count} VCP codes");
|
||||
|
||||
return string.Join(System.Environment.NewLine, lines);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update this monitor's properties from another MonitorInfo instance.
|
||||
/// This preserves the object reference while updating all properties.
|
||||
/// </summary>
|
||||
/// <param name="other">The source MonitorInfo to copy properties from</param>
|
||||
public void UpdateFrom(MonitorInfo other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update all properties that can change
|
||||
Name = other.Name;
|
||||
InternalName = other.InternalName;
|
||||
HardwareId = other.HardwareId;
|
||||
CommunicationMethod = other.CommunicationMethod;
|
||||
CurrentBrightness = other.CurrentBrightness;
|
||||
ColorTemperature = other.ColorTemperature;
|
||||
IsHidden = other.IsHidden;
|
||||
EnableContrast = other.EnableContrast;
|
||||
EnableVolume = other.EnableVolume;
|
||||
CapabilitiesRaw = other.CapabilitiesRaw;
|
||||
VcpCodes = other.VcpCodes;
|
||||
VcpCodesFormatted = other.VcpCodesFormatted;
|
||||
SupportsBrightness = other.SupportsBrightness;
|
||||
SupportsContrast = other.SupportsContrast;
|
||||
SupportsColorTemperature = other.SupportsColorTemperature;
|
||||
SupportsVolume = other.SupportsVolume;
|
||||
CapabilitiesStatus = other.CapabilitiesStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a color temperature preset item for VCP code 0x14
|
||||
/// </summary>
|
||||
public class ColorPresetItem : Observable
|
||||
{
|
||||
private int _vcpValue;
|
||||
private string _displayName = string.Empty;
|
||||
|
||||
[JsonPropertyName("vcpValue")]
|
||||
public int VcpValue
|
||||
{
|
||||
get => _vcpValue;
|
||||
set
|
||||
{
|
||||
if (_vcpValue != value)
|
||||
{
|
||||
_vcpValue = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public string DisplayName
|
||||
{
|
||||
get => _displayName;
|
||||
set
|
||||
{
|
||||
if (_displayName != value)
|
||||
{
|
||||
_displayName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorPresetItem()
|
||||
{
|
||||
}
|
||||
|
||||
public ColorPresetItem(int vcpValue, string displayName)
|
||||
{
|
||||
VcpValue = vcpValue;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/settings-ui/Settings.UI.Library/MonitorInfoData.cs
Normal file
42
src/settings-ui/Settings.UI.Library/MonitorInfoData.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
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("currentBrightness")]
|
||||
public int CurrentBrightness { get; set; }
|
||||
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int ColorTemperature { get; set; }
|
||||
|
||||
[JsonPropertyName("capabilitiesRaw")]
|
||||
public string CapabilitiesRaw { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("vcpCodes")]
|
||||
public List<string> VcpCodes { get; set; } = new List<string>();
|
||||
|
||||
[JsonPropertyName("vcpCodesFormatted")]
|
||||
public List<VcpCodeDisplayInfo> VcpCodesFormatted { get; set; } = new List<VcpCodeDisplayInfo>();
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public struct ConnectionRequest
|
||||
#pragma warning restore SA1649 // File name should match first type name
|
||||
{
|
||||
public string PCName;
|
||||
public string SecurityKey;
|
||||
public string PCName { get; set; }
|
||||
|
||||
public string SecurityKey { get; set; }
|
||||
}
|
||||
|
||||
public struct NewKeyGenerationRequest
|
||||
|
||||
@@ -33,6 +33,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("ReplaceVariables")]
|
||||
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()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingGeneralSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user