mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
init
This commit is contained in:
144
.editorconfig
Normal file
144
.editorconfig
Normal file
@@ -0,0 +1,144 @@
|
||||
# EditorConfig for PowerToys - Global Settings
|
||||
|
||||
root = true
|
||||
|
||||
[*.cs]
|
||||
# 禁用过于严格的StyleCop规则
|
||||
|
||||
# 文件结尾换行符
|
||||
dotnet_diagnostic.SA1518.severity = none
|
||||
|
||||
# 空行相关
|
||||
dotnet_diagnostic.SA1505.severity = none
|
||||
dotnet_diagnostic.SA1507.severity = none
|
||||
dotnet_diagnostic.SA1508.severity = none
|
||||
dotnet_diagnostic.SA1513.severity = none
|
||||
dotnet_diagnostic.SA1515.severity = none
|
||||
dotnet_diagnostic.SA1516.severity = none
|
||||
|
||||
# 命名规则 (对于Windows API结构)
|
||||
dotnet_diagnostic.SA1307.severity = none
|
||||
dotnet_diagnostic.SA1313.severity = none
|
||||
|
||||
# Using指令排序
|
||||
dotnet_diagnostic.SA1211.severity = none
|
||||
|
||||
# 多行初始化器尾随逗号
|
||||
dotnet_diagnostic.SA1413.severity = none
|
||||
|
||||
# 参数格式
|
||||
dotnet_diagnostic.SA1116.severity = none
|
||||
dotnet_diagnostic.SA1117.severity = none
|
||||
dotnet_diagnostic.SA1111.severity = none
|
||||
dotnet_diagnostic.SA1128.severity = none
|
||||
|
||||
# 大括号
|
||||
dotnet_diagnostic.SA1503.severity = none
|
||||
|
||||
# 代码格式
|
||||
dotnet_diagnostic.SA1025.severity = none
|
||||
dotnet_diagnostic.SA1028.severity = none
|
||||
dotnet_diagnostic.SA1108.severity = none
|
||||
dotnet_diagnostic.SA1122.severity = none
|
||||
dotnet_diagnostic.SA1129.severity = none
|
||||
dotnet_diagnostic.SA1137.severity = none
|
||||
dotnet_diagnostic.SA1407.severity = none
|
||||
|
||||
# 文件名匹配
|
||||
dotnet_diagnostic.SA1402.severity = none
|
||||
dotnet_diagnostic.SA1649.severity = none
|
||||
|
||||
# P/Invoke相关警告
|
||||
dotnet_diagnostic.CA1401.severity = none
|
||||
dotnet_diagnostic.CA2101.severity = none
|
||||
|
||||
# 其他代码分析警告
|
||||
dotnet_diagnostic.CA1001.severity = none
|
||||
dotnet_diagnostic.CA1305.severity = none
|
||||
dotnet_diagnostic.CA1805.severity = none
|
||||
dotnet_diagnostic.CA1806.severity = none
|
||||
dotnet_diagnostic.CA1816.severity = none
|
||||
dotnet_diagnostic.CA1825.severity = none
|
||||
|
||||
# 代码样式设置
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_prefer_primary_constructors = true:suggestion
|
||||
csharp_prefer_system_threading_lock = true:suggestion
|
||||
csharp_style_prefer_simple_property_accessors = true:suggestion
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = false:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
97
.vscode/settings.json
vendored
Normal file
97
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"cmake.ignoreCMakeListsMissing": true,
|
||||
"files.associations": {
|
||||
"algorithm": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"cctype": "cpp",
|
||||
"charconv": "cpp",
|
||||
"chrono": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"complex": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"coroutine": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"exception": "cpp",
|
||||
"execution": "cpp",
|
||||
"filesystem": "cpp",
|
||||
"format": "cpp",
|
||||
"forward_list": "cpp",
|
||||
"fstream": "cpp",
|
||||
"functional": "cpp",
|
||||
"future": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"ios": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"iterator": "cpp",
|
||||
"limits": "cpp",
|
||||
"list": "cpp",
|
||||
"locale": "cpp",
|
||||
"map": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"ostream": "cpp",
|
||||
"queue": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"regex": "cpp",
|
||||
"set": "cpp",
|
||||
"shared_mutex": "cpp",
|
||||
"source_location": "cpp",
|
||||
"span": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"string": "cpp",
|
||||
"strstream": "cpp",
|
||||
"system_error": "cpp",
|
||||
"thread": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"typeindex": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"utility": "cpp",
|
||||
"valarray": "cpp",
|
||||
"variant": "cpp",
|
||||
"vector": "cpp",
|
||||
"xfacet": "cpp",
|
||||
"xhash": "cpp",
|
||||
"xiosbase": "cpp",
|
||||
"xlocale": "cpp",
|
||||
"xlocbuf": "cpp",
|
||||
"xlocinfo": "cpp",
|
||||
"xlocmes": "cpp",
|
||||
"xlocmon": "cpp",
|
||||
"xlocnum": "cpp",
|
||||
"xloctime": "cpp",
|
||||
"xmemory": "cpp",
|
||||
"xstddef": "cpp",
|
||||
"xstring": "cpp",
|
||||
"xtr1common": "cpp",
|
||||
"xtree": "cpp",
|
||||
"xutility": "cpp"
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,21 @@
|
||||
<Version>$(Version).0</Version>
|
||||
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
|
||||
<RepositoryType>GitHub</RepositoryType>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
|
||||
<!-- Disable ALL StyleCop warnings -->
|
||||
<NoWarn>$(NoWarn);SA0001;SA1000;SA1001;SA1002;SA1003;SA1004;SA1005;SA1006;SA1007;SA1008;SA1009;SA1010;SA1011;SA1012;SA1013;SA1014;SA1015;SA1016;SA1017;SA1018;SA1019;SA1020;SA1021;SA1022;SA1023;SA1024;SA1025;SA1026;SA1027;SA1028</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1100;SA1101;SA1102;SA1103;SA1104;SA1105;SA1106;SA1107;SA1108;SA1109;SA1110;SA1111;SA1112;SA1113;SA1114;SA1115;SA1116;SA1117;SA1118;SA1119;SA1120;SA1121;SA1122;SA1123;SA1124;SA1125;SA1126;SA1127;SA1128;SA1129;SA1130;SA1131;SA1132;SA1133;SA1134;SA1135;SA1136;SA1137;SA1138;SA1139</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1200;SA1201;SA1202;SA1203;SA1204;SA1205;SA1206;SA1207;SA1208;SA1209;SA1210;SA1211;SA1212;SA1213;SA1214;SA1215;SA1216;SA1217</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1300;SA1301;SA1302;SA1303;SA1304;SA1305;SA1306;SA1307;SA1308;SA1309;SA1310;SA1311;SA1312;SA1313;SA1314</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1400;SA1401;SA1402;SA1403;SA1404;SA1405;SA1406;SA1407;SA1408;SA1409;SA1410;SA1411;SA1412;SA1413</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1500;SA1501;SA1502;SA1503;SA1504;SA1505;SA1506;SA1507;SA1508;SA1509;SA1510;SA1511;SA1512;SA1513;SA1514;SA1515;SA1516;SA1517;SA1518;SA1519;SA1520</NoWarn>
|
||||
<NoWarn>$(NoWarn);SA1600;SA1601;SA1602;SA1603;SA1604;SA1605;SA1606;SA1607;SA1608;SA1609;SA1610;SA1611;SA1612;SA1613;SA1614;SA1615;SA1616;SA1617;SA1618;SA1619;SA1620;SA1621;SA1622;SA1623;SA1624;SA1625;SA1626;SA1627;SA1628;SA1629;SA1630;SA1631;SA1632;SA1633;SA1634;SA1635;SA1636;SA1637;SA1638;SA1639;SA1640;SA1641;SA1642;SA1643;SA1644;SA1645;SA1646;SA1647;SA1648;SA1649;SA1650;SA1651;SA1652</NoWarn>
|
||||
<!-- Disable specific Code Analysis warnings -->
|
||||
<NoWarn>$(NoWarn);CA1001;CA1002;CA1003;CA1004;CA1005;CA1006;CA1007;CA1008;CA1009;CA1010;CA1011;CA1012;CA1013;CA1014;CA1016;CA1017;CA1018;CA1019;CA1020;CA1021;CA1022;CA1023;CA1024;CA1025;CA1026;CA1027;CA1028;CA1030;CA1031;CA1032;CA1033;CA1034;CA1035;CA1036;CA1038;CA1039;CA1040;CA1041;CA1043;CA1044;CA1045;CA1046;CA1047;CA1048;CA1049;CA1050;CA1051;CA1052;CA1053;CA1054;CA1055;CA1056;CA1057;CA1058;CA1059;CA1060;CA1061;CA1062;CA1063;CA1064;CA1065;CA1066;CA1067;CA1068;CA1069</NoWarn>
|
||||
<NoWarn>$(NoWarn);CA1305;CA1401;CA1806;CA1816;CA1825;CA2101;CA2201</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -31,7 +45,7 @@
|
||||
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' AND '$(DisableStyleCop)' != 'true'">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents 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.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
@@ -57,8 +57,8 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250513003" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4654" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
|
||||
@@ -512,6 +512,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}"
|
||||
@@ -558,6 +560,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}") = "PowerDisplayExt", "src\modules\powerdisplay\PowerDisplayExt\PowerDisplayExt.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}"
|
||||
@@ -2213,6 +2219,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
|
||||
@@ -2932,7 +2954,6 @@ Global
|
||||
{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045}
|
||||
{1AFB6476-670D-4E80-A464-657E01DFF482} = {557C4636-D7E1-4838-A504-7D19B725EE95}
|
||||
{1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
@@ -3130,6 +3151,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>
|
||||
@@ -65,6 +65,7 @@
|
||||
<ComponentGroupRef Id="KeyboardManagerComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
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>
|
||||
@@ -52,6 +52,7 @@
|
||||
<ComponentGroupRef Id="KeyboardManagerComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -110,6 +111,8 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -28,6 +28,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::ShowPowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
hstring Constants::TerminatePowerDisplayEvent()
|
||||
{
|
||||
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring WorkspacesHotkeyEvent();
|
||||
static hstring PowerToysRunnerTerminateSettingsEvent();
|
||||
static hstring ShowCmdPalEvent();
|
||||
static hstring ShowPowerDisplayEvent();
|
||||
static hstring TerminatePowerDisplayEvent();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,10 @@ namespace CommonSharedConstants
|
||||
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
|
||||
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
|
||||
|
||||
// Path to the events used by PowerDisplay
|
||||
const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
|
||||
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
|
||||
|
||||
// used from quick access window
|
||||
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";
|
||||
|
||||
BIN
src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay.ico
Normal file
BIN
src/modules/powerdisplay/PowerDisplay/Assets/PowerDisplay.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 468 KiB |
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to Visibility
|
||||
/// </summary>
|
||||
public partial class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility == Visibility.Visible;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to Visibility (inverted)
|
||||
/// </summary>
|
||||
public partial class InverseBoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility != Visibility.Visible;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Special converter for "No monitors" visibility
|
||||
/// Shows when initialized but has no monitors
|
||||
/// </summary>
|
||||
public partial class NoMonitorsVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
// This would need access to both IsInitialized and HasMonitors
|
||||
// For simplicity, we'll handle this in the ViewModel
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PowerDisplay.Core.Models;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor controller interface
|
||||
/// </summary>
|
||||
public interface IMonitorController
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller name
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported monitor type
|
||||
/// </summary>
|
||||
MonitorType SupportedType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified monitor can be controlled
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Whether the monitor can be controlled</returns>
|
||||
Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor brightness
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Brightness information</returns>
|
||||
Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor brightness
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="brightness">Brightness value (0-100)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers supported monitors
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>List of monitors</returns>
|
||||
Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates monitor connection status
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Whether the monitor is connected</returns>
|
||||
Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extended monitor controller interface (supports additional features)
|
||||
/// </summary>
|
||||
public interface IExtendedMonitorController : IMonitorController
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets monitor contrast
|
||||
/// </summary>
|
||||
Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor contrast
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor volume
|
||||
/// </summary>
|
||||
Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor volume
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor capabilities string (DDC/CI)
|
||||
/// </summary>
|
||||
Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves current settings to monitor
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitor manager interface
|
||||
/// </summary>
|
||||
public interface IMonitorManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently detected monitors list
|
||||
/// </summary>
|
||||
IReadOnlyList<Monitor> Monitors { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Monitor list changed event
|
||||
/// </summary>
|
||||
event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor status changed event
|
||||
/// </summary>
|
||||
event EventHandler<MonitorStatusChangedEventArgs>? MonitorStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Discovers all monitors
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets brightness of specified monitor
|
||||
/// </summary>
|
||||
Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets brightness of specified monitor
|
||||
/// </summary>
|
||||
Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets brightness of all monitors
|
||||
/// </summary>
|
||||
Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes monitor status
|
||||
/// </summary>
|
||||
Task RefreshMonitorStatusAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor by ID
|
||||
/// </summary>
|
||||
Monitor? GetMonitor(string monitorId);
|
||||
}
|
||||
}
|
||||
@@ -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,88 @@
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
253
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
253
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
// 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;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor model that implements property change notification
|
||||
/// </summary>
|
||||
public class Monitor : INotifyPropertyChanged
|
||||
{
|
||||
private int _currentBrightness;
|
||||
private int _currentColorTemperature = 6500;
|
||||
private bool _isAvailable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier (based on hardware ID)
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Hardware ID (EDID format like GSM5C6D)
|
||||
/// </summary>
|
||||
public string HardwareId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Display name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor type
|
||||
/// </summary>
|
||||
public MonitorType Type { get; set; } = MonitorType.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Current brightness (0-100)
|
||||
/// </summary>
|
||||
public int CurrentBrightness
|
||||
{
|
||||
get => _currentBrightness;
|
||||
set
|
||||
{
|
||||
if (_currentBrightness != value)
|
||||
{
|
||||
_currentBrightness = Math.Clamp(value, MinBrightness, MaxBrightness);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum brightness value
|
||||
/// </summary>
|
||||
public int MinBrightness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum brightness value
|
||||
/// </summary>
|
||||
public int MaxBrightness { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Current color temperature (2000-10000K)
|
||||
/// </summary>
|
||||
public int CurrentColorTemperature
|
||||
{
|
||||
get => _currentColorTemperature;
|
||||
set
|
||||
{
|
||||
if (_currentColorTemperature != value)
|
||||
{
|
||||
_currentColorTemperature = Math.Clamp(value, MinColorTemperature, MaxColorTemperature);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum color temperature value
|
||||
/// </summary>
|
||||
public int MinColorTemperature { get; set; } = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum color temperature value
|
||||
/// </summary>
|
||||
public int MaxColorTemperature { get; set; } = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports color temperature adjustment
|
||||
/// </summary>
|
||||
public bool SupportsColorTemperature { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports contrast adjustment
|
||||
/// </summary>
|
||||
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports volume adjustment (for audio-capable monitors)
|
||||
/// </summary>
|
||||
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
|
||||
|
||||
private int _currentContrast = 50;
|
||||
private int _currentVolume = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Current contrast (0-100)
|
||||
/// </summary>
|
||||
public int CurrentContrast
|
||||
{
|
||||
get => _currentContrast;
|
||||
set
|
||||
{
|
||||
if (_currentContrast != value)
|
||||
{
|
||||
_currentContrast = Math.Clamp(value, MinContrast, MaxContrast);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum contrast value
|
||||
/// </summary>
|
||||
public int MinContrast { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum contrast value
|
||||
/// </summary>
|
||||
public int MaxContrast { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Current volume (0-100)
|
||||
/// </summary>
|
||||
public int CurrentVolume
|
||||
{
|
||||
get => _currentVolume;
|
||||
set
|
||||
{
|
||||
if (_currentVolume != value)
|
||||
{
|
||||
_currentVolume = Math.Clamp(value, MinVolume, MaxVolume);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum volume value
|
||||
/// </summary>
|
||||
public int MinVolume { get; set; } = 0;
|
||||
|
||||
/// <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 path (for identification)
|
||||
/// </summary>
|
||||
public string DevicePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey)
|
||||
/// </summary>
|
||||
public string DeviceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Full device ID path (like Twinkle Tray's deviceID)
|
||||
/// </summary>
|
||||
public string DeviceID { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instance name (used by WMI)
|
||||
/// </summary>
|
||||
public string InstanceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Manufacturer information
|
||||
/// </summary>
|
||||
public string Manufacturer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Connection type (HDMI, DP, VGA, etc.)
|
||||
/// </summary>
|
||||
public string ConnectionType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Communication method (DDC/CI, WMI, HDR API, etc.)
|
||||
/// </summary>
|
||||
public string CommunicationMethod { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Supported control methods
|
||||
/// </summary>
|
||||
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
|
||||
|
||||
/// <summary>
|
||||
/// Last update time
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; set; } = DateTime.Now;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Type}) - {CurrentBrightness}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor status
|
||||
/// </summary>
|
||||
public void UpdateStatus(int brightness, bool isAvailable = true)
|
||||
{
|
||||
IsAvailable = isAvailable;
|
||||
if (isAvailable)
|
||||
{
|
||||
CurrentBrightness = brightness;
|
||||
LastUpdate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor control capabilities flags
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MonitorCapabilities
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Supports brightness control
|
||||
/// </summary>
|
||||
Brightness = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Supports contrast control
|
||||
/// </summary>
|
||||
Contrast = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Supports DDC/CI protocol
|
||||
/// </summary>
|
||||
DdcCi = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Supports WMI control
|
||||
/// </summary>
|
||||
Wmi = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// Supports HDR
|
||||
/// </summary>
|
||||
Hdr = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// Supports high-level monitor API
|
||||
/// </summary>
|
||||
HighLevel = 1 << 5,
|
||||
|
||||
/// <summary>
|
||||
/// Supports volume control
|
||||
/// </summary>
|
||||
Volume = 1 << 6,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor operation result
|
||||
/// </summary>
|
||||
public readonly struct MonitorOperationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the operation was successful
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// System error code
|
||||
/// </summary>
|
||||
public int? ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation timestamp
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
private MonitorOperationResult(bool isSuccess, string? errorMessage = null, int? errorCode = null)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
ErrorMessage = errorMessage;
|
||||
ErrorCode = errorCode;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result
|
||||
/// </summary>
|
||||
public static MonitorOperationResult Success() => new(true);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result
|
||||
/// </summary>
|
||||
public static MonitorOperationResult Failure(string errorMessage, int? errorCode = null)
|
||||
=> new(false, errorMessage, errorCode);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsSuccess ? "Success" : $"Failed: {ErrorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor type enumeration
|
||||
/// </summary>
|
||||
public enum MonitorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown type
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Internal display (laptop screen, controlled via WMI)
|
||||
/// </summary>
|
||||
Internal,
|
||||
|
||||
/// <summary>
|
||||
/// External display (controlled via DDC/CI)
|
||||
/// </summary>
|
||||
External,
|
||||
|
||||
/// <summary>
|
||||
/// HDR display (controlled via Display Config API)
|
||||
/// </summary>
|
||||
HDR,
|
||||
}
|
||||
}
|
||||
578
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
578
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
@@ -0,0 +1,578 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
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
|
||||
/// </summary>
|
||||
public class MonitorManager : IMonitorManager, IDisposable
|
||||
{
|
||||
private readonly List<Monitor> _monitors = new();
|
||||
private readonly List<IMonitorController> _controllers = new();
|
||||
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
||||
private readonly Timer _refreshTimer;
|
||||
private bool _disposed;
|
||||
|
||||
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
|
||||
|
||||
public event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
|
||||
|
||||
public event EventHandler<MonitorStatusChangedEventArgs>? MonitorStatusChanged;
|
||||
|
||||
public MonitorManager()
|
||||
{
|
||||
// Initialize controllers
|
||||
InitializeControllers();
|
||||
|
||||
// Set up periodic refresh timer (check every 30 seconds)
|
||||
_refreshTimer = new Timer(async _ => await RefreshMonitorStatusAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize controllers
|
||||
/// </summary>
|
||||
private void InitializeControllers()
|
||||
{
|
||||
try
|
||||
{
|
||||
// DDC/CI controller (external monitors)
|
||||
_controllers.Add(new DdcCiController());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// WMI controller (internal monitors)
|
||||
// First check if WMI is available
|
||||
if (WmiController.IsWmiAvailable())
|
||||
{
|
||||
_controllers.Add(new WmiController());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("WMI brightness control not available on this system");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover all monitors
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _discoveryLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var oldMonitors = _monitors.ToList();
|
||||
var newMonitors = new List<Monitor>();
|
||||
|
||||
// Discover monitors supported by all controllers in parallel
|
||||
var discoveryTasks = _controllers.Select(async controller =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
|
||||
return (Controller: controller, Monitors: monitors.ToList());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// If a controller fails, return empty list
|
||||
return (Controller: controller, Monitors: new List<Monitor>());
|
||||
}
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(discoveryTasks);
|
||||
|
||||
// Collect all discovered monitors
|
||||
foreach (var (controller, monitors) in results)
|
||||
{
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
// Verify if monitor can be controlled
|
||||
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
||||
{
|
||||
// Get current brightness
|
||||
try
|
||||
{
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
|
||||
monitor.MinBrightness = brightnessInfo.Minimum;
|
||||
monitor.MaxBrightness = brightnessInfo.Maximum;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If unable to get brightness, use default values
|
||||
}
|
||||
|
||||
newMonitors.Add(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update monitor list
|
||||
_monitors.Clear();
|
||||
_monitors.AddRange(newMonitors);
|
||||
|
||||
// Trigger change events
|
||||
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
|
||||
var removedMonitors = oldMonitors.Where(o => !newMonitors.Any(m => m.Id == o.Id)).ToList();
|
||||
|
||||
if (addedMonitors.Count > 0 || removedMonitors.Count > 0)
|
||||
{
|
||||
MonitorsChanged?.Invoke(this, new MonitorListChangedEventArgs(
|
||||
addedMonitors.AsReadOnly(),
|
||||
removedMonitors.AsReadOnly(),
|
||||
_monitors.AsReadOnly()));
|
||||
}
|
||||
|
||||
return _monitors.AsReadOnly();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_discoveryLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get brightness of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
|
||||
// Update cached brightness value
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
var oldBrightness = monitor.CurrentBrightness;
|
||||
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
|
||||
|
||||
// Trigger status change event
|
||||
if (oldBrightness != monitor.CurrentBrightness)
|
||||
{
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, oldBrightness, monitor.CurrentBrightness, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
return brightnessInfo;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Mark monitor as unavailable
|
||||
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)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldBrightness = monitor.CurrentBrightness;
|
||||
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
// Update monitor status
|
||||
monitor.UpdateStatus(brightness, true);
|
||||
|
||||
// Trigger status change event
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, oldBrightness, brightness, true, 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 async Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as IExtendedMonitorController;
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No extended controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldContrast = monitor.CurrentContrast;
|
||||
var result = await controller.SetContrastAsync(monitor, contrast, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
monitor.CurrentContrast = contrast;
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting contrast: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set volume of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as IExtendedMonitorController;
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No extended controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldVolume = monitor.CurrentVolume;
|
||||
var result = await controller.SetVolumeAsync(monitor, volume, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
monitor.CurrentVolume = volume;
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting volume: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor color temperature
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as DdcCiController;
|
||||
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 async Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor) as DdcCiController;
|
||||
if (controller == null)
|
||||
{
|
||||
return MonitorOperationResult.Failure("DDC/CI controller not available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var oldTemperature = monitor.CurrentColorTemperature;
|
||||
var result = await controller.SetColorTemperatureAsync(monitor, colorTemperature, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
monitor.CurrentColorTemperature = colorTemperature;
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
|
||||
// Trigger status change event
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor,
|
||||
$"Color temperature changed from {oldTemperature}K to {colorTemperature}K",
|
||||
MonitorStatusChangedEventArgs.ChangeType.ColorTemperature
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize color temperature for a monitor (async operation)
|
||||
/// </summary>
|
||||
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
|
||||
if (tempInfo.IsValid)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor != null)
|
||||
{
|
||||
// Convert VCP value to approximate Kelvin temperature
|
||||
// This is a rough mapping - actual values depend on monitor implementation
|
||||
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
|
||||
monitor.CurrentColorTemperature = kelvin;
|
||||
|
||||
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert VCP value to approximate Kelvin temperature
|
||||
/// </summary>
|
||||
private static int ConvertVcpValueToKelvin(int vcpValue, int maxVcpValue)
|
||||
{
|
||||
// Standard color temperature range mapping
|
||||
const int minKelvin = 2000; // Warm
|
||||
const int maxKelvin = 10000; // Cool
|
||||
|
||||
// Normalize VCP value to 0-1 range
|
||||
double normalizedVcp = maxVcpValue > 0 ? (double)vcpValue / maxVcpValue : 0.5;
|
||||
|
||||
// Map to Kelvin range
|
||||
int kelvin = (int)(minKelvin + (normalizedVcp * (maxKelvin - minKelvin)));
|
||||
|
||||
return Math.Clamp(kelvin, minKelvin, maxKelvin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh monitor status
|
||||
/// </summary>
|
||||
public async Task RefreshMonitorStatusAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = _monitors.Select(async monitor =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate connection status
|
||||
var isConnected = await controller.ValidateConnectionAsync(monitor, cancellationToken);
|
||||
var oldAvailability = monitor.IsAvailable;
|
||||
|
||||
if (isConnected)
|
||||
{
|
||||
// 获取当前亮度
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
var oldBrightness = monitor.CurrentBrightness;
|
||||
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
|
||||
|
||||
// Trigger status change event
|
||||
if (oldBrightness != monitor.CurrentBrightness || oldAvailability != monitor.IsAvailable)
|
||||
{
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, oldBrightness, monitor.CurrentBrightness, oldAvailability, monitor.IsAvailable));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
|
||||
// Trigger availability change event
|
||||
if (oldAvailability != monitor.IsAvailable)
|
||||
{
|
||||
MonitorStatusChanged?.Invoke(this, new MonitorStatusChangedEventArgs(
|
||||
monitor, monitor.CurrentBrightness, monitor.CurrentBrightness, oldAvailability, monitor.IsAvailable));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Refresh failed, mark as unavailable
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor by ID
|
||||
/// </summary>
|
||||
public Monitor? GetMonitor(string monitorId)
|
||||
{
|
||||
return _monitors.FirstOrDefault(m => m.Id == monitorId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get controller for the monitor
|
||||
/// </summary>
|
||||
private IMonitorController? GetControllerForMonitor(Monitor monitor)
|
||||
{
|
||||
return _controllers.FirstOrDefault(c => c.SupportedType == monitor.Type);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_refreshTimer?.Dispose();
|
||||
_discoveryLock?.Dispose();
|
||||
|
||||
// Release all controllers
|
||||
foreach (var controller in _controllers)
|
||||
{
|
||||
controller?.Dispose();
|
||||
}
|
||||
|
||||
_controllers.Clear();
|
||||
_monitors.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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>
|
||||
/// Serializes monitor property updates to prevent race conditions
|
||||
/// Simple approach: one operation at a time, newest replaces pending
|
||||
/// </summary>
|
||||
public class MonitorPropertyManager : IDisposable
|
||||
{
|
||||
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
|
||||
private int _pendingValue = -1; // Value waiting to be executed
|
||||
private bool _hasPendingValue = false;
|
||||
private Task? _currentTask;
|
||||
private readonly string _monitorId;
|
||||
private readonly string _propertyName;
|
||||
|
||||
public MonitorPropertyManager(string monitorId, string propertyName)
|
||||
{
|
||||
_monitorId = monitorId;
|
||||
_propertyName = propertyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a property update - replaces any pending update
|
||||
/// </summary>
|
||||
public void QueueUpdate(int newValue, Func<int, CancellationToken, Task> updateAction)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
// Always update the pending value (newest wins)
|
||||
_pendingValue = newValue;
|
||||
_hasPendingValue = true;
|
||||
|
||||
// If no operation is currently running, start one
|
||||
if (_currentTask == null || _currentTask.IsCompleted)
|
||||
{
|
||||
_currentTask = ExecuteUpdatesAsync(updateAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteUpdatesAsync(Func<int, CancellationToken, Task> updateAction)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
int valueToUpdate;
|
||||
|
||||
// Get the next value to update
|
||||
lock (this)
|
||||
{
|
||||
if (!_hasPendingValue)
|
||||
{
|
||||
// No more updates pending
|
||||
break;
|
||||
}
|
||||
|
||||
valueToUpdate = _pendingValue;
|
||||
_hasPendingValue = false;
|
||||
}
|
||||
|
||||
// Execute the hardware update
|
||||
try
|
||||
{
|
||||
await _operationSemaphore.WaitAsync();
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
||||
await updateAction(valueToUpdate, cts.Token);
|
||||
|
||||
Logger.LogDebug($"[{_monitorId}] {_propertyName} updated to {valueToUpdate}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[{_monitorId}] Failed to update {_propertyName} to {valueToUpdate}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for all pending updates to complete
|
||||
/// </summary>
|
||||
public async Task FlushAsync()
|
||||
{
|
||||
var currentTask = _currentTask;
|
||||
if (currentTask != null && !currentTask.IsCompleted)
|
||||
{
|
||||
try
|
||||
{
|
||||
await currentTask;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors during flush
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_operationSemaphore?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
347
src/modules/powerdisplay/PowerDisplay/Helpers/SettingsManager.cs
Normal file
347
src/modules/powerdisplay/PowerDisplay/Helpers/SettingsManager.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
// 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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings manager with periodic save mechanism (write-only during runtime)
|
||||
/// </summary>
|
||||
public class SettingsManager : IDisposable
|
||||
{
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly SemaphoreSlim _fileAccessSemaphore = new(1, 1);
|
||||
private readonly Timer _periodicSaveTimer;
|
||||
private readonly ConcurrentDictionary<string, MonitorSettings> _pendingSettings = new();
|
||||
private const string MODULE_NAME = "PowerDisplay";
|
||||
private const int PERIODIC_SAVE_INTERVAL_MS = 1000; // Check every 1 second
|
||||
private bool _disposed;
|
||||
private bool _hasPendingChanges = false;
|
||||
|
||||
/// <summary>
|
||||
/// Represents all settings for a single monitor
|
||||
/// </summary>
|
||||
private class MonitorSettings
|
||||
{
|
||||
public int Brightness { get; set; }
|
||||
public int ColorTemperature { get; set; }
|
||||
public int Contrast { get; set; }
|
||||
public int Volume { get; set; }
|
||||
public bool IsDirty { get; set; } = false;
|
||||
}
|
||||
|
||||
public SettingsManager(ISettingsUtils settingsUtils)
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
// Start periodic timer that checks every second
|
||||
_periodicSaveTimer = new Timer(CheckAndSavePendingChanges, null, PERIODIC_SAVE_INTERVAL_MS, PERIODIC_SAVE_INTERVAL_MS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a setting value in memory (will be saved on next periodic check)
|
||||
/// </summary>
|
||||
public void QueueSettingChange(string monitorId, string property, object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var intValue = Convert.ToInt32(value);
|
||||
|
||||
// Get or create monitor settings
|
||||
var settings = _pendingSettings.GetOrAdd(monitorId, _ => new MonitorSettings());
|
||||
|
||||
// Update the specific property
|
||||
switch (property)
|
||||
{
|
||||
case "Brightness":
|
||||
settings.Brightness = intValue;
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
settings.ColorTemperature = intValue;
|
||||
break;
|
||||
case "Contrast":
|
||||
settings.Contrast = intValue;
|
||||
break;
|
||||
case "Volume":
|
||||
settings.Volume = intValue;
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unknown property: {property}");
|
||||
return;
|
||||
}
|
||||
|
||||
settings.IsDirty = true;
|
||||
_hasPendingChanges = true;
|
||||
|
||||
Logger.LogTrace($"[Queue] Updated {property}={intValue} for monitor '{monitorId}' in memory");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to queue setting change: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodic check (every 1 second) - save if there are pending changes
|
||||
/// </summary>
|
||||
private async void CheckAndSavePendingChanges(object? state)
|
||||
{
|
||||
// Quick check without lock - if no changes, skip
|
||||
if (_disposed || !_hasPendingChanges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _fileAccessSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
// Double check after acquiring lock
|
||||
if (!_hasPendingChanges)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Load current settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(MODULE_NAME);
|
||||
|
||||
if (settings.Properties.SavedMonitorSettings == null)
|
||||
{
|
||||
settings.Properties.SavedMonitorSettings = new Dictionary<string, MonitorSavedSettings>();
|
||||
}
|
||||
|
||||
int changeCount = 0;
|
||||
var now = DateTime.Now;
|
||||
|
||||
// Apply all dirty monitor settings
|
||||
foreach (var kvp in _pendingSettings)
|
||||
{
|
||||
var monitorId = kvp.Key;
|
||||
var monitorSettings = kvp.Value;
|
||||
|
||||
if (!monitorSettings.IsDirty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get or create saved settings for this monitor
|
||||
if (!settings.Properties.SavedMonitorSettings.ContainsKey(monitorId))
|
||||
{
|
||||
settings.Properties.SavedMonitorSettings[monitorId] = new MonitorSavedSettings();
|
||||
}
|
||||
|
||||
var savedSettings = settings.Properties.SavedMonitorSettings[monitorId];
|
||||
|
||||
// Update all properties
|
||||
savedSettings.Brightness = monitorSettings.Brightness;
|
||||
savedSettings.ColorTemperature = monitorSettings.ColorTemperature;
|
||||
savedSettings.Contrast = monitorSettings.Contrast;
|
||||
savedSettings.Volume = monitorSettings.Volume;
|
||||
savedSettings.LastUpdated = now;
|
||||
|
||||
// Mark as clean
|
||||
monitorSettings.IsDirty = false;
|
||||
changeCount++;
|
||||
}
|
||||
|
||||
// If we saved anything, write to file
|
||||
if (changeCount > 0)
|
||||
{
|
||||
_settingsUtils.SaveSettings(settings.ToJsonString(), MODULE_NAME);
|
||||
Logger.LogInfo($"[Periodic Save] Saved settings for {changeCount} monitor(s)");
|
||||
}
|
||||
|
||||
// Reset flag
|
||||
_hasPendingChanges = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save pending changes: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileAccessSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force immediate save of all pending changes
|
||||
/// </summary>
|
||||
public async Task FlushPendingChangesAsync()
|
||||
{
|
||||
// Temporarily stop the timer to prevent concurrent execution
|
||||
_periodicSaveTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
try
|
||||
{
|
||||
// Trigger immediate save
|
||||
CheckAndSavePendingChanges(null);
|
||||
|
||||
// Give it a moment to complete
|
||||
await Task.Delay(100);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Restart the periodic timer
|
||||
_periodicSaveTimer.Change(PERIODIC_SAVE_INTERVAL_MS, PERIODIC_SAVE_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe method to save monitor info
|
||||
/// </summary>
|
||||
public async Task SaveMonitorInfoAsync(IReadOnlyList<PowerDisplay.Core.Models.Monitor> monitors)
|
||||
{
|
||||
await _fileAccessSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(MODULE_NAME);
|
||||
|
||||
var monitorInfoList = monitors.Select(monitor =>
|
||||
{
|
||||
var existingMonitor = settings.Properties.Monitors.FirstOrDefault(m =>
|
||||
m.HardwareId == monitor.HardwareId || m.InternalName == GetInternalName(monitor));
|
||||
|
||||
var monitorInfo = new MonitorInfo(
|
||||
monitor.Name,
|
||||
GetInternalName(monitor),
|
||||
monitor.HardwareId,
|
||||
GetCommunicationMethod(monitor),
|
||||
GetMonitorType(monitor),
|
||||
monitor.CurrentBrightness,
|
||||
monitor.CurrentColorTemperature
|
||||
);
|
||||
|
||||
if (existingMonitor != null)
|
||||
{
|
||||
monitorInfo.EnableColorTemperature = existingMonitor.EnableColorTemperature;
|
||||
monitorInfo.EnableContrast = existingMonitor.EnableContrast;
|
||||
monitorInfo.EnableVolume = existingMonitor.EnableVolume;
|
||||
monitorInfo.IsHidden = existingMonitor.IsHidden;
|
||||
}
|
||||
|
||||
return monitorInfo;
|
||||
}).ToList();
|
||||
|
||||
settings.Properties.Monitors = monitorInfoList;
|
||||
_settingsUtils.SaveSettings(settings.ToJsonString(), MODULE_NAME);
|
||||
|
||||
Logger.LogInfo($"Synchronized save of monitor info for {monitors.Count} monitors");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save monitor info: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileAccessSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetInternalName(PowerDisplay.Core.Models.Monitor monitor)
|
||||
{
|
||||
if (monitor.Type == PowerDisplay.Core.Models.MonitorType.Internal)
|
||||
{
|
||||
return "Internal Display";
|
||||
}
|
||||
|
||||
var id = monitor.Id;
|
||||
if (id.StartsWith("DDC_"))
|
||||
{
|
||||
return id.Substring(4);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private string GetCommunicationMethod(PowerDisplay.Core.Models.Monitor monitor)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(monitor.CommunicationMethod))
|
||||
{
|
||||
return monitor.CommunicationMethod;
|
||||
}
|
||||
|
||||
switch (monitor.Type)
|
||||
{
|
||||
case PowerDisplay.Core.Models.MonitorType.External:
|
||||
return "DDC/CI";
|
||||
case PowerDisplay.Core.Models.MonitorType.Internal:
|
||||
return "WMI";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMonitorType(PowerDisplay.Core.Models.Monitor monitor)
|
||||
{
|
||||
switch (monitor.Type)
|
||||
{
|
||||
case PowerDisplay.Core.Models.MonitorType.External:
|
||||
return "External Monitor";
|
||||
case PowerDisplay.Core.Models.MonitorType.Internal:
|
||||
return "Internal Monitor";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Periodically save monitor information to settings file
|
||||
/// </summary>
|
||||
public async Task SaveMonitorInfoPeriodicallyAsync(IReadOnlyList<PowerDisplay.Core.Models.Monitor> monitors, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait for a while to ensure the application is fully initialized
|
||||
await Task.Delay(2000, cancellationToken);
|
||||
|
||||
// First update to settings file
|
||||
await SaveMonitorInfoAsync(monitors);
|
||||
|
||||
// Periodically update settings file (every 60 seconds to reduce file conflicts)
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(60000, cancellationToken); // 60 seconds
|
||||
await SaveMonitorInfoAsync(monitors);
|
||||
|
||||
Logger.LogInfo("Periodic monitor info update completed");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Normal cancellation
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error in periodic monitor info update: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
// Stop the periodic timer
|
||||
_periodicSaveTimer?.Dispose();
|
||||
|
||||
// Flush any remaining changes
|
||||
if (_hasPendingChanges)
|
||||
{
|
||||
CheckAndSavePendingChanges(null);
|
||||
}
|
||||
|
||||
_fileAccessSemaphore?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/modules/powerdisplay/PowerDisplay/Helpers/ThemeManager.cs
Normal file
206
src/modules/powerdisplay/PowerDisplay/Helpers/ThemeManager.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 管理应用程序主题设置
|
||||
/// </summary>
|
||||
public static class ThemeManager
|
||||
{
|
||||
private const string ThemeSettingKey = "AppTheme";
|
||||
private static readonly string SettingsFilePath;
|
||||
private static readonly ISettingsUtils _settingsUtils = new SettingsUtils();
|
||||
|
||||
static ThemeManager()
|
||||
{
|
||||
// 使用本地AppData文件夹存储设置
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var appFolder = Path.Combine(localAppData, "PowerDisplay");
|
||||
|
||||
// 确保文件夹存在
|
||||
if (!Directory.Exists(appFolder))
|
||||
{
|
||||
Directory.CreateDirectory(appFolder);
|
||||
}
|
||||
|
||||
SettingsFilePath = Path.Combine(appFolder, "theme.settings");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取保存的主题设置
|
||||
/// </summary>
|
||||
public static ElementTheme GetSavedTheme()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsFilePath))
|
||||
{
|
||||
var savedTheme = File.ReadAllText(SettingsFilePath);
|
||||
return savedTheme switch
|
||||
{
|
||||
"Light" => ElementTheme.Light,
|
||||
"Dark" => ElementTheme.Dark,
|
||||
_ => ElementTheme.Default
|
||||
};
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果读取失败,返回默认值
|
||||
}
|
||||
|
||||
return ElementTheme.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存主题设置
|
||||
/// </summary>
|
||||
public static void SaveTheme(ElementTheme theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(SettingsFilePath, theme.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略保存错误
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用主题到窗口
|
||||
/// </summary>
|
||||
public static void ApplyTheme(Window window, ElementTheme theme)
|
||||
{
|
||||
if (window?.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.RequestedTheme = theme;
|
||||
SaveTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换主题(深色/浅色)
|
||||
/// </summary>
|
||||
public static ElementTheme ToggleTheme(Window window)
|
||||
{
|
||||
var currentTheme = GetCurrentTheme(window);
|
||||
var newTheme = currentTheme == ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light;
|
||||
|
||||
ApplyTheme(window, newTheme);
|
||||
return newTheme;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前窗口的主题
|
||||
/// </summary>
|
||||
public static ElementTheme GetCurrentTheme(Window window)
|
||||
{
|
||||
if (window?.Content is FrameworkElement rootElement)
|
||||
{
|
||||
return rootElement.RequestedTheme switch
|
||||
{
|
||||
ElementTheme.Light => ElementTheme.Light,
|
||||
ElementTheme.Dark => ElementTheme.Dark,
|
||||
_ => Application.Current.RequestedTheme == ApplicationTheme.Light
|
||||
? ElementTheme.Light
|
||||
: ElementTheme.Dark
|
||||
};
|
||||
}
|
||||
|
||||
return ElementTheme.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为深色主题
|
||||
/// </summary>
|
||||
public static bool IsDarkTheme(Window window)
|
||||
{
|
||||
return GetCurrentTheme(window) == ElementTheme.Dark;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从PowerToys设置中获取主题
|
||||
/// </summary>
|
||||
public static ElementTheme GetThemeFromPowerToysSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
return settings.Properties.Theme switch
|
||||
{
|
||||
"Light" => ElementTheme.Light,
|
||||
"Dark" => ElementTheme.Dark,
|
||||
_ => ElementTheme.Default
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ElementTheme.Default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将主题保存到PowerToys设置
|
||||
/// </summary>
|
||||
public static void SaveThemeToPowerToysSettings(ElementTheme theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
settings.Properties.Theme = theme.ToString();
|
||||
_settingsUtils.SaveSettings(settings.ToJsonString(), "PowerDisplay");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误但不阻止操作
|
||||
try
|
||||
{
|
||||
Logger.LogError($"Failed to save theme to PowerToys settings: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略日志错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取保存的主题设置(优先从PowerToys设置读取)
|
||||
/// </summary>
|
||||
public static ElementTheme GetSavedThemeWithPriority()
|
||||
{
|
||||
// 首先尝试从PowerToys设置读取
|
||||
var powerToysTheme = GetThemeFromPowerToysSettings();
|
||||
if (powerToysTheme != ElementTheme.Default)
|
||||
{
|
||||
// 同步到本地设置
|
||||
SaveTheme(powerToysTheme);
|
||||
return powerToysTheme;
|
||||
}
|
||||
|
||||
// 如果PowerToys设置没有或失败,回退到本地设置
|
||||
return GetSavedTheme();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用主题并同步到两个设置系统
|
||||
/// </summary>
|
||||
public static void ApplyThemeAndSync(Window window, ElementTheme theme)
|
||||
{
|
||||
// 应用到窗口
|
||||
ApplyTheme(window, theme);
|
||||
|
||||
// 同步到PowerToys设置
|
||||
SaveThemeToPowerToysSettings(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
571
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconHelper.cs
Normal file
571
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconHelper.cs
Normal file
@@ -0,0 +1,571 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// System tray icon helper class
|
||||
/// </summary>
|
||||
public class TrayIconHelper : IDisposable
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct NOTIFYICONDATA
|
||||
{
|
||||
public uint CbSize;
|
||||
public IntPtr HWnd;
|
||||
public uint UID;
|
||||
public uint UFlags;
|
||||
public uint UCallbackMessage;
|
||||
public IntPtr HIcon;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string SzTip;
|
||||
public uint DwState;
|
||||
public uint DwStateMask;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||
public string SzInfo;
|
||||
public uint UTimeout;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||
public string SzInfoTitle;
|
||||
public uint DwInfoFlags;
|
||||
}
|
||||
|
||||
private const uint NifMessage = 0x00000001;
|
||||
private const uint NifIcon = 0x00000002;
|
||||
private const uint NifTip = 0x00000004;
|
||||
private const uint NifInfo = 0x00000010;
|
||||
|
||||
private const uint NimAdd = 0x00000000;
|
||||
private const uint NimModify = 0x00000001;
|
||||
private const uint NimDelete = 0x00000002;
|
||||
|
||||
private const uint WmUser = 0x0400;
|
||||
private const uint WmTrayicon = WmUser + 1;
|
||||
private const uint WmLbuttonup = 0x0202;
|
||||
private const uint WmRbuttonup = 0x0205;
|
||||
private const uint WmCommand = 0x0111;
|
||||
|
||||
private uint _wmTaskbarCreated; // TaskbarCreated message ID
|
||||
|
||||
// Menu item IDs
|
||||
private const int IdShow = 1001;
|
||||
private const int IdExit = 1002;
|
||||
private const int IdRefresh = 1003;
|
||||
private const int IdSettings = 1004;
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpData);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern IntPtr CreateWindowEx(
|
||||
uint dwExStyle,
|
||||
string lpClassName,
|
||||
string lpWindowName,
|
||||
uint dwStyle,
|
||||
int x,
|
||||
int y,
|
||||
int nWidth,
|
||||
int nHeight,
|
||||
IntPtr hWndParent,
|
||||
IntPtr hMenu,
|
||||
IntPtr hInstance,
|
||||
IntPtr lpParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DestroyWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr CreatePopupMenu();
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern bool AppendMenu(IntPtr hMenu, uint uFlags, uint uIDNewItem, string lpNewItem);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool DestroyMenu(IntPtr hMenu);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int TrackPopupMenu(
|
||||
IntPtr hMenu,
|
||||
uint uFlags,
|
||||
int x,
|
||||
int y,
|
||||
int nReserved,
|
||||
IntPtr hWnd,
|
||||
IntPtr prcRect);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern uint RegisterWindowMessage(string lpString);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
|
||||
private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
private const uint MfString = 0x00000000;
|
||||
private const uint MfSeparator = 0x00000800;
|
||||
private const uint TpmLeftalign = 0x0000;
|
||||
private const uint TpmReturncmd = 0x0100;
|
||||
|
||||
private const int SwHide = 0;
|
||||
private const int SwShow = 5;
|
||||
|
||||
private IntPtr _messageWindowHandle;
|
||||
private NOTIFYICONDATA _notifyIconData;
|
||||
private bool _isDisposed;
|
||||
private WndProc _wndProc;
|
||||
private Window _mainWindow;
|
||||
private Action? _onShowWindow;
|
||||
private Action? _onExitApplication;
|
||||
private Action? _onRefresh;
|
||||
private Action? _onSettings;
|
||||
private bool _isWindowVisible = true;
|
||||
private System.Drawing.Icon? _trayIcon; // Keep icon reference to prevent garbage collection
|
||||
|
||||
public TrayIconHelper(Window mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
_wndProc = WindowProc;
|
||||
|
||||
// Register TaskbarCreated message
|
||||
_wmTaskbarCreated = RegisterWindowMessage("TaskbarCreated");
|
||||
Logger.LogInfo($"Registered TaskbarCreated message: {_wmTaskbarCreated}");
|
||||
|
||||
if (!CreateMessageWindow())
|
||||
{
|
||||
Logger.LogError("Failed to create message window");
|
||||
return;
|
||||
}
|
||||
|
||||
CreateTrayIcon();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set callback functions
|
||||
/// </summary>
|
||||
public void SetCallbacks(Action onShow, Action onExit, Action? onRefresh = null, Action? onSettings = null)
|
||||
{
|
||||
_onShowWindow = onShow;
|
||||
_onExitApplication = onExit;
|
||||
_onRefresh = onRefresh;
|
||||
_onSettings = onSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create message window - using system predefined Message window class
|
||||
/// </summary>
|
||||
private bool CreateMessageWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Creating message window using system Message class...");
|
||||
|
||||
// Use system predefined "Message" window class, no registration needed
|
||||
// HWND_MESSAGE (-3) creates pure message window, no hInstance needed
|
||||
_messageWindowHandle = CreateWindowEx(
|
||||
0, // dwExStyle
|
||||
"Message", // lpClassName - system predefined message window class
|
||||
string.Empty, // lpWindowName
|
||||
0, // dwStyle
|
||||
0, 0, 0, 0, // x, y, width, height
|
||||
new IntPtr(-3), // hWndParent = HWND_MESSAGE (pure message window)
|
||||
IntPtr.Zero, // hMenu
|
||||
IntPtr.Zero, // hInstance - not needed
|
||||
IntPtr.Zero // lpParam
|
||||
);
|
||||
|
||||
if (_messageWindowHandle == IntPtr.Zero)
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
Logger.LogError($"CreateWindowEx failed with error: {error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Message window created successfully: {_messageWindowHandle}");
|
||||
|
||||
// Set window procedure to handle our messages
|
||||
SetWindowLongPtr(_messageWindowHandle, -4, Marshal.GetFunctionPointerForDelegate(_wndProc));
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"CreateMessageWindow exception: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create tray icon
|
||||
/// </summary>
|
||||
private void CreateTrayIcon()
|
||||
{
|
||||
if (_messageWindowHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Cannot create tray icon: invalid message window handle");
|
||||
return;
|
||||
}
|
||||
|
||||
// First try to delete any existing old icon (if any)
|
||||
var tempData = new NOTIFYICONDATA
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATA)),
|
||||
HWnd = _messageWindowHandle,
|
||||
UID = 1
|
||||
};
|
||||
Shell_NotifyIcon(NimDelete, ref tempData);
|
||||
|
||||
// Get icon handle
|
||||
var iconHandle = GetDefaultIcon();
|
||||
if (iconHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Cannot create tray icon: invalid icon handle");
|
||||
return;
|
||||
}
|
||||
|
||||
_notifyIconData = new NOTIFYICONDATA
|
||||
{
|
||||
CbSize = (uint)Marshal.SizeOf(typeof(NOTIFYICONDATA)),
|
||||
HWnd = _messageWindowHandle,
|
||||
UID = 1,
|
||||
UFlags = NifMessage | NifIcon | NifTip,
|
||||
UCallbackMessage = WmTrayicon,
|
||||
HIcon = iconHandle,
|
||||
SzTip = "Power Display",
|
||||
};
|
||||
|
||||
// Retry mechanism: try up to 3 times to create tray icon
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 500;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
Logger.LogDebug($"Creating tray icon (attempt {attempt}/{maxRetries})...");
|
||||
|
||||
bool result = Shell_NotifyIcon(NimAdd, ref _notifyIconData);
|
||||
if (result)
|
||||
{
|
||||
Logger.LogInfo($"Tray icon created successfully on attempt {attempt}");
|
||||
return;
|
||||
}
|
||||
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
Logger.LogWarning($"Failed to create tray icon on attempt {attempt}. Error: {lastError}");
|
||||
|
||||
// Analyze specific error and provide suggestions
|
||||
switch (lastError)
|
||||
{
|
||||
case 0: // ERROR_SUCCESS - may be false success
|
||||
Logger.LogWarning("Shell_NotifyIcon returned false but GetLastWin32Error is 0");
|
||||
break;
|
||||
case 1400: // ERROR_INVALID_WINDOW_HANDLE
|
||||
Logger.LogWarning("Invalid window handle - message window may not be properly created");
|
||||
break;
|
||||
case 1418: // ERROR_THREAD_1_INACTIVE
|
||||
Logger.LogWarning("Thread inactive - may need to wait for Explorer to be ready");
|
||||
break;
|
||||
case 1414: // ERROR_INVALID_ICON_HANDLE
|
||||
Logger.LogWarning("Invalid icon handle - icon may have been garbage collected");
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unexpected error code: {lastError}");
|
||||
break;
|
||||
}
|
||||
|
||||
// If not the last attempt, wait and retry
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Logger.LogDebug($"Retrying in {retryDelayMs}ms...");
|
||||
System.Threading.Thread.Sleep(retryDelayMs);
|
||||
|
||||
// Re-get icon handle to prevent handle invalidation
|
||||
iconHandle = GetDefaultIcon();
|
||||
_notifyIconData.HIcon = iconHandle;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogError($"Failed to create tray icon after {maxRetries} attempts");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get default icon
|
||||
/// </summary>
|
||||
private IntPtr GetDefaultIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
// First release previous icon
|
||||
_trayIcon?.Dispose();
|
||||
_trayIcon = null;
|
||||
|
||||
// Try to load icon from Assets folder in exe directory
|
||||
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
|
||||
if (!string.IsNullOrEmpty(exePath))
|
||||
{
|
||||
var exeDir = System.IO.Path.GetDirectoryName(exePath);
|
||||
if (!string.IsNullOrEmpty(exeDir))
|
||||
{
|
||||
var iconPath = System.IO.Path.Combine(exeDir, "Assets", "PowerDisplay.ico");
|
||||
|
||||
Logger.LogDebug($"Attempting to load icon from: {iconPath}");
|
||||
|
||||
if (System.IO.File.Exists(iconPath))
|
||||
{
|
||||
// Create icon and save as class member to prevent garbage collection
|
||||
_trayIcon = new System.Drawing.Icon(iconPath);
|
||||
Logger.LogInfo($"Successfully loaded custom icon from {iconPath}");
|
||||
return _trayIcon.Handle;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Icon file not found at: {iconPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load PowerDisplay icon: {ex.Message}");
|
||||
_trayIcon?.Dispose();
|
||||
_trayIcon = null;
|
||||
}
|
||||
|
||||
// If loading fails, use system default icon
|
||||
var systemIconHandle = LoadIcon(IntPtr.Zero, new IntPtr(32512)); // IDI_APPLICATION
|
||||
Logger.LogInfo($"Using system default icon: {systemIconHandle}");
|
||||
return systemIconHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Window message processing
|
||||
/// </summary>
|
||||
private IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (msg == _wmTaskbarCreated)
|
||||
{
|
||||
// Explorer restarted, need to recreate tray icon
|
||||
Logger.LogInfo("TaskbarCreated message received - recreating tray icon");
|
||||
CreateTrayIcon();
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case WmTrayicon:
|
||||
HandleTrayIconMessage(lParam);
|
||||
break;
|
||||
case WmCommand:
|
||||
HandleMenuCommand(wParam);
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle tray icon messages
|
||||
/// </summary>
|
||||
private void HandleTrayIconMessage(IntPtr lParam)
|
||||
{
|
||||
switch ((uint)lParam)
|
||||
{
|
||||
case WmLbuttonup:
|
||||
// Left click - show/hide window
|
||||
ToggleWindowVisibility();
|
||||
break;
|
||||
case WmRbuttonup:
|
||||
// Right click - show menu
|
||||
ShowContextMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle window visibility state
|
||||
/// </summary>
|
||||
private void ToggleWindowVisibility()
|
||||
{
|
||||
_isWindowVisible = !_isWindowVisible;
|
||||
if (_isWindowVisible)
|
||||
{
|
||||
_onShowWindow?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide window logic will be implemented in MainWindow
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow);
|
||||
ShowWindow(hWnd, SwHide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show right-click menu
|
||||
/// </summary>
|
||||
private void ShowContextMenu()
|
||||
{
|
||||
var hMenu = CreatePopupMenu();
|
||||
|
||||
AppendMenu(hMenu, MfString, IdShow, _isWindowVisible ? "Hide Window" : "Show Window");
|
||||
if (_onRefresh != null)
|
||||
{
|
||||
AppendMenu(hMenu, MfString, IdRefresh, "Refresh Monitors");
|
||||
}
|
||||
if (_onSettings != null)
|
||||
{
|
||||
AppendMenu(hMenu, MfString, IdSettings, "Settings");
|
||||
}
|
||||
|
||||
AppendMenu(hMenu, MfSeparator, 0, string.Empty);
|
||||
AppendMenu(hMenu, MfString, IdExit, "Exit");
|
||||
|
||||
GetCursorPos(out POINT pt);
|
||||
SetForegroundWindow(_messageWindowHandle);
|
||||
|
||||
var cmd = TrackPopupMenu(hMenu, TpmLeftalign | TpmReturncmd, pt.X, pt.Y, 0, _messageWindowHandle, IntPtr.Zero);
|
||||
|
||||
if (cmd != 0)
|
||||
{
|
||||
HandleMenuCommand(new IntPtr(cmd));
|
||||
}
|
||||
|
||||
DestroyMenu(hMenu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle menu commands
|
||||
/// </summary>
|
||||
private void HandleMenuCommand(IntPtr commandId)
|
||||
{
|
||||
switch (commandId.ToInt32())
|
||||
{
|
||||
case IdShow:
|
||||
ToggleWindowVisibility();
|
||||
break;
|
||||
case IdRefresh:
|
||||
_onRefresh?.Invoke();
|
||||
break;
|
||||
case IdSettings:
|
||||
_onSettings?.Invoke();
|
||||
break;
|
||||
case IdExit:
|
||||
_onExitApplication?.Invoke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show balloon tip
|
||||
/// </summary>
|
||||
public void ShowBalloonTip(string title, string text, uint timeout = 3000)
|
||||
{
|
||||
_notifyIconData.UFlags |= NifInfo;
|
||||
_notifyIconData.SzInfoTitle = title;
|
||||
_notifyIconData.SzInfo = text;
|
||||
_notifyIconData.UTimeout = timeout;
|
||||
_notifyIconData.DwInfoFlags = 1; // NIIF_INFO
|
||||
|
||||
Shell_NotifyIcon(NimModify, ref _notifyIconData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update tray icon tooltip text
|
||||
/// </summary>
|
||||
public void UpdateTooltip(string tooltip)
|
||||
{
|
||||
_notifyIconData.SzTip = tooltip;
|
||||
Shell_NotifyIcon(NimModify, ref _notifyIconData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreate tray icon (for failure recovery)
|
||||
/// </summary>
|
||||
public void RecreateTrayIcon()
|
||||
{
|
||||
Logger.LogInfo("Manually recreating tray icon...");
|
||||
CreateTrayIcon();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
Logger.LogDebug("Disposing TrayIconHelper...");
|
||||
|
||||
// Remove tray icon
|
||||
try
|
||||
{
|
||||
Shell_NotifyIcon(NimDelete, ref _notifyIconData);
|
||||
Logger.LogInfo("Tray icon removed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error removing tray icon: {ex.Message}");
|
||||
}
|
||||
|
||||
// Release icon resources
|
||||
try
|
||||
{
|
||||
_trayIcon?.Dispose();
|
||||
_trayIcon = null;
|
||||
Logger.LogInfo("Icon resources disposed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error disposing icon: {ex.Message}");
|
||||
}
|
||||
|
||||
// Destroy message window
|
||||
try
|
||||
{
|
||||
if (_messageWindowHandle != IntPtr.Zero)
|
||||
{
|
||||
DestroyWindow(_messageWindowHandle);
|
||||
_messageWindowHandle = IntPtr.Zero;
|
||||
Logger.LogInfo("Message window destroyed successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error destroying message window: {ex.Message}");
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
Logger.LogDebug("TrayIconHelper disposed completely");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1131
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs
Normal file
1131
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiController.cs
Normal file
File diff suppressed because it is too large
Load Diff
610
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
610
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
@@ -0,0 +1,610 @@
|
||||
// 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;
|
||||
|
||||
// 类型别名,兼容 Windows API 命名约定
|
||||
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
|
||||
using LUID = PowerDisplay.Native.Luid;
|
||||
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Native.DISPLAYCONFIG_TARGET_DEVICE_NAME;
|
||||
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
|
||||
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Native.DISPLAYCONFIG_PATH_INFO;
|
||||
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
|
||||
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// DDC/CI 原生 API 封装
|
||||
/// </summary>
|
||||
public static class DdcCiNative
|
||||
{
|
||||
// DLL Imports
|
||||
private const string Dxva2Dll = "Dxva2.dll";
|
||||
private const string User32Dll = "User32.dll";
|
||||
|
||||
// Physical Monitor API
|
||||
|
||||
/// <summary>
|
||||
/// 从 HMONITOR 获取物理显示器数量
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetNumberOfPhysicalMonitorsFromHMONITOR(
|
||||
IntPtr hMonitor,
|
||||
ref uint pdwNumberOfPhysicalMonitors);
|
||||
|
||||
/// <summary>
|
||||
/// 从 HMONITOR 获取物理显示器数组
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetPhysicalMonitorsFromHMONITOR(
|
||||
IntPtr hMonitor,
|
||||
uint dwPhysicalMonitorArraySize,
|
||||
[Out] PHYSICAL_MONITOR[] pPhysicalMonitorArray);
|
||||
|
||||
/// <summary>
|
||||
/// 销毁物理显示器句柄
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool DestroyPhysicalMonitor(IntPtr hPhysicalMonitor);
|
||||
|
||||
/// <summary>
|
||||
/// 销毁物理显示器数组
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DestroyPhysicalMonitors(
|
||||
uint dwPhysicalMonitorArraySize,
|
||||
PHYSICAL_MONITOR[] pPhysicalMonitorArray);
|
||||
|
||||
/// <summary>
|
||||
/// 获取 VCP 功能和回复
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetVCPFeatureAndVCPFeatureReply(
|
||||
IntPtr hPhysicalMonitor,
|
||||
byte bVCPCode,
|
||||
IntPtr pvct,
|
||||
out uint pdwCurrentValue,
|
||||
out uint pdwMaximumValue);
|
||||
|
||||
/// <summary>
|
||||
/// 设置 VCP 功能
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetVCPFeature(
|
||||
IntPtr hPhysicalMonitor,
|
||||
byte bVCPCode,
|
||||
uint dwNewValue);
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前设置
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool SaveCurrentSettings(IntPtr hPhysicalMonitor);
|
||||
|
||||
/// <summary>
|
||||
/// 获取功能字符串长度
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetCapabilitiesStringLength(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwCapabilitiesStringLengthInCharacters);
|
||||
|
||||
/// <summary>
|
||||
/// 功能请求和功能回复
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool CapabilitiesRequestAndCapabilitiesReply(
|
||||
IntPtr hPhysicalMonitor,
|
||||
[Out] IntPtr pszASCIICapabilitiesString,
|
||||
uint dwCapabilitiesStringLengthInCharacters);
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器亮度
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetMonitorBrightness(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwMinimumBrightness,
|
||||
out uint pdwCurrentBrightness,
|
||||
out uint pdwMaximumBrightness);
|
||||
|
||||
/// <summary>
|
||||
/// 设置显示器亮度
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetMonitorBrightness(
|
||||
IntPtr hPhysicalMonitor,
|
||||
uint dwNewBrightness);
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器对比度
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetMonitorContrast(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwMinimumContrast,
|
||||
out uint pdwCurrentContrast,
|
||||
out uint pdwMaximumContrast);
|
||||
|
||||
/// <summary>
|
||||
/// 设置显示器对比度
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetMonitorContrast(
|
||||
IntPtr hPhysicalMonitor,
|
||||
uint dwNewContrast);
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器功能
|
||||
/// </summary>
|
||||
[DllImport(Dxva2Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetMonitorCapabilities(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwMonitorCapabilities,
|
||||
out uint pdwSupportedColorTemperatures);
|
||||
|
||||
// Display Configuration API
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示配置缓冲区大小
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true)]
|
||||
private static extern int GetDisplayConfigBufferSizes(
|
||||
uint flags,
|
||||
out uint numPathArrayElements,
|
||||
out uint numModeInfoArrayElements);
|
||||
|
||||
/// <summary>
|
||||
/// 查询显示配置
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true)]
|
||||
private static extern int QueryDisplayConfig(
|
||||
uint flags,
|
||||
ref uint numPathArrayElements,
|
||||
[Out] DISPLAYCONFIG_PATH_INFO[] pathArray,
|
||||
ref uint numModeInfoArrayElements,
|
||||
[Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray,
|
||||
IntPtr currentTopologyId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示配置设备信息
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true)]
|
||||
private static extern int DisplayConfigGetDeviceInfo(
|
||||
ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName);
|
||||
|
||||
// Display Configuration 常量
|
||||
public const uint QdcAllPaths = 0x00000001;
|
||||
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
|
||||
|
||||
/// <summary>
|
||||
/// 枚举显示监视器
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool EnumDisplayMonitors(
|
||||
IntPtr hdc,
|
||||
IntPtr lprcClip,
|
||||
MonitorEnumProc lpfnEnum,
|
||||
IntPtr dwData);
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器信息
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true, CharSet = CharSet.Auto)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetMonitorInfo(
|
||||
IntPtr hMonitor,
|
||||
ref MONITORINFOEX lpmi);
|
||||
|
||||
/// <summary>
|
||||
/// 枚举显示设备
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true, CharSet = CharSet.Ansi)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumDisplayDevices(
|
||||
string? lpDevice,
|
||||
uint iDevNum,
|
||||
ref DISPLAY_DEVICE lpDisplayDevice,
|
||||
uint dwFlags);
|
||||
|
||||
/// <summary>
|
||||
/// 从窗口获取显示器句柄
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true)]
|
||||
public static extern IntPtr MonitorFromWindow(
|
||||
IntPtr hwnd,
|
||||
uint dwFlags);
|
||||
|
||||
/// <summary>
|
||||
/// 从点获取显示器句柄
|
||||
/// </summary>
|
||||
[DllImport(User32Dll, SetLastError = true)]
|
||||
public static extern IntPtr MonitorFromPoint(
|
||||
POINT pt,
|
||||
uint dwFlags);
|
||||
|
||||
/// <summary>
|
||||
/// 获取最后错误
|
||||
/// </summary>
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
public static extern uint GetLastError();
|
||||
|
||||
// Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 获取 VCP 功能值的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="vcpCode">VCP 代码</param>
|
||||
/// <param name="currentValue">当前值</param>
|
||||
/// <param name="maxValue">最大值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
|
||||
{
|
||||
currentValue = 0;
|
||||
maxValue = 0;
|
||||
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return GetVCPFeatureAndVCPFeatureReply(hPhysicalMonitor, vcpCode, IntPtr.Zero, out currentValue, out maxValue);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 VCP 功能值的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="vcpCode">VCP 代码</param>
|
||||
/// <param name="value">新值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return SetVCPFeature(hPhysicalMonitor, vcpCode, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取高级亮度信息的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="minBrightness">最小亮度</param>
|
||||
/// <param name="currentBrightness">当前亮度</param>
|
||||
/// <param name="maxBrightness">最大亮度</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
|
||||
{
|
||||
minBrightness = 0;
|
||||
currentBrightness = 0;
|
||||
maxBrightness = 0;
|
||||
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return GetMonitorBrightness(hPhysicalMonitor, out minBrightness, out currentBrightness, out maxBrightness);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置高级亮度的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="brightness">亮度值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TrySetMonitorBrightness(IntPtr hPhysicalMonitor, uint brightness)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return SetMonitorBrightness(hPhysicalMonitor, brightness);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 DDC/CI 连接的有效性
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <returns>是否连接有效</returns>
|
||||
public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试读取基本的 VCP 代码来验证连接
|
||||
var testCodes = new byte[] { NativeConstants.VcpCodeBrightness, NativeConstants.VcpCodeNewControlValue, NativeConstants.VcpCodeVcpVersion };
|
||||
|
||||
foreach (var code in testCodes)
|
||||
{
|
||||
if (TryGetVCPFeature(hPhysicalMonitor, code, out _, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器友好名称
|
||||
/// </summary>
|
||||
/// <param name="adapterId">适配器 ID</param>
|
||||
/// <param name="targetId">目标 ID</param>
|
||||
/// <returns>显示器友好名称,如果获取失败返回 null</returns>
|
||||
public static string? GetMonitorFriendlyName(LUID adapterId, uint targetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||
{
|
||||
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||
{
|
||||
Type = DisplayconfigDeviceInfoGetTargetName,
|
||||
Size = (uint)Marshal.SizeOf<DISPLAYCONFIG_TARGET_DEVICE_NAME>(),
|
||||
AdapterId = adapterId,
|
||||
Id = targetId,
|
||||
},
|
||||
};
|
||||
|
||||
var result = DisplayConfigGetDeviceInfo(ref deviceName);
|
||||
if (result == 0) // ERROR_SUCCESS
|
||||
{
|
||||
return deviceName.MonitorFriendlyDeviceName;
|
||||
}
|
||||
}
|
||||
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) // ERROR_SUCCESS
|
||||
{
|
||||
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 string? GetMonitorHardwareId(LUID adapterId, uint targetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||
{
|
||||
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||
{
|
||||
Type = DisplayconfigDeviceInfoGetTargetName,
|
||||
Size = (uint)Marshal.SizeOf<DISPLAYCONFIG_TARGET_DEVICE_NAME>(),
|
||||
AdapterId = adapterId,
|
||||
Id = targetId,
|
||||
},
|
||||
};
|
||||
|
||||
var result = DisplayConfigGetDeviceInfo(ref deviceName);
|
||||
if (result == 0) // ERROR_SUCCESS
|
||||
{
|
||||
// 将制造商ID转换为3字符字符串
|
||||
var manufacturerId = deviceName.EdidManufactureId;
|
||||
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
|
||||
|
||||
// 将产品ID转换为4位十六进制字符串
|
||||
var productCode = deviceName.EdidProductCodeId.ToString("X4");
|
||||
|
||||
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) // ERROR_SUCCESS
|
||||
{
|
||||
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>
|
||||
/// 显示器显示信息结构
|
||||
/// </summary>
|
||||
public struct MonitorDisplayInfo
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
public string HardwareId { get; set; }
|
||||
public LUID AdapterId { get; set; }
|
||||
public uint TargetId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
304
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
304
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows API constant definitions
|
||||
/// </summary>
|
||||
public static class NativeConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code: Brightness (primary usage)
|
||||
/// </summary>
|
||||
public const byte VcpCodeBrightness = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Contrast
|
||||
/// </summary>
|
||||
public const byte VcpCodeContrast = 0x12;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Backlight control (alternative brightness)
|
||||
/// </summary>
|
||||
public const byte VcpCodeBacklightControl = 0x13;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: White backlight level
|
||||
/// </summary>
|
||||
public const byte VcpCodeBacklightLevelWhite = 0x6B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio speaker volume
|
||||
/// </summary>
|
||||
public const byte VcpCodeVolume = 0x62;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio mute
|
||||
/// </summary>
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature request (主要色温控制)
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperature = 0x0C;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature increment (色温增量调节)
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Gamma correction (Gamma调节)
|
||||
/// </summary>
|
||||
public const byte VcpCodeGamma = 0x72;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Select color preset (颜色预设选择)
|
||||
/// </summary>
|
||||
public const byte VcpCodeSelectColorPreset = 0x14;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: VCP version
|
||||
/// </summary>
|
||||
public const byte VcpCodeVcpVersion = 0xDF;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: New control value
|
||||
/// </summary>
|
||||
public const byte VcpCodeNewControlValue = 0x02;
|
||||
|
||||
/// <summary>
|
||||
/// Display device attached to desktop
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceAttachedToDesktop = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-monitor primary display
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceMultiDriver = 0x00000002;
|
||||
|
||||
/// <summary>
|
||||
/// Primary device
|
||||
/// </summary>
|
||||
public const uint DisplayDevicePrimaryDevice = 0x00000004;
|
||||
|
||||
/// <summary>
|
||||
/// Mirroring driver
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceMirroringDriver = 0x00000008;
|
||||
|
||||
/// <summary>
|
||||
/// VGA compatible
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceVgaCompatible = 0x00000010;
|
||||
|
||||
/// <summary>
|
||||
/// Removable device
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceRemovable = 0x00000020;
|
||||
|
||||
/// <summary>
|
||||
/// Get device interface name
|
||||
/// </summary>
|
||||
public const uint EddGetDeviceInterfaceName = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Primary monitor
|
||||
/// </summary>
|
||||
public const uint MonitorinfoFPrimary = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Query display config: only active paths
|
||||
/// </summary>
|
||||
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||
|
||||
/// <summary>
|
||||
/// Query display config: all paths
|
||||
/// </summary>
|
||||
public const uint QdcAllPaths = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: apply
|
||||
/// </summary>
|
||||
public const uint SdcApply = 0x00000080;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: use supplied display config
|
||||
/// </summary>
|
||||
public const uint SdcUseSuppliedDisplayConfig = 0x00000020;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: save to database
|
||||
/// </summary>
|
||||
public const uint SdcSaveToDatabase = 0x00000200;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: topology supplied
|
||||
/// </summary>
|
||||
public const uint SdcTopologySupplied = 0x00000010;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: allow path order changes
|
||||
/// </summary>
|
||||
public const uint SdcAllowPathOrderChanges = 0x00002000;
|
||||
|
||||
/// <summary>
|
||||
/// Get target name
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoGetTargetName = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Get SDR white level
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoGetSdrWhiteLevel = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Get advanced color information
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoGetAdvancedColorInfo = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Set SDR white level (custom)
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoSetSdrWhiteLevel = 0xFFFFFFEE;
|
||||
|
||||
/// <summary>
|
||||
/// Path active
|
||||
/// </summary>
|
||||
public const uint DisplayconfigPathActive = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Path mode index invalid
|
||||
/// </summary>
|
||||
public const uint DisplayconfigPathModeIdxInvalid = 0xFFFFFFFF;
|
||||
|
||||
/// <summary>
|
||||
/// COM initialization: multithreaded
|
||||
/// </summary>
|
||||
public const uint CoinitMultithreaded = 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authentication level: connect
|
||||
/// </summary>
|
||||
public const uint RpcCAuthnLevelConnect = 2;
|
||||
|
||||
/// <summary>
|
||||
/// RPC impersonation level: impersonate
|
||||
/// </summary>
|
||||
public const uint RpcCImpLevelImpersonate = 3;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authentication service: Win NT
|
||||
/// </summary>
|
||||
public const uint RpcCAuthnWinnt = 10;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authorization service: none
|
||||
/// </summary>
|
||||
public const uint RpcCAuthzNone = 0;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authentication level: call
|
||||
/// </summary>
|
||||
public const uint RpcCAuthnLevelCall = 3;
|
||||
|
||||
/// <summary>
|
||||
/// EOAC: none
|
||||
/// </summary>
|
||||
public const uint EoacNone = 0;
|
||||
|
||||
/// <summary>
|
||||
/// WMI flag: forward only
|
||||
/// </summary>
|
||||
public const long WbemFlagForwardOnly = 0x20;
|
||||
|
||||
/// <summary>
|
||||
/// WMI flag: return immediately
|
||||
/// </summary>
|
||||
public const long WbemFlagReturnImmediately = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// WMI flag: connect use max wait
|
||||
/// </summary>
|
||||
public const long WbemFlagConnectUseMaxWait = 0x80;
|
||||
|
||||
/// <summary>
|
||||
/// Success
|
||||
/// </summary>
|
||||
public const int ErrorSuccess = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Insufficient buffer
|
||||
/// </summary>
|
||||
public const int ErrorInsufficientBuffer = 122;
|
||||
|
||||
/// <summary>
|
||||
/// Invalid parameter
|
||||
/// </summary>
|
||||
public const int ErrorInvalidParameter = 87;
|
||||
|
||||
/// <summary>
|
||||
/// Access denied
|
||||
/// </summary>
|
||||
public const int ErrorAccessDenied = 5;
|
||||
|
||||
/// <summary>
|
||||
/// General failure
|
||||
/// </summary>
|
||||
public const int ErrorGenFailure = 31;
|
||||
|
||||
/// <summary>
|
||||
/// Unsupported VCP code
|
||||
/// </summary>
|
||||
public const int ErrorGraphicsDdcciVcpNotSupported = -1071243251;
|
||||
|
||||
/// <summary>
|
||||
/// Infinite wait
|
||||
/// </summary>
|
||||
public const uint Infinite = 0xFFFFFFFF;
|
||||
|
||||
/// <summary>
|
||||
/// User message
|
||||
/// </summary>
|
||||
public const uint WmUser = 0x0400;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: HDMI
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyHdmi = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: DVI
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyDvi = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: DisplayPort
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyDisplayportExternal = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: internal
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyInternal = 0x80000000;
|
||||
|
||||
/// <summary>
|
||||
/// HDR minimum SDR white level (nits)
|
||||
/// </summary>
|
||||
public const int HdrMinSdrWhiteLevel = 80;
|
||||
|
||||
/// <summary>
|
||||
/// HDR maximum SDR white level (nits)
|
||||
/// </summary>
|
||||
public const int HdrMaxSdrWhiteLevel = 480;
|
||||
|
||||
/// <summary>
|
||||
/// SDR white level conversion factor
|
||||
/// </summary>
|
||||
public const int SdrWhiteLevelFactor = 80;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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;
|
||||
|
||||
// 类型别名,兼容 Windows API 命名约定
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
namespace PowerDisplay.Native;
|
||||
|
||||
/// <summary>
|
||||
/// 委托类型定义
|
||||
/// </summary>
|
||||
public static class NativeDelegates
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示器枚举过程委托
|
||||
/// </summary>
|
||||
/// <param name="hMonitor">显示器句柄</param>
|
||||
/// <param name="hdcMonitor">显示器 DC</param>
|
||||
/// <param name="lprcMonitor">显示器矩形</param>
|
||||
/// <param name="dwData">用户数据</param>
|
||||
/// <returns>继续枚举返回 true</returns>
|
||||
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);
|
||||
|
||||
/// <summary>
|
||||
/// 线程启动例程委托
|
||||
/// </summary>
|
||||
/// <param name="lpParameter">线程参数</param>
|
||||
/// <returns>线程退出代码</returns>
|
||||
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
|
||||
}
|
||||
355
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
355
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
// 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>
|
||||
/// Physical monitor structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct PhysicalMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Physical monitor handle
|
||||
/// </summary>
|
||||
public IntPtr HPhysicalMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Physical monitor description (128 characters)
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string SzPhysicalMonitorDescription;
|
||||
}
|
||||
|
||||
/// <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.Auto)]
|
||||
public 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")
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string SzDevice;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display device structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
|
||||
public struct DisplayDevice
|
||||
{
|
||||
/// <summary>
|
||||
/// Structure size
|
||||
/// </summary>
|
||||
public uint Cb;
|
||||
|
||||
/// <summary>
|
||||
/// Device name (e.g., "\\.\DISPLAY1\Monitor0")
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string DeviceName;
|
||||
|
||||
/// <summary>
|
||||
/// Device description string
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceString;
|
||||
|
||||
/// <summary>
|
||||
/// Status flags
|
||||
/// </summary>
|
||||
public uint StateFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Device ID
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceID;
|
||||
|
||||
/// <summary>
|
||||
/// Registry device key
|
||||
/// </summary>
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string DeviceKey;
|
||||
}
|
||||
|
||||
/// <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)]
|
||||
public DISPLAYCONFIG_TARGET_MODE targetMode;
|
||||
|
||||
[FieldOffset(0)]
|
||||
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 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;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||
public string MonitorFriendlyDeviceName;
|
||||
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string MonitorDevicePath;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
// 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.Management;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Native.WMI
|
||||
{
|
||||
/// <summary>
|
||||
/// WMI monitor controller for controlling internal laptop displays
|
||||
/// </summary>
|
||||
public 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 = false;
|
||||
|
||||
public string Name => "WMI Monitor Controller";
|
||||
public MonitorType SupportedType => MonitorType.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (monitor.Type != MonitorType.Internal)
|
||||
return false;
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(WmiNamespace, $"SELECT * FROM {BrightnessQueryClass}");
|
||||
using var collection = searcher.Get();
|
||||
return collection.Count > 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
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 searcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT CurrentBrightness FROM {BrightnessQueryClass}");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
using (obj)
|
||||
{
|
||||
var currentBrightness = Convert.ToInt32(obj["CurrentBrightness"]);
|
||||
return new BrightnessInfo(currentBrightness, 0, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Silent failure
|
||||
}
|
||||
|
||||
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 searcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT * FROM {BrightnessMethodClass}");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
using (obj)
|
||||
{
|
||||
// Call WmiSetBrightness method
|
||||
var result = obj.InvokeMethod("WmiSetBrightness", new object[] { 0, (byte)brightness });
|
||||
|
||||
// Check return value (0 indicates success)
|
||||
var returnValue = result != null ? Convert.ToInt32(result) : -1;
|
||||
|
||||
if (returnValue == 0)
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
else
|
||||
{
|
||||
return MonitorOperationResult.Failure($"WMI method returned error code: {returnValue}", returnValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MonitorOperationResult.Failure("No WMI brightness methods found");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
|
||||
}
|
||||
catch (ManagementException ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"WMI error: {ex.Message}", (int)ex.ErrorCode);
|
||||
}
|
||||
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
|
||||
{
|
||||
// First check if WMI brightness support is available
|
||||
using var brightnessSearcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT * FROM {BrightnessQueryClass}");
|
||||
using var brightnessCollection = brightnessSearcher.Get();
|
||||
|
||||
if (brightnessCollection.Count == 0)
|
||||
return monitors;
|
||||
|
||||
// Get monitor information
|
||||
using var idSearcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT * FROM {MonitorIdClass}");
|
||||
using var idCollection = idSearcher.Get();
|
||||
|
||||
var monitorInfos = new Dictionary<string, (string Name, string InstanceName)>();
|
||||
|
||||
foreach (ManagementObject obj in idCollection)
|
||||
{
|
||||
using (obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
var instanceName = obj["InstanceName"]?.ToString() ?? "";
|
||||
var userFriendlyName = GetUserFriendlyName(obj) ?? "Internal Display";
|
||||
|
||||
if (!string.IsNullOrEmpty(instanceName))
|
||||
{
|
||||
monitorInfos[instanceName] = (userFriendlyName, instanceName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip problematic entries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create monitor objects for each supported brightness instance
|
||||
foreach (ManagementObject obj in brightnessCollection)
|
||||
{
|
||||
using (obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
var instanceName = obj["InstanceName"]?.ToString() ?? "";
|
||||
var currentBrightness = Convert.ToInt32(obj["CurrentBrightness"]);
|
||||
|
||||
var name = "Internal Display";
|
||||
if (monitorInfos.TryGetValue(instanceName, out var info))
|
||||
{
|
||||
name = info.Name;
|
||||
}
|
||||
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = $"WMI_{instanceName}",
|
||||
Name = name,
|
||||
Type = MonitorType.Internal,
|
||||
CurrentBrightness = currentBrightness,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
IsAvailable = true,
|
||||
InstanceName = instanceName,
|
||||
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
|
||||
ConnectionType = "Internal",
|
||||
CommunicationMethod = "WMI",
|
||||
Manufacturer = "Internal",
|
||||
SupportsColorTemperature = false // Internal monitors don't support DDC/CI color temperature
|
||||
};
|
||||
|
||||
monitors.Add(monitor);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip problematic monitors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Return empty list instead of throwing exception
|
||||
}
|
||||
|
||||
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 searcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT CurrentBrightness FROM {BrightnessQueryClass} WHERE InstanceName='{monitor.InstanceName}'");
|
||||
using var collection = searcher.Get();
|
||||
return collection.Count > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user-friendly name
|
||||
/// </summary>
|
||||
private static string? GetUserFriendlyName(ManagementObject monitorObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userFriendlyName = monitorObject["UserFriendlyName"] as ushort[];
|
||||
if (userFriendlyName != null && 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
|
||||
{
|
||||
// Ignore conversion errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check WMI service availability
|
||||
/// </summary>
|
||||
public static bool IsWmiAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT * FROM {BrightnessQueryClass}");
|
||||
using var collection = searcher.Get();
|
||||
return collection.Count > 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if administrator privileges are required
|
||||
/// </summary>
|
||||
public static bool RequiresElevation()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(WmiNamespace,
|
||||
$"SELECT * FROM {BrightnessMethodClass}");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
using (obj)
|
||||
{
|
||||
// Try to call method to check permissions
|
||||
try
|
||||
{
|
||||
obj.InvokeMethod("WmiSetBrightness", new object[] { 0, 50 });
|
||||
return false; // If successful, no elevation required
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return true; // Administrator privileges required
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Other errors may not be permission issues
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Cannot determine, assume privileges required
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
// WMI objects are automatically cleaned up, no specific cleanup needed here
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/modules/powerdisplay/PowerDisplay/Native/WindowHelper.cs
Normal file
161
src/modules/powerdisplay/PowerDisplay/Native/WindowHelper.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
// 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
|
||||
{
|
||||
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;
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
private const uint SwpNosize = 0x0001;
|
||||
private const uint SwpNomove = 0x0002;
|
||||
private const uint SwpFramechanged = 0x0020;
|
||||
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
|
||||
private static readonly IntPtr HwndNotopmost = new IntPtr(-2);
|
||||
|
||||
// ShowWindow commands
|
||||
private const int SwHide = 0;
|
||||
private const int SwShow = 5;
|
||||
private const int SwMinimize = 6;
|
||||
private const int SwRestore = 9;
|
||||
|
||||
/// <summary>
|
||||
/// 禁用窗口的拖动和缩放功能
|
||||
/// </summary>
|
||||
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
|
||||
{
|
||||
// 获取当前窗口样式
|
||||
int style = GetWindowLong(hWnd, GwlStyle);
|
||||
|
||||
// 移除可调整大小的边框、标题栏和系统菜单
|
||||
style &= ~WsThickframe;
|
||||
style &= ~WsMaximizebox;
|
||||
style &= ~WsMinimizebox;
|
||||
style &= ~WsCaption; // 移除整个标题栏
|
||||
style &= ~WsSysmenu; // 移除系统菜单
|
||||
|
||||
// 设置新的窗口样式
|
||||
_ = SetWindowLong(hWnd, GwlStyle, style);
|
||||
|
||||
// 获取扩展样式并移除相关边框
|
||||
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||
exStyle &= ~WsExDlgmodalframe;
|
||||
exStyle &= ~WsExWindowedge;
|
||||
exStyle &= ~WsExClientedge;
|
||||
exStyle &= ~WsExStaticedge;
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
|
||||
// 刷新窗口框架
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize | SwpFramechanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口是否置顶
|
||||
/// </summary>
|
||||
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
|
||||
{
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
topmost ? HwndTopmost : HwndNotopmost,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示或隐藏窗口
|
||||
/// </summary>
|
||||
public static void ShowWindow(IntPtr hWnd, bool show)
|
||||
{
|
||||
ShowWindow(hWnd, show ? SwShow : SwHide);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小化窗口
|
||||
/// </summary>
|
||||
public static void MinimizeWindow(IntPtr hWnd)
|
||||
{
|
||||
ShowWindow(hWnd, SwMinimize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复窗口
|
||||
/// </summary>
|
||||
public static void RestoreWindow(IntPtr hWnd)
|
||||
{
|
||||
ShowWindow(hWnd, SwRestore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口不在任务栏显示
|
||||
/// </summary>
|
||||
public static void HideFromTaskbar(IntPtr hWnd)
|
||||
{
|
||||
// 获取当前扩展样式
|
||||
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||
|
||||
// 添加 WS_EX_TOOLWINDOW 样式,这会让窗口不在任务栏显示
|
||||
exStyle |= WsExToolwindow;
|
||||
|
||||
// 设置新的扩展样式
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
|
||||
// 刷新窗口框架
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize | SwpFramechanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
78
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
@@ -0,0 +1,78 @@
|
||||
<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" />
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PowerDisplay</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<AssemblyName>PowerToys.PowerDisplay</AssemblyName>
|
||||
<ApplicationIcon>Assets\PowerDisplay.ico</ApplicationIcon>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>PowerToys.PowerDisplay.pri</ProjectPriFileName>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Disable StyleCop for this project -->
|
||||
<DisableStyleCop>true</DisableStyleCop>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
</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="System.Management" />
|
||||
<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>
|
||||
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Copy Assets folder to output directory -->
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
137
src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml
Normal file
137
src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml
Normal file
@@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application x:Class="PowerDisplay.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:PowerDisplay.Converters"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- WinUI 3 System Resources -->
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Converters -->
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
|
||||
<converters:NoMonitorsVisibilityConverter x:Key="NoMonitorsVisibilityConverter"/>
|
||||
|
||||
<!-- Fluent 2 Design Tokens -->
|
||||
|
||||
<!-- Corner Radius -->
|
||||
<CornerRadius x:Key="ControlCornerRadius">4</CornerRadius>
|
||||
<CornerRadius x:Key="OverlayCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
|
||||
|
||||
<!-- Spacing -->
|
||||
<x:Double x:Key="SmallSpacing">4</x:Double>
|
||||
<x:Double x:Key="MediumSpacing">8</x:Double>
|
||||
<x:Double x:Key="LargeSpacing">16</x:Double>
|
||||
<x:Double x:Key="ExtraLargeSpacing">24</x:Double>
|
||||
|
||||
<!-- Elevation (Shadows) -->
|
||||
<!-- Shadow definitions removed - not supported in WinUI 3 XAML resources -->
|
||||
|
||||
<!-- Animations -->
|
||||
<TransitionCollection x:Key="SettingsCardsAnimations">
|
||||
<EntranceThemeTransition FromVerticalOffset="50" />
|
||||
<RepositionThemeTransition IsStaggeringEnabled="False" />
|
||||
</TransitionCollection>
|
||||
|
||||
<!-- Modern Monitor Card Style -->
|
||||
<Style x:Key="MonitorCardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource CardCornerRadius}" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
</Style>
|
||||
|
||||
<!-- Modern Slider Style -->
|
||||
<Style x:Key="ModernSliderStyle" TargetType="Slider">
|
||||
<Setter Property="Foreground" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
|
||||
</Style>
|
||||
|
||||
<!-- Icon Button Style -->
|
||||
<Style x:Key="IconButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Width" Value="40" />
|
||||
<Setter Property="Height" Value="40" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
|
||||
<Setter Property="Padding" Value="8" />
|
||||
</Style>
|
||||
|
||||
<!-- Monitor Action Button Style -->
|
||||
<Style x:Key="MonitorActionButtonStyle" TargetType="Button" BasedOn="{StaticResource IconButtonStyle}">
|
||||
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Title Text Style -->
|
||||
<Style x:Key="MonitorTitleTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Control Label Style -->
|
||||
<Style x:Key="ControlLabelStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Value Text Style -->
|
||||
<Style x:Key="ValueTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- Compact Slider Style -->
|
||||
<Style x:Key="CompactSliderStyle" TargetType="Slider">
|
||||
<Setter Property="Foreground" Value="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
|
||||
<Setter Property="MinHeight" Value="32" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,2,0,2" />
|
||||
</Style>
|
||||
|
||||
<!-- Compact Icon Style -->
|
||||
<Style x:Key="CompactIconStyle" TargetType="FontIcon">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- Compact Value Text Style -->
|
||||
<Style x:Key="CompactValueTextStyle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimaryBrush}" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
|
||||
<!-- Status Bar Style -->
|
||||
<Style x:Key="StatusBarStyle" TargetType="Grid">
|
||||
<Setter Property="Background" Value="{ThemeResource LayerFillColorDefaultBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0" />
|
||||
<Setter Property="Padding" Value="8,8" />
|
||||
</Style>
|
||||
|
||||
<!-- Window Background -->
|
||||
<Style x:Key="WindowBackgroundStyle" TargetType="Grid">
|
||||
<Setter Property="Background" Value="{ThemeResource ApplicationPageBackgroundThemeBrush}" />
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,323 @@
|
||||
// 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.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using PowerDisplay.Helpers;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using PowerToys.Interop;
|
||||
|
||||
namespace PowerDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// PowerDisplay 应用程序主类
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _mainWindow;
|
||||
private int _powerToysRunnerPid;
|
||||
private static Mutex? _mutex;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化 PowerDisplay 应用程序
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 应用保存的主题设置(优先从PowerToys设置读取)
|
||||
var savedTheme = ThemeManager.GetSavedThemeWithPriority();
|
||||
if (savedTheme != ElementTheme.Default)
|
||||
{
|
||||
// 转换ElementTheme到ApplicationTheme
|
||||
this.RequestedTheme = savedTheme == ElementTheme.Light
|
||||
? ApplicationTheme.Light
|
||||
: ApplicationTheme.Dark;
|
||||
}
|
||||
|
||||
// 处理未捕获的异常
|
||||
this.UnhandledException += OnUnhandledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理未捕获的异常
|
||||
/// </summary>
|
||||
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
// 尝试显示错误信息
|
||||
ShowStartupError(e.Exception);
|
||||
|
||||
// 标记异常已处理,防止应用崩溃
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在应用程序启动时调用
|
||||
/// </summary>
|
||||
/// <param name="args">启动参数</param>
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用 Mutex 确保只有一个 PowerDisplay 实例运行
|
||||
_mutex = new Mutex(true, "PowerDisplay", out bool isNewInstance);
|
||||
|
||||
if (!isNewInstance)
|
||||
{
|
||||
// PowerDisplay 已经在运行,退出当前实例
|
||||
Logger.LogInfo("PowerDisplay is already running. Exiting duplicate instance.");
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保在应用退出时释放 Mutex
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => _mutex?.ReleaseMutex();
|
||||
|
||||
// 解析命令行参数
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
{
|
||||
// 支持两种格式:直接PID或者 --pid PID
|
||||
int pidValue = -1;
|
||||
|
||||
// 检查是否是 --pid 格式
|
||||
for (int i = 1; i < cmdArgs.Length - 1; i++)
|
||||
{
|
||||
if (cmdArgs[i] == "--pid" && int.TryParse(cmdArgs[i + 1], out pidValue))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是 --pid 格式,尝试解析最后一个参数(兼容旧格式)
|
||||
if (pidValue == -1 && cmdArgs.Length > 1)
|
||||
{
|
||||
int.TryParse(cmdArgs[cmdArgs.Length - 1], out pidValue);
|
||||
}
|
||||
|
||||
if (pidValue > 0)
|
||||
{
|
||||
_powerToysRunnerPid = pidValue;
|
||||
|
||||
// 从PowerToys Runner启动
|
||||
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
|
||||
|
||||
// 监控父进程
|
||||
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
|
||||
ForceExit();
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 独立运行模式
|
||||
Logger.LogInfo("PowerDisplay started detached from PowerToys Runner.");
|
||||
_powerToysRunnerPid = -1;
|
||||
}
|
||||
|
||||
// 创建主窗口但不激活,窗口会在初始化后自动隐藏
|
||||
_mainWindow = new MainWindow();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowStartupError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示启动错误
|
||||
/// </summary>
|
||||
private void ShowStartupError(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
var errorWindow = new Window
|
||||
{
|
||||
Title = "PowerDisplay - 启动错误"
|
||||
};
|
||||
|
||||
var rootPanel = new StackPanel
|
||||
{
|
||||
Margin = new Thickness(20),
|
||||
Spacing = 16
|
||||
};
|
||||
|
||||
var titleText = new TextBlock
|
||||
{
|
||||
Text = "PowerDisplay 启动失败",
|
||||
FontSize = 20,
|
||||
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold
|
||||
};
|
||||
|
||||
var messageText = new TextBlock
|
||||
{
|
||||
Text = $"错误信息:{ex.Message}",
|
||||
FontSize = 14,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
var detailsText = new TextBlock
|
||||
{
|
||||
Text = $"详细信息:\n{ex}",
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Gray),
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
var closeButton = new Button
|
||||
{
|
||||
Content = "关闭",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
closeButton.Click += (_, _) => errorWindow.Close();
|
||||
|
||||
rootPanel.Children.Add(titleText);
|
||||
rootPanel.Children.Add(messageText);
|
||||
rootPanel.Children.Add(detailsText);
|
||||
rootPanel.Children.Add(closeButton);
|
||||
|
||||
var scrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = rootPanel,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
MaxHeight = 600,
|
||||
MaxWidth = 800
|
||||
};
|
||||
|
||||
errorWindow.Content = scrollViewer;
|
||||
errorWindow.Activate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果连错误窗口都无法显示,静默退出
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取主窗口实例
|
||||
/// </summary>
|
||||
public Window? MainWindow => _mainWindow;
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否独立运行(不是从PowerToys Runner启动)
|
||||
/// </summary>
|
||||
public bool IsRunningDetachedFromPowerToys()
|
||||
{
|
||||
return _powerToysRunnerPid == -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用程序退出时的快速清理
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 启动超时机制,确保1秒内必须退出
|
||||
var timeoutTimer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
Logger.LogWarning("Shutdown timeout reached, forcing exit");
|
||||
Environment.Exit(0);
|
||||
}, null, 1000, System.Threading.Timeout.Infinite);
|
||||
|
||||
// 立即通知 MainWindow 程序正在退出,启用快速退出模式
|
||||
if (_mainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.SetExiting();
|
||||
mainWindow.FastShutdown(); // 新增快速关闭方法
|
||||
}
|
||||
|
||||
_mainWindow = null;
|
||||
|
||||
// 立即释放 Mutex
|
||||
_mutex?.ReleaseMutex();
|
||||
_mutex?.Dispose();
|
||||
_mutex = null;
|
||||
|
||||
// 取消超时计时器
|
||||
timeoutTimer?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略清理错误,确保能够退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制退出应用程序,确保完全终止
|
||||
/// </summary>
|
||||
private void ForceExit()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 立即启动超时机制,500ms内必须退出
|
||||
var emergencyTimer = new System.Threading.Timer(_ =>
|
||||
{
|
||||
Logger.LogWarning("Emergency exit timeout reached, terminating process");
|
||||
Environment.Exit(0);
|
||||
}, null, 500, System.Threading.Timeout.Infinite);
|
||||
|
||||
PerformForceExit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 如果所有其他方法都失败,立即强制退出进程
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行快速退出操作
|
||||
/// </summary>
|
||||
private void PerformForceExit()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 快速关闭
|
||||
Shutdown();
|
||||
|
||||
// 立即退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 确保能够退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window x:Class="PowerDisplay.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PowerDisplay.ViewModels"
|
||||
xmlns:converters="using:PowerDisplay.Converters"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations">
|
||||
<Grid x:Name="RootGrid" Style="{StaticResource WindowBackgroundStyle}">
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform x:Name="RootGridTransform" X="0" />
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<Grid.Resources>
|
||||
<!-- Enhanced Slide-in animation storyboard -->
|
||||
<Storyboard x:Key="SlideInStoryboard">
|
||||
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
From="300"
|
||||
To="0"
|
||||
Duration="0:0:0.4">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="RootGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0"
|
||||
To="1"
|
||||
Duration="0:0:0.3" />
|
||||
<DoubleAnimation Storyboard.TargetName="MainContainer"
|
||||
Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
|
||||
From="20"
|
||||
To="0"
|
||||
Duration="0:0:0.5">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
|
||||
<!-- Enhanced Slide-out animation storyboard -->
|
||||
<Storyboard x:Key="SlideOutStoryboard">
|
||||
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
From="0"
|
||||
To="300"
|
||||
Duration="0:0:0.3">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseIn" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="RootGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="1"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<!-- Main Container with modern design -->
|
||||
<Border x:Name="MainContainer"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Margin="0"
|
||||
MaxWidth="640"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform Y="0" />
|
||||
</Border.RenderTransform>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Content Area -->
|
||||
<ScrollViewer Grid.Row="0"
|
||||
ZoomMode="Disabled"
|
||||
HorizontalScrollMode="Disabled"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
MaxHeight="420"
|
||||
Padding="8">
|
||||
|
||||
<StackPanel Spacing="{StaticResource SmallSpacing}">
|
||||
|
||||
<!-- Loading State with modern progress -->
|
||||
<StackPanel Orientation="Vertical"
|
||||
Spacing="{StaticResource LargeSpacing}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsScanning, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ProgressRing IsActive="True"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Foreground="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<TextBlock x:Name="ScanningMonitorsTextBlock"
|
||||
Style="{StaticResource ControlLabelStyle}"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- No Monitors State with InfoBar -->
|
||||
<InfoBar x:Name="NoMonitorsInfoBar"
|
||||
IsOpen="{Binding ShowNoMonitorsMessage}"
|
||||
Severity="Informational"
|
||||
IsClosable="False"
|
||||
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}">
|
||||
<InfoBar.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</InfoBar.IconSource>
|
||||
<TextBlock x:Name="NoMonitorsTextBlock" />
|
||||
</InfoBar>
|
||||
|
||||
<!-- Monitors List with modern card design -->
|
||||
<ItemsControl ItemsSource="{Binding Monitors}"
|
||||
Visibility="{Binding HasMonitors, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"
|
||||
Spacing="0">
|
||||
<StackPanel.ChildrenTransitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="20" />
|
||||
<RepositionThemeTransition IsStaggeringEnabled="True" />
|
||||
</TransitionCollection>
|
||||
</StackPanel.ChildrenTransitions>
|
||||
</StackPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MainViewModel+MonitorViewModel">
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Stretch">
|
||||
<StackPanel.ChildrenTransitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="8" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</StackPanel.ChildrenTransitions>
|
||||
|
||||
<!-- Monitor Name -->
|
||||
<TextBlock Text="{Binding Name}"
|
||||
Style="{StaticResource MonitorTitleTextStyle}"
|
||||
Padding="8,4" />
|
||||
|
||||
<!-- Brightness Control -->
|
||||
<Grid Height="40" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
Style="{StaticResource CompactIconStyle}" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource CompactSliderStyle}"
|
||||
Minimum="{Binding MinBrightness}"
|
||||
Maximum="{Binding MaxBrightness}"
|
||||
Value="{Binding Brightness, Mode=TwoWay}"
|
||||
IsEnabled="{Binding IsAvailable}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Style="{StaticResource CompactValueTextStyle}"
|
||||
Text="{Binding Brightness, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Color Temperature Control -->
|
||||
<Grid Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{Binding ShowColorTemperature, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
Style="{StaticResource CompactIconStyle}" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource CompactSliderStyle}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{Binding ColorTemperaturePercent, Mode=TwoWay}"
|
||||
IsEnabled="{Binding IsAvailable}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Style="{StaticResource CompactValueTextStyle}">
|
||||
<Run Text="{Binding ColorTemperature}" />
|
||||
<Run Text="K" FontSize="9" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<!-- Contrast Control -->
|
||||
<Grid Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{Binding ShowContrast, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
Style="{StaticResource CompactIconStyle}" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource CompactSliderStyle}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{Binding ContrastPercent, Mode=TwoWay}"
|
||||
IsEnabled="{Binding IsAvailable}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Style="{StaticResource CompactValueTextStyle}">
|
||||
<Run Text="{Binding Contrast}" />
|
||||
<Run Text="%" FontSize="9" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<!-- Volume Control -->
|
||||
<Grid Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{Binding ShowVolume, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
Style="{StaticResource CompactIconStyle}" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Style="{StaticResource CompactSliderStyle}"
|
||||
Minimum="{Binding MinVolume}"
|
||||
Maximum="{Binding MaxVolume}"
|
||||
Value="{Binding Volume, Mode=TwoWay}"
|
||||
IsEnabled="{Binding IsAvailable}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
Style="{StaticResource CompactValueTextStyle}">
|
||||
<Run Text="{Binding Volume}" />
|
||||
<Run Text="%" FontSize="9" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Status Bar with modern design -->
|
||||
<Grid Grid.Row="1" Style="{StaticResource StatusBarStyle}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Status Information -->
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="AdjustBrightnessTextBlock"
|
||||
Style="{StaticResource MonitorTitleTextStyle}" />
|
||||
<TextBlock Text="{Binding StatusText}"
|
||||
Style="{StaticResource ControlLabelStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="{StaticResource SmallSpacing}"
|
||||
VerticalAlignment="Center">
|
||||
<Button x:Name="LinkButton"
|
||||
Style="{StaticResource MonitorActionButtonStyle}"
|
||||
ToolTipService.ToolTip="Sync all monitors">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
<Button x:Name="DisableButton"
|
||||
Style="{StaticResource MonitorActionButtonStyle}"
|
||||
ToolTipService.ToolTip="Toggle control">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
<Button x:Name="ThemeButton"
|
||||
Style="{StaticResource MonitorActionButtonStyle}"
|
||||
ToolTipService.ToolTip="Switch theme">
|
||||
<FontIcon x:Name="ThemeIcon"
|
||||
Glyph=""
|
||||
FontSize="16" />
|
||||
</Button>
|
||||
<Button x:Name="RefreshButton"
|
||||
Style="{StaticResource MonitorActionButtonStyle}"
|
||||
ToolTipService.ToolTip="Refresh monitors">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,735 @@
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Management;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Common.UI;
|
||||
using Microsoft.UI;
|
||||
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 Microsoft.UI.Composition;
|
||||
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 Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// PowerDisplay main window
|
||||
/// </summary>
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
private MainViewModel _viewModel = null!;
|
||||
private TrayIconHelper _trayIcon = null!;
|
||||
private AppWindow _appWindow = null!;
|
||||
private bool _isExiting = false;
|
||||
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
// Initialize ViewModel and bind to root Grid
|
||||
_viewModel = new MainViewModel();
|
||||
RootGrid.DataContext = _viewModel;
|
||||
|
||||
// Initialize ViewModel event handlers
|
||||
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
_viewModel.ThemeChangeRequested += OnThemeChangeRequested;
|
||||
|
||||
// Bind button events
|
||||
LinkButton.Click += OnLinkClick;
|
||||
DisableButton.Click += OnDisableClick;
|
||||
ThemeButton.Click += OnThemeButtonClick;
|
||||
RefreshButton.Click += OnRefreshClick;
|
||||
|
||||
// Setup window properties
|
||||
SetupWindow();
|
||||
|
||||
// Initialize theme and icons
|
||||
InitializeTheme();
|
||||
|
||||
// Initialize tray icon
|
||||
InitializeTrayIcon();
|
||||
|
||||
// Clean up resources on window close
|
||||
this.Closed += OnWindowClosed;
|
||||
|
||||
// Initialize UI text
|
||||
InitializeUIText();
|
||||
|
||||
// Initialize on startup
|
||||
_ = InitializeAsync();
|
||||
|
||||
// Hide window on startup, show only tray icon
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100); // Wait for window to fully initialize
|
||||
DispatcherQueue.TryEnqueue(() => HideWindow());
|
||||
});
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ShowError($"Unable to start main window: {e.Message}");
|
||||
}
|
||||
}
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(500);
|
||||
|
||||
await _viewModel.RefreshMonitorsAsync();
|
||||
_viewModel.ReloadMonitorSettings();
|
||||
|
||||
// Delay to allow UI to render, then adjust window size
|
||||
await Task.Delay(100);
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
}
|
||||
catch (System.Management.ManagementException)
|
||||
{
|
||||
ShowError("Unable to access internal display control, administrator privileges may be required.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError($"Initialization failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void InitializeUIText()
|
||||
{
|
||||
try
|
||||
{
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
// Set text block content
|
||||
ScanningMonitorsTextBlock.Text = loader.GetString("ScanningMonitorsText");
|
||||
NoMonitorsTextBlock.Text = loader.GetString("NoMonitorsText");
|
||||
AdjustBrightnessTextBlock.Text = loader.GetString("AdjustBrightnessText");
|
||||
|
||||
// Set button tooltips
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, loader.GetString("SyncAllMonitorsTooltip"));
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, loader.GetString("ToggleControlTooltip"));
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(ThemeButton, loader.GetString("ToggleThemeTooltip"));
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, loader.GetString("RefreshTooltip"));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Use English defaults if resource loading fails
|
||||
ScanningMonitorsTextBlock.Text = "Scanning monitors...";
|
||||
NoMonitorsTextBlock.Text = "No monitors detected";
|
||||
AdjustBrightnessTextBlock.Text = "Adjust Brightness";
|
||||
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, "Synchronize all monitors to the same brightness");
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, "Enable or disable brightness control");
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(ThemeButton, "Switch between light and dark themes");
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, "Rescan connected monitors");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
_viewModel.StatusText = $"Error: {message}";
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
// Allow window to close if program is exiting
|
||||
if (_isExiting)
|
||||
{
|
||||
// Clean up event subscriptions
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
_viewModel.ThemeChangeRequested -= OnThemeChangeRequested;
|
||||
}
|
||||
args.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If only user operation (although we hide close button), just hide window
|
||||
args.Handled = true; // Prevent window closing
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private void InitializeTrayIcon()
|
||||
{
|
||||
_trayIcon = new TrayIconHelper(this);
|
||||
_trayIcon.SetCallbacks(
|
||||
onShow: ShowWindow,
|
||||
onExit: ExitApplication,
|
||||
onRefresh: () => _viewModel?.RefreshCommand?.Execute(null),
|
||||
onSettings: OpenSettings
|
||||
);
|
||||
}
|
||||
|
||||
private void OpenSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Open PowerToys Settings to PowerDisplay page
|
||||
// Use true for WinUI 3 apps as PowerToys.exe is in parent directory
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerDisplay, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to open settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowWindow()
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
// Adjust window size before showing
|
||||
AdjustWindowSizeToContent();
|
||||
|
||||
// Reposition to bottom right (set position before showing)
|
||||
if (_appWindow != null)
|
||||
{
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
}
|
||||
|
||||
// Set initial state for animation
|
||||
RootGrid.Opacity = 0;
|
||||
|
||||
// Show window
|
||||
WindowHelper.ShowWindow(hWnd, true);
|
||||
|
||||
// Bring window to foreground
|
||||
WindowHelper.SetForegroundWindow(hWnd);
|
||||
|
||||
// Use storyboard animation for window entrance
|
||||
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
|
||||
{
|
||||
var slideInStoryboard = (Storyboard)RootGrid.Resources["SlideInStoryboard"];
|
||||
slideInStoryboard.Begin();
|
||||
}
|
||||
}
|
||||
|
||||
private void HideWindow()
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
// Use storyboard animation for window exit
|
||||
if (RootGrid.Resources.ContainsKey("SlideOutStoryboard"))
|
||||
{
|
||||
var slideOutStoryboard = (Storyboard)RootGrid.Resources["SlideOutStoryboard"];
|
||||
slideOutStoryboard.Completed += (s, e) =>
|
||||
{
|
||||
// Hide window after animation completes
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
};
|
||||
slideOutStoryboard.Begin();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: hide immediately if animation not found
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIRefreshRequested(object? sender, EventArgs e)
|
||||
{
|
||||
Logger.LogInfo("UI refresh requested due to settings change");
|
||||
_viewModel.ReloadMonitorSettings();
|
||||
|
||||
// Adjust window size after settings change to accommodate visibility changes
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100); // Allow UI to update with new visibility settings
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
});
|
||||
}
|
||||
|
||||
private void OnThemeChangeRequested(object? sender, ElementTheme theme)
|
||||
{
|
||||
Logger.LogInfo($"Theme change requested: {theme}");
|
||||
ApplyThemeFromSettings(theme);
|
||||
}
|
||||
|
||||
private void ApplyThemeFromSettings(ElementTheme theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Apply theme to window
|
||||
PowerDisplay.Helpers.ThemeManager.ApplyTheme(this, theme);
|
||||
|
||||
// Update theme icon
|
||||
UpdateThemeIcon(theme == ElementTheme.Dark);
|
||||
|
||||
Logger.LogInfo($"Theme applied: {theme}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to apply theme: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMonitorsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
// Adjust window size when monitors collection changes (add/remove monitors)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100); // Small delay to allow UI to update
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
});
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
// Adjust window size when relevant properties change
|
||||
if (e.PropertyName == nameof(_viewModel.IsScanning) ||
|
||||
e.PropertyName == nameof(_viewModel.HasMonitors) ||
|
||||
e.PropertyName == nameof(_viewModel.ShowNoMonitorsMessage))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(50); // Small delay to allow UI to update
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set exit flag to allow window to close normally
|
||||
/// </summary>
|
||||
public void SetExiting()
|
||||
{
|
||||
_isExiting = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速关闭窗口,跳过动画和复杂清理
|
||||
/// </summary>
|
||||
public void FastShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isExiting = true;
|
||||
|
||||
// 立即释放托盘图标
|
||||
_trayIcon?.Dispose();
|
||||
|
||||
// 快速清理 ViewModel
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// 取消事件订阅
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
_viewModel.ThemeChangeRequested -= OnThemeChangeRequested;
|
||||
|
||||
// 立即释放
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
|
||||
// 直接关闭窗口,不等待动画
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略清理错误,确保能够关闭
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApplication()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用快速关闭
|
||||
FastShutdown();
|
||||
|
||||
// 直接调用应用程序快速退出
|
||||
if (Application.Current is App app)
|
||||
{
|
||||
app.Shutdown();
|
||||
}
|
||||
|
||||
// 确保立即退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 确保能够退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRefreshClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Refresh monitor list
|
||||
if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
|
||||
{
|
||||
_viewModel.RefreshCommand.Execute(null);
|
||||
// Adjust window size after refresh
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(200); // Allow data to load
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnThemeButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
var currentTheme = PowerDisplay.Helpers.ThemeManager.GetCurrentTheme(this);
|
||||
var newTheme = currentTheme == ElementTheme.Light ? ElementTheme.Dark : ElementTheme.Light;
|
||||
|
||||
// Apply theme and sync to both settings systems
|
||||
PowerDisplay.Helpers.ThemeManager.ApplyThemeAndSync(this, newTheme);
|
||||
UpdateThemeIcon(newTheme == ElementTheme.Dark);
|
||||
|
||||
Logger.LogInfo($"Theme toggled to: {newTheme}");
|
||||
}
|
||||
|
||||
private void UpdateThemeIcon(bool isDark)
|
||||
{
|
||||
if (ThemeIcon != null)
|
||||
{
|
||||
// Dark theme shows sun icon, light theme shows moon icon
|
||||
ThemeIcon.Glyph = isDark ? "\uE706" : "\uE708";
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeTheme()
|
||||
{
|
||||
// Load saved theme settings (with PowerToys settings priority)
|
||||
var savedTheme = PowerDisplay.Helpers.ThemeManager.GetSavedThemeWithPriority();
|
||||
if (savedTheme != ElementTheme.Default)
|
||||
{
|
||||
PowerDisplay.Helpers.ThemeManager.ApplyTheme(this, savedTheme);
|
||||
}
|
||||
|
||||
// Update theme icon
|
||||
var isDark = PowerDisplay.Helpers.ThemeManager.IsDarkTheme(this);
|
||||
UpdateThemeIcon(isDark);
|
||||
}
|
||||
|
||||
private async void OnLinkClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Link all monitor brightness (synchronized adjustment)
|
||||
if (_viewModel != null && _viewModel.Monitors.Count > 0)
|
||||
{
|
||||
// Get first monitor brightness as reference
|
||||
var baseBrightness = _viewModel.Monitors.First().Brightness;
|
||||
_ = _viewModel.SetAllBrightnessAsync(baseBrightness);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDisableClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Disable/enable all monitor controls
|
||||
if (_viewModel != null)
|
||||
{
|
||||
foreach (var monitor in _viewModel.Monitors)
|
||||
{
|
||||
monitor.IsAvailable = !monitor.IsAvailable;
|
||||
}
|
||||
_viewModel.StatusText = _viewModel.Monitors.Any(m => m.IsAvailable)
|
||||
? "Display control enabled"
|
||||
: "Display control disabled";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get internal monitor name, consistent with SettingsManager logic
|
||||
/// </summary>
|
||||
|
||||
private async void OnTestClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Test monitor discovery functionality
|
||||
var dlg = new ContentDialog
|
||||
{
|
||||
Title = "Monitor Detection Test",
|
||||
Content = "Starting monitor detection...",
|
||||
CloseButtonText = "Close",
|
||||
XamlRoot = this.Content.XamlRoot
|
||||
};
|
||||
|
||||
_ = dlg.ShowAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var manager = new Core.MonitorManager();
|
||||
var monitors = await manager.DiscoverMonitorsAsync(new System.Threading.CancellationToken());
|
||||
|
||||
string message = $"Found {monitors.Count} monitors:\n\n";
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
message += $"• {monitor.Name}\n";
|
||||
message += $" Type: {monitor.Type}\n";
|
||||
message += $" Brightness: {monitor.CurrentBrightness}%\n\n";
|
||||
}
|
||||
|
||||
if (monitors.Count == 0)
|
||||
{
|
||||
message = "No monitors found.\n\n";
|
||||
message += "Possible reasons:\n";
|
||||
message += "• DDC/CI not supported\n";
|
||||
message += "• Driver issues\n";
|
||||
message += "• Permission issues\n";
|
||||
message += "• Cable doesn't support DDC/CI";
|
||||
}
|
||||
|
||||
dlg.Content = message;
|
||||
|
||||
// Don't dispose manager, use existing manager
|
||||
// Initialize ViewModel and bind to root Grid refresh
|
||||
if (monitors.Count > 0)
|
||||
{
|
||||
// Use existing refresh command
|
||||
await _viewModel.RefreshMonitorsAsync(); }
|
||||
|
||||
manager.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dlg.Content = $"Error: {ex.Message}\n\nType: {ex.GetType().Name}";
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get window handle
|
||||
var hWnd = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
if (_appWindow != null)
|
||||
{
|
||||
// Set initial window size - will be adjusted later based on content
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = 480 });
|
||||
|
||||
// Position window at bottom right corner
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
|
||||
// Set window icon and title bar
|
||||
_appWindow.Title = "PowerDisplay";
|
||||
|
||||
// Remove title bar and system buttons
|
||||
var presenter = _appWindow.Presenter as OverlappedPresenter;
|
||||
if (presenter != null)
|
||||
{
|
||||
// Disable resizing
|
||||
presenter.IsResizable = false;
|
||||
// Disable maximize button
|
||||
presenter.IsMaximizable = false;
|
||||
// Disable minimize button
|
||||
presenter.IsMinimizable = false;
|
||||
// Set borderless mode
|
||||
presenter.SetBorderAndTitleBar(false, false);
|
||||
}
|
||||
|
||||
// Custom title bar - completely remove all buttons
|
||||
var titleBar = _appWindow.TitleBar;
|
||||
if (titleBar != null)
|
||||
{
|
||||
// Extend content into title bar area
|
||||
titleBar.ExtendsContentIntoTitleBar = true;
|
||||
// Completely remove title bar height
|
||||
titleBar.PreferredHeightOption = Microsoft.UI.Windowing.TitleBarHeightOption.Collapsed;
|
||||
// Set all button colors to transparent
|
||||
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
// Disable title bar interaction area
|
||||
titleBar.SetDragRectangles(new Windows.Graphics.RectInt32[0]);
|
||||
}
|
||||
|
||||
// Set modern Mica Alt backdrop for Windows 11
|
||||
try
|
||||
{
|
||||
// Use Mica Alt for a more modern appearance
|
||||
if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
|
||||
{
|
||||
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to basic backdrop for older systems
|
||||
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.DesktopAcrylicBackdrop();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: use solid color background
|
||||
this.SystemBackdrop = null;
|
||||
}
|
||||
|
||||
// Use Win32 API to further disable window moving
|
||||
WindowHelper.DisableWindowMovingAndResizing(hWnd);
|
||||
// Hide window from taskbar
|
||||
WindowHelper.HideFromTaskbar(hWnd);
|
||||
// Optional: set window topmost
|
||||
// WindowHelper.SetWindowTopmost(hWnd, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore window setup errors
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustWindowSizeToContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_appWindow == null || RootGrid == null)
|
||||
return;
|
||||
|
||||
// Force layout update to ensure proper measurement
|
||||
RootGrid.UpdateLayout();
|
||||
|
||||
// Get precise content height
|
||||
var availableWidth = 640.0;
|
||||
var contentHeight = GetContentHeight(availableWidth);
|
||||
|
||||
// Account for display scaling
|
||||
var scale = RootGrid.XamlRoot?.RasterizationScale ?? 1.0;
|
||||
var scaledHeight = (int)Math.Ceiling(contentHeight * scale);
|
||||
|
||||
// Only set maximum height for scrollable content
|
||||
scaledHeight = Math.Min(scaledHeight, 650);
|
||||
|
||||
// Check if resize is needed
|
||||
var currentSize = _appWindow.Size;
|
||||
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
|
||||
{
|
||||
Logger.LogInfo($"Adjusting window height from {currentSize.Height} to {scaledHeight} (content: {contentHeight})");
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
|
||||
|
||||
// Update clip region to match new window size
|
||||
UpdateClipRegion(640, scaledHeight / scale);
|
||||
|
||||
// Reposition to maintain bottom-right position
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error adjusting window size: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private double GetContentHeight(double availableWidth)
|
||||
{
|
||||
// Try to measure MainContainer directly for precise content size
|
||||
if (RootGrid.FindName("MainContainer") is Border mainContainer)
|
||||
{
|
||||
mainContainer.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||
return mainContainer.DesiredSize.Height;
|
||||
}
|
||||
|
||||
// Fallback: Measure the root grid
|
||||
RootGrid.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||
return RootGrid.DesiredSize.Height + 4; // Small padding for fallback method
|
||||
}
|
||||
|
||||
private void UpdateClipRegion(double width, double height)
|
||||
{
|
||||
// Clip region removed to allow automatic sizing
|
||||
// No longer needed as we removed the fixed clip from RootGrid
|
||||
}
|
||||
|
||||
private void PositionWindowAtBottomRight(AppWindow appWindow)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get display area
|
||||
var displayArea = DisplayArea.GetFromWindowId(appWindow.Id, DisplayAreaFallback.Nearest);
|
||||
if (displayArea != null)
|
||||
{
|
||||
var workArea = displayArea.WorkArea;
|
||||
var windowSize = appWindow.Size;
|
||||
|
||||
// Calculate bottom-right position, close to taskbar
|
||||
// WorkArea already excludes taskbar area, so use WorkArea bottom directly
|
||||
int rightMargin = 10; // Small margin from right edge
|
||||
int x = workArea.Width - windowSize.Width - rightMargin;
|
||||
int y = workArea.Height - windowSize.Height; // Close to taskbar top, no gap
|
||||
|
||||
// Move window to bottom right
|
||||
appWindow.Move(new PointInt32 { X = x, Y = y });
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors when positioning window
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates button press for modern interaction feedback
|
||||
/// </summary>
|
||||
/// <param name="button">The button to animate</param>
|
||||
private async Task AnimateButtonPress(Button button)
|
||||
{
|
||||
// Button animation disabled to avoid compilation errors
|
||||
// Using default button visual states instead
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ScanningMonitorsText" xml:space="preserve">
|
||||
<value>Scanning monitors...</value>
|
||||
</data>
|
||||
<data name="NoMonitorsText" xml:space="preserve">
|
||||
<value>No monitors detected</value>
|
||||
</data>
|
||||
<data name="AdjustBrightnessText" xml:space="preserve">
|
||||
<value>Adjust Brightness</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ScanningMonitorsText" xml:space="preserve">
|
||||
<value>正在扫描显示器...</value>
|
||||
</data>
|
||||
<data name="NoMonitorsText" xml:space="preserve">
|
||||
<value>未检测到显示器</value>
|
||||
</data>
|
||||
<data name="AdjustBrightnessText" xml:space="preserve">
|
||||
<value>调节亮度</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;
|
||||
}
|
||||
}
|
||||
1431
src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
Normal file
1431
src/modules/powerdisplay/PowerDisplay/ViewModels/MainViewModel.cs
Normal file
File diff suppressed because it is too large
Load Diff
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>
|
||||
7
src/modules/powerdisplay/PowerDisplayExt/Constants.h
Normal file
7
src/modules/powerdisplay/PowerDisplayExt/Constants.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#include <string>
|
||||
|
||||
namespace PowerDisplayConstants
|
||||
{
|
||||
// Name of the powertoy module.
|
||||
inline const std::wstring ModuleKey = L"PowerDisplay";
|
||||
}
|
||||
108
src/modules/powerdisplay/PowerDisplayExt/PowerDisplayExt.rc
Normal file
108
src/modules/powerdisplay/PowerDisplayExt/PowerDisplayExt.rc
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// String Table
|
||||
//
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
IDS_REGISTRYPREVIEW_NAME "RegistryPreview"
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
130
src/modules/powerdisplay/PowerDisplayExt/PowerDisplayExt.vcxproj
Normal file
130
src/modules/powerdisplay/PowerDisplayExt/PowerDisplayExt.vcxproj
Normal file
@@ -0,0 +1,130 @@
|
||||
<?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>PowerDisplayExt</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>
|
||||
<TargetName>PowerToys.PowerDisplayExt</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;POWERDISPLAYEXT_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;POWERDISPLAYEXT_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="PowerDisplayExt.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>
|
||||
32
src/modules/powerdisplay/PowerDisplayExt/Trace.cpp
Normal file
32
src/modules/powerdisplay/PowerDisplayExt/Trace.cpp
Normal file
@@ -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::EnableRegistryPreview(_In_ bool enabled) noexcept
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"RegistryPreview_EnableRegistryPreview",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
|
||||
TraceLoggingBoolean(enabled, "Enabled"));
|
||||
}
|
||||
|
||||
// Log that the user tried to activate the app
|
||||
void Trace::ActivateEditor() noexcept
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"RegistryPreview_Activate",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
13
src/modules/powerdisplay/PowerDisplayExt/Trace.h
Normal file
13
src/modules/powerdisplay/PowerDisplayExt/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 EnableRegistryPreview(const bool enabled) noexcept;
|
||||
|
||||
// Log that the user tried to activate the app
|
||||
static void ActivateEditor() noexcept;
|
||||
};
|
||||
387
src/modules/powerdisplay/PowerDisplayExt/dllmain.cpp
Normal file
387
src/modules/powerdisplay/PowerDisplayExt/dllmain.cpp
Normal file
@@ -0,0 +1,387 @@
|
||||
// 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 <common/utils/process_path.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";
|
||||
}
|
||||
|
||||
class PowerDisplayModule : public PowertoyModuleIface
|
||||
{
|
||||
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
bool m_hotkey_enabled = false;
|
||||
|
||||
PROCESS_INFORMATION p_info = {};
|
||||
|
||||
bool is_process_running()
|
||||
{
|
||||
return WaitForSingleObject(p_info.hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
bool graceful_shutdown_process()
|
||||
{
|
||||
if (!is_process_running())
|
||||
{
|
||||
return true; // Already not running
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Send WM_CLOSE message to the main window
|
||||
DWORD processId = GetProcessId(p_info.hProcess);
|
||||
if (processId != 0)
|
||||
{
|
||||
// Find the main window of the PowerDisplay process
|
||||
HWND hwnd = find_main_window(processId);
|
||||
if (hwnd != NULL)
|
||||
{
|
||||
Logger::trace(L"Sending WM_CLOSE to PowerDisplay window");
|
||||
PostMessage(hwnd, WM_CLOSE, 0, 0);
|
||||
|
||||
// Wait up to 2 seconds for graceful shutdown
|
||||
DWORD wait_result = WaitForSingleObject(p_info.hProcess, 2000);
|
||||
if (wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
return true; // Process exited gracefully
|
||||
}
|
||||
|
||||
// If WM_CLOSE didn't work, try WM_QUIT
|
||||
Logger::trace(L"WM_CLOSE failed, trying WM_QUIT");
|
||||
PostMessage(hwnd, WM_QUIT, 0, 0);
|
||||
wait_result = WaitForSingleObject(p_info.hProcess, 1000);
|
||||
if (wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Exception during graceful shutdown attempt");
|
||||
}
|
||||
|
||||
return false; // Graceful shutdown failed
|
||||
}
|
||||
|
||||
struct EnumWindowsData
|
||||
{
|
||||
DWORD processId;
|
||||
HWND foundWindow;
|
||||
};
|
||||
|
||||
static BOOL CALLBACK enum_windows_callback(HWND hwnd, LPARAM lParam)
|
||||
{
|
||||
EnumWindowsData* data = reinterpret_cast<EnumWindowsData*>(lParam);
|
||||
DWORD windowProcessId;
|
||||
GetWindowThreadProcessId(hwnd, &windowProcessId);
|
||||
|
||||
if (windowProcessId == data->processId)
|
||||
{
|
||||
// Check if this is a main window (visible and has no parent)
|
||||
if (IsWindowVisible(hwnd) && GetParent(hwnd) == NULL)
|
||||
{
|
||||
wchar_t className[256];
|
||||
GetClassName(hwnd, className, sizeof(className) / sizeof(wchar_t));
|
||||
|
||||
// Look for WinUI3 window class or PowerDisplay specific window
|
||||
if (wcsstr(className, L"WindowsForms") ||
|
||||
wcsstr(className, L"WinUIDesktopWin32WindowClass") ||
|
||||
wcsstr(className, L"PowerDisplay"))
|
||||
{
|
||||
data->foundWindow = hwnd;
|
||||
return FALSE; // Stop enumeration
|
||||
}
|
||||
}
|
||||
}
|
||||
return TRUE; // Continue enumeration
|
||||
}
|
||||
|
||||
HWND find_main_window(DWORD processId)
|
||||
{
|
||||
EnumWindowsData data = { processId, NULL };
|
||||
EnumWindows(enum_windows_callback, reinterpret_cast<LPARAM>(&data));
|
||||
return data.foundWindow;
|
||||
}
|
||||
|
||||
void launch_process()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
Logger::trace(L"Starting Power Display process");
|
||||
unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
std::wstring executable_args = L"--pid " + std::to_wstring(powertoys_pid);
|
||||
std::wstring application_path = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
|
||||
std::wstring full_command_path = application_path + L" " + executable_args;
|
||||
Logger::trace(L"PowerDisplay launching with parameters: " + executable_args);
|
||||
|
||||
STARTUPINFO info = { sizeof(info) };
|
||||
|
||||
if (!CreateProcess(NULL, full_command_path.data(), NULL, NULL, true, NULL, NULL, NULL, &info, &p_info))
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wstring message = L"PowerDisplay failed to start with error: ";
|
||||
message += std::to_wstring(error);
|
||||
Logger::error(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace("Successfully started the PowerDisplay process");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
|
||||
{
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
try
|
||||
{
|
||||
auto hotkey_enabled = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedBoolean(JSON_KEY_HOTKEY_ENABLED);
|
||||
m_hotkey_enabled = hotkey_enabled;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::info("Failed to parse hotkey settings, using defaults");
|
||||
m_hotkey_enabled = false; // Use default value
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info("Power Display settings are empty");
|
||||
m_hotkey_enabled = false; // Use default value
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
void init_settings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load and parse the settings file for this PowerToy.
|
||||
PowerToysSettings::PowerToyValues settings =
|
||||
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
|
||||
|
||||
parse_hotkey_settings(settings);
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error("Invalid json when trying to load the Power Display settings json from file.");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
PowerDisplayModule()
|
||||
{
|
||||
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "PowerDisplay");
|
||||
Logger::info("Power Display object is constructing");
|
||||
|
||||
init_settings();
|
||||
}
|
||||
|
||||
~PowerDisplayModule()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
TerminateProcess(p_info.hProcess, 1);
|
||||
CloseHandle(p_info.hProcess);
|
||||
CloseHandle(p_info.hThread);
|
||||
}
|
||||
m_enabled = false;
|
||||
}
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
virtual void destroy() override
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
|
||||
// Return the localized display name of the powertoy
|
||||
virtual const wchar_t* get_name() override
|
||||
{
|
||||
return MODULE_NAME;
|
||||
}
|
||||
|
||||
// Return the non localized key of the powertoy, this will be cached by the runner
|
||||
virtual const wchar_t* get_key() override
|
||||
{
|
||||
return MODULE_NAME;
|
||||
}
|
||||
|
||||
// Return the configured status for the gpo policy for the module
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
||||
{
|
||||
return powertoys_gpo::gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
// Return JSON with the configuration options.
|
||||
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||
{
|
||||
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
|
||||
// Create a Settings object.
|
||||
PowerToysSettings::Settings settings(hinstance, get_name());
|
||||
settings.set_description(MODULE_DESC);
|
||||
|
||||
return settings.serialize_to_buffer(buffer, buffer_size);
|
||||
}
|
||||
|
||||
// Pop open the app, if the OOBE page asks it to
|
||||
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")
|
||||
{
|
||||
if (is_process_running())
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process is already running. Skipping launch.");
|
||||
}
|
||||
else
|
||||
{
|
||||
launch_process();
|
||||
}
|
||||
Trace::ActivateEditor();
|
||||
}
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error(L"Failed to parse action. {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the runner to pass the updated settings values as a serialized JSON.
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse the input JSON string.
|
||||
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
|
||||
|
||||
parse_hotkey_settings(values);
|
||||
|
||||
values.save_to_settings_file();
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error(L"Invalid json when trying to parse Power Display settings json.");
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the powertoy
|
||||
virtual void enable()
|
||||
{
|
||||
m_enabled = true;
|
||||
if (!is_process_running())
|
||||
{
|
||||
launch_process(); // Start the PowerDisplay process
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process is already running. Skipping launch on enable.");
|
||||
}
|
||||
Logger::trace(L"PowerDisplay enabled");
|
||||
};
|
||||
|
||||
virtual void disable()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
Logger::trace(L"Disabling Power Display...");
|
||||
|
||||
// Try graceful shutdown first
|
||||
if (graceful_shutdown_process())
|
||||
{
|
||||
Logger::trace(L"PowerDisplay shutdown gracefully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"PowerDisplay graceful shutdown failed, forcing termination");
|
||||
// Fallback to force termination
|
||||
TerminateProcess(p_info.hProcess, 1);
|
||||
}
|
||||
|
||||
// Clean up handles
|
||||
CloseHandle(p_info.hProcess);
|
||||
CloseHandle(p_info.hThread);
|
||||
}
|
||||
|
||||
m_enabled = false;
|
||||
}
|
||||
|
||||
// Returns if the powertoys is enabled
|
||||
virtual bool is_enabled() override
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
// Respond to a "click" from the launcher
|
||||
virtual bool on_hotkey(size_t /*hotkeyId*/) override
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
Logger::trace(L"Power Display hotkey pressed");
|
||||
if (is_process_running())
|
||||
{
|
||||
TerminateProcess(p_info.hProcess, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
launch_process();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new PowerDisplayModule();
|
||||
}
|
||||
5
src/modules/powerdisplay/PowerDisplayExt/packages.config
Normal file
5
src/modules/powerdisplay/PowerDisplayExt/packages.config
Normal file
@@ -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>
|
||||
1
src/modules/powerdisplay/PowerDisplayExt/pch.cpp
Normal file
1
src/modules/powerdisplay/PowerDisplayExt/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
15
src/modules/powerdisplay/PowerDisplayExt/pch.h
Normal file
15
src/modules/powerdisplay/PowerDisplayExt/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>
|
||||
21
src/modules/powerdisplay/PowerDisplayExt/resource.h
Normal file
21
src/modules/powerdisplay/PowerDisplayExt/resource.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by Awake.rc
|
||||
//
|
||||
#define IDS_REGISTRYPREVIEW_NAME 101
|
||||
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys Registry Preview Module"
|
||||
#define INTERNAL_NAME "PowerToys.RegistryPreview"
|
||||
#define ORIGINAL_FILENAME "PowerToys.RegistryPreview.dll"
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
@@ -177,6 +177,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
L"PowerToys.WorkspacesModuleInterface.dll",
|
||||
L"PowerToys.CmdPalModuleInterface.dll",
|
||||
L"PowerToys.ZoomItModuleInterface.dll",
|
||||
L"PowerToys.PowerDisplayExt.dll",
|
||||
};
|
||||
|
||||
for (auto moduleSubdir : knownModules)
|
||||
|
||||
@@ -807,6 +807,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));
|
||||
@@ -942,6 +944,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));
|
||||
|
||||
@@ -35,6 +35,7 @@ enum class ESettingsWindowNames
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
};
|
||||
|
||||
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
|
||||
|
||||
@@ -513,6 +513,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();
|
||||
|
||||
201
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
201
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class MonitorInfo : Observable
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
private string _internalName = string.Empty;
|
||||
private string _hardwareId = string.Empty;
|
||||
private string _communicationMethod = string.Empty;
|
||||
private string _monitorType = string.Empty;
|
||||
private int _currentBrightness = 0;
|
||||
private int _colorTemperature = 6500;
|
||||
private bool _isHidden = false;
|
||||
private bool _enableColorTemperature = false;
|
||||
private bool _enableContrast = false;
|
||||
private bool _enableVolume = false;
|
||||
|
||||
public MonitorInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public MonitorInfo(string name, string internalName, string communicationMethod)
|
||||
{
|
||||
Name = name;
|
||||
InternalName = internalName;
|
||||
CommunicationMethod = communicationMethod;
|
||||
}
|
||||
|
||||
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, string monitorType, int currentBrightness, int colorTemperature)
|
||||
{
|
||||
Name = name;
|
||||
InternalName = internalName;
|
||||
HardwareId = hardwareId;
|
||||
CommunicationMethod = communicationMethod;
|
||||
MonitorType = monitorType;
|
||||
CurrentBrightness = currentBrightness;
|
||||
ColorTemperature = colorTemperature;
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (_name != value)
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("internalName")]
|
||||
public string InternalName
|
||||
{
|
||||
get => _internalName;
|
||||
set
|
||||
{
|
||||
if (_internalName != value)
|
||||
{
|
||||
_internalName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("communicationMethod")]
|
||||
public string CommunicationMethod
|
||||
{
|
||||
get => _communicationMethod;
|
||||
set
|
||||
{
|
||||
if (_communicationMethod != value)
|
||||
{
|
||||
_communicationMethod = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("hardwareId")]
|
||||
public string HardwareId
|
||||
{
|
||||
get => _hardwareId;
|
||||
set
|
||||
{
|
||||
if (_hardwareId != value)
|
||||
{
|
||||
_hardwareId = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("monitorType")]
|
||||
public string MonitorType
|
||||
{
|
||||
get => _monitorType;
|
||||
set
|
||||
{
|
||||
if (_monitorType != value)
|
||||
{
|
||||
_monitorType = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("currentBrightness")]
|
||||
public int CurrentBrightness
|
||||
{
|
||||
get => _currentBrightness;
|
||||
set
|
||||
{
|
||||
if (_currentBrightness != value)
|
||||
{
|
||||
_currentBrightness = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int ColorTemperature
|
||||
{
|
||||
get => _colorTemperature;
|
||||
set
|
||||
{
|
||||
if (_colorTemperature != value)
|
||||
{
|
||||
_colorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("isHidden")]
|
||||
public bool IsHidden
|
||||
{
|
||||
get => _isHidden;
|
||||
set
|
||||
{
|
||||
if (_isHidden != value)
|
||||
{
|
||||
_isHidden = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("enableColorTemperature")]
|
||||
public bool EnableColorTemperature
|
||||
{
|
||||
get => _enableColorTemperature;
|
||||
set
|
||||
{
|
||||
if (_enableColorTemperature != value)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[MonitorInfo] EnableColorTemperature changing from {_enableColorTemperature} to {value} for monitor {Name}");
|
||||
_enableColorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("enableContrast")]
|
||||
public bool EnableContrast
|
||||
{
|
||||
get => _enableContrast;
|
||||
set
|
||||
{
|
||||
if (_enableContrast != value)
|
||||
{
|
||||
_enableContrast = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("enableVolume")]
|
||||
public bool EnableVolume
|
||||
{
|
||||
get => _enableVolume;
|
||||
set
|
||||
{
|
||||
if (_enableVolume != value)
|
||||
{
|
||||
_enableVolume = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/settings-ui/Settings.UI.Library/MonitorSavedSettings.cs
Normal file
43
src/settings-ui/Settings.UI.Library/MonitorSavedSettings.cs
Normal file
@@ -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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Saved settings for a monitor that can be restored on startup
|
||||
/// </summary>
|
||||
public class MonitorSavedSettings
|
||||
{
|
||||
[JsonPropertyName("brightness")]
|
||||
public int Brightness { get; set; } = 30;
|
||||
|
||||
[JsonPropertyName("color_temperature")]
|
||||
public int ColorTemperature { get; set; } = 6500;
|
||||
|
||||
[JsonPropertyName("contrast")]
|
||||
public int Contrast { get; set; } = 50;
|
||||
|
||||
[JsonPropertyName("volume")]
|
||||
public int Volume { get; set; } = 50;
|
||||
|
||||
[JsonPropertyName("last_updated")]
|
||||
public DateTime LastUpdated { get; set; } = DateTime.Now;
|
||||
|
||||
public MonitorSavedSettings()
|
||||
{
|
||||
}
|
||||
|
||||
public MonitorSavedSettings(int brightness, int colorTemperature, int contrast, int volume)
|
||||
{
|
||||
Brightness = brightness;
|
||||
ColorTemperature = colorTemperature;
|
||||
Contrast = contrast;
|
||||
Volume = volume;
|
||||
LastUpdated = 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.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class PowerDisplayProperties
|
||||
{
|
||||
public PowerDisplayProperties()
|
||||
{
|
||||
LaunchAtStartup = false;
|
||||
Theme = "Light";
|
||||
BrightnessUpdateRate = "1s";
|
||||
Monitors = new List<MonitorInfo>();
|
||||
RestoreSettingsOnStartup = true;
|
||||
SavedMonitorSettings = new Dictionary<string, MonitorSavedSettings>();
|
||||
EnableMcpServer = false;
|
||||
McpServerPort = 5000;
|
||||
McpAutoStart = false;
|
||||
}
|
||||
|
||||
[JsonPropertyName("launch_at_startup")]
|
||||
public bool LaunchAtStartup { get; set; }
|
||||
|
||||
[JsonPropertyName("theme")]
|
||||
public string Theme { get; set; }
|
||||
|
||||
[JsonPropertyName("brightness_update_rate")]
|
||||
public string BrightnessUpdateRate { get; set; }
|
||||
|
||||
[JsonPropertyName("monitors")]
|
||||
public List<MonitorInfo> Monitors { get; set; }
|
||||
|
||||
[JsonPropertyName("restore_settings_on_startup")]
|
||||
public bool RestoreSettingsOnStartup { get; set; }
|
||||
|
||||
[JsonPropertyName("saved_monitor_settings")]
|
||||
public Dictionary<string, MonitorSavedSettings> SavedMonitorSettings { get; set; }
|
||||
|
||||
[JsonPropertyName("enable_mcp_server")]
|
||||
public bool EnableMcpServer { get; set; }
|
||||
|
||||
[JsonPropertyName("mcp_server_port")]
|
||||
public int McpServerPort { get; set; }
|
||||
|
||||
[JsonPropertyName("mcp_auto_start")]
|
||||
public bool McpAutoStart { get; set; }
|
||||
}
|
||||
}
|
||||
32
src/settings-ui/Settings.UI.Library/PowerDisplaySettings.cs
Normal file
32
src/settings-ui/Settings.UI.Library/PowerDisplaySettings.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class PowerDisplaySettings : BasePTModuleSettings, ISettingsConfig
|
||||
{
|
||||
public const string ModuleName = "PowerDisplay";
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public PowerDisplayProperties Properties { get; set; }
|
||||
|
||||
public PowerDisplaySettings()
|
||||
{
|
||||
Properties = new PowerDisplayProperties();
|
||||
Version = "1";
|
||||
Name = ModuleName;
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
=> Name;
|
||||
|
||||
// This can be utilized in the future if the settings.json file is to be modified/deleted.
|
||||
public bool UpgradeSettingsConfiguration()
|
||||
=> false;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -73,6 +73,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
case ModuleType.MeasureTool: return generalSettingsConfig.Enabled.MeasureTool;
|
||||
case ModuleType.ShortcutGuide: return generalSettingsConfig.Enabled.ShortcutGuide;
|
||||
case ModuleType.PowerOCR: return generalSettingsConfig.Enabled.PowerOcr;
|
||||
case ModuleType.PowerDisplay: return generalSettingsConfig.Enabled.PowerDisplay;
|
||||
case ModuleType.ZoomIt: return generalSettingsConfig.Enabled.ZoomIt;
|
||||
default: return false;
|
||||
}
|
||||
@@ -109,6 +110,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
case ModuleType.MeasureTool: generalSettingsConfig.Enabled.MeasureTool = isEnabled; break;
|
||||
case ModuleType.ShortcutGuide: generalSettingsConfig.Enabled.ShortcutGuide = isEnabled; break;
|
||||
case ModuleType.PowerOCR: generalSettingsConfig.Enabled.PowerOcr = isEnabled; break;
|
||||
case ModuleType.PowerDisplay: generalSettingsConfig.Enabled.PowerDisplay = isEnabled; break;
|
||||
case ModuleType.ZoomIt: generalSettingsConfig.Enabled.ZoomIt = isEnabled; break;
|
||||
}
|
||||
}
|
||||
@@ -144,6 +146,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
case ModuleType.MeasureTool: return GPOWrapper.GetConfiguredScreenRulerEnabledValue();
|
||||
case ModuleType.ShortcutGuide: return GPOWrapper.GetConfiguredShortcutGuideEnabledValue();
|
||||
case ModuleType.PowerOCR: return GPOWrapper.GetConfiguredTextExtractorEnabledValue();
|
||||
case ModuleType.PowerDisplay: return GpoRuleConfigured.Unavailable;
|
||||
case ModuleType.ZoomIt: return GPOWrapper.GetConfiguredZoomItEnabledValue();
|
||||
default: return GpoRuleConfigured.Unavailable;
|
||||
}
|
||||
@@ -180,6 +183,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
ModuleType.MeasureTool => typeof(MeasureToolPage),
|
||||
ModuleType.ShortcutGuide => typeof(ShortcutGuidePage),
|
||||
ModuleType.PowerOCR => typeof(PowerOcrPage),
|
||||
ModuleType.PowerDisplay => typeof(PowerDisplayPage),
|
||||
ModuleType.ZoomIt => typeof(ZoomItPage),
|
||||
_ => typeof(DashboardPage), // never called, all values listed above
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums
|
||||
Workspaces,
|
||||
WhatsNew,
|
||||
RegistryPreview,
|
||||
PowerDisplay,
|
||||
NewPlus,
|
||||
ZoomIt,
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
[JsonSerializable(typeof(PowerLauncherSettings))]
|
||||
[JsonSerializable(typeof(PowerOcrSettings))]
|
||||
[JsonSerializable(typeof(PowerOcrSettings))]
|
||||
[JsonSerializable(typeof(PowerDisplaySettings))]
|
||||
[JsonSerializable(typeof(RegistryPreviewSettings))]
|
||||
[JsonSerializable(typeof(ShortcutGuideSettings))]
|
||||
[JsonSerializable(typeof(WINDOWPLACEMENT))]
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Windows.Data.Json;
|
||||
@@ -19,6 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
public static IPCResponseService Instance => _instance ??= new IPCResponseService();
|
||||
|
||||
public static event EventHandler<AllHotkeyConflictsEventArgs> AllHotkeyConflictsReceived;
|
||||
public static event EventHandler<MonitorInfo[]> PowerDisplayMonitorsReceived;
|
||||
|
||||
public void RegisterForIPC()
|
||||
{
|
||||
@@ -47,6 +50,10 @@ namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
ProcessAllHotkeyConflicts(json);
|
||||
}
|
||||
else if (responseType.Equals("powerdisplay_monitors", StringComparison.Ordinal))
|
||||
{
|
||||
ProcessPowerDisplayMonitors(json);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -195,5 +202,85 @@ namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
|
||||
return conflictGroup;
|
||||
}
|
||||
|
||||
private void ProcessPowerDisplayMonitors(JsonObject json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var monitors = new List<MonitorInfo>();
|
||||
|
||||
if (json.TryGetValue("monitors", out IJsonValue monitorsValue) &&
|
||||
monitorsValue.ValueType == JsonValueType.Array)
|
||||
{
|
||||
var monitorsArray = monitorsValue.GetArray();
|
||||
foreach (var monitorItem in monitorsArray)
|
||||
{
|
||||
if (monitorItem.ValueType == JsonValueType.Object)
|
||||
{
|
||||
var monitorObj = monitorItem.GetObject();
|
||||
|
||||
string name = string.Empty;
|
||||
string internalName = string.Empty;
|
||||
string hardwareId = string.Empty;
|
||||
string communicationMethod = string.Empty;
|
||||
string monitorType = string.Empty;
|
||||
int currentBrightness = 0;
|
||||
int colorTemperature = 6500;
|
||||
|
||||
if (monitorObj.TryGetValue("name", out IJsonValue nameValue) &&
|
||||
nameValue.ValueType == JsonValueType.String)
|
||||
{
|
||||
name = nameValue.GetString();
|
||||
}
|
||||
|
||||
if (monitorObj.TryGetValue("internalName", out IJsonValue internalNameValue) &&
|
||||
internalNameValue.ValueType == JsonValueType.String)
|
||||
{
|
||||
internalName = internalNameValue.GetString();
|
||||
}
|
||||
|
||||
if (monitorObj.TryGetValue("hardwareId", out IJsonValue hardwareIdValue) &&
|
||||
hardwareIdValue.ValueType == JsonValueType.String)
|
||||
{
|
||||
hardwareId = hardwareIdValue.GetString();
|
||||
}
|
||||
|
||||
if (monitorObj.TryGetValue("communicationMethod", out IJsonValue communicationMethodValue) &&
|
||||
communicationMethodValue.ValueType == JsonValueType.String)
|
||||
{
|
||||
communicationMethod = communicationMethodValue.GetString();
|
||||
}
|
||||
|
||||
if (monitorObj.TryGetValue("monitorType", out IJsonValue monitorTypeValue) &&
|
||||
monitorTypeValue.ValueType == JsonValueType.String)
|
||||
{
|
||||
monitorType = monitorTypeValue.GetString();
|
||||
}
|
||||
|
||||
if (monitorObj.TryGetValue("currentBrightness", out IJsonValue currentBrightnessValue) &&
|
||||
currentBrightnessValue.ValueType == JsonValueType.Number)
|
||||
{
|
||||
currentBrightness = (int)currentBrightnessValue.GetNumber();
|
||||
}
|
||||
|
||||
if (monitorObj.TryGetValue("colorTemperature", out IJsonValue colorTemperatureValue) &&
|
||||
colorTemperatureValue.ValueType == JsonValueType.Number)
|
||||
{
|
||||
colorTemperature = (int)colorTemperatureValue.GetNumber();
|
||||
}
|
||||
|
||||
var monitorInfo = new MonitorInfo(name, internalName, hardwareId, communicationMethod, monitorType, currentBrightness, colorTemperature);
|
||||
monitors.Add(monitorInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PowerDisplayMonitorsReceived?.Invoke(this, monitors.ToArray());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore JSON parsing errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.OobePowerDisplay"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<controls:OOBEPageControl x:Uid="Oobe_PowerDisplay" HeroImage="ms-appx:///Assets/Settings/Modules/OOBE/PowerDisplay.png">
|
||||
<controls:OOBEPageControl.PageContent>
|
||||
<StackPanel Orientation="Vertical" Spacing="12">
|
||||
<TextBlock x:Uid="Oobe_HowToUse" Style="{ThemeResource OobeSubtitleStyle}" />
|
||||
|
||||
<tk7controls:MarkdownTextBlock x:Uid="Oobe_PowerDisplay_HowToUse" Background="Transparent" />
|
||||
|
||||
<TextBlock x:Uid="Oobe_TipsAndTricks" Style="{ThemeResource OobeSubtitleStyle}" />
|
||||
|
||||
<tk7controls:MarkdownTextBlock x:Uid="Oobe_PowerDisplay_TipsAndTricks" Background="Transparent" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button
|
||||
x:Uid="Launch_PowerDisplay"
|
||||
Click="Launch_PowerDisplay_Click"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button x:Uid="OOBE_Settings" Click="SettingsLaunchButton_Click" />
|
||||
|
||||
<HyperlinkButton NavigateUri="https://aka.ms/PowerToysOverview_PowerDisplay" Style="{StaticResource TextButtonStyle}">
|
||||
<TextBlock x:Uid="LearnMore_PowerDisplay" TextWrapping="Wrap" />
|
||||
</HyperlinkButton>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:OOBEPageControl.PageContent>
|
||||
</controls:OOBEPageControl>
|
||||
</Page>
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||
/// </summary>
|
||||
public sealed partial class OobePowerDisplay : Page
|
||||
{
|
||||
public OobePowerToysModule ViewModel { get; set; }
|
||||
|
||||
public OobePowerDisplay()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.PowerDisplay]);
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
|
||||
private void Launch_PowerDisplay_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
ShellPage.SendDefaultIPCMessage("{\"action\":{\"PowerDisplay\":{\"action_name\":\"Launch\", \"value\":\"\"}}}");
|
||||
}
|
||||
|
||||
private void SettingsLaunchButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
if (OobeShellPage.OpenMainWindowCallback != null)
|
||||
{
|
||||
OobeShellPage.OpenMainWindowCallback(typeof(PowerDisplayPage));
|
||||
}
|
||||
|
||||
ViewModel.LogOpeningSettingsEvent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogOpeningModuleEvent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||
{
|
||||
ViewModel.LogClosingModuleEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<local:NavigablePage
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.Views.PowerDisplayPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:library="using:Microsoft.PowerToys.Settings.UI.Library"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<controls:SettingsPageControl x:Uid="PowerDisplay" ModuleImageSource="ms-appx:///Assets/Settings/Modules/PowerDisplay.png">
|
||||
<controls:SettingsPageControl.ModuleContent>
|
||||
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_Enable_PowerDisplay"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<controls:SettingsGroup x:Uid="PowerDisplay_Configuration_GroupSettings" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_LaunchButtonControl"
|
||||
ActionIcon="{ui:FontIcon Glyph=}"
|
||||
Command="{x:Bind ViewModel.LaunchEventHandler}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsClickEnabled="True" />
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_LaunchAtStartup"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_LaunchAtStartup_ToggleSwitch" IsOn="{x:Bind ViewModel.IsLaunchAtStartupEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_Theme"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
ItemsSource="{x:Bind ViewModel.ThemeOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.Theme, Mode=TwoWay}"
|
||||
MinWidth="120" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
Header="Restore saved values on startup"
|
||||
Description="When enabled, PowerDisplay will restore the last saved brightness, contrast, color temperature and volume values on startup. When disabled, it will read current monitor values."
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.RestoreSettingsOnStartup, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_BrightnessUpdateRate"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
ItemsSource="{x:Bind ViewModel.BrightnessUpdateRateOptions}"
|
||||
SelectedItem="{x:Bind ViewModel.BrightnessUpdateRate, Mode=TwoWay}"
|
||||
MinWidth="120" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="PowerDisplay_MCP_GroupSettings" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_EnableMcpServer"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableMcpServer, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_McpServerPort"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.EnableMcpServer, Mode=OneWay}">
|
||||
<NumberBox
|
||||
Value="{x:Bind ViewModel.McpServerPort, Mode=TwoWay}"
|
||||
Minimum="1000"
|
||||
Maximum="65535"
|
||||
MinWidth="120" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PowerDisplay_McpAutoStart"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsEnabled="{x:Bind ViewModel.EnableMcpServer, Mode=OneWay}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.McpAutoStart, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="PowerDisplay_Monitors" IsEnabled="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}">
|
||||
<!-- 空状态提示 -->
|
||||
<InfoBar
|
||||
x:Uid="PowerDisplay_NoMonitorsDetected"
|
||||
IsClosable="False"
|
||||
IsOpen="{x:Bind ViewModel.IsPowerDisplayEnabled, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"
|
||||
Severity="Informational">
|
||||
<InfoBar.IconSource>
|
||||
<FontIconSource FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
||||
</InfoBar.IconSource>
|
||||
</InfoBar>
|
||||
|
||||
<!-- 显示器列表 -->
|
||||
<ItemsControl
|
||||
x:Name="MonitorsList"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{x:Bind ViewModel.HasMonitors, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
|
||||
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="library:MonitorInfo">
|
||||
<tkcontrols:SettingsExpander
|
||||
Margin="0,0,0,2"
|
||||
Header="{x:Bind Name}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="False">
|
||||
<tkcontrols:SettingsExpander.Items>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Name">
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_InternalName">
|
||||
<TextBlock Text="{x:Bind InternalName, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Type">
|
||||
<TextBlock Text="{x:Bind MonitorType, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_CommunicationMethod">
|
||||
<TextBlock Text="{x:Bind CommunicationMethod, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Brightness">
|
||||
<TextBlock Text="{x:Bind CurrentBrightness, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_ColorTemperature">
|
||||
<TextBlock Text="{x:Bind ColorTemperature, Mode=OneWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_Reset">
|
||||
<Button x:Uid="PowerDisplay_Monitor_ResetButton" Click="ResetButton_Click" Tag="{x:Bind}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableColorTemperature">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch" IsOn="{x:Bind EnableColorTemperature, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableContrast">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableContrast_ToggleSwitch" IsOn="{x:Bind EnableContrast, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_EnableVolume">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_EnableVolume_ToggleSwitch" IsOn="{x:Bind EnableVolume, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard x:Uid="PowerDisplay_Monitor_HideMonitor">
|
||||
<ToggleSwitch x:Uid="PowerDisplay_Monitor_HideMonitor_ToggleSwitch" IsOn="{x:Bind IsHidden, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</controls:SettingsGroup>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:PageLink x:Uid="LearnMore_PowerDisplay" Link="https://aka.ms/PowerToysOverview_PowerDisplay" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
</local:NavigablePage>
|
||||
@@ -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 Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
public sealed partial class PowerDisplayPage : NavigablePage, IRefreshablePage
|
||||
{
|
||||
private PowerDisplayViewModel ViewModel { get; set; }
|
||||
|
||||
|
||||
public PowerDisplayPage()
|
||||
{
|
||||
var settingsUtils = new SettingsUtils();
|
||||
ViewModel = new PowerDisplayViewModel(
|
||||
SettingsRepository<GeneralSettings>.GetInstance(settingsUtils),
|
||||
SettingsRepository<PowerDisplaySettings>.GetInstance(settingsUtils),
|
||||
ShellPage.SendDefaultIPCMessage);
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
ViewModel.RefreshEnabledState();
|
||||
}
|
||||
|
||||
private void ResetButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.Tag is MonitorInfo monitor)
|
||||
{
|
||||
ViewModel.ResetMonitorSettings(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,12 @@
|
||||
helpers:NavHelper.NavigateTo="views:ColorPickerPage"
|
||||
AutomationProperties.AutomationId="ColorPickerNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/ColorPicker.png}" />
|
||||
<NavigationViewItem
|
||||
x:Name="PowerDisplayNavigationItem"
|
||||
x:Uid="Shell_PowerDisplay"
|
||||
helpers:NavHelper.NavigateTo="views:PowerDisplayPage"
|
||||
AutomationProperties.AutomationId="PowerDisplayNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/PowerDisplay.png}" />
|
||||
<NavigationViewItem
|
||||
x:Name="PowerLauncherNavigationItem"
|
||||
x:Uid="Shell_PowerLauncher"
|
||||
|
||||
@@ -555,6 +555,10 @@ opera.exe</value>
|
||||
<value>Color Picker</value>
|
||||
<comment>Product name: Navigation view item name for Color Picker</comment>
|
||||
</data>
|
||||
<data name="Shell_PowerDisplay.Content" xml:space="preserve">
|
||||
<value>Power Display</value>
|
||||
<comment>Product name: Navigation view item name for Power Display</comment>
|
||||
</data>
|
||||
<data name="Shell_KeyboardManager.Content" xml:space="preserve">
|
||||
<value>Keyboard Manager</value>
|
||||
<comment>Product name: Navigation view item name for Keyboard Manager</comment>
|
||||
@@ -5299,4 +5303,163 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="UtilitiesHeader.Title" xml:space="preserve">
|
||||
<value>Utilities</value>
|
||||
</data>
|
||||
<data name="PowerDisplay.ModuleTitle" xml:space="preserve">
|
||||
<value>Power Display</value>
|
||||
</data>
|
||||
<data name="PowerDisplay.ModuleDescription" xml:space="preserve">
|
||||
<value>A display management utility for brightness and power control</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Enable_PowerDisplay.Header" xml:space="preserve">
|
||||
<value>Enable Power Display</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Configuration_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Configuration</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_LaunchButtonControl.Header" xml:space="preserve">
|
||||
<value>Open Power Display</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_LaunchButtonControl.Description" xml:space="preserve">
|
||||
<value>Launch the Power Display utility</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_LaunchAtStartup.Header" xml:space="preserve">
|
||||
<value>Launch at startup</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_LaunchAtStartup.Description" xml:space="preserve">
|
||||
<value>Automatically start Power Display when Windows starts</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_LaunchAtStartup_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_LaunchAtStartup_ToggleSwitch.OffContent" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Theme.Header" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Theme.Description" xml:space="preserve">
|
||||
<value>Select theme for Power Display</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_BrightnessUpdateRate.Header" xml:space="preserve">
|
||||
<value>Brightness update rate</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_BrightnessUpdateRate.Description" xml:space="preserve">
|
||||
<value>How frequently to update brightness values</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_HideMonitor.Header" xml:space="preserve">
|
||||
<value>Hide monitor</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_HideMonitor.Description" xml:space="preserve">
|
||||
<value>Hide this monitor from the Power Display interface</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_HideMonitor_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_HideMonitor_ToggleSwitch.OffContent" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitors.Header" xml:space="preserve">
|
||||
<value>Monitors</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_NoMonitorsDetected.Title" xml:space="preserve">
|
||||
<value>No monitors detected</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_NoMonitorsDetected.Message" xml:space="preserve">
|
||||
<value>Please ensure your external monitors are connected and powered on.</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_Name.Header" xml:space="preserve">
|
||||
<value>Display name</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_InternalName.Header" xml:space="preserve">
|
||||
<value>Internal name</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_Type.Header" xml:space="preserve">
|
||||
<value>Monitor type</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_CommunicationMethod.Header" xml:space="preserve">
|
||||
<value>Communication method</value>
|
||||
</data>
|
||||
<data name="LearnMore_PowerDisplay.Text" xml:space="preserve">
|
||||
<value>Learn more about Power Display</value>
|
||||
</data>
|
||||
<data name="Oobe_PowerDisplay.Title" xml:space="preserve">
|
||||
<value>Power Display</value>
|
||||
</data>
|
||||
<data name="Oobe_PowerDisplay.Description" xml:space="preserve">
|
||||
<value>Power Display provides advanced display management features including brightness control, auto-brightness, and power management settings.</value>
|
||||
</data>
|
||||
<data name="Launch_PowerDisplay.Content" xml:space="preserve">
|
||||
<value>Open Power Display</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_Brightness.Header" xml:space="preserve">
|
||||
<value>Brightness</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_ColorTemperature.Header" xml:space="preserve">
|
||||
<value>Color Temperature</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_Reset.Header" xml:space="preserve">
|
||||
<value>Reset Settings</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_Reset.Description" xml:space="preserve">
|
||||
<value>Reset brightness and color temperature to default values</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_ResetButton.Content" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableColorTemperature.Header" xml:space="preserve">
|
||||
<value>Enable color temperature</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableColorTemperature.Description" xml:space="preserve">
|
||||
<value>Show color temperature control for this monitor</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableColorTemperature_ToggleSwitch.OffContent" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableContrast.Header" xml:space="preserve">
|
||||
<value>Enable contrast</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableContrast.Description" xml:space="preserve">
|
||||
<value>Show contrast control for this monitor</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableContrast_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableContrast_ToggleSwitch.OffContent" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableVolume.Header" xml:space="preserve">
|
||||
<value>Enable volume</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableVolume.Description" xml:space="preserve">
|
||||
<value>Show volume control for this monitor</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableVolume_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_Monitor_EnableVolume_ToggleSwitch.OffContent" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_MCP_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Model Context Protocol (MCP)</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableMcpServer.Header" xml:space="preserve">
|
||||
<value>Enable MCP Server</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_EnableMcpServer.Description" xml:space="preserve">
|
||||
<value>Allow MCP clients like Claude Desktop to control monitor settings through standardized protocol</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_McpServerPort.Header" xml:space="preserve">
|
||||
<value>MCP Server Port</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_McpServerPort.Description" xml:space="preserve">
|
||||
<value>Port number for the MCP server to listen on (default: 5000)</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_McpAutoStart.Header" xml:space="preserve">
|
||||
<value>MCP Auto Start</value>
|
||||
</data>
|
||||
<data name="PowerDisplay_McpAutoStart.Description" xml:space="preserve">
|
||||
<value>Automatically start MCP server when PowerDisplay launches</value>
|
||||
</data>
|
||||
</root>
|
||||
594
src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs
Normal file
594
src/settings-ui/Settings.UI/ViewModels/PowerDisplayViewModel.cs
Normal file
@@ -0,0 +1,594 @@
|
||||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class PowerDisplayViewModel : Observable
|
||||
{
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
private ISettingsRepository<PowerDisplaySettings> _powerDisplayRepository;
|
||||
private FileSystemWatcher _settingsWatcher;
|
||||
|
||||
public ButtonClickCommand LaunchEventHandler => new ButtonClickCommand(Launch);
|
||||
|
||||
public ButtonClickCommand GetResetMonitorCommand(MonitorInfo monitor) => new ButtonClickCommand(() => ResetMonitorSettings(monitor));
|
||||
|
||||
public PowerDisplayViewModel(ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<PowerDisplaySettings> powerDisplaySettingsRepository, Func<string, int> ipcMSGCallBackFunc)
|
||||
{
|
||||
// To obtain the general settings configurations of PowerToys Settings.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
|
||||
_powerDisplayRepository = powerDisplaySettingsRepository;
|
||||
_settings = powerDisplaySettingsRepository.SettingsConfig;
|
||||
|
||||
InitializeEnabledValue();
|
||||
|
||||
// Initialize monitors collection
|
||||
_monitors = new ObservableCollection<MonitorInfo>(_settings.Properties.Monitors);
|
||||
_hasMonitors = _monitors.Count > 0;
|
||||
|
||||
// Subscribe to PropertyChanged events for existing monitors
|
||||
SubscribeToAllMonitorChanges();
|
||||
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
// Subscribe to monitor information updates
|
||||
IPCResponseService.PowerDisplayMonitorsReceived += OnMonitorsReceived;
|
||||
|
||||
// Don't setup settings file watcher for PowerDisplay's settings.json
|
||||
// as it creates circular dependencies and the two apps use different formats
|
||||
// SetupSettingsWatcher();
|
||||
}
|
||||
|
||||
private void InitializeEnabledValue()
|
||||
{
|
||||
_isPowerDisplayEnabled = GeneralSettingsConfig.Enabled.PowerDisplay;
|
||||
}
|
||||
|
||||
public bool IsPowerDisplayEnabled
|
||||
{
|
||||
get => _isPowerDisplayEnabled;
|
||||
set
|
||||
{
|
||||
if (_isPowerDisplayEnabled != value)
|
||||
{
|
||||
_isPowerDisplayEnabled = value;
|
||||
OnPropertyChanged(nameof(IsPowerDisplayEnabled));
|
||||
|
||||
GeneralSettingsConfig.Enabled.PowerDisplay = value;
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLaunchAtStartupEnabled
|
||||
{
|
||||
get => _settings.Properties.LaunchAtStartup;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.LaunchAtStartup != value)
|
||||
{
|
||||
_settings.Properties.LaunchAtStartup = value;
|
||||
OnPropertyChanged(nameof(IsLaunchAtStartupEnabled));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool RestoreSettingsOnStartup
|
||||
{
|
||||
get => _settings.Properties.RestoreSettingsOnStartup;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.RestoreSettingsOnStartup != value)
|
||||
{
|
||||
_settings.Properties.RestoreSettingsOnStartup = value;
|
||||
OnPropertyChanged(nameof(RestoreSettingsOnStartup));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Theme
|
||||
{
|
||||
get => _settings.Properties.Theme;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.Theme != value)
|
||||
{
|
||||
_settings.Properties.Theme = value;
|
||||
OnPropertyChanged(nameof(Theme));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string BrightnessUpdateRate
|
||||
{
|
||||
get => _settings.Properties.BrightnessUpdateRate;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.BrightnessUpdateRate != value)
|
||||
{
|
||||
_settings.Properties.BrightnessUpdateRate = value;
|
||||
OnPropertyChanged(nameof(BrightnessUpdateRate));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableMcpServer
|
||||
{
|
||||
get => _settings.Properties.EnableMcpServer;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.EnableMcpServer != value)
|
||||
{
|
||||
_settings.Properties.EnableMcpServer = value;
|
||||
OnPropertyChanged(nameof(EnableMcpServer));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int McpServerPort
|
||||
{
|
||||
get => _settings.Properties.McpServerPort;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.McpServerPort != value)
|
||||
{
|
||||
_settings.Properties.McpServerPort = value;
|
||||
OnPropertyChanged(nameof(McpServerPort));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool McpAutoStart
|
||||
{
|
||||
get => _settings.Properties.McpAutoStart;
|
||||
set
|
||||
{
|
||||
if (_settings.Properties.McpAutoStart != value)
|
||||
{
|
||||
_settings.Properties.McpAutoStart = value;
|
||||
OnPropertyChanged(nameof(McpAutoStart));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<string> _brightnessUpdateRateOptions = new List<string>
|
||||
{
|
||||
"never",
|
||||
"250ms",
|
||||
"500ms",
|
||||
"1s",
|
||||
"2s"
|
||||
};
|
||||
|
||||
public List<string> BrightnessUpdateRateOptions => _brightnessUpdateRateOptions;
|
||||
|
||||
private readonly List<string> _themeOptions = new List<string>
|
||||
{
|
||||
"Light",
|
||||
"Dark"
|
||||
};
|
||||
|
||||
public List<string> ThemeOptions => _themeOptions;
|
||||
|
||||
public ObservableCollection<MonitorInfo> Monitors
|
||||
{
|
||||
get => _monitors;
|
||||
set
|
||||
{
|
||||
if (_monitors != value)
|
||||
{
|
||||
// Unsubscribe from old collection
|
||||
if (_monitors != null)
|
||||
{
|
||||
UnsubscribeFromAllMonitorChanges();
|
||||
}
|
||||
|
||||
_monitors = value;
|
||||
|
||||
// Subscribe to new collection
|
||||
if (_monitors != null)
|
||||
{
|
||||
SubscribeToAllMonitorChanges();
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMonitors
|
||||
{
|
||||
get => _hasMonitors;
|
||||
set
|
||||
{
|
||||
if (_hasMonitors != value)
|
||||
{
|
||||
_hasMonitors = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMonitorsReceived(object sender, MonitorInfo[] monitors)
|
||||
{
|
||||
UpdateMonitors(monitors);
|
||||
}
|
||||
|
||||
public void UpdateMonitors(MonitorInfo[] monitors)
|
||||
{
|
||||
_isUpdatingSettings = true;
|
||||
try
|
||||
{
|
||||
if (monitors == null)
|
||||
{
|
||||
// Unsubscribe from all existing monitors
|
||||
UnsubscribeFromAllMonitorChanges();
|
||||
|
||||
_monitors.Clear();
|
||||
HasMonitors = false;
|
||||
_settings.Properties.Monitors = new List<MonitorInfo>();
|
||||
NotifySettingsChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unsubscribe from all existing monitors
|
||||
UnsubscribeFromAllMonitorChanges();
|
||||
|
||||
// Create a lookup of existing monitors to preserve user settings
|
||||
var existingMonitors = _monitors.ToDictionary(m => GetMonitorKey(m), m => m);
|
||||
|
||||
_monitors.Clear();
|
||||
foreach (var newMonitor in monitors)
|
||||
{
|
||||
var monitorKey = GetMonitorKey(newMonitor);
|
||||
|
||||
// Check if we have an existing monitor with the same key
|
||||
if (existingMonitors.TryGetValue(monitorKey, out var existingMonitor))
|
||||
{
|
||||
// Preserve user settings from existing monitor
|
||||
newMonitor.EnableColorTemperature = existingMonitor.EnableColorTemperature;
|
||||
newMonitor.EnableContrast = existingMonitor.EnableContrast;
|
||||
newMonitor.EnableVolume = existingMonitor.EnableVolume;
|
||||
newMonitor.IsHidden = existingMonitor.IsHidden;
|
||||
}
|
||||
|
||||
_monitors.Add(newMonitor);
|
||||
|
||||
// Subscribe to PropertyChanged for the new monitor
|
||||
SubscribeToMonitorChanges(newMonitor);
|
||||
}
|
||||
|
||||
// Update HasMonitors property
|
||||
HasMonitors = monitors.Length > 0;
|
||||
|
||||
// Update settings
|
||||
_settings.Properties.Monitors = _monitors.ToList();
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a unique key for monitor matching based on hardware ID and internal name
|
||||
/// </summary>
|
||||
private string GetMonitorKey(MonitorInfo monitor)
|
||||
{
|
||||
// Use hardware ID if available, otherwise fall back to internal name
|
||||
if (!string.IsNullOrEmpty(monitor.HardwareId))
|
||||
{
|
||||
return monitor.HardwareId;
|
||||
}
|
||||
|
||||
return monitor.InternalName ?? monitor.Name ?? string.Empty;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Unsubscribe from monitor property changes
|
||||
UnsubscribeFromAllMonitorChanges();
|
||||
|
||||
// Unsubscribe from events
|
||||
IPCResponseService.PowerDisplayMonitorsReceived -= OnMonitorsReceived;
|
||||
|
||||
// Clean up settings file watcher
|
||||
if (_settingsWatcher != null)
|
||||
{
|
||||
_settingsWatcher.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to PropertyChanged events for all monitors in the collection
|
||||
/// </summary>
|
||||
private void SubscribeToAllMonitorChanges()
|
||||
{
|
||||
foreach (var monitor in _monitors)
|
||||
{
|
||||
monitor.PropertyChanged += OnMonitorPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from PropertyChanged events for all monitors in the collection
|
||||
/// </summary>
|
||||
private void UnsubscribeFromAllMonitorChanges()
|
||||
{
|
||||
foreach (var monitor in _monitors)
|
||||
{
|
||||
monitor.PropertyChanged -= OnMonitorPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to PropertyChanged event for a specific monitor
|
||||
/// </summary>
|
||||
private void SubscribeToMonitorChanges(MonitorInfo monitor)
|
||||
{
|
||||
monitor.PropertyChanged += OnMonitorPropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from PropertyChanged event for a specific monitor
|
||||
/// </summary>
|
||||
private void UnsubscribeFromMonitorChanges(MonitorInfo monitor)
|
||||
{
|
||||
monitor.PropertyChanged -= OnMonitorPropertyChanged;
|
||||
}
|
||||
|
||||
private bool _isUpdatingSettings = false;
|
||||
|
||||
/// <summary>
|
||||
/// Handle PropertyChanged events from MonitorInfo objects
|
||||
/// </summary>
|
||||
private void OnMonitorPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
// Prevent infinite loops during settings updates
|
||||
if (_isUpdatingSettings) return;
|
||||
|
||||
if (sender is MonitorInfo monitor)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Monitor {monitor.Name} property {e.PropertyName} changed to: EnableColorTemp={monitor.EnableColorTemperature}, EnableContrast={monitor.EnableContrast}, EnableVolume={monitor.EnableVolume}");
|
||||
}
|
||||
|
||||
// Update the settings object to keep it in sync
|
||||
_settings.Properties.Monitors = _monitors.ToList();
|
||||
|
||||
// Save settings when any monitor property changes
|
||||
NotifySettingsChanged();
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Monitor property changed: {e.PropertyName}");
|
||||
}
|
||||
|
||||
public void Launch()
|
||||
{
|
||||
var actionName = "Launch";
|
||||
|
||||
SendConfigMSG("{\"action\":{\"PowerDisplay\":{\"action_name\":\"" + actionName + "\", \"value\":\"\"}}}");
|
||||
}
|
||||
|
||||
public void ResetMonitorSettings(MonitorInfo monitor)
|
||||
{
|
||||
if (monitor == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Reset monitor values to defaults
|
||||
monitor.CurrentBrightness = 30;
|
||||
monitor.ColorTemperature = 6500;
|
||||
|
||||
// Update the saved settings with default values
|
||||
if (_settings.Properties.SavedMonitorSettings == null)
|
||||
{
|
||||
_settings.Properties.SavedMonitorSettings = new Dictionary<string, MonitorSavedSettings>();
|
||||
}
|
||||
|
||||
_settings.Properties.SavedMonitorSettings[monitor.InternalName] = new MonitorSavedSettings
|
||||
{
|
||||
Brightness = 30,
|
||||
ColorTemperature = 6500,
|
||||
Contrast = 50,
|
||||
Volume = 50,
|
||||
LastUpdated = DateTime.Now
|
||||
};
|
||||
|
||||
// Save settings - this will trigger PowerDisplay's file watcher to apply the reset values
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle error gracefully
|
||||
System.Diagnostics.Debug.WriteLine($"Failed to reset monitor settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
private bool _isPowerDisplayEnabled;
|
||||
private PowerDisplaySettings _settings;
|
||||
private ObservableCollection<MonitorInfo> _monitors;
|
||||
private bool _hasMonitors;
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
InitializeEnabledValue();
|
||||
OnPropertyChanged(nameof(IsPowerDisplayEnabled));
|
||||
}
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
{
|
||||
// Using InvariantCulture as this is an IPC message
|
||||
SendConfigMSG(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
|
||||
PowerDisplaySettings.ModuleName,
|
||||
JsonSerializer.Serialize(_settings, SourceGenerationContextContext.Default.PowerDisplaySettings)));
|
||||
|
||||
// Also save directly to PowerDisplay's settings file for immediate pickup
|
||||
SaveToPowerDisplaySettingsFile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save settings directly to PowerDisplay's settings.json file
|
||||
/// </summary>
|
||||
private void SaveToPowerDisplaySettingsFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft", "PowerToys", "PowerDisplay", "settings.json");
|
||||
|
||||
var directory = Path.GetDirectoryName(settingsPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var jsonString = JsonSerializer.Serialize(_settings, SourceGenerationContextContext.Default.PowerDisplaySettings);
|
||||
|
||||
// Use retry logic to handle file access conflicts
|
||||
int retryCount = 3;
|
||||
for (int i = 0; i < retryCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(settingsPath, jsonString);
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings saved to PowerDisplay file: {settingsPath}");
|
||||
break; // Success, exit retry loop
|
||||
}
|
||||
catch (IOException) when (i < retryCount - 1)
|
||||
{
|
||||
// File is locked, wait and retry
|
||||
System.Threading.Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Failed to save to PowerDisplay settings file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置设置文件监视器,当 PowerDisplay.exe 更新设置文件时自动刷新 UI
|
||||
/// </summary>
|
||||
private void SetupSettingsWatcher()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft", "PowerToys", "PowerDisplay", "settings.json");
|
||||
|
||||
var directory = Path.GetDirectoryName(settingsPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
// 确保目录存在
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
_settingsWatcher = new FileSystemWatcher(directory)
|
||||
{
|
||||
Filter = "settings.json",
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
_settingsWatcher.Changed += OnSettingsFileChanged;
|
||||
_settingsWatcher.Created += OnSettingsFileChanged;
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings file watcher setup for: {settingsPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Failed to setup settings file watcher: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理设置文件变化事件
|
||||
/// </summary>
|
||||
private void OnSettingsFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings file changed: {e.FullPath}");
|
||||
|
||||
// 添加延迟确保文件写入完成
|
||||
Task.Delay(500).ContinueWith(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Application.Current?.Dispatcher?.Invoke(() =>
|
||||
{
|
||||
// 重新加载设置
|
||||
if (_powerDisplayRepository.ReloadSettings())
|
||||
{
|
||||
var newSettings = _powerDisplayRepository.SettingsConfig;
|
||||
|
||||
// 更新监视器列表
|
||||
UpdateMonitors(newSettings.Properties.Monitors.ToArray());
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Settings reloaded, monitor count: {newSettings.Properties.Monitors.Count}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Failed to reload settings: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[PowerDisplayViewModel] Error handling settings file change: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user