mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
Introduce PowerDisplay
This commit is contained in:
@@ -85,6 +85,7 @@
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
|
||||
@@ -111,6 +112,7 @@
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
|
||||
<PackageVersion Include="WinUIEx" Version="2.8.0" />
|
||||
<PackageVersion Include="WmiLight" Version="6.14.0" />
|
||||
<PackageVersion Include="WPF-UI" Version="3.0.5" />
|
||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
|
||||
|
||||
@@ -517,6 +517,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\m
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerDisplay", "PowerDisplay", "{B5E6F789-0123-4567-8901-23456789ABCD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilePreviewCommon", "src\common\FilePreviewCommon\FilePreviewCommon.csproj", "{9EBAA524-0EDA-470B-95D4-39383285CBB2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.PowerToys", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.csproj", "{500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}"
|
||||
@@ -563,6 +565,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerDisplay", "src\modules\powerdisplay\PowerDisplay\PowerDisplay.csproj", "{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}"
|
||||
EndProject
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerDisplayModuleInterface", "src\modules\powerdisplay\PowerDisplayModuleInterface\PowerDisplayModuleInterface.vcxproj", "{D1234567-8901-2345-6789-ABCDEF012345}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}"
|
||||
@@ -2246,6 +2252,22 @@ Global
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.ActiveCfg = Release|x64
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.Build.0 = Release|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Debug|x64.Build.0 = Debug|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.ActiveCfg = Release|x64
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF}.Release|x64.Build.0 = Release|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Debug|x64.Build.0 = Debug|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.ActiveCfg = Release|x64
|
||||
{D1234567-8901-2345-6789-ABCDEF012345}.Release|x64.Build.0 = Release|x64
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.ActiveCfg = Debug|x64
|
||||
@@ -3222,6 +3244,9 @@ Global
|
||||
{C32D254F-7597-4CBE-BF74-D922D81CDF29} = {9873BA05-4C41-4819-9283-CF45D795431B}
|
||||
{02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
|
||||
{8E23E173-7127-4A5F-9F93-3049F2B68047} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
||||
{A1B2C3D4-E5F6-7890-1234-567890ABCDEF} = {B5E6F789-0123-4567-8901-23456789ABCD}
|
||||
{D1234567-8901-2345-6789-ABCDEF012345} = {B5E6F789-0123-4567-8901-23456789ABCD}
|
||||
{B5E6F789-0123-4567-8901-23456789ABCD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
|
||||
{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}
|
||||
{C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}
|
||||
{1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95}
|
||||
|
||||
30
installer/PowerToysSetup/PowerDisplay.wxs
Normal file
30
installer/PowerToysSetup/PowerDisplay.wxs
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
30
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
30
installer/PowerToysSetupVNext/PowerDisplay.wxs
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" >
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define PowerDisplayAssetsFiles=?>
|
||||
<?define PowerDisplayAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\PowerDisplay?>
|
||||
|
||||
<Fragment>
|
||||
<!-- Power Display -->
|
||||
<DirectoryRef Id="WinUI3AppsAssetsFolder">
|
||||
<Directory Id="PowerDisplayAssetsInstallFolder" Name="PowerDisplay" />
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="PowerDisplayAssetsInstallFolder" FileSource="$(var.PowerDisplayAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--PowerDisplayAssetsFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="PowerDisplayComponentGroup">
|
||||
<Component Id="RemovePowerDisplayFolder" Guid="B8F2E3A5-72C1-4A2D-9B3F-8E5D7C6A4F9B" Directory="PowerDisplayAssetsInstallFolder" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemovePowerDisplayFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerDisplayAssetsFolder" Directory="PowerDisplayAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -53,6 +53,7 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace Common.UI
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
}
|
||||
|
||||
private static string SettingsWindowNameToString(SettingsWindow value)
|
||||
@@ -113,6 +114,8 @@ namespace Common.UI
|
||||
return "CmdPal";
|
||||
case SettingsWindow.ZoomIt:
|
||||
return "ZoomIt";
|
||||
case SettingsWindow.PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
return string.Empty;
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace ManagedCommon
|
||||
PowerRename,
|
||||
PowerLauncher,
|
||||
PowerAccent,
|
||||
PowerDisplay,
|
||||
RegistryPreview,
|
||||
MeasureTool,
|
||||
ShortcutGuide,
|
||||
|
||||
@@ -195,4 +195,12 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
|
||||
}
|
||||
hstring Constants::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,56 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace PowerDisplay.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic relay command implementation for parameterless actions
|
||||
/// </summary>
|
||||
public partial class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool>? _canExecute;
|
||||
|
||||
public RelayCommand(Action execute, Func<bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
|
||||
|
||||
public void Execute(object? parameter) => _execute();
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic relay command implementation for parameterized actions
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the command parameter</typeparam>
|
||||
public partial class RelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Action<T?> _execute;
|
||||
private readonly Func<T?, bool>? _canExecute;
|
||||
|
||||
public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute = null)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
|
||||
public bool CanExecute(object? parameter) => _canExecute?.Invoke((T?)parameter) ?? true;
|
||||
|
||||
public void Execute(object? parameter) => _execute((T?)parameter);
|
||||
|
||||
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerDisplay.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Application-wide constants and configuration values
|
||||
/// </summary>
|
||||
public static class AppConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// State management configuration
|
||||
/// </summary>
|
||||
public static class State
|
||||
{
|
||||
/// <summary>
|
||||
/// Interval in milliseconds to check for pending state changes to save
|
||||
/// </summary>
|
||||
public const int SaveIntervalMs = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the state file for monitor parameters
|
||||
/// </summary>
|
||||
public const string StateFileName = "monitor_state.json";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitor parameter defaults and ranges
|
||||
/// </summary>
|
||||
public static class MonitorDefaults
|
||||
{
|
||||
// Brightness
|
||||
public const int MinBrightness = 0;
|
||||
public const int MaxBrightness = 100;
|
||||
public const int DefaultBrightness = 50;
|
||||
|
||||
// Color Temperature (Kelvin)
|
||||
public const int MinColorTemp = 2000; // Warm
|
||||
public const int MaxColorTemp = 10000; // Cool
|
||||
public const int DefaultColorTemp = 6500; // Neutral
|
||||
|
||||
// Contrast
|
||||
public const int MinContrast = 0;
|
||||
public const int MaxContrast = 100;
|
||||
public const int DefaultContrast = 50;
|
||||
|
||||
// Volume
|
||||
public const int MinVolume = 0;
|
||||
public const int MaxVolume = 100;
|
||||
public const int DefaultVolume = 50;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI layout and timing constants
|
||||
/// </summary>
|
||||
public static class UI
|
||||
{
|
||||
// Window dimensions
|
||||
public const int WindowWidth = 640;
|
||||
public const int MaxWindowHeight = 650;
|
||||
public const int WindowRightMargin = 10;
|
||||
|
||||
// Animation and layout update delays (milliseconds)
|
||||
public const int AnimationDelayMs = 100;
|
||||
public const int LayoutUpdateDelayMs = 50;
|
||||
public const int MonitorDiscoveryDelayMs = 200;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application lifecycle timing constants
|
||||
/// </summary>
|
||||
public static class Lifetime
|
||||
{
|
||||
/// <summary>
|
||||
/// Normal shutdown timeout in milliseconds
|
||||
/// </summary>
|
||||
public const int NormalShutdownTimeoutMs = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Emergency shutdown timeout in milliseconds
|
||||
/// </summary>
|
||||
public const int EmergencyShutdownTimeoutMs = 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to Visibility
|
||||
/// </summary>
|
||||
public partial class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility == Visibility.Visible;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to their inverse
|
||||
/// </summary>
|
||||
public partial class InverseBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return !boolValue;
|
||||
}
|
||||
|
||||
return true; // Default to enabled if value is not bool
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return !boolValue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace PowerDisplay.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts boolean values to Visibility (inverted)
|
||||
/// </summary>
|
||||
public partial class InverseBoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
return boolValue ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Visibility visibility)
|
||||
{
|
||||
return visibility != Visibility.Visible;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PowerDisplay.Core.Models;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor controller interface
|
||||
/// </summary>
|
||||
public interface IMonitorController
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller name
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Supported monitor type
|
||||
/// </summary>
|
||||
MonitorType SupportedType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified monitor can be controlled
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Whether the monitor can be controlled</returns>
|
||||
Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor brightness
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Brightness information</returns>
|
||||
Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor brightness
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="brightness">Brightness value (0-100)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers supported monitors
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>List of monitors</returns>
|
||||
Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Validates monitor connection status
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Whether the monitor is connected</returns>
|
||||
Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor contrast
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Contrast information</returns>
|
||||
Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor contrast
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="contrast">Contrast value (0-100)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor volume
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Volume information</returns>
|
||||
Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor volume
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="volume">Volume value (0-100)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor color temperature
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Color temperature information</returns>
|
||||
Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sets monitor color temperature
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="colorTemperature">Color temperature value (2000-10000K)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets monitor capabilities string (DDC/CI)
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Capabilities string</returns>
|
||||
Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves current settings to monitor
|
||||
/// </summary>
|
||||
/// <param name="monitor">Monitor object</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Operation result</returns>
|
||||
Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Releases resources
|
||||
/// </summary>
|
||||
void Dispose();
|
||||
}
|
||||
|
||||
// IMonitorManager interface removed - YAGNI principle
|
||||
// Only one implementation exists (MonitorManager), so interface abstraction is unnecessary
|
||||
// This simplifies the codebase and eliminates maintenance overhead
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PowerDisplay.Core.Models;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor list changed event arguments
|
||||
/// </summary>
|
||||
public class MonitorListChangedEventArgs : EventArgs
|
||||
{
|
||||
public IReadOnlyList<Monitor> AddedMonitors { get; }
|
||||
|
||||
public IReadOnlyList<Monitor> RemovedMonitors { get; }
|
||||
|
||||
public IReadOnlyList<Monitor> AllMonitors { get; }
|
||||
|
||||
public MonitorListChangedEventArgs(
|
||||
IReadOnlyList<Monitor> addedMonitors,
|
||||
IReadOnlyList<Monitor> removedMonitors,
|
||||
IReadOnlyList<Monitor> allMonitors)
|
||||
{
|
||||
AddedMonitors = addedMonitors;
|
||||
RemovedMonitors = removedMonitors;
|
||||
AllMonitors = allMonitors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using PowerDisplay.Core.Models;
|
||||
|
||||
namespace PowerDisplay.Core.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor status changed event arguments
|
||||
/// </summary>
|
||||
public class MonitorStatusChangedEventArgs : EventArgs
|
||||
{
|
||||
public Monitor Monitor { get; }
|
||||
|
||||
public int? OldBrightness { get; }
|
||||
|
||||
public int NewBrightness { get; }
|
||||
|
||||
public bool? OldAvailability { get; }
|
||||
|
||||
public bool NewAvailability { get; }
|
||||
|
||||
public string Message { get; }
|
||||
|
||||
public ChangeType Type { get; }
|
||||
|
||||
public enum ChangeType
|
||||
{
|
||||
Brightness,
|
||||
Contrast,
|
||||
Volume,
|
||||
ColorTemperature,
|
||||
Availability,
|
||||
General,
|
||||
}
|
||||
|
||||
public MonitorStatusChangedEventArgs(
|
||||
Monitor monitor,
|
||||
int? oldBrightness,
|
||||
int newBrightness,
|
||||
bool? oldAvailability,
|
||||
bool newAvailability)
|
||||
{
|
||||
Monitor = monitor;
|
||||
OldBrightness = oldBrightness;
|
||||
NewBrightness = newBrightness;
|
||||
OldAvailability = oldAvailability;
|
||||
NewAvailability = newAvailability;
|
||||
Message = $"Brightness changed from {oldBrightness} to {newBrightness}";
|
||||
Type = ChangeType.Brightness;
|
||||
}
|
||||
|
||||
public MonitorStatusChangedEventArgs(
|
||||
Monitor monitor,
|
||||
string message,
|
||||
ChangeType changeType)
|
||||
{
|
||||
Monitor = monitor;
|
||||
Message = message;
|
||||
Type = changeType;
|
||||
|
||||
// Set defaults for compatibility
|
||||
OldBrightness = null;
|
||||
NewBrightness = monitor.CurrentBrightness;
|
||||
OldAvailability = null;
|
||||
NewAvailability = monitor.IsAvailable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Brightness information structure
|
||||
/// </summary>
|
||||
public readonly struct BrightnessInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Current brightness value
|
||||
/// </summary>
|
||||
public int Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum brightness value
|
||||
/// </summary>
|
||||
public int Minimum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum brightness value
|
||||
/// </summary>
|
||||
public int Maximum { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the brightness information is valid
|
||||
/// </summary>
|
||||
public bool IsValid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when the brightness information was obtained
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
public BrightnessInfo(int current, int minimum, int maximum)
|
||||
{
|
||||
Current = current;
|
||||
Minimum = minimum;
|
||||
Maximum = maximum;
|
||||
IsValid = current >= minimum && current <= maximum && maximum > minimum;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
public BrightnessInfo(int current, int maximum)
|
||||
: this(current, 0, maximum)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates invalid brightness information
|
||||
/// </summary>
|
||||
public static BrightnessInfo Invalid => new(-1, -1, -1);
|
||||
|
||||
/// <summary>
|
||||
/// Converts brightness value to percentage (0-100)
|
||||
/// </summary>
|
||||
public int ToPercentage()
|
||||
{
|
||||
if (!IsValid || Maximum == Minimum)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)Math.Round((double)(Current - Minimum) * 100 / (Maximum - Minimum));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates brightness value from percentage
|
||||
/// </summary>
|
||||
public int FromPercentage(int percentage)
|
||||
{
|
||||
if (!IsValid)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
percentage = Math.Clamp(percentage, 0, 100);
|
||||
return Minimum + (int)Math.Round((double)(Maximum - Minimum) * percentage / 100);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsValid ? $"{Current}/{Maximum} ({ToPercentage()}%)" : "Invalid";
|
||||
}
|
||||
}
|
||||
}
|
||||
249
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
249
src/modules/powerdisplay/PowerDisplay/Core/Models/Monitor.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using PowerDisplay.Configuration;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor model that implements property change notification
|
||||
/// </summary>
|
||||
public partial class Monitor : INotifyPropertyChanged
|
||||
{
|
||||
private int _currentBrightness;
|
||||
private int _currentColorTemperature = AppConstants.MonitorDefaults.DefaultColorTemp;
|
||||
private bool _isAvailable = true;
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier (based on hardware ID)
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Hardware ID (EDID format like GSM5C6D)
|
||||
/// </summary>
|
||||
public string HardwareId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Display name
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor type
|
||||
/// </summary>
|
||||
public MonitorType Type { get; set; } = MonitorType.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// Current brightness (0-100)
|
||||
/// </summary>
|
||||
public int CurrentBrightness
|
||||
{
|
||||
get => _currentBrightness;
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinBrightness, MaxBrightness);
|
||||
if (_currentBrightness != clamped)
|
||||
{
|
||||
_currentBrightness = clamped;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum brightness value
|
||||
/// </summary>
|
||||
public int MinBrightness { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum brightness value
|
||||
/// </summary>
|
||||
public int MaxBrightness { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Current color temperature (2000-10000K)
|
||||
/// </summary>
|
||||
public int CurrentColorTemperature
|
||||
{
|
||||
get => _currentColorTemperature;
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinColorTemperature, MaxColorTemperature);
|
||||
if (_currentColorTemperature != clamped)
|
||||
{
|
||||
_currentColorTemperature = clamped;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum color temperature value
|
||||
/// </summary>
|
||||
public int MinColorTemperature { get; set; } = AppConstants.MonitorDefaults.MinColorTemp;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum color temperature value
|
||||
/// </summary>
|
||||
public int MaxColorTemperature { get; set; } = AppConstants.MonitorDefaults.MaxColorTemp;
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports color temperature adjustment
|
||||
/// </summary>
|
||||
public bool SupportsColorTemperature { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports contrast adjustment
|
||||
/// </summary>
|
||||
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
|
||||
|
||||
/// <summary>
|
||||
/// Whether supports volume adjustment (for audio-capable monitors)
|
||||
/// </summary>
|
||||
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
|
||||
|
||||
private int _currentContrast = 50;
|
||||
private int _currentVolume = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Current contrast (0-100)
|
||||
/// </summary>
|
||||
public int CurrentContrast
|
||||
{
|
||||
get => _currentContrast;
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinContrast, MaxContrast);
|
||||
if (_currentContrast != clamped)
|
||||
{
|
||||
_currentContrast = clamped;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum contrast value
|
||||
/// </summary>
|
||||
public int MinContrast { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum contrast value
|
||||
/// </summary>
|
||||
public int MaxContrast { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Current volume (0-100)
|
||||
/// </summary>
|
||||
public int CurrentVolume
|
||||
{
|
||||
get => _currentVolume;
|
||||
set
|
||||
{
|
||||
var clamped = Math.Clamp(value, MinVolume, MaxVolume);
|
||||
if (_currentVolume != clamped)
|
||||
{
|
||||
_currentVolume = clamped;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimum volume value
|
||||
/// </summary>
|
||||
public int MinVolume { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum volume value
|
||||
/// </summary>
|
||||
public int MaxVolume { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Whether available/online
|
||||
/// </summary>
|
||||
public bool IsAvailable
|
||||
{
|
||||
get => _isAvailable;
|
||||
set
|
||||
{
|
||||
if (_isAvailable != value)
|
||||
{
|
||||
_isAvailable = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Physical monitor handle (for DDC/CI)
|
||||
/// </summary>
|
||||
public IntPtr Handle { get; set; } = IntPtr.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Device key - unique identifier part of device path (like Twinkle Tray's deviceKey)
|
||||
/// </summary>
|
||||
public string DeviceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Instance name (used by WMI)
|
||||
/// </summary>
|
||||
public string InstanceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Manufacturer information
|
||||
/// </summary>
|
||||
public string Manufacturer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Connection type (HDMI, DP, VGA, etc.)
|
||||
/// </summary>
|
||||
public string ConnectionType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Communication method (DDC/CI, WMI, HDR API, etc.)
|
||||
/// </summary>
|
||||
public string CommunicationMethod { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Supported control methods
|
||||
/// </summary>
|
||||
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
|
||||
|
||||
/// <summary>
|
||||
/// Last update time
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; set; } = DateTime.Now;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} ({Type}) - {CurrentBrightness}%";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor status
|
||||
/// </summary>
|
||||
public void UpdateStatus(int brightness, bool isAvailable = true)
|
||||
{
|
||||
IsAvailable = isAvailable;
|
||||
if (isAvailable)
|
||||
{
|
||||
CurrentBrightness = brightness;
|
||||
LastUpdate = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor control capabilities flags
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MonitorCapabilities
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Supports brightness control
|
||||
/// </summary>
|
||||
Brightness = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Supports contrast control
|
||||
/// </summary>
|
||||
Contrast = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Supports DDC/CI protocol
|
||||
/// </summary>
|
||||
DdcCi = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Supports WMI control
|
||||
/// </summary>
|
||||
Wmi = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// Supports HDR
|
||||
/// </summary>
|
||||
Hdr = 1 << 4,
|
||||
|
||||
/// <summary>
|
||||
/// Supports high-level monitor API
|
||||
/// </summary>
|
||||
HighLevel = 1 << 5,
|
||||
|
||||
/// <summary>
|
||||
/// Supports volume control
|
||||
/// </summary>
|
||||
Volume = 1 << 6,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor operation result
|
||||
/// </summary>
|
||||
public readonly struct MonitorOperationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the operation was successful
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message
|
||||
/// </summary>
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// System error code
|
||||
/// </summary>
|
||||
public int? ErrorCode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Operation timestamp
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
private MonitorOperationResult(bool isSuccess, string? errorMessage = null, int? errorCode = null)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
ErrorMessage = errorMessage;
|
||||
ErrorCode = errorCode;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a successful result
|
||||
/// </summary>
|
||||
public static MonitorOperationResult Success() => new(true);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a failed result
|
||||
/// </summary>
|
||||
public static MonitorOperationResult Failure(string errorMessage, int? errorCode = null)
|
||||
=> new(false, errorMessage, errorCode);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return IsSuccess ? "Success" : $"Failed: {ErrorMessage}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerDisplay.Core.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor type enumeration
|
||||
/// </summary>
|
||||
public enum MonitorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Unknown type
|
||||
/// </summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>
|
||||
/// Internal display (laptop screen, controlled via WMI)
|
||||
/// </summary>
|
||||
Internal,
|
||||
|
||||
/// <summary>
|
||||
/// External display (controlled via DDC/CI)
|
||||
/// </summary>
|
||||
External,
|
||||
|
||||
/// <summary>
|
||||
/// HDR display (controlled via Display Config API)
|
||||
/// </summary>
|
||||
HDR,
|
||||
}
|
||||
}
|
||||
455
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
455
src/modules/powerdisplay/PowerDisplay/Core/MonitorManager.cs
Normal file
@@ -0,0 +1,455 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Core.Utils;
|
||||
using PowerDisplay.Native.DDC;
|
||||
using PowerDisplay.Native.WMI;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor manager for unified control of all monitors
|
||||
/// No interface abstraction - KISS principle (only one implementation needed)
|
||||
/// </summary>
|
||||
public partial class MonitorManager : IDisposable
|
||||
{
|
||||
private readonly List<Monitor> _monitors = new();
|
||||
private readonly List<IMonitorController> _controllers = new();
|
||||
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
|
||||
private bool _disposed;
|
||||
|
||||
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
|
||||
|
||||
public event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
|
||||
|
||||
public MonitorManager()
|
||||
{
|
||||
// Initialize controllers
|
||||
InitializeControllers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize controllers
|
||||
/// </summary>
|
||||
private void InitializeControllers()
|
||||
{
|
||||
try
|
||||
{
|
||||
// DDC/CI controller (external monitors)
|
||||
_controllers.Add(new DdcCiController());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// WMI controller (internal monitors)
|
||||
// First check if WMI is available
|
||||
if (WmiController.IsWmiAvailable())
|
||||
{
|
||||
_controllers.Add(new WmiController());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("WMI brightness control not available on this system");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover all monitors
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _discoveryLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
var oldMonitors = _monitors.ToList();
|
||||
var newMonitors = new List<Monitor>();
|
||||
|
||||
// Discover monitors supported by all controllers in parallel
|
||||
var discoveryTasks = _controllers.Select(async controller =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
|
||||
return (Controller: controller, Monitors: monitors.ToList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If a controller fails, log the error and return empty list
|
||||
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
|
||||
return (Controller: controller, Monitors: new List<Monitor>());
|
||||
}
|
||||
});
|
||||
|
||||
var results = await Task.WhenAll(discoveryTasks);
|
||||
|
||||
// Collect all discovered monitors
|
||||
foreach (var (controller, monitors) in results)
|
||||
{
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
// Verify if monitor can be controlled
|
||||
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
|
||||
{
|
||||
// Get current brightness
|
||||
try
|
||||
{
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
|
||||
monitor.MinBrightness = brightnessInfo.Minimum;
|
||||
monitor.MaxBrightness = brightnessInfo.Maximum;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If unable to get brightness, use default values
|
||||
Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}");
|
||||
}
|
||||
|
||||
newMonitors.Add(monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update monitor list
|
||||
_monitors.Clear();
|
||||
_monitors.AddRange(newMonitors);
|
||||
|
||||
// Trigger change events
|
||||
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
|
||||
var removedMonitors = oldMonitors.Where(o => !newMonitors.Any(m => m.Id == o.Id)).ToList();
|
||||
|
||||
if (addedMonitors.Count > 0 || removedMonitors.Count > 0)
|
||||
{
|
||||
MonitorsChanged?.Invoke(this, new MonitorListChangedEventArgs(
|
||||
addedMonitors.AsReadOnly(),
|
||||
removedMonitors.AsReadOnly(),
|
||||
_monitors.AsReadOnly()));
|
||||
}
|
||||
|
||||
return _monitors.AsReadOnly();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_discoveryLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get brightness of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
|
||||
|
||||
// Update cached brightness value
|
||||
if (brightnessInfo.IsValid)
|
||||
{
|
||||
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
|
||||
}
|
||||
|
||||
return brightnessInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Mark monitor as unavailable
|
||||
Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}");
|
||||
monitor.IsAvailable = false;
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set brightness of the specified monitor
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Logger.LogDebug($"[MonitorManager] SetBrightnessAsync called for {monitorId}, brightness={brightness}");
|
||||
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
Logger.LogDebug($"[MonitorManager] Monitor found: {monitor.Id}, Type={monitor.Type}, Handle=0x{monitor.Handle:X}, DeviceKey={monitor.DeviceKey}");
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}");
|
||||
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||
}
|
||||
|
||||
Logger.LogDebug($"[MonitorManager] Controller found: {controller.GetType().Name}");
|
||||
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"[MonitorManager] Calling controller.SetBrightnessAsync for {monitor.Id}");
|
||||
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
|
||||
Logger.LogDebug($"[MonitorManager] controller.SetBrightnessAsync returned: IsSuccess={result.IsSuccess}, ErrorMessage={result.ErrorMessage}");
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
// Update monitor status
|
||||
monitor.UpdateStatus(brightness, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If setting fails, monitor may be unavailable
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set brightness of all monitors
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = _monitors
|
||||
.Where(m => m.IsAvailable)
|
||||
.Select(async monitor =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
|
||||
return await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set contrast of the specified monitor
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
|
||||
=> ExecuteMonitorOperationAsync(
|
||||
monitorId,
|
||||
contrast,
|
||||
(ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct),
|
||||
(mon, val) => mon.CurrentContrast = val,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Set volume of the specified monitor
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
|
||||
=> ExecuteMonitorOperationAsync(
|
||||
monitorId,
|
||||
volume,
|
||||
(ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct),
|
||||
(mon, val) => mon.CurrentVolume = val,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor color temperature
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor color temperature
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
=> ExecuteMonitorOperationAsync(
|
||||
monitorId,
|
||||
colorTemperature,
|
||||
(ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct),
|
||||
(mon, val) => mon.CurrentColorTemperature = val,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Initialize color temperature for a monitor (async operation)
|
||||
/// </summary>
|
||||
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
|
||||
if (tempInfo.IsValid)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor != null)
|
||||
{
|
||||
// Convert VCP value to approximate Kelvin temperature
|
||||
// This is a rough mapping - actual values depend on monitor implementation
|
||||
var kelvin = ConvertVcpValueToKelvin(tempInfo.Current, tempInfo.Maximum);
|
||||
monitor.CurrentColorTemperature = kelvin;
|
||||
|
||||
Logger.LogInfo($"Initialized color temperature for {monitorId}: {kelvin}K (VCP: {tempInfo.Current}/{tempInfo.Maximum})");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert VCP value to approximate Kelvin temperature (uses unified converter)
|
||||
/// </summary>
|
||||
private static int ConvertVcpValueToKelvin(int vcpValue, int maxVcpValue)
|
||||
{
|
||||
return ColorTemperatureConverter.VcpToKelvin(vcpValue, maxVcpValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor by ID
|
||||
/// </summary>
|
||||
public Monitor? GetMonitor(string monitorId)
|
||||
{
|
||||
return _monitors.FirstOrDefault(m => m.Id == monitorId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get controller for the monitor
|
||||
/// </summary>
|
||||
private IMonitorController? GetControllerForMonitor(Monitor monitor)
|
||||
{
|
||||
return _controllers.FirstOrDefault(c => c.SupportedType == monitor.Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic helper to execute monitor operations with common error handling.
|
||||
/// Eliminates code duplication across Set* methods.
|
||||
/// </summary>
|
||||
private async Task<MonitorOperationResult> ExecuteMonitorOperationAsync<T>(
|
||||
string monitorId,
|
||||
T value,
|
||||
Func<IMonitorController, Monitor, T, CancellationToken, Task<MonitorOperationResult>> operation,
|
||||
Action<Monitor, T> onSuccess,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var monitor = GetMonitor(monitorId);
|
||||
if (monitor == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
|
||||
return MonitorOperationResult.Failure("Monitor not found");
|
||||
}
|
||||
|
||||
var controller = GetControllerForMonitor(monitor);
|
||||
if (controller == null)
|
||||
{
|
||||
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}, Type={monitor.Type}");
|
||||
return MonitorOperationResult.Failure("No controller available for this monitor");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await operation(controller, monitor, value, cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
onSuccess(monitor, value);
|
||||
monitor.LastUpdate = DateTime.Now;
|
||||
}
|
||||
else
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
monitor.IsAvailable = false;
|
||||
Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}");
|
||||
return MonitorOperationResult.Failure($"Exception: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_discoveryLock?.Dispose();
|
||||
|
||||
// Release all controllers
|
||||
foreach (var controller in _controllers)
|
||||
{
|
||||
controller?.Dispose();
|
||||
}
|
||||
|
||||
_controllers.Clear();
|
||||
_monitors.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Core.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility class for converting between Kelvin color temperature and VCP values.
|
||||
/// Centralizes temperature conversion logic to eliminate code duplication (KISS principle).
|
||||
/// </summary>
|
||||
public static class ColorTemperatureConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum color temperature in Kelvin (warm)
|
||||
/// </summary>
|
||||
public const int MinKelvin = 2000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum color temperature in Kelvin (cool)
|
||||
/// </summary>
|
||||
public const int MaxKelvin = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Convert VCP value to Kelvin temperature
|
||||
/// </summary>
|
||||
/// <param name="vcpValue">Current VCP value</param>
|
||||
/// <param name="vcpMax">Maximum VCP value</param>
|
||||
/// <returns>Temperature in Kelvin (2000-10000K)</returns>
|
||||
public static int VcpToKelvin(int vcpValue, int vcpMax)
|
||||
{
|
||||
if (vcpMax <= 0)
|
||||
{
|
||||
return (MinKelvin + MaxKelvin) / 2; // Default to neutral 6000K
|
||||
}
|
||||
|
||||
// Normalize VCP value to 0-1 range
|
||||
double normalized = Math.Clamp((double)vcpValue / vcpMax, 0.0, 1.0);
|
||||
|
||||
// Map to Kelvin range
|
||||
int kelvin = (int)(MinKelvin + (normalized * (MaxKelvin - MinKelvin)));
|
||||
|
||||
return Math.Clamp(kelvin, MinKelvin, MaxKelvin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Kelvin temperature to VCP value
|
||||
/// </summary>
|
||||
/// <param name="kelvin">Temperature in Kelvin (2000-10000K)</param>
|
||||
/// <param name="vcpMax">Maximum VCP value</param>
|
||||
/// <returns>VCP value (0 to vcpMax)</returns>
|
||||
public static int KelvinToVcp(int kelvin, int vcpMax)
|
||||
{
|
||||
// Clamp input to valid range
|
||||
kelvin = Math.Clamp(kelvin, MinKelvin, MaxKelvin);
|
||||
|
||||
// Normalize kelvin to 0-1 range
|
||||
double normalized = (double)(kelvin - MinKelvin) / (MaxKelvin - MinKelvin);
|
||||
|
||||
// Map to VCP range
|
||||
int vcpValue = (int)(normalized * vcpMax);
|
||||
|
||||
return Math.Clamp(vcpValue, 0, vcpMax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a temperature value is in valid Kelvin range
|
||||
/// </summary>
|
||||
public static bool IsValidKelvin(int kelvin)
|
||||
{
|
||||
return kelvin >= MinKelvin && kelvin <= MaxKelvin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a human-readable description of color temperature
|
||||
/// </summary>
|
||||
public static string GetTemperatureDescription(int kelvin)
|
||||
{
|
||||
return kelvin switch
|
||||
{
|
||||
< 3500 => "Warm",
|
||||
< 5500 => "Neutral",
|
||||
< 7500 => "Cool",
|
||||
_ => "Very Cool",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs
Normal file
9
src/modules/powerdisplay/PowerDisplay/GlobalUsings.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// Enable compile-time marshalling for all P/Invoke declarations
|
||||
// This allows LibraryImport to handle array marshalling and achieve 100% coverage
|
||||
[assembly: DisableRuntimeMarshalling]
|
||||
@@ -0,0 +1,270 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Configuration;
|
||||
using PowerDisplay.Serialization;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages monitor parameter state in a separate file from main settings.
|
||||
/// This avoids FileSystemWatcher feedback loops by separating read-only config (settings.json)
|
||||
/// from frequently-updated state (monitor_state.json).
|
||||
/// Simplified to use direct save strategy for reliability and simplicity (KISS principle).
|
||||
/// </summary>
|
||||
public partial class MonitorStateManager : IDisposable
|
||||
{
|
||||
private readonly string _stateFilePath;
|
||||
private readonly Dictionary<string, MonitorState> _states = new();
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor state data (internal tracking, not serialized)
|
||||
/// </summary>
|
||||
private sealed class MonitorState
|
||||
{
|
||||
public int Brightness { get; set; }
|
||||
|
||||
public int ColorTemperature { get; set; }
|
||||
|
||||
public int Contrast { get; set; }
|
||||
|
||||
public int Volume { get; set; }
|
||||
}
|
||||
|
||||
public MonitorStateManager()
|
||||
{
|
||||
// Store state file in same location as settings.json but with different name
|
||||
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
|
||||
|
||||
if (!Directory.Exists(powerToysPath))
|
||||
{
|
||||
Directory.CreateDirectory(powerToysPath);
|
||||
}
|
||||
|
||||
_stateFilePath = Path.Combine(powerToysPath, AppConstants.State.StateFileName);
|
||||
|
||||
// Load existing state if available
|
||||
LoadStateFromDisk();
|
||||
|
||||
Logger.LogInfo($"MonitorStateManager initialized with direct-save strategy, state file: {_stateFilePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update monitor parameter and save immediately to disk.
|
||||
/// Uses HardwareId as the stable key.
|
||||
/// Direct-save strategy ensures no data loss and simplifies code (KISS principle).
|
||||
/// </summary>
|
||||
public void UpdateMonitorParameter(string hardwareId, string property, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
Logger.LogWarning($"Cannot update monitor parameter: HardwareId is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Get or create state entry using HardwareId
|
||||
if (!_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
state = new MonitorState();
|
||||
_states[hardwareId] = state;
|
||||
}
|
||||
|
||||
// Update the specific property
|
||||
switch (property)
|
||||
{
|
||||
case "Brightness":
|
||||
state.Brightness = value;
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
state.ColorTemperature = value;
|
||||
break;
|
||||
case "Contrast":
|
||||
state.Contrast = value;
|
||||
break;
|
||||
case "Volume":
|
||||
state.Volume = value;
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unknown property: {property}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save immediately after update - simple and reliable!
|
||||
SaveStateToDisk();
|
||||
|
||||
Logger.LogTrace($"[State] Updated and saved {property}={value} for monitor HardwareId='{hardwareId}'");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get saved parameters for a monitor using HardwareId
|
||||
/// </summary>
|
||||
public (int Brightness, int ColorTemperature, int Contrast, int Volume)? GetMonitorParameters(string hardwareId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_states.TryGetValue(hardwareId, out var state))
|
||||
{
|
||||
return (state.Brightness, state.ColorTemperature, state.Contrast, state.Volume);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if state exists for a monitor (by HardwareId)
|
||||
/// </summary>
|
||||
public bool HasMonitorState(string hardwareId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _states.ContainsKey(hardwareId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load state from disk
|
||||
/// </summary>
|
||||
private void LoadStateFromDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_stateFilePath))
|
||||
{
|
||||
Logger.LogInfo("[State] No existing state file found, starting fresh");
|
||||
return;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(_stateFilePath);
|
||||
var stateFile = JsonSerializer.Deserialize(json, AppJsonContext.Default.MonitorStateFile);
|
||||
|
||||
if (stateFile?.Monitors != null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var kvp in stateFile.Monitors)
|
||||
{
|
||||
var monitorKey = kvp.Key; // Should be HardwareId (e.g., "GSM5C6D")
|
||||
var entry = kvp.Value;
|
||||
|
||||
_states[monitorKey] = new MonitorState
|
||||
{
|
||||
Brightness = entry.Brightness,
|
||||
ColorTemperature = entry.ColorTemperature,
|
||||
Contrast = entry.Contrast,
|
||||
Volume = entry.Volume,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[State] Loaded state for {stateFile.Monitors.Count} monitors from {_stateFilePath}");
|
||||
Logger.LogInfo($"[State] Monitor keys in state file: {string.Join(", ", stateFile.Monitors.Keys)}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load monitor state: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current state to disk immediately.
|
||||
/// Simplified direct-save approach - no timer, no dirty flags, just save!
|
||||
/// </summary>
|
||||
private void SaveStateToDisk()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Build state file
|
||||
var stateFile = new MonitorStateFile
|
||||
{
|
||||
LastUpdated = DateTime.Now,
|
||||
};
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var kvp in _states)
|
||||
{
|
||||
var monitorId = kvp.Key;
|
||||
var state = kvp.Value;
|
||||
|
||||
stateFile.Monitors[monitorId] = new MonitorStateEntry
|
||||
{
|
||||
Brightness = state.Brightness,
|
||||
ColorTemperature = state.ColorTemperature,
|
||||
Contrast = state.Contrast,
|
||||
Volume = state.Volume,
|
||||
LastUpdated = now,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Write to disk
|
||||
var json = JsonSerializer.Serialize(stateFile, AppJsonContext.Default.MonitorStateFile);
|
||||
File.WriteAllText(_stateFilePath, json);
|
||||
|
||||
Logger.LogDebug($"[State] Saved state for {stateFile.Monitors.Count} monitors");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save monitor state: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
// State is already saved with each update, no need for final flush!
|
||||
Logger.LogInfo("MonitorStateManager disposed");
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
public static class ResourceLoaderInstance
|
||||
{
|
||||
public static ResourceLoader ResourceLoader { get; private set; }
|
||||
|
||||
static ResourceLoaderInstance()
|
||||
{
|
||||
ResourceLoader = new ResourceLoader("PowerToys.PowerDisplay.pri");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to open PowerToys Settings application.
|
||||
/// Simplified version for PowerDisplay module (AOT compatible).
|
||||
/// </summary>
|
||||
internal static class SettingsDeepLink
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens PowerToys Settings to PowerDisplay page
|
||||
/// </summary>
|
||||
public static void OpenPowerDisplaySettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// PowerDisplay is a WinUI3 app, PowerToys.exe is in parent directory
|
||||
var directoryPath = Path.Combine(AppContext.BaseDirectory, "..", "PowerToys.exe");
|
||||
|
||||
var startInfo = new ProcessStartInfo(directoryPath)
|
||||
{
|
||||
Arguments = "--open-settings=PowerDisplay",
|
||||
UseShellExecute = true,
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
Logger.LogInfo("Opened PowerToys Settings to PowerDisplay page");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to open PowerToys Settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/modules/powerdisplay/PowerDisplay/Helpers/SimpleDebouncer.cs
Normal file
106
src/modules/powerdisplay/PowerDisplay/Helpers/SimpleDebouncer.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple debouncer that delays execution of an action until a quiet period.
|
||||
/// Replaces the complex PropertyUpdateQueue with a much simpler approach (KISS principle).
|
||||
/// </summary>
|
||||
public partial class SimpleDebouncer : IDisposable
|
||||
{
|
||||
private readonly int _delayMs;
|
||||
private readonly object _lock = new object();
|
||||
private CancellationTokenSource? _cts;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SimpleDebouncer"/> class.
|
||||
/// Create a debouncer with specified delay
|
||||
/// </summary>
|
||||
/// <param name="delayMs">Delay in milliseconds before executing action</param>
|
||||
public SimpleDebouncer(int delayMs = 300)
|
||||
{
|
||||
_delayMs = delayMs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debounce an async action. Cancels previous invocation if still pending.
|
||||
/// </summary>
|
||||
/// <param name="action">Async action to execute after delay</param>
|
||||
public async void Debounce(Func<Task> action)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationTokenSource cts;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Cancel previous invocation
|
||||
_cts?.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
cts = _cts;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Wait for quiet period
|
||||
await Task.Delay(_delayMs, cts.Token);
|
||||
|
||||
// Execute action if not cancelled
|
||||
if (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await action();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Expected when debouncing - a newer call cancelled this one
|
||||
Logger.LogTrace("Debounced action cancelled (replaced by newer call)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Debounced action failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Debounce a synchronous action
|
||||
/// </summary>
|
||||
public void Debounce(Action action)
|
||||
{
|
||||
Debounce(() =>
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_disposed = true;
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
484
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconHelper.cs
Normal file
484
src/modules/powerdisplay/PowerDisplay/Helpers/TrayIconHelper.cs
Normal file
@@ -0,0 +1,484 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using PowerDisplay.Native;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// System tray icon helper class
|
||||
/// </summary>
|
||||
public partial class TrayIconHelper : IDisposable
|
||||
{
|
||||
private const uint NifMessage = 0x00000001;
|
||||
private const uint NifIcon = 0x00000002;
|
||||
private const uint NifTip = 0x00000004;
|
||||
private const uint NifInfo = 0x00000010;
|
||||
|
||||
private const uint NimAdd = 0x00000000;
|
||||
private const uint NimModify = 0x00000001;
|
||||
private const uint NimDelete = 0x00000002;
|
||||
|
||||
private const uint WmUser = 0x0400;
|
||||
private const uint WmTrayicon = WmUser + 1;
|
||||
private const uint WmLbuttonup = 0x0202;
|
||||
private const uint WmRbuttonup = 0x0205;
|
||||
private const uint WmCommand = 0x0111;
|
||||
|
||||
private uint _wmTaskbarCreated; // TaskbarCreated message ID
|
||||
|
||||
// Menu item IDs
|
||||
private const int IdShow = 1001;
|
||||
private const int IdExit = 1002;
|
||||
private const int IdRefresh = 1003;
|
||||
private const int IdSettings = 1004;
|
||||
|
||||
private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
private const uint MfString = 0x00000000;
|
||||
private const uint MfSeparator = 0x00000800;
|
||||
private const uint TpmLeftalign = 0x0000;
|
||||
private const uint TpmReturncmd = 0x0100;
|
||||
|
||||
private const int SwHide = 0;
|
||||
private const int SwShow = 5;
|
||||
|
||||
private IntPtr _messageWindowHandle;
|
||||
private NOTIFYICONDATA _notifyIconData;
|
||||
private bool _isDisposed;
|
||||
private WndProc _wndProc;
|
||||
private Window _mainWindow;
|
||||
private Action? _onShowWindow;
|
||||
private Action? _onExitApplication;
|
||||
private Action? _onRefresh;
|
||||
private Action? _onSettings;
|
||||
private bool _isWindowVisible = true;
|
||||
private System.Drawing.Icon? _trayIcon; // Keep icon reference to prevent garbage collection
|
||||
|
||||
public TrayIconHelper(Window mainWindow)
|
||||
{
|
||||
_mainWindow = mainWindow;
|
||||
_wndProc = WindowProc;
|
||||
|
||||
// Register TaskbarCreated message
|
||||
_wmTaskbarCreated = RegisterWindowMessage("TaskbarCreated");
|
||||
Logger.LogInfo($"Registered TaskbarCreated message: {_wmTaskbarCreated}");
|
||||
|
||||
if (!CreateMessageWindow())
|
||||
{
|
||||
Logger.LogError("Failed to create message window");
|
||||
return;
|
||||
}
|
||||
|
||||
CreateTrayIcon();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set callback functions
|
||||
/// </summary>
|
||||
public void SetCallbacks(Action onShow, Action onExit, Action? onRefresh = null, Action? onSettings = null)
|
||||
{
|
||||
_onShowWindow = onShow;
|
||||
_onExitApplication = onExit;
|
||||
_onRefresh = onRefresh;
|
||||
_onSettings = onSettings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create message window - using system predefined Message window class
|
||||
/// </summary>
|
||||
private bool CreateMessageWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Creating message window using system Message class...");
|
||||
|
||||
// Use system predefined "Message" window class, no registration needed
|
||||
// HWND_MESSAGE (-3) creates pure message window, no hInstance needed
|
||||
_messageWindowHandle = CreateWindowEx(
|
||||
0, // dwExStyle
|
||||
"Message", // lpClassName - system predefined message window class
|
||||
string.Empty, // lpWindowName
|
||||
0, // dwStyle
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0, // x, y, width, height
|
||||
new IntPtr(-3), // hWndParent = HWND_MESSAGE (pure message window)
|
||||
IntPtr.Zero, // hMenu
|
||||
IntPtr.Zero, // hInstance - not needed
|
||||
IntPtr.Zero); // lpParam
|
||||
|
||||
if (_messageWindowHandle == IntPtr.Zero)
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
Logger.LogError($"CreateWindowEx failed with error: {error}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"Message window created successfully: {_messageWindowHandle}");
|
||||
|
||||
// Set window procedure to handle our messages
|
||||
SetWindowLongPtr(_messageWindowHandle, -4, Marshal.GetFunctionPointerForDelegate(_wndProc));
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"CreateMessageWindow exception: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create tray icon
|
||||
/// </summary>
|
||||
private unsafe void CreateTrayIcon()
|
||||
{
|
||||
if (_messageWindowHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Cannot create tray icon: invalid message window handle");
|
||||
return;
|
||||
}
|
||||
|
||||
// First try to delete any existing old icon (if any)
|
||||
var tempData = new NOTIFYICONDATA
|
||||
{
|
||||
CbSize = (uint)sizeof(NOTIFYICONDATA),
|
||||
HWnd = _messageWindowHandle,
|
||||
UID = 1,
|
||||
};
|
||||
Shell_NotifyIcon(NimDelete, ref tempData);
|
||||
|
||||
// Get icon handle
|
||||
var iconHandle = GetDefaultIcon();
|
||||
if (iconHandle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogError("Cannot create tray icon: invalid icon handle");
|
||||
return;
|
||||
}
|
||||
|
||||
_notifyIconData = new NOTIFYICONDATA
|
||||
{
|
||||
CbSize = (uint)sizeof(NOTIFYICONDATA),
|
||||
HWnd = _messageWindowHandle,
|
||||
UID = 1,
|
||||
UFlags = NifMessage | NifIcon | NifTip,
|
||||
UCallbackMessage = WmTrayicon,
|
||||
HIcon = iconHandle,
|
||||
};
|
||||
_notifyIconData.SetTip("Power Display");
|
||||
|
||||
// Retry mechanism: try up to 3 times to create tray icon
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 500;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++)
|
||||
{
|
||||
Logger.LogDebug($"Creating tray icon (attempt {attempt}/{maxRetries})...");
|
||||
|
||||
bool result = Shell_NotifyIcon(NimAdd, ref _notifyIconData);
|
||||
if (result)
|
||||
{
|
||||
Logger.LogInfo($"Tray icon created successfully on attempt {attempt}");
|
||||
return;
|
||||
}
|
||||
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
Logger.LogWarning($"Failed to create tray icon on attempt {attempt}. Error: {lastError}");
|
||||
|
||||
// Analyze specific error and provide suggestions
|
||||
switch (lastError)
|
||||
{
|
||||
case 0: // ERROR_SUCCESS - may be false success
|
||||
Logger.LogWarning("Shell_NotifyIcon returned false but GetLastWin32Error is 0");
|
||||
break;
|
||||
case 1400: // ERROR_INVALID_WINDOW_HANDLE
|
||||
Logger.LogWarning("Invalid window handle - message window may not be properly created");
|
||||
break;
|
||||
case 1418: // ERROR_THREAD_1_INACTIVE
|
||||
Logger.LogWarning("Thread inactive - may need to wait for Explorer to be ready");
|
||||
break;
|
||||
case 1414: // ERROR_INVALID_ICON_HANDLE
|
||||
Logger.LogWarning("Invalid icon handle - icon may have been garbage collected");
|
||||
break;
|
||||
default:
|
||||
Logger.LogWarning($"Unexpected error code: {lastError}");
|
||||
break;
|
||||
}
|
||||
|
||||
// If not the last attempt, wait and retry
|
||||
if (attempt < maxRetries)
|
||||
{
|
||||
Logger.LogDebug($"Retrying in {retryDelayMs}ms...");
|
||||
System.Threading.Thread.Sleep(retryDelayMs);
|
||||
|
||||
// Re-get icon handle to prevent handle invalidation
|
||||
iconHandle = GetDefaultIcon();
|
||||
_notifyIconData.HIcon = iconHandle;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogError($"Failed to create tray icon after {maxRetries} attempts");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get default icon
|
||||
/// </summary>
|
||||
private IntPtr GetDefaultIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
// First release previous icon
|
||||
_trayIcon?.Dispose();
|
||||
_trayIcon = null;
|
||||
|
||||
// Try to load icon from Assets folder in exe directory
|
||||
var exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName;
|
||||
if (!string.IsNullOrEmpty(exePath))
|
||||
{
|
||||
var exeDir = System.IO.Path.GetDirectoryName(exePath);
|
||||
if (!string.IsNullOrEmpty(exeDir))
|
||||
{
|
||||
var iconPath = System.IO.Path.Combine(exeDir, "Assets", "PowerDisplay.ico");
|
||||
|
||||
Logger.LogDebug($"Attempting to load icon from: {iconPath}");
|
||||
|
||||
if (System.IO.File.Exists(iconPath))
|
||||
{
|
||||
// Create icon and save as class member to prevent garbage collection
|
||||
_trayIcon = new System.Drawing.Icon(iconPath);
|
||||
Logger.LogInfo($"Successfully loaded custom icon from {iconPath}");
|
||||
return _trayIcon.Handle;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Icon file not found at: {iconPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load PowerDisplay icon: {ex.Message}");
|
||||
_trayIcon?.Dispose();
|
||||
_trayIcon = null;
|
||||
}
|
||||
|
||||
// If loading fails, use system default icon
|
||||
var systemIconHandle = LoadIcon(IntPtr.Zero, new IntPtr(32512)); // IDI_APPLICATION
|
||||
Logger.LogInfo($"Using system default icon: {systemIconHandle}");
|
||||
return systemIconHandle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Window message processing
|
||||
/// </summary>
|
||||
private IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (msg == _wmTaskbarCreated)
|
||||
{
|
||||
// Explorer restarted, need to recreate tray icon
|
||||
Logger.LogInfo("TaskbarCreated message received - recreating tray icon");
|
||||
CreateTrayIcon();
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case WmTrayicon:
|
||||
HandleTrayIconMessage(lParam);
|
||||
break;
|
||||
case WmCommand:
|
||||
HandleMenuCommand(wParam);
|
||||
break;
|
||||
}
|
||||
|
||||
return DefWindowProc(hWnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle tray icon messages
|
||||
/// </summary>
|
||||
private void HandleTrayIconMessage(IntPtr lParam)
|
||||
{
|
||||
switch ((uint)lParam)
|
||||
{
|
||||
case WmLbuttonup:
|
||||
// Left click - show/hide window
|
||||
ToggleWindowVisibility();
|
||||
break;
|
||||
case WmRbuttonup:
|
||||
// Right click - show menu
|
||||
ShowContextMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle window visibility state
|
||||
/// </summary>
|
||||
private void ToggleWindowVisibility()
|
||||
{
|
||||
_isWindowVisible = !_isWindowVisible;
|
||||
if (_isWindowVisible)
|
||||
{
|
||||
_onShowWindow?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hide window logic will be implemented in MainWindow
|
||||
if (_mainWindow != null)
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow);
|
||||
ShowWindow(hWnd, SwHide);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show right-click menu
|
||||
/// </summary>
|
||||
private void ShowContextMenu()
|
||||
{
|
||||
var hMenu = CreatePopupMenu();
|
||||
|
||||
AppendMenu(hMenu, MfString, IdShow, _isWindowVisible ? "Hide Window" : "Show Window");
|
||||
if (_onRefresh != null)
|
||||
{
|
||||
AppendMenu(hMenu, MfString, IdRefresh, "Refresh Monitors");
|
||||
}
|
||||
|
||||
if (_onSettings != null)
|
||||
{
|
||||
AppendMenu(hMenu, MfString, IdSettings, "Settings");
|
||||
}
|
||||
|
||||
AppendMenu(hMenu, MfSeparator, 0, string.Empty);
|
||||
AppendMenu(hMenu, MfString, IdExit, "Exit");
|
||||
|
||||
GetCursorPos(out POINT pt);
|
||||
SetForegroundWindow(_messageWindowHandle);
|
||||
|
||||
var cmd = TrackPopupMenu(hMenu, TpmLeftalign | TpmReturncmd, pt.X, pt.Y, 0, _messageWindowHandle, IntPtr.Zero);
|
||||
|
||||
if (cmd != 0)
|
||||
{
|
||||
HandleMenuCommand(new IntPtr(cmd));
|
||||
}
|
||||
|
||||
DestroyMenu(hMenu);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle menu commands
|
||||
/// </summary>
|
||||
private void HandleMenuCommand(IntPtr commandId)
|
||||
{
|
||||
switch (commandId.ToInt32())
|
||||
{
|
||||
case IdShow:
|
||||
ToggleWindowVisibility();
|
||||
break;
|
||||
case IdRefresh:
|
||||
_onRefresh?.Invoke();
|
||||
break;
|
||||
case IdSettings:
|
||||
_onSettings?.Invoke();
|
||||
break;
|
||||
case IdExit:
|
||||
_onExitApplication?.Invoke();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show balloon tip
|
||||
/// </summary>
|
||||
public void ShowBalloonTip(string title, string text, uint timeout = 3000)
|
||||
{
|
||||
_notifyIconData.UFlags |= NifInfo;
|
||||
_notifyIconData.SetInfoTitle(title);
|
||||
_notifyIconData.SetInfo(text);
|
||||
_notifyIconData.UTimeout = timeout;
|
||||
_notifyIconData.DwInfoFlags = 1; // NIIF_INFO
|
||||
|
||||
Shell_NotifyIcon(NimModify, ref _notifyIconData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update tray icon tooltip text
|
||||
/// </summary>
|
||||
public void UpdateTooltip(string tooltip)
|
||||
{
|
||||
_notifyIconData.SetTip(tooltip);
|
||||
Shell_NotifyIcon(NimModify, ref _notifyIconData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreate tray icon (for failure recovery)
|
||||
/// </summary>
|
||||
public void RecreateTrayIcon()
|
||||
{
|
||||
Logger.LogInfo("Manually recreating tray icon...");
|
||||
CreateTrayIcon();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
Logger.LogDebug("Disposing TrayIconHelper...");
|
||||
|
||||
// Remove tray icon
|
||||
try
|
||||
{
|
||||
Shell_NotifyIcon(NimDelete, ref _notifyIconData);
|
||||
Logger.LogInfo("Tray icon removed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error removing tray icon: {ex.Message}");
|
||||
}
|
||||
|
||||
// Release icon resources
|
||||
try
|
||||
{
|
||||
_trayIcon?.Dispose();
|
||||
_trayIcon = null;
|
||||
Logger.LogInfo("Icon resources disposed successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error disposing icon: {ex.Message}");
|
||||
}
|
||||
|
||||
// Destroy message window
|
||||
try
|
||||
{
|
||||
if (_messageWindowHandle != IntPtr.Zero)
|
||||
{
|
||||
DestroyWindow(_messageWindowHandle);
|
||||
_messageWindowHandle = IntPtr.Zero;
|
||||
Logger.LogInfo("Message window destroyed successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error destroying message window: {ex.Message}");
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
Logger.LogDebug("TrayIconHelper disposed completely");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace PowerDisplay.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// This class ensures types used in XAML are preserved during AOT compilation.
|
||||
/// Framework types cannot have attributes added directly to their definitions since they're external types.
|
||||
/// Use DynamicDependency to preserve all members of these WinUI3 framework types.
|
||||
/// </summary>
|
||||
internal static class TypePreservation
|
||||
{
|
||||
// Core WinUI3 Controls used in XAML
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Window))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Application))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Grid))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Border))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ScrollViewer))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.StackPanel))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ItemsControl))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Slider))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.TextBlock))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.Button))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIcon))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ProgressRing))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.InfoBar))]
|
||||
|
||||
// Animation and Transform types
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.Storyboard))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.DoubleAnimation))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.CubicEase))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.TranslateTransform))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.TransitionCollection))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.EntranceThemeTransition))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.RepositionThemeTransition))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Animation.EasingFunctionBase))]
|
||||
|
||||
// Template and Resource types
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DataTemplate))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ItemsPanelTemplate))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Style))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.FontIconSource))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.ResourceDictionary))]
|
||||
|
||||
// Text and Document types
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Documents.Run))]
|
||||
|
||||
// Layout types
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.RowDefinition))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ColumnDefinition))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.RowDefinitionCollection))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ColumnDefinitionCollection))]
|
||||
|
||||
// Media types for brushes and visuals
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.SolidColorBrush))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Brush))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Media.Transform))]
|
||||
|
||||
// Core UI element types
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.UIElement))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.FrameworkElement))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.DependencyObject))]
|
||||
|
||||
// Thickness and other value types used in XAML (structs, not enums)
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Thickness))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.CornerRadius))]
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.GridLength))]
|
||||
|
||||
// ToolTip service used in buttons
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Microsoft.UI.Xaml.Controls.ToolTipService))]
|
||||
|
||||
public static void PreserveTypes()
|
||||
{
|
||||
// This method exists only to hold the DynamicDependency attributes above.
|
||||
// It must be called to ensure the types are not trimmed during AOT compilation.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using static PowerDisplay.Native.NativeConstants;
|
||||
using static PowerDisplay.Native.NativeDelegates;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
|
||||
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
|
||||
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// DDC/CI monitor controller for controlling external monitors
|
||||
/// </summary>
|
||||
public partial class DdcCiController : IMonitorController, IDisposable
|
||||
{
|
||||
private readonly PhysicalMonitorHandleManager _handleManager = new();
|
||||
private readonly VcpCodeResolver _vcpResolver = new();
|
||||
private readonly MonitorDiscoveryHelper _discoveryHelper;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public DdcCiController()
|
||||
{
|
||||
_discoveryHelper = new MonitorDiscoveryHelper(_vcpResolver);
|
||||
}
|
||||
|
||||
public string Name => "DDC/CI Monitor Controller";
|
||||
|
||||
public MonitorType SupportedType => MonitorType.External;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (monitor.Type != MonitorType.External)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
var physicalHandle = GetPhysicalHandle(monitor);
|
||||
return physicalHandle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(physicalHandle);
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor brightness
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
var physicalHandle = GetPhysicalHandle(monitor);
|
||||
if (physicalHandle == IntPtr.Zero)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// First try high-level API
|
||||
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint minBrightness, out uint currentBrightness, out uint maxBrightness))
|
||||
{
|
||||
return new BrightnessInfo((int)currentBrightness, (int)minBrightness, (int)maxBrightness);
|
||||
}
|
||||
|
||||
// Try different VCP codes
|
||||
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor brightness
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
brightness = Math.Clamp(brightness, 0, 100);
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
var physicalHandle = GetPhysicalHandle(monitor);
|
||||
if (physicalHandle == IntPtr.Zero)
|
||||
{
|
||||
return MonitorOperationResult.Failure("No physical handle found");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var currentInfo = GetBrightnessInfo(monitor, physicalHandle);
|
||||
if (!currentInfo.IsValid)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Cannot read current brightness");
|
||||
}
|
||||
|
||||
uint targetValue = (uint)currentInfo.FromPercentage(brightness);
|
||||
|
||||
// First try high-level API
|
||||
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
// Try VCP codes
|
||||
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(physicalHandle, vcpCode.Value, targetValue))
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor contrast
|
||||
/// </summary>
|
||||
public Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
=> GetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor contrast
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
|
||||
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, 0, 100, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor volume
|
||||
/// </summary>
|
||||
public Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
=> GetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor volume
|
||||
/// </summary>
|
||||
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
|
||||
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, 0, 100, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor color temperature
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// Try different VCP codes for color temperature
|
||||
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode.Value, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor color temperature
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
colorTemperature = Math.Clamp(colorTemperature, 2000, 10000);
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get current color temperature info to understand the range
|
||||
var currentInfo = _vcpResolver.GetCurrentColorTemperature(monitor.Handle);
|
||||
if (!currentInfo.IsValid)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Cannot read current color temperature");
|
||||
}
|
||||
|
||||
// Convert Kelvin temperature to VCP value
|
||||
uint targetValue = _vcpResolver.ConvertKelvinToVcpValue(colorTemperature, currentInfo);
|
||||
|
||||
// Try to set using the best available VCP code
|
||||
var vcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitor.Id, monitor.Handle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode.Value, targetValue))
|
||||
{
|
||||
Logger.LogInfo($"Successfully set color temperature to {colorTemperature}K via DDC/CI (VCP 0x{vcpCode.Value:X2})");
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
return MonitorOperationResult.Failure($"Failed to set color temperature via DDC/CI", (int)lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor capabilities string
|
||||
/// </summary>
|
||||
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (GetCapabilitiesStringLength(monitor.Handle, out uint length) && length > 0)
|
||||
{
|
||||
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
|
||||
try
|
||||
{
|
||||
if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length))
|
||||
{
|
||||
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to get capabilities string: {ex.Message}");
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save current settings
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (SaveCurrentSettings(monitor.Handle))
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
return MonitorOperationResult.Failure($"Failed to save settings", (int)lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception saving settings: {ex.Message}");
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover supported monitors
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
async () =>
|
||||
{
|
||||
var monitors = new List<Monitor>();
|
||||
var newHandleMap = new Dictionary<string, IntPtr>();
|
||||
|
||||
try
|
||||
{
|
||||
// Get all display devices with stable device IDs (Twinkle Tray style)
|
||||
var displayDevices = DdcCiNative.GetAllDisplayDevices();
|
||||
Logger.LogInfo($"DDC: Found {displayDevices.Count} display devices via EnumDisplayDevices");
|
||||
|
||||
// Also get hardware info for friendly names
|
||||
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
|
||||
Logger.LogDebug($"DDC: GetAllMonitorDisplayInfo returned {monitorDisplayInfo.Count} items");
|
||||
|
||||
// Enumerate all monitors
|
||||
var monitorHandles = new List<IntPtr>();
|
||||
Logger.LogDebug($"DDC: About to call EnumDisplayMonitors...");
|
||||
|
||||
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
|
||||
{
|
||||
Logger.LogDebug($"DDC: EnumProc callback - hMonitor=0x{hMonitor:X}");
|
||||
monitorHandles.Add(hMonitor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero);
|
||||
Logger.LogDebug($"DDC: EnumDisplayMonitors returned {enumResult}, found {monitorHandles.Count} monitor handles");
|
||||
|
||||
if (!enumResult)
|
||||
{
|
||||
Logger.LogWarning($"DDC: EnumDisplayMonitors failed");
|
||||
return monitors;
|
||||
}
|
||||
|
||||
// Get physical handles for each monitor
|
||||
foreach (var hMonitor in monitorHandles)
|
||||
{
|
||||
var adapterName = _discoveryHelper.GetMonitorDeviceId(hMonitor);
|
||||
if (string.IsNullOrEmpty(adapterName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sometimes Windows returns NULL handles. Implement Twinkle Tray's retry logic.
|
||||
// See: twinkle-tray/src/Monitors.js line 617
|
||||
PHYSICAL_MONITOR[]? physicalMonitors = null;
|
||||
const int maxRetries = 3;
|
||||
const int retryDelayMs = 200;
|
||||
|
||||
for (int attempt = 0; attempt < maxRetries; attempt++)
|
||||
{
|
||||
if (attempt > 0)
|
||||
{
|
||||
Logger.LogInfo($"DDC: Retry attempt {attempt}/{maxRetries - 1} for hMonitor 0x{hMonitor:X} after {retryDelayMs}ms delay");
|
||||
await Task.Delay(retryDelayMs, cancellationToken);
|
||||
}
|
||||
|
||||
physicalMonitors = _discoveryHelper.GetPhysicalMonitors(hMonitor);
|
||||
|
||||
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||
{
|
||||
if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null/empty on attempt {attempt + 1}, will retry");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if any handle is NULL (Twinkle Tray checks handleIsValid)
|
||||
bool hasNullHandle = false;
|
||||
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||
{
|
||||
if (physicalMonitors[i].HPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
hasNullHandle = true;
|
||||
Logger.LogWarning($"DDC: Physical monitor [{i}] has NULL handle on attempt {attempt + 1}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasNullHandle)
|
||||
{
|
||||
// Success! All handles are valid
|
||||
if (attempt > 0)
|
||||
{
|
||||
Logger.LogInfo($"DDC: Successfully obtained valid handles on attempt {attempt + 1}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else if (attempt < maxRetries - 1)
|
||||
{
|
||||
Logger.LogWarning($"DDC: NULL handle detected, will retry (attempt {attempt + 1}/{maxRetries})");
|
||||
physicalMonitors = null; // Reset for retry
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"DDC: NULL handle still present after {maxRetries} attempts, continuing anyway");
|
||||
}
|
||||
}
|
||||
|
||||
if (physicalMonitors == null || physicalMonitors.Length == 0)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Failed to get physical monitors for hMonitor 0x{hMonitor:X} after {maxRetries} attempts");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match physical monitors with DisplayDeviceInfo (Twinkle Tray logic)
|
||||
// For each physical monitor on this adapter, find the corresponding DisplayDeviceInfo
|
||||
for (int i = 0; i < physicalMonitors.Length; i++)
|
||||
{
|
||||
var physicalMonitor = physicalMonitors[i];
|
||||
if (physicalMonitor.HPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find matching DisplayDeviceInfo for this physical monitor
|
||||
DisplayDeviceInfo? matchedDevice = null;
|
||||
int foundCount = 0;
|
||||
|
||||
foreach (var displayDevice in displayDevices)
|
||||
{
|
||||
if (displayDevice.AdapterName == adapterName)
|
||||
{
|
||||
if (foundCount == i)
|
||||
{
|
||||
matchedDevice = displayDevice;
|
||||
break;
|
||||
}
|
||||
|
||||
foundCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine device key for handle reuse logic
|
||||
string deviceKey = matchedDevice?.DeviceKey ?? $"{adapterName}_{i}";
|
||||
|
||||
// Use HandleManager to reuse or create handle
|
||||
var (handleToUse, reusingOldHandle) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor);
|
||||
|
||||
// Validate DDC/CI connection for the handle we're going to use
|
||||
if (!reusingOldHandle && !DdcCiNative.ValidateDdcCiConnection(handleToUse))
|
||||
{
|
||||
Logger.LogWarning($"DDC: New handle 0x{handleToUse:X} failed DDC/CI validation, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update physical monitor handle to use the correct one
|
||||
var monitorToCreate = physicalMonitor;
|
||||
monitorToCreate.HPhysicalMonitor = handleToUse;
|
||||
|
||||
var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice);
|
||||
if (monitor != null)
|
||||
{
|
||||
Logger.LogInfo($"DDC: Created monitor {monitor.Id} with handle 0x{monitor.Handle:X} (reused: {reusingOldHandle}), deviceKey: {monitor.DeviceKey}");
|
||||
monitors.Add(monitor);
|
||||
|
||||
// Store in new map for cleanup
|
||||
newHandleMap[monitor.DeviceKey] = handleToUse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update handle manager with new mapping
|
||||
_handleManager.UpdateHandleMap(newHandleMap);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Logger.LogDebug($"DDC: DiscoverMonitorsAsync returning {monitors.Count} monitors");
|
||||
}
|
||||
|
||||
return monitors;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate monitor connection status
|
||||
/// </summary>
|
||||
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() => monitor.Handle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(monitor.Handle),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to get VCP feature value
|
||||
/// </summary>
|
||||
private async Task<BrightnessInfo> GetVcpFeatureAsync(
|
||||
Monitor monitor,
|
||||
byte vcpCode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic method to set VCP feature value
|
||||
/// </summary>
|
||||
private async Task<MonitorOperationResult> SetVcpFeatureAsync(
|
||||
Monitor monitor,
|
||||
byte vcpCode,
|
||||
int value,
|
||||
int min = 0,
|
||||
int max = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
value = Math.Clamp(value, min, max);
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (monitor.Handle == IntPtr.Zero)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Invalid monitor handle");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Get current value to determine range
|
||||
var currentInfo = GetVcpFeatureAsync(monitor, vcpCode).Result;
|
||||
if (!currentInfo.IsValid)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Cannot read current value for VCP 0x{vcpCode:X2}");
|
||||
}
|
||||
|
||||
uint targetValue = (uint)currentInfo.FromPercentage(value);
|
||||
|
||||
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode, targetValue))
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
|
||||
var lastError = GetLastError();
|
||||
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", (int)lastError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Exception setting VCP 0x{vcpCode:X2}: {ex.Message}");
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get brightness information (with explicit handle)
|
||||
/// </summary>
|
||||
private BrightnessInfo GetBrightnessInfo(Monitor monitor, IntPtr physicalHandle)
|
||||
{
|
||||
if (physicalHandle == IntPtr.Zero)
|
||||
{
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
// First try high-level API
|
||||
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint min, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||
}
|
||||
|
||||
// Try VCP codes
|
||||
var vcpCode = _vcpResolver.GetBrightnessVcpCode(monitor.Id, physicalHandle);
|
||||
if (vcpCode.HasValue && DdcCiNative.TryGetVCPFeature(physicalHandle, vcpCode.Value, out current, out max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get physical handle for monitor using stable deviceKey
|
||||
/// </summary>
|
||||
private IntPtr GetPhysicalHandle(Monitor monitor)
|
||||
{
|
||||
return _handleManager.GetPhysicalHandle(monitor);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
_handleManager?.Dispose();
|
||||
_vcpResolver?.ClearCache();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
499
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
499
src/modules/powerdisplay/PowerDisplay/Native/DDC/DdcCiNative.cs
Normal file
@@ -0,0 +1,499 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using static PowerDisplay.Native.NativeConstants;
|
||||
using static PowerDisplay.Native.NativeDelegates;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
|
||||
// 类型别名,兼容 Windows API 命名约定
|
||||
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
|
||||
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
|
||||
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
|
||||
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Native.DISPLAYCONFIG_PATH_INFO;
|
||||
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Native.DISPLAYCONFIG_TARGET_DEVICE_NAME;
|
||||
using LUID = PowerDisplay.Native.Luid;
|
||||
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
#pragma warning disable SA1649 // File name should match first type name - Multiple related types for DDC/CI
|
||||
#pragma warning disable SA1402 // File may only contain a single type - Related DDC/CI types grouped together
|
||||
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示设备信息类
|
||||
/// </summary>
|
||||
public class DisplayDeviceInfo
|
||||
{
|
||||
public string DeviceName { get; set; } = string.Empty;
|
||||
|
||||
public string AdapterName { get; set; } = string.Empty;
|
||||
|
||||
public string DeviceID { get; set; } = string.Empty;
|
||||
|
||||
public string DeviceKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DDC/CI 原生 API 封装
|
||||
/// </summary>
|
||||
public static class DdcCiNative
|
||||
{
|
||||
// Display Configuration 常量
|
||||
public const uint QdcAllPaths = 0x00000001;
|
||||
|
||||
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||
|
||||
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
|
||||
|
||||
// Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 获取 VCP 功能值的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="vcpCode">VCP 代码</param>
|
||||
/// <param name="currentValue">当前值</param>
|
||||
/// <param name="maxValue">最大值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
|
||||
{
|
||||
currentValue = 0;
|
||||
maxValue = 0;
|
||||
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return GetVCPFeatureAndVCPFeatureReply(hPhysicalMonitor, vcpCode, IntPtr.Zero, out currentValue, out maxValue);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 VCP 功能值的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="vcpCode">VCP 代码</param>
|
||||
/// <param name="value">新值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return SetVCPFeature(hPhysicalMonitor, vcpCode, value);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取高级亮度信息的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="minBrightness">最小亮度</param>
|
||||
/// <param name="currentBrightness">当前亮度</param>
|
||||
/// <param name="maxBrightness">最大亮度</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
|
||||
{
|
||||
minBrightness = 0;
|
||||
currentBrightness = 0;
|
||||
maxBrightness = 0;
|
||||
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return GetMonitorBrightness(hPhysicalMonitor, out minBrightness, out currentBrightness, out maxBrightness);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置高级亮度的安全包装
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <param name="brightness">亮度值</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TrySetMonitorBrightness(IntPtr hPhysicalMonitor, uint brightness)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return SetMonitorBrightness(hPhysicalMonitor, brightness);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查 DDC/CI 连接的有效性
|
||||
/// </summary>
|
||||
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
|
||||
/// <returns>是否连接有效</returns>
|
||||
public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor)
|
||||
{
|
||||
if (hPhysicalMonitor == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 尝试读取基本的 VCP 代码来验证连接
|
||||
var testCodes = new byte[] { NativeConstants.VcpCodeBrightness, NativeConstants.VcpCodeNewControlValue, NativeConstants.VcpCodeVcpVersion };
|
||||
|
||||
foreach (var code in testCodes)
|
||||
{
|
||||
if (TryGetVCPFeature(hPhysicalMonitor, code, out _, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器友好名称
|
||||
/// </summary>
|
||||
/// <param name="adapterId">适配器 ID</param>
|
||||
/// <param name="targetId">目标 ID</param>
|
||||
/// <returns>显示器友好名称,如果获取失败返回 null</returns>
|
||||
public static unsafe string? GetMonitorFriendlyName(LUID adapterId, uint targetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||
{
|
||||
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||
{
|
||||
Type = DisplayconfigDeviceInfoGetTargetName,
|
||||
Size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME),
|
||||
AdapterId = adapterId,
|
||||
Id = targetId,
|
||||
},
|
||||
};
|
||||
|
||||
var result = DisplayConfigGetDeviceInfo(ref deviceName);
|
||||
if (result == 0)
|
||||
{
|
||||
return deviceName.GetMonitorFriendlyDeviceName();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过枚举显示配置获取所有显示器友好名称
|
||||
/// </summary>
|
||||
/// <returns>设备路径到友好名称的映射</returns>
|
||||
public static Dictionary<string, string> GetAllMonitorFriendlyNames()
|
||||
{
|
||||
var friendlyNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
try
|
||||
{
|
||||
// 获取缓冲区大小
|
||||
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
|
||||
if (result != 0)
|
||||
{
|
||||
return friendlyNames;
|
||||
}
|
||||
|
||||
// 分配缓冲区
|
||||
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
|
||||
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
|
||||
|
||||
// 查询显示配置
|
||||
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
|
||||
if (result != 0)
|
||||
{
|
||||
return friendlyNames;
|
||||
}
|
||||
|
||||
// 获取每个路径的友好名称
|
||||
for (int i = 0; i < pathCount; i++)
|
||||
{
|
||||
var path = paths[i];
|
||||
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(friendlyName))
|
||||
{
|
||||
// 使用适配器和目标 ID 作为键
|
||||
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
|
||||
friendlyNames[key] = friendlyName;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return friendlyNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取显示器的EDID硬件ID信息
|
||||
/// </summary>
|
||||
/// <param name="adapterId">适配器ID</param>
|
||||
/// <param name="targetId">目标ID</param>
|
||||
/// <returns>硬件ID字符串,格式为制造商代码+产品代码</returns>
|
||||
public static unsafe string? GetMonitorHardwareId(LUID adapterId, uint targetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||
{
|
||||
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||
{
|
||||
Type = DisplayconfigDeviceInfoGetTargetName,
|
||||
Size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME),
|
||||
AdapterId = adapterId,
|
||||
Id = targetId,
|
||||
},
|
||||
};
|
||||
|
||||
var result = DisplayConfigGetDeviceInfo(ref deviceName);
|
||||
if (result == 0)
|
||||
{
|
||||
// 将制造商ID转换为3字符字符串
|
||||
var manufacturerId = deviceName.EdidManufactureId;
|
||||
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
|
||||
|
||||
// 将产品ID转换为4位十六进制字符串
|
||||
var productCode = deviceName.EdidProductCodeId.ToString("X4", System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
var hardwareId = $"{manufactureCode}{productCode}";
|
||||
Logger.LogDebug($"GetMonitorHardwareId - ManufacturerId: 0x{manufacturerId:X4}, Code: '{manufactureCode}', ProductCode: '{productCode}', Result: '{hardwareId}'");
|
||||
|
||||
return hardwareId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"GetMonitorHardwareId - DisplayConfigGetDeviceInfo failed with result: {result}");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将制造商ID转换为3字符制造商代码
|
||||
/// </summary>
|
||||
/// <param name="manufacturerId">制造商ID</param>
|
||||
/// <returns>3字符制造商代码</returns>
|
||||
private static string ConvertManufactureIdToString(ushort manufacturerId)
|
||||
{
|
||||
// EDID制造商ID需要先进行字节序交换
|
||||
manufacturerId = (ushort)(((manufacturerId & 0xff00) >> 8) | ((manufacturerId & 0x00ff) << 8));
|
||||
|
||||
// 提取3个5位字符(每个字符是A-Z,其中A=1, B=2, ..., Z=26)
|
||||
var char1 = (char)('A' - 1 + ((manufacturerId >> 0) & 0x1f));
|
||||
var char2 = (char)('A' - 1 + ((manufacturerId >> 5) & 0x1f));
|
||||
var char3 = (char)('A' - 1 + ((manufacturerId >> 10) & 0x1f));
|
||||
|
||||
// 按正确顺序组合字符
|
||||
return $"{char3}{char2}{char1}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有显示器的完整信息,包括友好名称和硬件ID
|
||||
/// </summary>
|
||||
/// <returns>包含显示器信息的字典</returns>
|
||||
public static Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
|
||||
{
|
||||
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
try
|
||||
{
|
||||
// 获取缓冲区大小
|
||||
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
|
||||
if (result != 0)
|
||||
{
|
||||
return monitorInfo;
|
||||
}
|
||||
|
||||
// 分配缓冲区
|
||||
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
|
||||
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
|
||||
|
||||
// 查询显示配置
|
||||
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
|
||||
if (result != 0)
|
||||
{
|
||||
return monitorInfo;
|
||||
}
|
||||
|
||||
// 获取每个路径的信息
|
||||
for (int i = 0; i < pathCount; i++)
|
||||
{
|
||||
var path = paths[i];
|
||||
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
|
||||
var hardwareId = GetMonitorHardwareId(path.TargetInfo.AdapterId, path.TargetInfo.Id);
|
||||
|
||||
if (!string.IsNullOrEmpty(friendlyName) || !string.IsNullOrEmpty(hardwareId))
|
||||
{
|
||||
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
|
||||
monitorInfo[key] = new MonitorDisplayInfo
|
||||
{
|
||||
FriendlyName = friendlyName ?? string.Empty,
|
||||
HardwareId = hardwareId ?? string.Empty,
|
||||
AdapterId = path.TargetInfo.AdapterId,
|
||||
TargetId = path.TargetInfo.Id,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return monitorInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有显示设备信息(使用 EnumDisplayDevices API)
|
||||
/// 与 Twinkle Tray 实现保持一致
|
||||
/// </summary>
|
||||
/// <returns>显示设备信息列表</returns>
|
||||
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
|
||||
{
|
||||
var devices = new List<DisplayDeviceInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
// 枚举所有适配器
|
||||
uint adapterIndex = 0;
|
||||
var adapter = default(DISPLAY_DEVICE);
|
||||
adapter.Cb = (uint)sizeof(DisplayDevice);
|
||||
|
||||
while (EnumDisplayDevices(null, adapterIndex, ref adapter, EddGetDeviceInterfaceName))
|
||||
{
|
||||
// 跳过镜像驱动程序
|
||||
if ((adapter.StateFlags & DisplayDeviceMirroringDriver) != 0)
|
||||
{
|
||||
adapterIndex++;
|
||||
adapter = default(DISPLAY_DEVICE);
|
||||
adapter.Cb = (uint)sizeof(DisplayDevice);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 只处理已连接到桌面的适配器
|
||||
if ((adapter.StateFlags & DisplayDeviceAttachedToDesktop) != 0)
|
||||
{
|
||||
// 枚举该适配器上的所有显示器
|
||||
uint displayIndex = 0;
|
||||
var display = default(DISPLAY_DEVICE);
|
||||
display.Cb = (uint)sizeof(DisplayDevice);
|
||||
|
||||
string adapterDeviceName = adapter.GetDeviceName();
|
||||
while (EnumDisplayDevices(adapterDeviceName, displayIndex, ref display, EddGetDeviceInterfaceName))
|
||||
{
|
||||
string displayDeviceID = display.GetDeviceID();
|
||||
|
||||
// 只处理活动的显示器
|
||||
if ((display.StateFlags & DisplayDeviceAttachedToDesktop) != 0 &&
|
||||
!string.IsNullOrEmpty(displayDeviceID))
|
||||
{
|
||||
var deviceInfo = new DisplayDeviceInfo
|
||||
{
|
||||
DeviceName = display.GetDeviceName(),
|
||||
AdapterName = adapterDeviceName,
|
||||
DeviceID = displayDeviceID,
|
||||
};
|
||||
|
||||
// 提取 DeviceKey:移除 GUID 部分(#{...} 及之后的内容)
|
||||
// 例如:\\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID} -> \\?\DISPLAY#GSM5C6D#5&1234&0&UID
|
||||
int guidIndex = deviceInfo.DeviceID.IndexOf("#{", StringComparison.Ordinal);
|
||||
if (guidIndex >= 0)
|
||||
{
|
||||
deviceInfo.DeviceKey = deviceInfo.DeviceID.Substring(0, guidIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
deviceInfo.DeviceKey = deviceInfo.DeviceID;
|
||||
}
|
||||
|
||||
devices.Add(deviceInfo);
|
||||
|
||||
Logger.LogDebug($"Found display device - Name: {deviceInfo.DeviceName}, Adapter: {deviceInfo.AdapterName}, DeviceKey: {deviceInfo.DeviceKey}");
|
||||
}
|
||||
|
||||
displayIndex++;
|
||||
display = default(DISPLAY_DEVICE);
|
||||
display.Cb = (uint)sizeof(DisplayDevice);
|
||||
}
|
||||
}
|
||||
|
||||
adapterIndex++;
|
||||
adapter = default(DISPLAY_DEVICE);
|
||||
adapter.Cb = (uint)sizeof(DisplayDevice);
|
||||
}
|
||||
|
||||
Logger.LogInfo($"GetAllDisplayDevices found {devices.Count} display devices");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"GetAllDisplayDevices exception: {ex.Message}");
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示器显示信息结构
|
||||
/// </summary>
|
||||
public struct MonitorDisplayInfo
|
||||
{
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
public string HardwareId { get; set; }
|
||||
|
||||
public LUID AdapterId { get; set; }
|
||||
|
||||
public uint TargetId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Models;
|
||||
using static PowerDisplay.Native.NativeConstants;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
|
||||
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
|
||||
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for discovering and creating monitor objects
|
||||
/// </summary>
|
||||
public class MonitorDiscoveryHelper
|
||||
{
|
||||
private readonly VcpCodeResolver _vcpResolver;
|
||||
|
||||
public MonitorDiscoveryHelper(VcpCodeResolver vcpResolver)
|
||||
{
|
||||
_vcpResolver = vcpResolver;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor device ID
|
||||
/// </summary>
|
||||
public unsafe string GetMonitorDeviceId(IntPtr hMonitor)
|
||||
{
|
||||
try
|
||||
{
|
||||
var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MonitorInfoEx) };
|
||||
if (GetMonitorInfo(hMonitor, ref monitorInfo))
|
||||
{
|
||||
return monitorInfo.GetDeviceName() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silent failure
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get physical monitors for a logical monitor
|
||||
/// </summary>
|
||||
internal PHYSICAL_MONITOR[]? GetPhysicalMonitors(IntPtr hMonitor)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogDebug($"GetPhysicalMonitors: hMonitor=0x{hMonitor:X}");
|
||||
|
||||
if (!GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint numMonitors))
|
||||
{
|
||||
Logger.LogWarning($"GetPhysicalMonitors: GetNumberOfPhysicalMonitorsFromHMONITOR failed for 0x{hMonitor:X}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"GetPhysicalMonitors: numMonitors={numMonitors}");
|
||||
if (numMonitors == 0)
|
||||
{
|
||||
Logger.LogWarning($"GetPhysicalMonitors: numMonitors is 0");
|
||||
return null;
|
||||
}
|
||||
|
||||
var physicalMonitors = new PHYSICAL_MONITOR[numMonitors];
|
||||
bool apiResult;
|
||||
unsafe
|
||||
{
|
||||
fixed (PHYSICAL_MONITOR* ptr = physicalMonitors)
|
||||
{
|
||||
apiResult = GetPhysicalMonitorsFromHMONITOR(hMonitor, numMonitors, ptr);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR returned {apiResult}");
|
||||
|
||||
if (!apiResult)
|
||||
{
|
||||
Logger.LogWarning($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR failed");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Log each physical monitor
|
||||
for (int i = 0; i < numMonitors; i++)
|
||||
{
|
||||
string desc = physicalMonitors[i].GetDescription() ?? string.Empty;
|
||||
IntPtr handle = physicalMonitors[i].HPhysicalMonitor;
|
||||
Logger.LogDebug($"GetPhysicalMonitors: [{i}] Handle=0x{handle:X}, Desc='{desc}'");
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
Logger.LogWarning($"GetPhysicalMonitors: Monitor [{i}] has NULL handle despite successful API call!");
|
||||
}
|
||||
}
|
||||
|
||||
return physicalMonitors;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"GetPhysicalMonitors: Exception: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Monitor object from physical monitor
|
||||
/// </summary>
|
||||
internal Monitor? CreateMonitorFromPhysical(
|
||||
PHYSICAL_MONITOR physicalMonitor,
|
||||
string adapterName,
|
||||
int index,
|
||||
Dictionary<string, MonitorDisplayInfo> monitorDisplayInfo,
|
||||
DisplayDeviceInfo? displayDevice)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get hardware ID and friendly name from the display info
|
||||
string hardwareId = string.Empty;
|
||||
string name = physicalMonitor.GetDescription() ?? string.Empty;
|
||||
|
||||
// Try to find matching monitor info
|
||||
foreach (var kvp in monitorDisplayInfo.Values)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.HardwareId))
|
||||
{
|
||||
hardwareId = kvp.HardwareId;
|
||||
|
||||
if (!string.IsNullOrEmpty(kvp.FriendlyName) && !kvp.FriendlyName.Contains("Generic"))
|
||||
{
|
||||
name = kvp.FriendlyName;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Use stable device IDs from DisplayDeviceInfo
|
||||
string deviceKey;
|
||||
string monitorId;
|
||||
|
||||
if (displayDevice != null && !string.IsNullOrEmpty(displayDevice.DeviceKey))
|
||||
{
|
||||
// Use stable device key from EnumDisplayDevices
|
||||
deviceKey = displayDevice.DeviceKey;
|
||||
monitorId = $"DDC_{deviceKey.Replace(@"\\?\", string.Empty, StringComparison.Ordinal).Replace("#", "_", StringComparison.Ordinal).Replace("&", "_", StringComparison.Ordinal)}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: create device ID without handle in the key
|
||||
var baseDevice = adapterName.Replace(@"\\.\", string.Empty, StringComparison.Ordinal);
|
||||
deviceKey = $"{baseDevice}_{index}";
|
||||
monitorId = $"DDC_{deviceKey}";
|
||||
}
|
||||
|
||||
// If still no good name, use default value
|
||||
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
|
||||
{
|
||||
name = $"External Display {index + 1}";
|
||||
}
|
||||
|
||||
// Get current brightness
|
||||
var brightnessInfo = GetCurrentBrightness(physicalMonitor.HPhysicalMonitor);
|
||||
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = monitorId,
|
||||
HardwareId = hardwareId,
|
||||
Name = name.Trim(),
|
||||
Type = MonitorType.External,
|
||||
CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
IsAvailable = true,
|
||||
Handle = physicalMonitor.HPhysicalMonitor,
|
||||
DeviceKey = deviceKey,
|
||||
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.DdcCi,
|
||||
ConnectionType = "External",
|
||||
CommunicationMethod = "DDC/CI",
|
||||
Manufacturer = ExtractManufacturer(name),
|
||||
};
|
||||
|
||||
// Check contrast support
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeContrast, out _, out _))
|
||||
{
|
||||
monitor.Capabilities |= MonitorCapabilities.Contrast;
|
||||
}
|
||||
|
||||
// Check color temperature support (suppress logging for discovery)
|
||||
var colorTempVcpCode = _vcpResolver.GetColorTemperatureVcpCode(monitorId, physicalMonitor.HPhysicalMonitor);
|
||||
monitor.SupportsColorTemperature = colorTempVcpCode.HasValue;
|
||||
|
||||
// Check volume support
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalMonitor.HPhysicalMonitor, VcpCodeVolume, out _, out _))
|
||||
{
|
||||
monitor.Capabilities |= MonitorCapabilities.Volume;
|
||||
}
|
||||
|
||||
// Check high-level API support
|
||||
if (DdcCiNative.TryGetMonitorBrightness(physicalMonitor.HPhysicalMonitor, out _, out _, out _))
|
||||
{
|
||||
monitor.Capabilities |= MonitorCapabilities.HighLevel;
|
||||
}
|
||||
|
||||
return monitor;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"DDC: CreateMonitorFromPhysical exception: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current brightness using VCP codes
|
||||
/// </summary>
|
||||
private BrightnessInfo GetCurrentBrightness(IntPtr handle)
|
||||
{
|
||||
// Try high-level API
|
||||
if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, (int)min, (int)max);
|
||||
}
|
||||
|
||||
// Try VCP codes
|
||||
byte[] vcpCodes = { VcpCodeBrightness, VcpCodeBacklightControl, VcpCodeBacklightLevelWhite, VcpCodeContrast };
|
||||
foreach (var code in vcpCodes)
|
||||
{
|
||||
if (DdcCiNative.TryGetVCPFeature(handle, code, out current, out max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract manufacturer from name
|
||||
/// </summary>
|
||||
private string ExtractManufacturer(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// Common manufacturer prefixes
|
||||
var manufacturers = new[] { "DELL", "HP", "LG", "Samsung", "ASUS", "Acer", "BenQ", "AOC", "ViewSonic" };
|
||||
var upperName = name.ToUpperInvariant();
|
||||
|
||||
foreach (var manufacturer in manufacturers)
|
||||
{
|
||||
if (upperName.Contains(manufacturer))
|
||||
{
|
||||
return manufacturer;
|
||||
}
|
||||
}
|
||||
|
||||
// Return first word as manufacturer
|
||||
var firstWord = name.Split(' ')[0];
|
||||
return firstWord.Length > 2 ? firstWord : "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Models;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages physical monitor handles - reuse, cleanup, and validation
|
||||
/// Twinkle Tray style handle management
|
||||
/// </summary>
|
||||
public partial class PhysicalMonitorHandleManager : IDisposable
|
||||
{
|
||||
// Twinkle Tray style mapping: deviceKey -> physical handle
|
||||
private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Get physical handle for monitor using stable deviceKey
|
||||
/// </summary>
|
||||
public IntPtr GetPhysicalHandle(Monitor monitor)
|
||||
{
|
||||
// Primary lookup: use stable deviceKey from EnumDisplayDevices
|
||||
if (!string.IsNullOrEmpty(monitor.DeviceKey) &&
|
||||
_deviceKeyToHandleMap.TryGetValue(monitor.DeviceKey, out var handle))
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Fallback: use direct handle from monitor object
|
||||
if (monitor.Handle != IntPtr.Zero)
|
||||
{
|
||||
return monitor.Handle;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to reuse existing handle if valid, otherwise use new handle
|
||||
/// Returns the handle to use and whether it was reused
|
||||
/// </summary>
|
||||
public (IntPtr Handle, bool WasReused) ReuseOrCreateHandle(string deviceKey, IntPtr newHandle)
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceKey))
|
||||
{
|
||||
return (newHandle, false);
|
||||
}
|
||||
|
||||
// Try to reuse existing handle if it's still valid
|
||||
if (_deviceKeyToHandleMap.TryGetValue(deviceKey, out var existingHandle) &&
|
||||
existingHandle != IntPtr.Zero &&
|
||||
DdcCiNative.ValidateDdcCiConnection(existingHandle))
|
||||
{
|
||||
// Destroy the newly created handle since we're using the old one
|
||||
if (newHandle != existingHandle && newHandle != IntPtr.Zero)
|
||||
{
|
||||
DestroyPhysicalMonitor(newHandle);
|
||||
}
|
||||
|
||||
return (existingHandle, true);
|
||||
}
|
||||
|
||||
return (newHandle, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the handle mapping with new handles
|
||||
/// </summary>
|
||||
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
|
||||
{
|
||||
// Clean up unused handles before updating
|
||||
CleanupUnusedHandles(newHandleMap);
|
||||
|
||||
// Update the device key map
|
||||
_deviceKeyToHandleMap.Clear();
|
||||
foreach (var kvp in newHandleMap)
|
||||
{
|
||||
_deviceKeyToHandleMap[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clean up handles that are no longer in use
|
||||
/// </summary>
|
||||
private void CleanupUnusedHandles(Dictionary<string, IntPtr> newHandles)
|
||||
{
|
||||
if (_deviceKeyToHandleMap.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var handlesToDestroy = new List<IntPtr>();
|
||||
|
||||
// Find handles that are in old map but not being reused
|
||||
foreach (var oldMapping in _deviceKeyToHandleMap)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (var newMapping in newHandles)
|
||||
{
|
||||
// If the same handle is being reused, don't destroy it
|
||||
if (oldMapping.Value == newMapping.Value)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && oldMapping.Value != IntPtr.Zero)
|
||||
{
|
||||
handlesToDestroy.Add(oldMapping.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy unused handles
|
||||
foreach (var handle in handlesToDestroy)
|
||||
{
|
||||
try
|
||||
{
|
||||
DestroyPhysicalMonitor(handle);
|
||||
Logger.LogDebug($"DDC: Cleaned up unused handle 0x{handle:X}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"DDC: Failed to destroy handle 0x{handle:X}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Release all physical monitor handles
|
||||
foreach (var handle in _deviceKeyToHandleMap.Values)
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
DestroyPhysicalMonitor(handle);
|
||||
Logger.LogDebug($"Released physical monitor handle 0x{handle:X}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to destroy physical monitor handle 0x{handle:X}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_deviceKeyToHandleMap.Clear();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Core.Utils;
|
||||
|
||||
namespace PowerDisplay.Native.DDC
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves and caches VCP codes for monitor controls
|
||||
/// Handles brightness, color temperature, and other VCP feature codes
|
||||
/// </summary>
|
||||
public class VcpCodeResolver
|
||||
{
|
||||
private readonly Dictionary<string, byte> _cachedCodes = new();
|
||||
|
||||
// VCP code priority order (for brightness control)
|
||||
private static readonly byte[] BrightnessVcpCodes =
|
||||
{
|
||||
NativeConstants.VcpCodeBrightness, // 0x10 - Standard brightness
|
||||
NativeConstants.VcpCodeBacklightControl, // 0x13 - Backlight control
|
||||
NativeConstants.VcpCodeBacklightLevelWhite, // 0x6B - White backlight level
|
||||
NativeConstants.VcpCodeContrast, // 0x12 - Contrast as last resort
|
||||
};
|
||||
|
||||
// VCP code priority order (for color temperature control)
|
||||
private static readonly byte[] ColorTemperatureVcpCodes =
|
||||
{
|
||||
NativeConstants.VcpCodeColorTemperature, // 0x0C - Standard color temperature
|
||||
NativeConstants.VcpCodeColorTemperatureIncrement, // 0x0B - Color temperature increment
|
||||
NativeConstants.VcpCodeSelectColorPreset, // 0x14 - Color preset selection
|
||||
NativeConstants.VcpCodeGamma, // 0x72 - Gamma correction
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get best VCP code for brightness control
|
||||
/// </summary>
|
||||
public byte? GetBrightnessVcpCode(string monitorId, IntPtr physicalHandle)
|
||||
{
|
||||
// Return cached best code if available
|
||||
if (_cachedCodes.TryGetValue(monitorId, out var cachedCode))
|
||||
{
|
||||
return cachedCode;
|
||||
}
|
||||
|
||||
// Find first working VCP code (highest priority)
|
||||
foreach (var code in BrightnessVcpCodes)
|
||||
{
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out _, out _))
|
||||
{
|
||||
// Cache and return the best (first working) code
|
||||
_cachedCodes[monitorId] = code;
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get best VCP code for color temperature control
|
||||
/// </summary>
|
||||
public byte? GetColorTemperatureVcpCode(string monitorId, IntPtr physicalHandle)
|
||||
{
|
||||
var cacheKey = $"{monitorId}_colortemp";
|
||||
|
||||
// Return cached best code if available
|
||||
if (_cachedCodes.TryGetValue(cacheKey, out var cachedCode))
|
||||
{
|
||||
return cachedCode;
|
||||
}
|
||||
|
||||
// Find first working VCP code (highest priority)
|
||||
foreach (var code in ColorTemperatureVcpCodes)
|
||||
{
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out _, out _))
|
||||
{
|
||||
// Cache and return the best (first working) code
|
||||
_cachedCodes[cacheKey] = code;
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Kelvin temperature to VCP value (uses unified converter)
|
||||
/// </summary>
|
||||
public uint ConvertKelvinToVcpValue(int kelvin, BrightnessInfo vcpRange)
|
||||
{
|
||||
return (uint)ColorTemperatureConverter.KelvinToVcp(kelvin, vcpRange.Maximum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current color temperature information
|
||||
/// </summary>
|
||||
public BrightnessInfo GetCurrentColorTemperature(IntPtr physicalHandle)
|
||||
{
|
||||
// Try different VCP codes to get color temperature
|
||||
foreach (var code in ColorTemperatureVcpCodes)
|
||||
{
|
||||
if (DdcCiNative.TryGetVCPFeature(physicalHandle, code, out uint current, out uint max))
|
||||
{
|
||||
return new BrightnessInfo((int)current, 0, (int)max);
|
||||
}
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all cached codes
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_cachedCodes.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
304
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
304
src/modules/powerdisplay/PowerDisplay/Native/NativeConstants.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace PowerDisplay.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows API constant definitions
|
||||
/// </summary>
|
||||
public static class NativeConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// VCP code: Brightness (primary usage)
|
||||
/// </summary>
|
||||
public const byte VcpCodeBrightness = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Contrast
|
||||
/// </summary>
|
||||
public const byte VcpCodeContrast = 0x12;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Backlight control (alternative brightness)
|
||||
/// </summary>
|
||||
public const byte VcpCodeBacklightControl = 0x13;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: White backlight level
|
||||
/// </summary>
|
||||
public const byte VcpCodeBacklightLevelWhite = 0x6B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio speaker volume
|
||||
/// </summary>
|
||||
public const byte VcpCodeVolume = 0x62;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Audio mute
|
||||
/// </summary>
|
||||
public const byte VcpCodeMute = 0x8D;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature request (主要色温控制)
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperature = 0x0C;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Color temperature increment (色温增量调节)
|
||||
/// </summary>
|
||||
public const byte VcpCodeColorTemperatureIncrement = 0x0B;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Gamma correction (Gamma调节)
|
||||
/// </summary>
|
||||
public const byte VcpCodeGamma = 0x72;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: Select color preset (颜色预设选择)
|
||||
/// </summary>
|
||||
public const byte VcpCodeSelectColorPreset = 0x14;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: VCP version
|
||||
/// </summary>
|
||||
public const byte VcpCodeVcpVersion = 0xDF;
|
||||
|
||||
/// <summary>
|
||||
/// VCP code: New control value
|
||||
/// </summary>
|
||||
public const byte VcpCodeNewControlValue = 0x02;
|
||||
|
||||
/// <summary>
|
||||
/// Display device attached to desktop
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceAttachedToDesktop = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-monitor primary display
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceMultiDriver = 0x00000002;
|
||||
|
||||
/// <summary>
|
||||
/// Primary device
|
||||
/// </summary>
|
||||
public const uint DisplayDevicePrimaryDevice = 0x00000004;
|
||||
|
||||
/// <summary>
|
||||
/// Mirroring driver
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceMirroringDriver = 0x00000008;
|
||||
|
||||
/// <summary>
|
||||
/// VGA compatible
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceVgaCompatible = 0x00000010;
|
||||
|
||||
/// <summary>
|
||||
/// Removable device
|
||||
/// </summary>
|
||||
public const uint DisplayDeviceRemovable = 0x00000020;
|
||||
|
||||
/// <summary>
|
||||
/// Get device interface name
|
||||
/// </summary>
|
||||
public const uint EddGetDeviceInterfaceName = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Primary monitor
|
||||
/// </summary>
|
||||
public const uint MonitorinfoFPrimary = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Query display config: only active paths
|
||||
/// </summary>
|
||||
public const uint QdcOnlyActivePaths = 0x00000002;
|
||||
|
||||
/// <summary>
|
||||
/// Query display config: all paths
|
||||
/// </summary>
|
||||
public const uint QdcAllPaths = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: apply
|
||||
/// </summary>
|
||||
public const uint SdcApply = 0x00000080;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: use supplied display config
|
||||
/// </summary>
|
||||
public const uint SdcUseSuppliedDisplayConfig = 0x00000020;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: save to database
|
||||
/// </summary>
|
||||
public const uint SdcSaveToDatabase = 0x00000200;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: topology supplied
|
||||
/// </summary>
|
||||
public const uint SdcTopologySupplied = 0x00000010;
|
||||
|
||||
/// <summary>
|
||||
/// Set display config: allow path order changes
|
||||
/// </summary>
|
||||
public const uint SdcAllowPathOrderChanges = 0x00002000;
|
||||
|
||||
/// <summary>
|
||||
/// Get target name
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoGetTargetName = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Get SDR white level
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoGetSdrWhiteLevel = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Get advanced color information
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoGetAdvancedColorInfo = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Set SDR white level (custom)
|
||||
/// </summary>
|
||||
public const uint DisplayconfigDeviceInfoSetSdrWhiteLevel = 0xFFFFFFEE;
|
||||
|
||||
/// <summary>
|
||||
/// Path active
|
||||
/// </summary>
|
||||
public const uint DisplayconfigPathActive = 0x00000001;
|
||||
|
||||
/// <summary>
|
||||
/// Path mode index invalid
|
||||
/// </summary>
|
||||
public const uint DisplayconfigPathModeIdxInvalid = 0xFFFFFFFF;
|
||||
|
||||
/// <summary>
|
||||
/// COM initialization: multithreaded
|
||||
/// </summary>
|
||||
public const uint CoinitMultithreaded = 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authentication level: connect
|
||||
/// </summary>
|
||||
public const uint RpcCAuthnLevelConnect = 2;
|
||||
|
||||
/// <summary>
|
||||
/// RPC impersonation level: impersonate
|
||||
/// </summary>
|
||||
public const uint RpcCImpLevelImpersonate = 3;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authentication service: Win NT
|
||||
/// </summary>
|
||||
public const uint RpcCAuthnWinnt = 10;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authorization service: none
|
||||
/// </summary>
|
||||
public const uint RpcCAuthzNone = 0;
|
||||
|
||||
/// <summary>
|
||||
/// RPC authentication level: call
|
||||
/// </summary>
|
||||
public const uint RpcCAuthnLevelCall = 3;
|
||||
|
||||
/// <summary>
|
||||
/// EOAC: none
|
||||
/// </summary>
|
||||
public const uint EoacNone = 0;
|
||||
|
||||
/// <summary>
|
||||
/// WMI flag: forward only
|
||||
/// </summary>
|
||||
public const long WbemFlagForwardOnly = 0x20;
|
||||
|
||||
/// <summary>
|
||||
/// WMI flag: return immediately
|
||||
/// </summary>
|
||||
public const long WbemFlagReturnImmediately = 0x10;
|
||||
|
||||
/// <summary>
|
||||
/// WMI flag: connect use max wait
|
||||
/// </summary>
|
||||
public const long WbemFlagConnectUseMaxWait = 0x80;
|
||||
|
||||
/// <summary>
|
||||
/// Success
|
||||
/// </summary>
|
||||
public const int ErrorSuccess = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Insufficient buffer
|
||||
/// </summary>
|
||||
public const int ErrorInsufficientBuffer = 122;
|
||||
|
||||
/// <summary>
|
||||
/// Invalid parameter
|
||||
/// </summary>
|
||||
public const int ErrorInvalidParameter = 87;
|
||||
|
||||
/// <summary>
|
||||
/// Access denied
|
||||
/// </summary>
|
||||
public const int ErrorAccessDenied = 5;
|
||||
|
||||
/// <summary>
|
||||
/// General failure
|
||||
/// </summary>
|
||||
public const int ErrorGenFailure = 31;
|
||||
|
||||
/// <summary>
|
||||
/// Unsupported VCP code
|
||||
/// </summary>
|
||||
public const int ErrorGraphicsDdcciVcpNotSupported = -1071243251;
|
||||
|
||||
/// <summary>
|
||||
/// Infinite wait
|
||||
/// </summary>
|
||||
public const uint Infinite = 0xFFFFFFFF;
|
||||
|
||||
/// <summary>
|
||||
/// User message
|
||||
/// </summary>
|
||||
public const uint WmUser = 0x0400;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: HDMI
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyHdmi = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: DVI
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyDvi = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: DisplayPort
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyDisplayportExternal = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Output technology: internal
|
||||
/// </summary>
|
||||
public const uint DisplayconfigOutputTechnologyInternal = 0x80000000;
|
||||
|
||||
/// <summary>
|
||||
/// HDR minimum SDR white level (nits)
|
||||
/// </summary>
|
||||
public const int HdrMinSdrWhiteLevel = 80;
|
||||
|
||||
/// <summary>
|
||||
/// HDR maximum SDR white level (nits)
|
||||
/// </summary>
|
||||
public const int HdrMaxSdrWhiteLevel = 480;
|
||||
|
||||
/// <summary>
|
||||
/// SDR white level conversion factor
|
||||
/// </summary>
|
||||
public const int SdrWhiteLevelFactor = 80;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 类型别名,兼容 Windows API 命名约定
|
||||
using RECT = PowerDisplay.Native.Rect;
|
||||
|
||||
namespace PowerDisplay.Native;
|
||||
|
||||
/// <summary>
|
||||
/// 委托类型定义
|
||||
/// </summary>
|
||||
public static class NativeDelegates
|
||||
{
|
||||
/// <summary>
|
||||
/// 显示器枚举过程委托
|
||||
/// </summary>
|
||||
/// <param name="hMonitor">显示器句柄</param>
|
||||
/// <param name="hdcMonitor">显示器 DC</param>
|
||||
/// <param name="lprcMonitor">显示器矩形指针</param>
|
||||
/// <param name="dwData">用户数据</param>
|
||||
/// <returns>继续枚举返回 true</returns>
|
||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||
|
||||
/// <summary>
|
||||
/// 线程启动例程委托
|
||||
/// </summary>
|
||||
/// <param name="lpParameter">线程参数</param>
|
||||
/// <returns>线程退出代码</returns>
|
||||
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
|
||||
}
|
||||
532
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
532
src/modules/powerdisplay/PowerDisplay/Native/NativeStructures.cs
Normal file
@@ -0,0 +1,532 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
#pragma warning disable SA1649 // File name should match first type name - Multiple related P/Invoke structures
|
||||
|
||||
namespace PowerDisplay.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// Physical monitor structure for DDC/CI
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public unsafe struct PhysicalMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Physical monitor handle
|
||||
/// </summary>
|
||||
public IntPtr HPhysicalMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Physical monitor description string - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzPhysicalMonitorDescription[128];
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get description as string
|
||||
/// </summary>
|
||||
public readonly string GetDescription()
|
||||
{
|
||||
fixed (ushort* ptr = SzPhysicalMonitorDescription)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Rect
|
||||
{
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
|
||||
public int Width => Right - Left;
|
||||
|
||||
public int Height => Bottom - Top;
|
||||
|
||||
public Rect(int left, int top, int right, int bottom)
|
||||
{
|
||||
Left = left;
|
||||
Top = top;
|
||||
Right = right;
|
||||
Bottom = bottom;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitor information extended structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public unsafe struct MonitorInfoEx
|
||||
{
|
||||
/// <summary>
|
||||
/// Structure size
|
||||
/// </summary>
|
||||
public uint CbSize;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor rectangle area
|
||||
/// </summary>
|
||||
public Rect RcMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Work area rectangle
|
||||
/// </summary>
|
||||
public Rect RcWork;
|
||||
|
||||
/// <summary>
|
||||
/// Flags
|
||||
/// </summary>
|
||||
public uint DwFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Device name (e.g., "\\.\DISPLAY1") - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzDevice[32];
|
||||
|
||||
/// <summary>
|
||||
/// Helper property to get device name as string
|
||||
/// </summary>
|
||||
public readonly string GetDeviceName()
|
||||
{
|
||||
fixed (ushort* ptr = SzDevice)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display device structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public unsafe struct DisplayDevice
|
||||
{
|
||||
/// <summary>
|
||||
/// Structure size
|
||||
/// </summary>
|
||||
public uint Cb;
|
||||
|
||||
/// <summary>
|
||||
/// Device name (e.g., "\\.\DISPLAY1\Monitor0") - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort DeviceName[32];
|
||||
|
||||
/// <summary>
|
||||
/// Device description string - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort DeviceString[128];
|
||||
|
||||
/// <summary>
|
||||
/// Status flags
|
||||
/// </summary>
|
||||
public uint StateFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Device ID - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort DeviceID[128];
|
||||
|
||||
/// <summary>
|
||||
/// Registry device key - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort DeviceKey[128];
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get device name as string
|
||||
/// </summary>
|
||||
public readonly string GetDeviceName()
|
||||
{
|
||||
fixed (ushort* ptr = DeviceName)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get device string as string
|
||||
/// </summary>
|
||||
public readonly string GetDeviceString()
|
||||
{
|
||||
fixed (ushort* ptr = DeviceString)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get device ID as string
|
||||
/// </summary>
|
||||
public readonly string GetDeviceID()
|
||||
{
|
||||
fixed (ushort* ptr = DeviceID)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get device key as string
|
||||
/// </summary>
|
||||
public readonly string GetDeviceKey()
|
||||
{
|
||||
fixed (ushort* ptr = DeviceKey)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LUID (Locally Unique Identifier) structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Luid
|
||||
{
|
||||
public uint LowPart;
|
||||
public int HighPart;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{HighPart:X8}:{LowPart:X8}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration path information
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_PATH_INFO
|
||||
{
|
||||
public DISPLAYCONFIG_PATH_SOURCE_INFO SourceInfo;
|
||||
public DISPLAYCONFIG_PATH_TARGET_INFO TargetInfo;
|
||||
public uint Flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration path source information
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_PATH_SOURCE_INFO
|
||||
{
|
||||
public Luid AdapterId;
|
||||
public uint Id;
|
||||
public uint ModeInfoIdx;
|
||||
public uint StatusFlags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration path target information
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_PATH_TARGET_INFO
|
||||
{
|
||||
public Luid AdapterId;
|
||||
public uint Id;
|
||||
public uint ModeInfoIdx;
|
||||
public uint OutputTechnology;
|
||||
public uint Rotation;
|
||||
public uint Scaling;
|
||||
public DISPLAYCONFIG_RATIONAL RefreshRate;
|
||||
public uint ScanLineOrdering;
|
||||
public bool TargetAvailable;
|
||||
public uint StatusFlags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration rational number
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_RATIONAL
|
||||
{
|
||||
public uint Numerator;
|
||||
public uint Denominator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration mode information
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_MODE_INFO
|
||||
{
|
||||
public uint InfoType;
|
||||
public uint Id;
|
||||
public Luid AdapterId;
|
||||
public DISPLAYCONFIG_MODE_INFO_UNION ModeInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration mode information union
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct DISPLAYCONFIG_MODE_INFO_UNION
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Native API structure field")]
|
||||
public DISPLAYCONFIG_TARGET_MODE targetMode;
|
||||
|
||||
[FieldOffset(0)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Native API structure field")]
|
||||
public DISPLAYCONFIG_SOURCE_MODE sourceMode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration target mode
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_TARGET_MODE
|
||||
{
|
||||
public DISPLAYCONFIG_VIDEO_SIGNAL_INFO TargetVideoSignalInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration source mode
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_SOURCE_MODE
|
||||
{
|
||||
public uint Width;
|
||||
public uint Height;
|
||||
public uint PixelFormat;
|
||||
public DISPLAYCONFIG_POINT Position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration point
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_POINT
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration video signal information
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_VIDEO_SIGNAL_INFO
|
||||
{
|
||||
public ulong PixelRate;
|
||||
public DISPLAYCONFIG_RATIONAL HSyncFreq;
|
||||
public DISPLAYCONFIG_RATIONAL VSyncFreq;
|
||||
public DISPLAYCONFIG_2DREGION ActiveSize;
|
||||
public DISPLAYCONFIG_2DREGION TotalSize;
|
||||
public uint VideoStandard;
|
||||
public uint ScanLineOrdering;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration 2D region
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_2DREGION
|
||||
{
|
||||
public uint Cx;
|
||||
public uint Cy;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration device information header
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_DEVICE_INFO_HEADER
|
||||
{
|
||||
public uint Type;
|
||||
public uint Size;
|
||||
public Luid AdapterId;
|
||||
public uint Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration target device name
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public unsafe struct DISPLAYCONFIG_TARGET_DEVICE_NAME
|
||||
{
|
||||
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||
public uint Flags;
|
||||
public uint OutputTechnology;
|
||||
public ushort EdidManufactureId;
|
||||
public ushort EdidProductCodeId;
|
||||
public uint ConnectorInstance;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor friendly name - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort MonitorFriendlyDeviceName[64];
|
||||
|
||||
/// <summary>
|
||||
/// Monitor device path - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort MonitorDevicePath[128];
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get monitor friendly name as string
|
||||
/// </summary>
|
||||
public readonly string GetMonitorFriendlyDeviceName()
|
||||
{
|
||||
fixed (ushort* ptr = MonitorFriendlyDeviceName)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to get monitor device path as string
|
||||
/// </summary>
|
||||
public readonly string GetMonitorDevicePath()
|
||||
{
|
||||
fixed (ushort* ptr = MonitorDevicePath)
|
||||
{
|
||||
return new string((char*)ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration SDR white level
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_SDR_WHITE_LEVEL
|
||||
{
|
||||
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||
public uint SDRWhiteLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display configuration advanced color information
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO
|
||||
{
|
||||
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||
public uint AdvancedColorSupported;
|
||||
public uint AdvancedColorEnabled;
|
||||
public uint BitsPerColorChannel;
|
||||
public uint ColorEncoding;
|
||||
public uint FormatSupport;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom structure for setting SDR white level
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DISPLAYCONFIG_SET_SDR_WHITE_LEVEL
|
||||
{
|
||||
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
|
||||
public uint SDRWhiteLevel;
|
||||
public byte FinalValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Point structure
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
|
||||
public POINT(int x, int y)
|
||||
{
|
||||
this.X = x;
|
||||
this.Y = y;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify icon data structure (for system tray)
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public unsafe struct NOTIFYICONDATA
|
||||
{
|
||||
public uint CbSize;
|
||||
public IntPtr HWnd;
|
||||
public uint UID;
|
||||
public uint UFlags;
|
||||
public uint UCallbackMessage;
|
||||
public IntPtr HIcon;
|
||||
|
||||
/// <summary>
|
||||
/// Tooltip text - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzTip[128];
|
||||
|
||||
public uint DwState;
|
||||
public uint DwStateMask;
|
||||
|
||||
/// <summary>
|
||||
/// Info balloon text - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzInfo[256];
|
||||
|
||||
public uint UTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Info balloon title - fixed buffer for LibraryImport compatibility
|
||||
/// </summary>
|
||||
public fixed ushort SzInfoTitle[64];
|
||||
|
||||
public uint DwInfoFlags;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to set tooltip text
|
||||
/// </summary>
|
||||
public void SetTip(string tip)
|
||||
{
|
||||
fixed (ushort* ptr = SzTip)
|
||||
{
|
||||
int length = Math.Min(tip.Length, 127);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
ptr[i] = tip[i];
|
||||
}
|
||||
|
||||
ptr[length] = 0; // Null terminator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to set info balloon text
|
||||
/// </summary>
|
||||
public void SetInfo(string info)
|
||||
{
|
||||
fixed (ushort* ptr = SzInfo)
|
||||
{
|
||||
int length = Math.Min(info.Length, 255);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
ptr[i] = info[i];
|
||||
}
|
||||
|
||||
ptr[length] = 0; // Null terminator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to set info balloon title
|
||||
/// </summary>
|
||||
public void SetInfoTitle(string title)
|
||||
{
|
||||
fixed (ushort* ptr = SzInfoTitle)
|
||||
{
|
||||
int length = Math.Min(title.Length, 63);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
ptr[i] = title[i];
|
||||
}
|
||||
|
||||
ptr[length] = 0; // Null terminator
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
src/modules/powerdisplay/PowerDisplay/Native/PInvoke.cs
Normal file
275
src/modules/powerdisplay/PowerDisplay/Native/PInvoke.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PowerDisplay.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// P/Invoke declarations using LibraryImport source generator
|
||||
/// </summary>
|
||||
internal static partial class PInvoke
|
||||
{
|
||||
// ==================== User32.dll - Window Management ====================
|
||||
// GetWindowLong: On 64-bit use GetWindowLongPtrW, on 32-bit use GetWindowLongW
|
||||
#if WIN64
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
internal static partial IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
#else
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongW")]
|
||||
internal static partial int GetWindowLong(IntPtr hWnd, int nIndex);
|
||||
#endif
|
||||
|
||||
// SetWindowLong: On 64-bit use SetWindowLongPtrW, on 32-bit use SetWindowLongW
|
||||
#if WIN64
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
internal static partial IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
||||
#else
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongW")]
|
||||
internal static partial int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
#endif
|
||||
|
||||
// SetWindowLongPtr: Always uses the Ptr variant (64-bit)
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
internal static partial IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetWindowPos(
|
||||
IntPtr hWnd,
|
||||
IntPtr hWndInsertAfter,
|
||||
int x,
|
||||
int y,
|
||||
int cx,
|
||||
int cy,
|
||||
uint uFlags);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
// ==================== User32.dll - Window Creation and Messaging ====================
|
||||
[LibraryImport("user32.dll", EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
internal static partial IntPtr CreateWindowEx(
|
||||
uint dwExStyle,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpWindowName,
|
||||
uint dwStyle,
|
||||
int x,
|
||||
int y,
|
||||
int nWidth,
|
||||
int nHeight,
|
||||
IntPtr hWndParent,
|
||||
IntPtr hMenu,
|
||||
IntPtr hInstance,
|
||||
IntPtr lpParam);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool DestroyWindow(IntPtr hWnd);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")]
|
||||
internal static partial IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
internal static partial uint RegisterWindowMessage([MarshalAs(UnmanagedType.LPWStr)] string lpString);
|
||||
|
||||
// ==================== User32.dll - Menu Functions ====================
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial IntPtr CreatePopupMenu();
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "AppendMenuW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool AppendMenu(
|
||||
IntPtr hMenu,
|
||||
uint uFlags,
|
||||
uint uIDNewItem,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpNewItem);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool DestroyMenu(IntPtr hMenu);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial int TrackPopupMenu(
|
||||
IntPtr hMenu,
|
||||
uint uFlags,
|
||||
int x,
|
||||
int y,
|
||||
int nReserved,
|
||||
IntPtr hWnd,
|
||||
IntPtr prcRect);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
// ==================== User32.dll - Display Configuration ====================
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial int GetDisplayConfigBufferSizes(
|
||||
uint flags,
|
||||
out uint numPathArrayElements,
|
||||
out uint numModeInfoArrayElements);
|
||||
|
||||
// With DisableRuntimeMarshalling, LibraryImport can handle struct arrays
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial int QueryDisplayConfig(
|
||||
uint flags,
|
||||
ref uint numPathArrayElements,
|
||||
[Out] DISPLAYCONFIG_PATH_INFO[] pathArray,
|
||||
ref uint numModeInfoArrayElements,
|
||||
[Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray,
|
||||
IntPtr currentTopologyId);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial int DisplayConfigGetDeviceInfo(
|
||||
ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName);
|
||||
|
||||
// ==================== User32.dll - Monitor Enumeration ====================
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool EnumDisplayMonitors(
|
||||
IntPtr hdc,
|
||||
IntPtr lprcClip,
|
||||
NativeDelegates.MonitorEnumProc lpfnEnum,
|
||||
IntPtr dwData);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetMonitorInfo(
|
||||
IntPtr hMonitor,
|
||||
ref MonitorInfoEx lpmi);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "EnumDisplayDevicesW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool EnumDisplayDevices(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? lpDevice,
|
||||
uint iDevNum,
|
||||
ref DisplayDevice lpDisplayDevice,
|
||||
uint dwFlags);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial IntPtr MonitorFromWindow(
|
||||
IntPtr hwnd,
|
||||
uint dwFlags);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
internal static partial IntPtr MonitorFromPoint(
|
||||
POINT pt,
|
||||
uint dwFlags);
|
||||
|
||||
// ==================== Shell32.dll - Tray Icon ====================
|
||||
[LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Shell_NotifyIcon(
|
||||
uint dwMessage,
|
||||
ref NOTIFYICONDATA lpData);
|
||||
|
||||
// ==================== Dxva2.dll - DDC/CI Monitor Control ====================
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetNumberOfPhysicalMonitorsFromHMONITOR(
|
||||
IntPtr hMonitor,
|
||||
out uint pdwNumberOfPhysicalMonitors);
|
||||
|
||||
// Use unsafe pointer to avoid ArraySubType limitation
|
||||
[LibraryImport("Dxva2.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static unsafe partial bool GetPhysicalMonitorsFromHMONITOR(
|
||||
IntPtr hMonitor,
|
||||
uint dwPhysicalMonitorArraySize,
|
||||
PhysicalMonitor* pPhysicalMonitorArray);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool DestroyPhysicalMonitor(IntPtr hMonitor);
|
||||
|
||||
// Use unsafe pointer to avoid LPArray limitation
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static unsafe partial bool DestroyPhysicalMonitors(
|
||||
uint dwPhysicalMonitorArraySize,
|
||||
PhysicalMonitor* pPhysicalMonitorArray);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetVCPFeatureAndVCPFeatureReply(
|
||||
IntPtr hPhysicalMonitor,
|
||||
byte bVCPCode,
|
||||
IntPtr pvct,
|
||||
out uint pdwCurrentValue,
|
||||
out uint pdwMaximumValue);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetVCPFeature(
|
||||
IntPtr hPhysicalMonitor,
|
||||
byte bVCPCode,
|
||||
uint dwNewValue);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SaveCurrentSettings(IntPtr hPhysicalMonitor);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetCapabilitiesStringLength(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwCapabilitiesStringLengthInCharacters);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool CapabilitiesRequestAndCapabilitiesReply(
|
||||
IntPtr hPhysicalMonitor,
|
||||
IntPtr pszASCIICapabilitiesString,
|
||||
uint dwCapabilitiesStringLengthInCharacters);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetMonitorBrightness(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwMinimumBrightness,
|
||||
out uint pdwCurrentBrightness,
|
||||
out uint pdwMaximumBrightness);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetMonitorBrightness(
|
||||
IntPtr hPhysicalMonitor,
|
||||
uint dwNewBrightness);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetMonitorContrast(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwMinimumContrast,
|
||||
out uint pdwCurrentContrast,
|
||||
out uint pdwMaximumContrast);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetMonitorContrast(
|
||||
IntPtr hPhysicalMonitor,
|
||||
uint dwNewContrast);
|
||||
|
||||
[LibraryImport("Dxva2.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetMonitorCapabilities(
|
||||
IntPtr hPhysicalMonitor,
|
||||
out uint pdwMonitorCapabilities,
|
||||
out uint pdwSupportedColorTemperatures);
|
||||
|
||||
// ==================== Kernel32.dll ====================
|
||||
[LibraryImport("kernel32.dll")]
|
||||
internal static partial uint GetLastError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using WmiLight;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.Native.WMI
|
||||
{
|
||||
/// <summary>
|
||||
/// WMI monitor controller for controlling internal laptop displays.
|
||||
/// Rewritten to use WmiLight library for Native AOT compatibility.
|
||||
/// </summary>
|
||||
public partial class WmiController : IMonitorController, IDisposable
|
||||
{
|
||||
private const string WmiNamespace = @"root\WMI";
|
||||
private const string BrightnessQueryClass = "WmiMonitorBrightness";
|
||||
private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
|
||||
private const string MonitorIdClass = "WmiMonitorID";
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public string Name => "WMI Monitor Controller (WmiLight)";
|
||||
|
||||
public MonitorType SupportedType => MonitorType.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Check if the specified monitor can be controlled
|
||||
/// </summary>
|
||||
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (monitor.Type != MonitorType.Internal)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
var query = $"SELECT * FROM {BrightnessQueryClass}";
|
||||
var results = connection.CreateQuery(query).ToList();
|
||||
return results.Count > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"WMI CanControlMonitor check failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get monitor brightness
|
||||
/// </summary>
|
||||
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
var query = $"SELECT CurrentBrightness FROM {BrightnessQueryClass}";
|
||||
var results = connection.CreateQuery(query);
|
||||
|
||||
foreach (var obj in results)
|
||||
{
|
||||
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
|
||||
return new BrightnessInfo(currentBrightness, 0, 100);
|
||||
}
|
||||
}
|
||||
catch (WmiException ex)
|
||||
{
|
||||
Logger.LogWarning($"WMI GetBrightness failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"WMI GetBrightness failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return BrightnessInfo.Invalid;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set monitor brightness
|
||||
/// </summary>
|
||||
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Validate brightness range
|
||||
brightness = Math.Clamp(brightness, 0, 100);
|
||||
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
var query = $"SELECT * FROM {BrightnessMethodClass}";
|
||||
var results = connection.CreateQuery(query);
|
||||
|
||||
foreach (var obj in results)
|
||||
{
|
||||
// Call WmiSetBrightness method
|
||||
// Parameters: Timeout (uint32), Brightness (byte)
|
||||
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
|
||||
using (WmiMethodParameters inParams = method.CreateInParameters())
|
||||
{
|
||||
inParams.SetPropertyValue("Timeout", 0u);
|
||||
inParams.SetPropertyValue("Brightness", (byte)brightness);
|
||||
|
||||
uint result = obj.ExecuteMethod<uint>(
|
||||
method,
|
||||
inParams,
|
||||
out WmiMethodParameters outParams);
|
||||
|
||||
// Check return value (0 indicates success)
|
||||
if (result == 0)
|
||||
{
|
||||
return MonitorOperationResult.Success();
|
||||
}
|
||||
else
|
||||
{
|
||||
return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MonitorOperationResult.Failure("No WMI brightness methods found");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
|
||||
}
|
||||
catch (WmiException ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"WMI error: {ex.Message}", ex.HResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MonitorOperationResult.Failure($"Unexpected error: {ex.Message}");
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover supported monitors
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
var monitors = new List<Monitor>();
|
||||
|
||||
try
|
||||
{
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
|
||||
// First check if WMI brightness support is available
|
||||
var brightnessQuery = $"SELECT * FROM {BrightnessQueryClass}";
|
||||
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
|
||||
|
||||
if (brightnessResults.Count == 0)
|
||||
{
|
||||
return monitors;
|
||||
}
|
||||
|
||||
// Get monitor information
|
||||
var idQuery = $"SELECT * FROM {MonitorIdClass}";
|
||||
var idResults = connection.CreateQuery(idQuery).ToList();
|
||||
|
||||
var monitorInfos = new Dictionary<string, (string Name, string InstanceName)>();
|
||||
|
||||
foreach (var obj in idResults)
|
||||
{
|
||||
try
|
||||
{
|
||||
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
|
||||
var userFriendlyName = GetUserFriendlyName(obj) ?? "Internal Display";
|
||||
|
||||
if (!string.IsNullOrEmpty(instanceName))
|
||||
{
|
||||
monitorInfos[instanceName] = (userFriendlyName, instanceName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Skip problematic entries
|
||||
Logger.LogDebug($"Failed to parse WMI monitor info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Create monitor objects for each supported brightness instance
|
||||
foreach (var obj in brightnessResults)
|
||||
{
|
||||
try
|
||||
{
|
||||
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
|
||||
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
|
||||
|
||||
var name = "Internal Display";
|
||||
if (monitorInfos.TryGetValue(instanceName, out var info))
|
||||
{
|
||||
name = info.Name;
|
||||
}
|
||||
|
||||
var monitor = new Monitor
|
||||
{
|
||||
Id = $"WMI_{instanceName}",
|
||||
Name = name,
|
||||
Type = MonitorType.Internal,
|
||||
CurrentBrightness = currentBrightness,
|
||||
MinBrightness = 0,
|
||||
MaxBrightness = 100,
|
||||
IsAvailable = true,
|
||||
InstanceName = instanceName,
|
||||
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
|
||||
ConnectionType = "Internal",
|
||||
CommunicationMethod = "WMI",
|
||||
Manufacturer = "Internal",
|
||||
SupportsColorTemperature = false,
|
||||
};
|
||||
|
||||
monitors.Add(monitor);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Skip problematic monitors
|
||||
Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WmiException ex)
|
||||
{
|
||||
// Return empty list instead of throwing exception
|
||||
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return monitors;
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate monitor connection status
|
||||
/// </summary>
|
||||
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Task.Run(
|
||||
() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to read current brightness to validate connection
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
var query = $"SELECT CurrentBrightness FROM {BrightnessQueryClass} WHERE InstanceName='{monitor.InstanceName}'";
|
||||
var results = connection.CreateQuery(query).ToList();
|
||||
return results.Count > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"WMI ValidateConnection failed for {monitor.InstanceName}: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user-friendly name from WMI object
|
||||
/// </summary>
|
||||
private static string? GetUserFriendlyName(WmiObject monitorObject)
|
||||
{
|
||||
try
|
||||
{
|
||||
// WmiLight returns arrays as object arrays
|
||||
var userFriendlyNameObj = monitorObject.GetPropertyValue<object>("UserFriendlyName");
|
||||
|
||||
if (userFriendlyNameObj is ushort[] userFriendlyName && userFriendlyName.Length > 0)
|
||||
{
|
||||
// Convert UINT16 array to string
|
||||
var chars = userFriendlyName
|
||||
.Where(c => c != 0)
|
||||
.Select(c => (char)c)
|
||||
.ToArray();
|
||||
|
||||
if (chars.Length > 0)
|
||||
{
|
||||
return new string(chars).Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore conversion errors
|
||||
Logger.LogDebug($"Failed to parse UserFriendlyName: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check WMI service availability
|
||||
/// </summary>
|
||||
public static bool IsWmiAvailable()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
var query = $"SELECT * FROM {BrightnessQueryClass}";
|
||||
var results = connection.CreateQuery(query).ToList();
|
||||
return results.Count > 0;
|
||||
}
|
||||
catch (WmiException ex) when (ex.HResult == 0x1068)
|
||||
{
|
||||
// Expected on systems without WMI brightness support (desktops, some laptops)
|
||||
Logger.LogInfo("WMI brightness control not supported on this system (expected for desktops)");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Unexpected error during WMI check
|
||||
Logger.LogDebug($"WMI availability check failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if administrator privileges are required
|
||||
/// </summary>
|
||||
public static bool RequiresElevation()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var connection = new WmiConnection(WmiNamespace);
|
||||
var query = $"SELECT * FROM {BrightnessMethodClass}";
|
||||
var results = connection.CreateQuery(query).ToList();
|
||||
|
||||
foreach (var obj in results)
|
||||
{
|
||||
// Try to call method to check permissions
|
||||
try
|
||||
{
|
||||
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
|
||||
using (WmiMethodParameters inParams = method.CreateInParameters())
|
||||
{
|
||||
inParams.SetPropertyValue("Timeout", 0u);
|
||||
inParams.SetPropertyValue("Brightness", (byte)50);
|
||||
obj.ExecuteMethod<uint>(method, inParams, out WmiMethodParameters outParams);
|
||||
return false; // If successful, no elevation required
|
||||
}
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return true; // Administrator privileges required
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Other errors may not be permission issues
|
||||
Logger.LogDebug($"WMI RequiresElevation check error: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Cannot determine, assume privileges required
|
||||
Logger.LogWarning($"WMI RequiresElevation check failed: {ex.Message}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extended features not supported by WMI
|
||||
public Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(BrightnessInfo.Invalid);
|
||||
}
|
||||
|
||||
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(MonitorOperationResult.Failure("Contrast control not supported via WMI"));
|
||||
}
|
||||
|
||||
public Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(BrightnessInfo.Invalid);
|
||||
}
|
||||
|
||||
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(MonitorOperationResult.Failure("Volume control not supported via WMI"));
|
||||
}
|
||||
|
||||
public Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(BrightnessInfo.Invalid);
|
||||
}
|
||||
|
||||
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(MonitorOperationResult.Failure("Color temperature control not supported via WMI"));
|
||||
}
|
||||
|
||||
public Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(MonitorOperationResult.Failure("Save settings not supported via WMI"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed && disposing)
|
||||
{
|
||||
// WmiLight objects are automatically cleaned up, no specific cleanup needed here
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
170
src/modules/powerdisplay/PowerDisplay/Native/WindowHelper.cs
Normal file
170
src/modules/powerdisplay/PowerDisplay/Native/WindowHelper.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
|
||||
namespace PowerDisplay.Native
|
||||
{
|
||||
internal static class WindowHelper
|
||||
{
|
||||
// Window Styles
|
||||
private const int GwlStyle = -16;
|
||||
private const int WsCaption = 0x00C00000;
|
||||
private const int WsThickframe = 0x00040000;
|
||||
private const int WsMinimizebox = 0x00020000;
|
||||
private const int WsMaximizebox = 0x00010000;
|
||||
private const int WsSysmenu = 0x00080000;
|
||||
|
||||
// Extended Window Styles
|
||||
private const int GwlExstyle = -20;
|
||||
private const int WsExDlgmodalframe = 0x00000001;
|
||||
private const int WsExWindowedge = 0x00000100;
|
||||
private const int WsExClientedge = 0x00000200;
|
||||
private const int WsExStaticedge = 0x00020000;
|
||||
private const int WsExToolwindow = 0x00000080;
|
||||
|
||||
// Window Messages
|
||||
private const int WmNclbuttondown = 0x00A1;
|
||||
private const int WmSyscommand = 0x0112;
|
||||
private const int ScMove = 0xF010;
|
||||
|
||||
private const uint SwpNosize = 0x0001;
|
||||
private const uint SwpNomove = 0x0002;
|
||||
private const uint SwpFramechanged = 0x0020;
|
||||
private static readonly IntPtr HwndTopmost = new IntPtr(-1);
|
||||
private static readonly IntPtr HwndNotopmost = new IntPtr(-2);
|
||||
|
||||
// ShowWindow commands
|
||||
private const int SwHide = 0;
|
||||
private const int SwShow = 5;
|
||||
private const int SwMinimize = 6;
|
||||
private const int SwRestore = 9;
|
||||
|
||||
/// <summary>
|
||||
/// 禁用窗口的拖动和缩放功能
|
||||
/// </summary>
|
||||
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
|
||||
{
|
||||
// 获取当前窗口样式
|
||||
#if WIN64
|
||||
int style = (int)GetWindowLong(hWnd, GwlStyle);
|
||||
#else
|
||||
int style = GetWindowLong(hWnd, GwlStyle);
|
||||
#endif
|
||||
|
||||
// 移除可调整大小的边框、标题栏和系统菜单
|
||||
style &= ~WsThickframe;
|
||||
style &= ~WsMaximizebox;
|
||||
style &= ~WsMinimizebox;
|
||||
style &= ~WsCaption; // 移除整个标题栏
|
||||
style &= ~WsSysmenu; // 移除系统菜单
|
||||
|
||||
// 设置新的窗口样式
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlStyle, style);
|
||||
#endif
|
||||
|
||||
// 获取扩展样式并移除相关边框
|
||||
#if WIN64
|
||||
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||
#else
|
||||
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||
#endif
|
||||
exStyle &= ~WsExDlgmodalframe;
|
||||
exStyle &= ~WsExWindowedge;
|
||||
exStyle &= ~WsExClientedge;
|
||||
exStyle &= ~WsExStaticedge;
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
#endif
|
||||
|
||||
// 刷新窗口框架
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize | SwpFramechanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口是否置顶
|
||||
/// </summary>
|
||||
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
|
||||
{
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
topmost ? HwndTopmost : HwndNotopmost,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示或隐藏窗口
|
||||
/// </summary>
|
||||
public static void ShowWindow(IntPtr hWnd, bool show)
|
||||
{
|
||||
PInvoke.ShowWindow(hWnd, show ? SwShow : SwHide);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最小化窗口
|
||||
/// </summary>
|
||||
public static void MinimizeWindow(IntPtr hWnd)
|
||||
{
|
||||
PInvoke.ShowWindow(hWnd, SwMinimize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 恢复窗口
|
||||
/// </summary>
|
||||
public static void RestoreWindow(IntPtr hWnd)
|
||||
{
|
||||
PInvoke.ShowWindow(hWnd, SwRestore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置窗口不在任务栏显示
|
||||
/// </summary>
|
||||
public static void HideFromTaskbar(IntPtr hWnd)
|
||||
{
|
||||
// 获取当前扩展样式
|
||||
#if WIN64
|
||||
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
|
||||
#else
|
||||
int exStyle = GetWindowLong(hWnd, GwlExstyle);
|
||||
#endif
|
||||
|
||||
// 添加 WS_EX_TOOLWINDOW 样式,这会让窗口不在任务栏显示
|
||||
exStyle |= WsExToolwindow;
|
||||
|
||||
// 设置新的扩展样式
|
||||
#if WIN64
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
|
||||
#else
|
||||
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
|
||||
#endif
|
||||
|
||||
// 刷新窗口框架
|
||||
SetWindowPos(
|
||||
hWnd,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SwpNomove | SwpNosize | SwpFramechanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
86
src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj
Normal file
@@ -0,0 +1,86 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.SelfContained.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PowerDisplay</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<AssemblyName>PowerToys.PowerDisplay</AssemblyName>
|
||||
<ApplicationIcon>Assets\PowerDisplay.ico</ApplicationIcon>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>PowerToys.PowerDisplay.pri</ProjectPriFileName>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Disable XAML-generated Main method, use custom Program.cs -->
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<!-- Native AOT Configuration -->
|
||||
<PropertyGroup>
|
||||
<PublishSingleFile>false</PublishSingleFile>
|
||||
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
|
||||
<PublishAot>true</PublishAot>
|
||||
<TrimmerSingleWarn>false</TrimmerSingleWarn>
|
||||
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Page Remove="PowerDisplayXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="PowerDisplayXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="PowerDisplayXAML\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="WmiLight" />
|
||||
<PackageReference Include="System.Collections.Immutable" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally -->
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
<!-- Copy Assets folder to output directory -->
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application x:Class="PowerDisplay.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:PowerDisplay.Converters"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- WinUI 3 System Resources -->
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- Converters -->
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
|
||||
<converters:InverseBoolConverter x:Key="InverseBoolConverter"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,540 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using PowerDisplay.Helpers;
|
||||
using PowerDisplay.Serialization;
|
||||
|
||||
namespace PowerDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// PowerDisplay application main class
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
private Window? _mainWindow;
|
||||
private int _powerToysRunnerPid;
|
||||
private string _pipeUuid = string.Empty;
|
||||
private static Mutex? _mutex;
|
||||
|
||||
// Bidirectional named pipes for IPC
|
||||
private static System.IO.Pipes.NamedPipeClientStream? _readPipe; // Read from ModuleInterface (OUT pipe)
|
||||
private static System.IO.Pipes.NamedPipeClientStream? _writePipe; // Write to ModuleInterface (IN pipe)
|
||||
private static Thread? _messageReceiverThread;
|
||||
private static bool _stopReceiver;
|
||||
|
||||
/// <summary>
|
||||
/// Sends IPC message to Settings UI via ModuleInterface
|
||||
/// </summary>
|
||||
public static void SendIPCMessage(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_writePipe != null && _writePipe.IsConnected)
|
||||
{
|
||||
var writer = new System.IO.StreamWriter(_writePipe) { AutoFlush = true };
|
||||
writer.WriteLine(message);
|
||||
Logger.LogTrace($"Sent IPC message: {message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Cannot send IPC message: pipe not connected");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to send IPC message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public App(int runnerPid, string pipeUuid)
|
||||
{
|
||||
_powerToysRunnerPid = runnerPid;
|
||||
_pipeUuid = pipeUuid;
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
// Ensure types used in XAML are preserved for AOT compilation
|
||||
TypePreservation.PreserveTypes();
|
||||
|
||||
// Initialize Logger
|
||||
Logger.InitializeLogger("\\PowerDisplay\\Logs");
|
||||
|
||||
// Initialize PowerToys telemetry
|
||||
try
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Telemetry errors should not crash the app
|
||||
}
|
||||
|
||||
// Initialize language settings
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
if (!string.IsNullOrEmpty(appLanguage))
|
||||
{
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||
}
|
||||
|
||||
// Handle unhandled exceptions
|
||||
this.UnhandledException += OnUnhandledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle unhandled exceptions
|
||||
/// </summary>
|
||||
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
// Try to display error information
|
||||
ShowStartupError(e.Exception);
|
||||
|
||||
// Mark exception as handled to prevent app crash
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the application is launched
|
||||
/// </summary>
|
||||
/// <param name="args">Launch arguments</param>
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use Mutex to ensure only one PowerDisplay instance is running
|
||||
_mutex = new Mutex(true, "PowerDisplay", out bool isNewInstance);
|
||||
|
||||
if (!isNewInstance)
|
||||
{
|
||||
// PowerDisplay is already running, exit current instance
|
||||
Logger.LogInfo("PowerDisplay is already running. Exiting duplicate instance.");
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure Mutex is released when app exits
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => _mutex?.ReleaseMutex();
|
||||
|
||||
// Parse command line arguments
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
{
|
||||
// Support two formats: direct PID or --pid PID
|
||||
int pidValue = -1;
|
||||
|
||||
// Check if using --pid format
|
||||
for (int i = 1; i < cmdArgs.Length - 1; i++)
|
||||
{
|
||||
if (cmdArgs[i] == "--pid" && int.TryParse(cmdArgs[i + 1], out pidValue))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not --pid format, try parsing last argument (compatible with old format)
|
||||
if (pidValue == -1 && cmdArgs.Length > 1)
|
||||
{
|
||||
_ = int.TryParse(cmdArgs[cmdArgs.Length - 1], out pidValue);
|
||||
}
|
||||
|
||||
if (pidValue > 0)
|
||||
{
|
||||
_powerToysRunnerPid = pidValue;
|
||||
|
||||
// Started from PowerToys Runner
|
||||
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
|
||||
|
||||
// Monitor parent process
|
||||
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
|
||||
{
|
||||
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
|
||||
ForceExit();
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standalone mode
|
||||
Logger.LogInfo("PowerDisplay started detached from PowerToys Runner.");
|
||||
_powerToysRunnerPid = -1;
|
||||
}
|
||||
|
||||
// Initialize IPC in background (non-blocking)
|
||||
// Only connect pipes when launched from PowerToys (not standalone)
|
||||
if (!string.IsNullOrEmpty(_pipeUuid) && _powerToysRunnerPid != -1)
|
||||
{
|
||||
// Async pipe connection in background - don't block UI thread
|
||||
_ = Task.Run(() => InitializeBidirectionalPipes(_pipeUuid));
|
||||
Logger.LogInfo("Starting IPC pipe connection in background");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("Running in standalone mode, IPC disabled");
|
||||
}
|
||||
|
||||
// Create main window but don't activate, window will auto-hide after initialization
|
||||
_mainWindow = new MainWindow();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowStartupError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize bidirectional named pipes for IPC with ModuleInterface
|
||||
/// </summary>
|
||||
private void InitializeBidirectionalPipes(string pipeUuid)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Pipe names based on UUID from ModuleInterface
|
||||
string pipeNameIn = $"powertoys_powerdisplay_{pipeUuid}_in"; // Write to this (ModuleInterface reads)
|
||||
string pipeNameOut = $"powertoys_powerdisplay_{pipeUuid}_out"; // Read from this (ModuleInterface writes)
|
||||
|
||||
Logger.LogInfo($"Connecting to pipes: IN={pipeNameIn}, OUT={pipeNameOut}");
|
||||
|
||||
// Connect to write pipe (IN pipe from ModuleInterface perspective)
|
||||
_writePipe = new System.IO.Pipes.NamedPipeClientStream(
|
||||
".",
|
||||
pipeNameIn,
|
||||
System.IO.Pipes.PipeDirection.Out);
|
||||
_writePipe.Connect(2000); // 2 second timeout (reduced from 5s, we're in background thread)
|
||||
|
||||
// Connect to read pipe (OUT pipe from ModuleInterface perspective)
|
||||
_readPipe = new System.IO.Pipes.NamedPipeClientStream(
|
||||
".",
|
||||
pipeNameOut,
|
||||
System.IO.Pipes.PipeDirection.In);
|
||||
_readPipe.Connect(2000); // 2 second timeout (reduced from 5s)
|
||||
|
||||
Logger.LogInfo("Successfully connected to bidirectional pipes");
|
||||
|
||||
// Start message receiver thread
|
||||
_stopReceiver = false;
|
||||
_messageReceiverThread = new Thread(MessageReceiverThreadProc)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "PowerDisplay IPC Receiver",
|
||||
};
|
||||
_messageReceiverThread.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize bidirectional pipes: {ex.Message}. App will continue in standalone mode.");
|
||||
|
||||
// Clean up on failure
|
||||
try
|
||||
{
|
||||
_writePipe?.Dispose();
|
||||
_readPipe?.Dispose();
|
||||
_writePipe = null;
|
||||
_readPipe = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message receiver thread procedure
|
||||
/// </summary>
|
||||
private static void MessageReceiverThreadProc()
|
||||
{
|
||||
Logger.LogInfo("Message receiver thread started");
|
||||
|
||||
try
|
||||
{
|
||||
if (_readPipe == null || !_readPipe.IsConnected)
|
||||
{
|
||||
Logger.LogError("Read pipe is not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
var reader = new System.IO.StreamReader(_readPipe);
|
||||
|
||||
while (!_stopReceiver && _readPipe.IsConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
string? message = reader.ReadLine();
|
||||
if (message != null)
|
||||
{
|
||||
OnIPCMessageReceived(message);
|
||||
}
|
||||
}
|
||||
catch (System.IO.IOException)
|
||||
{
|
||||
// Pipe disconnected
|
||||
Logger.LogWarning("Pipe disconnected");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error reading from pipe: {ex.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Message receiver thread error: {ex.Message}");
|
||||
}
|
||||
|
||||
Logger.LogInfo("Message receiver thread exiting");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle IPC messages received from ModuleInterface/Settings UI
|
||||
/// </summary>
|
||||
private static void OnIPCMessageReceived(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Received IPC message: {message}");
|
||||
|
||||
// Parse JSON message and handle commands (using source-generated context for AOT)
|
||||
// Expected format: {"action": "command_name", ...}
|
||||
var ipcMessage = System.Text.Json.JsonSerializer.Deserialize(message, AppJsonContext.Default.IPCMessageAction);
|
||||
if (ipcMessage?.Action != null)
|
||||
{
|
||||
string action = ipcMessage.Action;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "show_window":
|
||||
Logger.LogInfo("Received show_window command");
|
||||
|
||||
// TODO: Implement window show logic
|
||||
break;
|
||||
|
||||
case "toggle_window":
|
||||
Logger.LogInfo("Received toggle_window command");
|
||||
|
||||
// TODO: Implement window toggle logic
|
||||
break;
|
||||
|
||||
case "refresh_monitors":
|
||||
Logger.LogInfo("Received refresh_monitors command");
|
||||
|
||||
// TODO: Implement monitor refresh logic
|
||||
break;
|
||||
|
||||
case "settings_updated":
|
||||
Logger.LogInfo("Received settings_updated command");
|
||||
|
||||
// TODO: Implement settings update logic
|
||||
break;
|
||||
|
||||
case "terminate":
|
||||
Logger.LogInfo("Received terminate command");
|
||||
|
||||
// TODO: Implement graceful shutdown
|
||||
break;
|
||||
|
||||
default:
|
||||
Logger.LogWarning($"Unknown action received: {action}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error processing IPC message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show startup error
|
||||
/// </summary>
|
||||
private void ShowStartupError(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogError($"PowerDisplay startup failed: {ex.Message}");
|
||||
|
||||
var errorWindow = new Window { Title = "PowerDisplay - Startup Error" };
|
||||
var panel = new StackPanel { Margin = new Thickness(20), Spacing = 16 };
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "PowerDisplay Startup Failed",
|
||||
FontSize = 20,
|
||||
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
|
||||
});
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"Error: {ex.Message}",
|
||||
FontSize = 14,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"Details:\n{ex}",
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new SolidColorBrush(Microsoft.UI.Colors.Gray),
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
});
|
||||
|
||||
var closeButton = new Button
|
||||
{
|
||||
Content = "Close",
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 10, 0, 0),
|
||||
};
|
||||
closeButton.Click += (_, _) => errorWindow.Close();
|
||||
panel.Children.Add(closeButton);
|
||||
|
||||
errorWindow.Content = new ScrollViewer
|
||||
{
|
||||
Content = panel,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
MaxHeight = 600,
|
||||
MaxWidth = 800,
|
||||
};
|
||||
|
||||
errorWindow.Activate();
|
||||
}
|
||||
catch
|
||||
{
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main window instance
|
||||
/// </summary>
|
||||
public Window? MainWindow => _mainWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Check if running standalone (not launched from PowerToys Runner)
|
||||
/// </summary>
|
||||
public bool IsRunningDetachedFromPowerToys()
|
||||
{
|
||||
return _powerToysRunnerPid == -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quick cleanup when application exits
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Start timeout mechanism, ensure exit within 1 second
|
||||
var timeoutTimer = new System.Threading.Timer(
|
||||
_ =>
|
||||
{
|
||||
Logger.LogWarning("Shutdown timeout reached, forcing exit");
|
||||
Environment.Exit(0);
|
||||
},
|
||||
null,
|
||||
1000,
|
||||
System.Threading.Timeout.Infinite);
|
||||
|
||||
// Immediately notify MainWindow that program is exiting, enable fast shutdown mode
|
||||
if (_mainWindow is MainWindow mainWindow)
|
||||
{
|
||||
mainWindow.SetExiting();
|
||||
mainWindow.FastShutdown();
|
||||
}
|
||||
|
||||
_mainWindow = null;
|
||||
|
||||
// Clean up IPC pipes
|
||||
try
|
||||
{
|
||||
_stopReceiver = true;
|
||||
_messageReceiverThread?.Join(1000); // Wait max 1 second
|
||||
|
||||
_readPipe?.Close();
|
||||
_readPipe?.Dispose();
|
||||
_readPipe = null;
|
||||
|
||||
_writePipe?.Close();
|
||||
_writePipe?.Dispose();
|
||||
_writePipe = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore IPC cleanup errors
|
||||
}
|
||||
|
||||
// Immediately release Mutex
|
||||
_mutex?.ReleaseMutex();
|
||||
_mutex?.Dispose();
|
||||
_mutex = null;
|
||||
|
||||
// Cancel timeout timer
|
||||
timeoutTimer?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors, ensure exit
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force exit application, ensure complete termination
|
||||
/// </summary>
|
||||
private void ForceExit()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Immediately start timeout mechanism, must exit within 500ms
|
||||
var emergencyTimer = new System.Threading.Timer(
|
||||
_ =>
|
||||
{
|
||||
Logger.LogWarning("Emergency exit timeout reached, terminating process");
|
||||
Environment.Exit(0);
|
||||
},
|
||||
null,
|
||||
500,
|
||||
System.Threading.Timeout.Infinite);
|
||||
|
||||
PerformForceExit();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If all other methods fail, immediately force exit process
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform fast exit operation
|
||||
/// </summary>
|
||||
private void PerformForceExit()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Fast shutdown
|
||||
Shutdown();
|
||||
|
||||
// Immediately exit
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ensure exit
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window x:Class="PowerDisplay.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:PowerDisplay.ViewModels"
|
||||
xmlns:converters="using:PowerDisplay.Converters"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:animations="using:CommunityToolkit.WinUI.Animations">
|
||||
<Grid x:Name="RootGrid">
|
||||
<Grid.RenderTransform>
|
||||
<TranslateTransform x:Name="RootGridTransform" X="0" />
|
||||
</Grid.RenderTransform>
|
||||
|
||||
<Grid.Resources>
|
||||
<!-- Enhanced Slide-in animation storyboard -->
|
||||
<Storyboard x:Key="SlideInStoryboard">
|
||||
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
From="300"
|
||||
To="0"
|
||||
Duration="0:0:0.4">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="RootGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0"
|
||||
To="1"
|
||||
Duration="0:0:0.3" />
|
||||
<DoubleAnimation Storyboard.TargetName="MainContainer"
|
||||
Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
|
||||
From="20"
|
||||
To="0"
|
||||
Duration="0:0:0.5">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
|
||||
<!-- Enhanced Slide-out animation storyboard -->
|
||||
<Storyboard x:Key="SlideOutStoryboard">
|
||||
<DoubleAnimation Storyboard.TargetName="RootGridTransform"
|
||||
Storyboard.TargetProperty="X"
|
||||
From="0"
|
||||
To="300"
|
||||
Duration="0:0:0.3">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<CubicEase EasingMode="EaseIn" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="RootGrid"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="1"
|
||||
To="0"
|
||||
Duration="0:0:0.2" />
|
||||
</Storyboard>
|
||||
</Grid.Resources>
|
||||
|
||||
<!-- Main Container with modern design -->
|
||||
<Border x:Name="MainContainer"
|
||||
CornerRadius="8"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Margin="0"
|
||||
MaxWidth="640"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top">
|
||||
<Border.RenderTransform>
|
||||
<TranslateTransform Y="0" />
|
||||
</Border.RenderTransform>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Content Area -->
|
||||
<ScrollViewer Grid.Row="0"
|
||||
ZoomMode="Disabled"
|
||||
HorizontalScrollMode="Disabled"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
MaxHeight="420"
|
||||
Padding="8">
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
|
||||
<!-- Loading State with modern progress -->
|
||||
<StackPanel Orientation="Vertical"
|
||||
Spacing="16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.IsScanning), Mode=OneWay}">
|
||||
<ProgressRing IsActive="True"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Foreground="{ThemeResource AccentFillColorDefaultBrush}" />
|
||||
<TextBlock x:Name="ScanningMonitorsTextBlock"
|
||||
FontSize="12"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- No Monitors State with InfoBar -->
|
||||
<InfoBar x:Name="NoMonitorsInfoBar"
|
||||
IsOpen="{x:Bind ViewModel.ShowNoMonitorsMessage, Mode=OneWay}"
|
||||
Severity="Informational"
|
||||
IsClosable="False"
|
||||
Background="{ThemeResource InfoBarInformationalSeverityBackgroundBrush}">
|
||||
<InfoBar.IconSource>
|
||||
<FontIconSource Glyph="" />
|
||||
</InfoBar.IconSource>
|
||||
<TextBlock x:Name="NoMonitorsTextBlock" />
|
||||
</InfoBar>
|
||||
|
||||
<!-- Monitors List with modern card design -->
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.HasMonitors), Mode=OneWay}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Vertical"
|
||||
Spacing="0">
|
||||
<StackPanel.ChildrenTransitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="20" />
|
||||
<RepositionThemeTransition IsStaggeringEnabled="True" />
|
||||
</TransitionCollection>
|
||||
</StackPanel.ChildrenTransitions>
|
||||
</StackPanel>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MonitorViewModel">
|
||||
<StackPanel Spacing="2" HorizontalAlignment="Stretch">
|
||||
<StackPanel.ChildrenTransitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="8" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</StackPanel.ChildrenTransitions>
|
||||
|
||||
<!-- Monitor Name with Icon -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Padding="12,8">
|
||||
<FontIcon Glyph="{x:Bind MonitorIconGlyph, Mode=OneWay}"
|
||||
FontSize="20"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock Text="{x:Bind Name, Mode=OneWay}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Brightness Control -->
|
||||
<Grid Height="40" HorizontalAlignment="Stretch">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,2"
|
||||
Minimum="{x:Bind MinBrightness, Mode=OneWay}"
|
||||
Maximum="{x:Bind MaxBrightness, Mode=OneWay}"
|
||||
Value="{x:Bind Brightness, Mode=OneWay}"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="Brightness"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Brightness, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Color Temperature Control -->
|
||||
<Grid Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ShowColorTemperature), Mode=OneWay}">
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,2"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{x:Bind ColorTemperaturePercent, Mode=OneWay}"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="ColorTemperature"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Run Text="{x:Bind ColorTemperature, Mode=OneWay}" />
|
||||
<Run Text="K" FontSize="9" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<!-- Contrast Control -->
|
||||
<Grid Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ShowContrast), Mode=OneWay}">
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,2"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="{x:Bind ContrastPercent, Mode=OneWay}"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="Contrast"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Run Text="{x:Bind Contrast, Mode=OneWay}" />
|
||||
<Run Text="%" FontSize="9" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<!-- Volume Control -->
|
||||
<Grid Height="40"
|
||||
HorizontalAlignment="Stretch"
|
||||
Visibility="{x:Bind ConvertBoolToVisibility(ShowVolume), Mode=OneWay}">
|
||||
<Grid.Transitions>
|
||||
<TransitionCollection>
|
||||
<EntranceThemeTransition FromVerticalOffset="10" />
|
||||
<RepositionThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
<ColumnDefinition Width="7*"/>
|
||||
<ColumnDefinition Width="2*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
FontSize="14"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center" />
|
||||
|
||||
<Slider Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,2"
|
||||
Minimum="{x:Bind MinVolume, Mode=OneWay}"
|
||||
Maximum="{x:Bind MaxVolume, Mode=OneWay}"
|
||||
Value="{x:Bind Volume, Mode=OneWay}"
|
||||
ValueChanged="Slider_ValueChanged"
|
||||
PointerCaptureLost="Slider_PointerCaptureLost"
|
||||
Tag="Volume"
|
||||
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}" />
|
||||
|
||||
<TextBlock Grid.Column="2"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Run Text="{x:Bind Volume, Mode=OneWay}" />
|
||||
<Run Text="%" FontSize="9" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Status Bar with modern design -->
|
||||
<Grid Grid.Row="1"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Status Information -->
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="AdjustBrightnessTextBlock"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="4"
|
||||
VerticalAlignment="Center">
|
||||
<Button x:Name="LinkButton"
|
||||
Width="40"
|
||||
Height="40"
|
||||
CornerRadius="4"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ToolTipService.ToolTip="Sync all monitors">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
<Button x:Name="DisableButton"
|
||||
Width="40"
|
||||
Height="40"
|
||||
CornerRadius="4"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ToolTipService.ToolTip="Toggle control">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
<Button x:Name="RefreshButton"
|
||||
Width="40"
|
||||
Height="40"
|
||||
CornerRadius="4"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ToolTipService.ToolTip="Refresh monitors">
|
||||
<FontIcon Glyph="" FontSize="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,793 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using PowerDisplay.Core;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Helpers;
|
||||
using PowerDisplay.Native;
|
||||
using PowerDisplay.ViewModels;
|
||||
using Windows.Graphics;
|
||||
using WinRT.Interop;
|
||||
using static PowerDisplay.Native.PInvoke;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay
|
||||
{
|
||||
/// <summary>
|
||||
/// PowerDisplay main window
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public sealed partial class MainWindow : Window, IDisposable
|
||||
{
|
||||
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
|
||||
private MainViewModel _viewModel = null!;
|
||||
private TrayIconHelper _trayIcon = null!;
|
||||
private AppWindow _appWindow = null!;
|
||||
private bool _isExiting;
|
||||
|
||||
// Expose ViewModel as property for x:Bind
|
||||
public MainViewModel ViewModel => _viewModel;
|
||||
|
||||
// Conversion functions for x:Bind (AOT-compatible alternative to converters)
|
||||
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
// Lightweight initialization - no heavy operations in constructor
|
||||
// Setup window properties
|
||||
SetupWindow();
|
||||
|
||||
// Initialize tray icon
|
||||
InitializeTrayIcon();
|
||||
|
||||
// Initialize UI text
|
||||
InitializeUIText();
|
||||
|
||||
// Clean up resources on window close
|
||||
this.Closed += OnWindowClosed;
|
||||
|
||||
// Delay ViewModel creation until first activation (async)
|
||||
this.Activated += OnFirstActivated;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"MainWindow initialization failed: {ex.Message}");
|
||||
ShowError($"Unable to start main window: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool _hasInitialized;
|
||||
|
||||
private async void OnFirstActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// Only initialize once on first activation
|
||||
if (_hasInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_hasInitialized = true;
|
||||
this.Activated -= OnFirstActivated; // Unsubscribe after first run
|
||||
|
||||
// Create and initialize ViewModel asynchronously
|
||||
// This will trigger Loading UI (IsScanning) during monitor discovery
|
||||
_viewModel = new MainViewModel();
|
||||
RootGrid.DataContext = _viewModel;
|
||||
|
||||
// Notify bindings that ViewModel is now available (for x:Bind)
|
||||
Bindings.Update();
|
||||
|
||||
// Initialize ViewModel event handlers
|
||||
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
|
||||
// Bind button events
|
||||
LinkButton.Click += OnLinkClick;
|
||||
DisableButton.Click += OnDisableClick;
|
||||
RefreshButton.Click += OnRefreshClick;
|
||||
|
||||
// Start async initialization (monitor scanning happens here)
|
||||
await InitializeAsync();
|
||||
|
||||
// Hide window after initialization completes
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// No delays! Direct async operation
|
||||
await _viewModel.RefreshMonitorsAsync();
|
||||
await _viewModel.ReloadMonitorSettingsAsync();
|
||||
|
||||
// Adjust window size after data is loaded (event-driven)
|
||||
AdjustWindowSizeToContent();
|
||||
}
|
||||
catch (WmiLight.WmiException ex)
|
||||
{
|
||||
Logger.LogError($"WMI access failed: {ex.Message}");
|
||||
ShowError("Unable to access internal display control, administrator privileges may be required.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Initialization failed: {ex.Message}");
|
||||
ShowError($"Initialization failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeUIText()
|
||||
{
|
||||
try
|
||||
{
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
// Set text block content
|
||||
ScanningMonitorsTextBlock.Text = loader.GetString("ScanningMonitorsText");
|
||||
NoMonitorsTextBlock.Text = loader.GetString("NoMonitorsText");
|
||||
AdjustBrightnessTextBlock.Text = loader.GetString("AdjustBrightnessText");
|
||||
|
||||
// Set button tooltips
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, loader.GetString("SyncAllMonitorsTooltip"));
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, loader.GetString("ToggleControlTooltip"));
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, loader.GetString("RefreshTooltip"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Use English defaults if resource loading fails
|
||||
Logger.LogWarning($"Failed to load localized strings: {ex.Message}");
|
||||
ScanningMonitorsTextBlock.Text = "Scanning monitors...";
|
||||
NoMonitorsTextBlock.Text = "No monitors detected";
|
||||
AdjustBrightnessTextBlock.Text = "PowerDisplay";
|
||||
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(LinkButton, "Synchronize all monitors to the same brightness");
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(DisableButton, "Enable or disable brightness control");
|
||||
Microsoft.UI.Xaml.Controls.ToolTipService.SetToolTip(RefreshButton, "Rescan connected monitors");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.StatusText = $"Error: {message}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"Error (ViewModel not yet initialized): {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
// Allow window to close if program is exiting
|
||||
if (_isExiting)
|
||||
{
|
||||
// Clean up event subscriptions
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
args.Handled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// If only user operation (although we hide close button), just hide window
|
||||
args.Handled = true; // Prevent window closing
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private void InitializeTrayIcon()
|
||||
{
|
||||
_trayIcon = new TrayIconHelper(this);
|
||||
_trayIcon.SetCallbacks(
|
||||
onShow: ShowWindow,
|
||||
onExit: ExitApplication,
|
||||
onRefresh: () => _viewModel?.RefreshCommand?.Execute(null),
|
||||
onSettings: OpenSettings);
|
||||
}
|
||||
|
||||
private void OpenSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Open PowerToys Settings to PowerDisplay page
|
||||
PowerDisplay.Helpers.SettingsDeepLink.OpenPowerDisplaySettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to open settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowWindow()
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
// Adjust window size before showing
|
||||
AdjustWindowSizeToContent();
|
||||
|
||||
// Reposition to bottom right (set position before showing)
|
||||
if (_appWindow != null)
|
||||
{
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
}
|
||||
|
||||
// Set initial state for animation
|
||||
RootGrid.Opacity = 0;
|
||||
|
||||
// Show window
|
||||
WindowHelper.ShowWindow(hWnd, true);
|
||||
|
||||
// Bring window to foreground
|
||||
PInvoke.SetForegroundWindow(hWnd);
|
||||
|
||||
// Use storyboard animation for window entrance
|
||||
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
|
||||
{
|
||||
var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard;
|
||||
slideInStoryboard?.Begin();
|
||||
}
|
||||
}
|
||||
|
||||
private void HideWindow()
|
||||
{
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
// Use storyboard animation for window exit
|
||||
if (RootGrid.Resources.ContainsKey("SlideOutStoryboard"))
|
||||
{
|
||||
var slideOutStoryboard = RootGrid.Resources["SlideOutStoryboard"] as Storyboard;
|
||||
if (slideOutStoryboard != null)
|
||||
{
|
||||
slideOutStoryboard.Completed += (s, e) =>
|
||||
{
|
||||
// Hide window after animation completes
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
};
|
||||
slideOutStoryboard.Begin();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: hide immediately if animation not found
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnUIRefreshRequested(object? sender, EventArgs e)
|
||||
{
|
||||
Logger.LogInfo("UI refresh requested due to settings change");
|
||||
await _viewModel.ReloadMonitorSettingsAsync();
|
||||
|
||||
// Adjust window size after settings are reloaded (no delay needed!)
|
||||
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
|
||||
}
|
||||
|
||||
private void OnMonitorsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
// Adjust window size when monitors collection changes (event-driven!)
|
||||
// The UI binding will update first, then we adjust size
|
||||
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
AdjustWindowSizeToContent();
|
||||
});
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
// Adjust window size when relevant properties change (event-driven!)
|
||||
if (e.PropertyName == nameof(_viewModel.IsScanning) ||
|
||||
e.PropertyName == nameof(_viewModel.HasMonitors) ||
|
||||
e.PropertyName == nameof(_viewModel.ShowNoMonitorsMessage))
|
||||
{
|
||||
// Use Low priority to ensure UI bindings update first
|
||||
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
AdjustWindowSizeToContent();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set exit flag to allow window to close normally
|
||||
/// </summary>
|
||||
public void SetExiting()
|
||||
{
|
||||
_isExiting = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速关闭窗口,跳过动画和复杂清理
|
||||
/// </summary>
|
||||
public void FastShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isExiting = true;
|
||||
|
||||
// 立即释放托盘图标
|
||||
_trayIcon?.Dispose();
|
||||
|
||||
// 快速清理 ViewModel
|
||||
if (_viewModel != null)
|
||||
{
|
||||
// 取消事件订阅
|
||||
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
|
||||
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
|
||||
// 立即释放
|
||||
_viewModel.Dispose();
|
||||
}
|
||||
|
||||
// 直接关闭窗口,不等待动画
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
WindowHelper.ShowWindow(hWnd, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 忽略清理错误,确保能够关闭
|
||||
Logger.LogWarning($"FastShutdown error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitApplication()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用快速关闭
|
||||
FastShutdown();
|
||||
|
||||
// 直接调用应用程序快速退出
|
||||
if (Application.Current is App app)
|
||||
{
|
||||
app.Shutdown();
|
||||
}
|
||||
|
||||
// 确保立即退出
|
||||
Environment.Exit(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 确保能够退出
|
||||
Logger.LogError($"ExitApplication error: {ex.Message}");
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRefreshClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Refresh monitor list
|
||||
if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
|
||||
{
|
||||
_viewModel.RefreshCommand.Execute(null);
|
||||
|
||||
// Window size will be adjusted automatically by OnMonitorsCollectionChanged event!
|
||||
// No delay needed - event-driven design
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"OnRefreshClick failed: {ex}");
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.StatusText = "Refresh failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLinkClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Link all monitor brightness (synchronized adjustment)
|
||||
if (_viewModel != null && _viewModel.Monitors.Count > 0)
|
||||
{
|
||||
// Get first monitor brightness as reference
|
||||
var baseBrightness = _viewModel.Monitors.First().Brightness;
|
||||
_ = _viewModel.SetAllBrightnessAsync(baseBrightness);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"OnLinkClick failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDisableClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Add button press animation
|
||||
if (sender is Button button)
|
||||
{
|
||||
await AnimateButtonPress(button);
|
||||
}
|
||||
|
||||
// Disable/enable all monitor controls
|
||||
if (_viewModel != null)
|
||||
{
|
||||
foreach (var monitor in _viewModel.Monitors)
|
||||
{
|
||||
monitor.IsAvailable = !monitor.IsAvailable;
|
||||
}
|
||||
|
||||
_viewModel.StatusText = _viewModel.Monitors.Any(m => m.IsAvailable)
|
||||
? "Display control enabled"
|
||||
: "Display control disabled";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"OnDisableClick failed: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get internal monitor name, consistent with SettingsManager logic
|
||||
/// </summary>
|
||||
private async void OnTestClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ContentDialog? dlg = null;
|
||||
Core.MonitorManager? manager = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Test monitor discovery functionality
|
||||
dlg = new ContentDialog
|
||||
{
|
||||
Title = "Monitor Detection Test",
|
||||
Content = "Starting monitor detection...",
|
||||
CloseButtonText = "Close",
|
||||
XamlRoot = this.Content.XamlRoot,
|
||||
};
|
||||
|
||||
_ = dlg.ShowAsync();
|
||||
|
||||
manager = new Core.MonitorManager();
|
||||
var monitors = await manager.DiscoverMonitorsAsync(default(System.Threading.CancellationToken));
|
||||
|
||||
string message = $"Found {monitors.Count} monitors:\n\n";
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
message += $"• {monitor.Name}\n";
|
||||
message += $" Type: {monitor.Type}\n";
|
||||
message += $" Brightness: {monitor.CurrentBrightness}%\n\n";
|
||||
}
|
||||
|
||||
if (monitors.Count == 0)
|
||||
{
|
||||
message = "No monitors found.\n\n";
|
||||
message += "Possible reasons:\n";
|
||||
message += "• DDC/CI not supported\n";
|
||||
message += "• Driver issues\n";
|
||||
message += "• Permission issues\n";
|
||||
message += "• Cable doesn't support DDC/CI";
|
||||
}
|
||||
|
||||
dlg.Content = message;
|
||||
|
||||
// Don't dispose manager, use existing manager
|
||||
// Initialize ViewModel and bind to root Grid refresh
|
||||
if (monitors.Count > 0)
|
||||
{
|
||||
// Use existing refresh command
|
||||
await _viewModel.RefreshMonitorsAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"OnTestClick failed: {ex}");
|
||||
if (dlg != null)
|
||||
{
|
||||
dlg.Content = $"Error: {ex.Message}\n\nType: {ex.GetType().Name}";
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
manager?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get window handle
|
||||
var hWnd = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
|
||||
_appWindow = AppWindow.GetFromWindowId(windowId);
|
||||
|
||||
if (_appWindow != null)
|
||||
{
|
||||
// Set initial window size - will be adjusted later based on content
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = 480 });
|
||||
|
||||
// Position window at bottom right corner
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
|
||||
// Set window icon and title bar
|
||||
_appWindow.Title = "PowerDisplay";
|
||||
|
||||
// Remove title bar and system buttons
|
||||
var presenter = _appWindow.Presenter as OverlappedPresenter;
|
||||
if (presenter != null)
|
||||
{
|
||||
// Disable resizing
|
||||
presenter.IsResizable = false;
|
||||
|
||||
// Disable maximize button
|
||||
presenter.IsMaximizable = false;
|
||||
|
||||
// Disable minimize button
|
||||
presenter.IsMinimizable = false;
|
||||
|
||||
// Set borderless mode
|
||||
presenter.SetBorderAndTitleBar(false, false);
|
||||
}
|
||||
|
||||
// Custom title bar - completely remove all buttons
|
||||
var titleBar = _appWindow.TitleBar;
|
||||
if (titleBar != null)
|
||||
{
|
||||
// Extend content into title bar area
|
||||
titleBar.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
// Completely remove title bar height
|
||||
titleBar.PreferredHeightOption = Microsoft.UI.Windowing.TitleBarHeightOption.Collapsed;
|
||||
|
||||
// Set all button colors to transparent
|
||||
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
|
||||
|
||||
// Disable title bar interaction area
|
||||
titleBar.SetDragRectangles(Array.Empty<Windows.Graphics.RectInt32>());
|
||||
}
|
||||
|
||||
// Set modern Mica Alt backdrop for Windows 11
|
||||
try
|
||||
{
|
||||
// Use Mica Alt for a more modern appearance
|
||||
if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
|
||||
{
|
||||
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to basic backdrop for older systems
|
||||
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.DesktopAcrylicBackdrop();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: use solid color background
|
||||
this.SystemBackdrop = null;
|
||||
}
|
||||
|
||||
// Use Win32 API to further disable window moving
|
||||
WindowHelper.DisableWindowMovingAndResizing(hWnd);
|
||||
|
||||
// Hide window from taskbar
|
||||
WindowHelper.HideFromTaskbar(hWnd);
|
||||
|
||||
// Optional: set window topmost
|
||||
// WindowHelper.SetWindowTopmost(hWnd, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore window setup errors
|
||||
Logger.LogWarning($"Window setup error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustWindowSizeToContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_appWindow == null || RootGrid == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Force layout update to ensure proper measurement
|
||||
RootGrid.UpdateLayout();
|
||||
|
||||
// Get precise content height
|
||||
var availableWidth = 640.0;
|
||||
var contentHeight = GetContentHeight(availableWidth);
|
||||
|
||||
// Account for display scaling
|
||||
var scale = RootGrid.XamlRoot?.RasterizationScale ?? 1.0;
|
||||
var scaledHeight = (int)Math.Ceiling(contentHeight * scale);
|
||||
|
||||
// Only set maximum height for scrollable content
|
||||
scaledHeight = Math.Min(scaledHeight, 650);
|
||||
|
||||
// Check if resize is needed
|
||||
var currentSize = _appWindow.Size;
|
||||
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
|
||||
{
|
||||
Logger.LogInfo($"Adjusting window height from {currentSize.Height} to {scaledHeight} (content: {contentHeight})");
|
||||
_appWindow.Resize(new SizeInt32 { Width = 640, Height = scaledHeight });
|
||||
|
||||
// Update clip region to match new window size
|
||||
UpdateClipRegion(640, scaledHeight / scale);
|
||||
|
||||
// Reposition to maintain bottom-right position
|
||||
PositionWindowAtBottomRight(_appWindow);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error adjusting window size: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private double GetContentHeight(double availableWidth)
|
||||
{
|
||||
// Try to measure MainContainer directly for precise content size
|
||||
if (RootGrid.FindName("MainContainer") is Border mainContainer)
|
||||
{
|
||||
mainContainer.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||
return mainContainer.DesiredSize.Height;
|
||||
}
|
||||
|
||||
// Fallback: Measure the root grid
|
||||
RootGrid.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
|
||||
return RootGrid.DesiredSize.Height + 4; // Small padding for fallback method
|
||||
}
|
||||
|
||||
private void UpdateClipRegion(double width, double height)
|
||||
{
|
||||
// Clip region removed to allow automatic sizing
|
||||
// No longer needed as we removed the fixed clip from RootGrid
|
||||
}
|
||||
|
||||
private void PositionWindowAtBottomRight(AppWindow appWindow)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get display area
|
||||
var displayArea = DisplayArea.GetFromWindowId(appWindow.Id, DisplayAreaFallback.Nearest);
|
||||
if (displayArea != null)
|
||||
{
|
||||
var workArea = displayArea.WorkArea;
|
||||
var windowSize = appWindow.Size;
|
||||
|
||||
// Calculate bottom-right position, close to taskbar
|
||||
// WorkArea already excludes taskbar area, so use WorkArea bottom directly
|
||||
int rightMargin = 10; // Small margin from right edge
|
||||
int x = workArea.Width - windowSize.Width - rightMargin;
|
||||
int y = workArea.Height - windowSize.Height; // Close to taskbar top, no gap
|
||||
|
||||
// Move window to bottom right
|
||||
appWindow.Move(new PointInt32 { X = x, Y = y });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Ignore errors when positioning window
|
||||
Logger.LogDebug($"Failed to position window: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Animates button press for modern interaction feedback
|
||||
/// </summary>
|
||||
/// <param name="button">The button to animate</param>
|
||||
private async Task AnimateButtonPress(Button button)
|
||||
{
|
||||
// Button animation disabled to avoid compilation errors
|
||||
// Using default button visual states instead
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slider ValueChanged event handler - does nothing during drag
|
||||
/// This allows the slider UI to update smoothly without triggering hardware operations
|
||||
/// </summary>
|
||||
private void Slider_ValueChanged(object sender, Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
// During drag, this event fires 60-120 times per second
|
||||
// We intentionally do nothing here to keep UI smooth
|
||||
// The actual ViewModel update happens in PointerCaptureLost after drag completes
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slider PointerCaptureLost event handler - updates ViewModel when drag completes
|
||||
/// This is the WinUI3 recommended way to detect drag completion
|
||||
/// </summary>
|
||||
private void Slider_PointerCaptureLost(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
|
||||
{
|
||||
var slider = sender as Slider;
|
||||
if (slider == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyName = slider.Tag as string;
|
||||
var monitorVm = slider.DataContext as MonitorViewModel;
|
||||
|
||||
if (monitorVm == null || propertyName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get final value after drag completes
|
||||
int finalValue = (int)slider.Value;
|
||||
|
||||
// Now update the ViewModel, which will trigger hardware operation
|
||||
switch (propertyName)
|
||||
{
|
||||
case "Brightness":
|
||||
monitorVm.Brightness = finalValue;
|
||||
Logger.LogDebug($"[UI] Brightness drag completed: {finalValue}");
|
||||
break;
|
||||
case "ColorTemperature":
|
||||
monitorVm.ColorTemperaturePercent = finalValue;
|
||||
Logger.LogDebug($"[UI] ColorTemperature drag completed: {finalValue}%");
|
||||
break;
|
||||
case "Contrast":
|
||||
monitorVm.ContrastPercent = finalValue;
|
||||
Logger.LogDebug($"[UI] Contrast drag completed: {finalValue}%");
|
||||
break;
|
||||
case "Volume":
|
||||
monitorVm.Volume = finalValue;
|
||||
Logger.LogDebug($"[UI] Volume drag completed: {finalValue}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_viewModel?.Dispose();
|
||||
_trayIcon?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/modules/powerdisplay/PowerDisplay/Program.cs
Normal file
59
src/modules/powerdisplay/PowerDisplay/Program.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
|
||||
namespace PowerDisplay
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger("\\PowerDisplay\\Logs");
|
||||
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
// Parse command line arguments: args[0] = runner_pid, args[1] = pipe_uuid
|
||||
int runnerPid = -1;
|
||||
string pipeUuid = string.Empty;
|
||||
|
||||
if (args.Length >= 2)
|
||||
{
|
||||
if (int.TryParse(args[0], out int parsedPid))
|
||||
{
|
||||
runnerPid = parsedPid;
|
||||
}
|
||||
|
||||
pipeUuid = args[1];
|
||||
Logger.LogInfo($"PowerDisplay started with runner_pid={runnerPid}, pipe_uuid={pipeUuid}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("PowerDisplay started without command line arguments");
|
||||
Logger.LogWarning($"PowerDisplay started with insufficient arguments (expected 2, got {args.Length}). Running in standalone mode.");
|
||||
}
|
||||
|
||||
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance");
|
||||
|
||||
if (instanceKey.IsCurrent)
|
||||
{
|
||||
Microsoft.UI.Xaml.Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App(runnerPid, pipeUuid);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Another instance of PowerDisplay is running. Exiting.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type - Related JSON serialization types grouped together
|
||||
|
||||
namespace PowerDisplay.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON source generation context for AOT compatibility.
|
||||
/// Eliminates reflection-based JSON serialization.
|
||||
/// </summary>
|
||||
[JsonSerializable(typeof(PowerDisplayMonitorsIPCResponse))]
|
||||
[JsonSerializable(typeof(MonitorInfoData))]
|
||||
[JsonSerializable(typeof(IPCMessageAction))]
|
||||
[JsonSerializable(typeof(MonitorStateFile))]
|
||||
[JsonSerializable(typeof(MonitorStateEntry))]
|
||||
[JsonSourceGenerationOptions(
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
|
||||
internal sealed partial class AppJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IPC message wrapper for parsing action-based messages.
|
||||
/// Used in App.xaml.cs for dynamic IPC command handling.
|
||||
/// </summary>
|
||||
internal sealed class IPCMessageAction
|
||||
{
|
||||
[JsonPropertyName("action")]
|
||||
public string? Action { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monitor state file structure for JSON persistence.
|
||||
/// Made internal (from private) to support source generation.
|
||||
/// </summary>
|
||||
internal sealed class MonitorStateFile
|
||||
{
|
||||
[JsonPropertyName("monitors")]
|
||||
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual monitor state entry.
|
||||
/// Made internal (from private) to support source generation.
|
||||
/// </summary>
|
||||
internal sealed class MonitorStateEntry
|
||||
{
|
||||
[JsonPropertyName("brightness")]
|
||||
public int Brightness { get; set; }
|
||||
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int ColorTemperature { get; set; }
|
||||
|
||||
[JsonPropertyName("contrast")]
|
||||
public int Contrast { get; set; }
|
||||
|
||||
[JsonPropertyName("volume")]
|
||||
public int Volume { get; set; }
|
||||
|
||||
[JsonPropertyName("lastUpdated")]
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ScanningMonitorsText" xml:space="preserve">
|
||||
<value>Scanning monitors...</value>
|
||||
</data>
|
||||
<data name="NoMonitorsText" xml:space="preserve">
|
||||
<value>No monitors detected</value>
|
||||
</data>
|
||||
<data name="AdjustBrightnessText" xml:space="preserve">
|
||||
<value>PowerDisplay</value>
|
||||
</data>
|
||||
<data name="SyncAllMonitorsTooltip" xml:space="preserve">
|
||||
<value>Synchronize all monitors to the same brightness</value>
|
||||
</data>
|
||||
<data name="ToggleControlTooltip" xml:space="preserve">
|
||||
<value>Enable or disable brightness control</value>
|
||||
</data>
|
||||
<data name="RefreshTooltip" xml:space="preserve">
|
||||
<value>Rescan connected monitors</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
namespace PowerDisplay.Telemetry.Events
|
||||
{
|
||||
[EventData]
|
||||
public class PowerDisplayStartEvent : EventBase, IEvent
|
||||
{
|
||||
public new string EventName => "PowerDisplay_Start";
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using PowerDisplay.Commands;
|
||||
using PowerDisplay.Core;
|
||||
using PowerDisplay.Core.Interfaces;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Helpers;
|
||||
using PowerDisplay.Serialization;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Main ViewModel for the PowerDisplay application
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
|
||||
public partial class MainViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private readonly MonitorManager _monitorManager;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly MonitorStateManager _stateManager;
|
||||
private FileSystemWatcher? _settingsWatcher;
|
||||
|
||||
private ObservableCollection<MonitorViewModel> _monitors;
|
||||
private string _statusText;
|
||||
private bool _isScanning;
|
||||
private bool _isInitialized;
|
||||
private bool _isLoading;
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when UI refresh is requested due to settings changes
|
||||
/// </summary>
|
||||
public event EventHandler? UIRefreshRequested;
|
||||
|
||||
public MainViewModel()
|
||||
{
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
_monitors = new ObservableCollection<MonitorViewModel>();
|
||||
_statusText = "Initializing...";
|
||||
_isScanning = true;
|
||||
|
||||
// Initialize settings utils
|
||||
_settingsUtils = new SettingsUtils();
|
||||
_stateManager = new MonitorStateManager();
|
||||
|
||||
// Initialize the monitor manager
|
||||
_monitorManager = new MonitorManager();
|
||||
|
||||
// Subscribe to events
|
||||
_monitorManager.MonitorsChanged += OnMonitorsChanged;
|
||||
|
||||
// Setup settings file monitoring
|
||||
SetupSettingsFileWatcher();
|
||||
|
||||
// Start initial discovery
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
public ObservableCollection<MonitorViewModel> Monitors
|
||||
{
|
||||
get => _monitors;
|
||||
set
|
||||
{
|
||||
_monitors = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set
|
||||
{
|
||||
_statusText = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsScanning
|
||||
{
|
||||
get => _isScanning;
|
||||
set
|
||||
{
|
||||
_isScanning = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasMonitors));
|
||||
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
|
||||
OnPropertyChanged(nameof(IsInteractionEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMonitors => !IsScanning && Monitors.Count > 0;
|
||||
|
||||
public bool ShowNoMonitorsMessage => !IsScanning && Monitors.Count == 0;
|
||||
|
||||
public bool IsInitialized
|
||||
{
|
||||
get => _isInitialized;
|
||||
private set
|
||||
{
|
||||
_isInitialized = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => _isLoading;
|
||||
private set
|
||||
{
|
||||
_isLoading = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(IsInteractionEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether gets whether user interaction is enabled (not loading or scanning)
|
||||
/// </summary>
|
||||
public bool IsInteractionEnabled => !IsLoading && !IsScanning;
|
||||
|
||||
public ICommand RefreshCommand => new RelayCommand(async () => await RefreshMonitorsAsync());
|
||||
|
||||
public ICommand SetAllBrightnessCommand => new RelayCommand<int?>(async (brightness) =>
|
||||
{
|
||||
if (brightness.HasValue)
|
||||
{
|
||||
await SetAllBrightnessAsync(brightness.Value);
|
||||
}
|
||||
});
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "Scanning monitors...";
|
||||
IsScanning = true;
|
||||
|
||||
// Discover monitors
|
||||
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
|
||||
|
||||
// Update UI on the dispatcher thread
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
UpdateMonitorList(monitors);
|
||||
IsScanning = false;
|
||||
IsInitialized = true;
|
||||
|
||||
if (monitors.Count > 0)
|
||||
{
|
||||
StatusText = $"Found {monitors.Count} monitors";
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "No controllable monitors found";
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
StatusText = $"Scan failed: {ex.Message}";
|
||||
IsScanning = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshMonitorsAsync()
|
||||
{
|
||||
if (IsScanning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StatusText = "Refreshing monitor list...";
|
||||
IsScanning = true;
|
||||
|
||||
var monitors = await _monitorManager.DiscoverMonitorsAsync(_cancellationTokenSource.Token);
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
UpdateMonitorList(monitors);
|
||||
IsScanning = false;
|
||||
StatusText = $"Found {monitors.Count} monitors";
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
StatusText = $"Refresh failed: {ex.Message}";
|
||||
IsScanning = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateMonitorList(IReadOnlyList<Monitor> monitors)
|
||||
{
|
||||
Monitors.Clear();
|
||||
|
||||
var colorTempTasks = new List<Task>();
|
||||
foreach (var monitor in monitors)
|
||||
{
|
||||
var vm = new MonitorViewModel(monitor, _monitorManager, this);
|
||||
Monitors.Add(vm);
|
||||
|
||||
// Asynchronously initialize color temperature for DDC/CI monitors
|
||||
if (monitor.SupportsColorTemperature && monitor.Type == MonitorType.External)
|
||||
{
|
||||
var task = InitializeColorTemperatureSafeAsync(monitor.Id, vm);
|
||||
colorTempTasks.Add(task);
|
||||
}
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasMonitors));
|
||||
OnPropertyChanged(nameof(ShowNoMonitorsMessage));
|
||||
|
||||
// Send monitor information to Settings UI via IPC
|
||||
SendMonitorInfoToSettingsUI();
|
||||
|
||||
// Restore saved settings if enabled (async, don't block)
|
||||
// Pass color temperature initialization tasks so we can wait for them if needed
|
||||
_ = ReloadMonitorSettingsAsync(colorTempTasks);
|
||||
}
|
||||
|
||||
public async Task SetAllBrightnessAsync(int brightness)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = $"Setting all monitors brightness to {brightness}%...";
|
||||
await _monitorManager.SetAllBrightnessAsync(brightness, _cancellationTokenSource.Token);
|
||||
StatusText = $"All monitors brightness set to {brightness}%";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Failed to set brightness: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMonitorsChanged(object? sender, MonitorListChangedEventArgs e)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
// Handle monitors being added or removed
|
||||
if (e.AddedMonitors.Count > 0)
|
||||
{
|
||||
foreach (var monitor in e.AddedMonitors)
|
||||
{
|
||||
var existingVm = GetMonitorViewModel(monitor.Id);
|
||||
if (existingVm == null)
|
||||
{
|
||||
var vm = new MonitorViewModel(monitor, _monitorManager, this);
|
||||
Monitors.Add(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.RemovedMonitors.Count > 0)
|
||||
{
|
||||
foreach (var monitor in e.RemovedMonitors)
|
||||
{
|
||||
var vm = GetMonitorViewModel(monitor.Id);
|
||||
if (vm != null)
|
||||
{
|
||||
Monitors.Remove(vm);
|
||||
vm.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StatusText = $"Monitor list updated ({Monitors.Count} total)";
|
||||
|
||||
// Send updated monitor list to Settings UI via IPC
|
||||
SendMonitorInfoToSettingsUI();
|
||||
});
|
||||
}
|
||||
|
||||
private MonitorViewModel? GetMonitorViewModel(string monitorId)
|
||||
{
|
||||
foreach (var vm in Monitors)
|
||||
{
|
||||
if (vm.Id == monitorId)
|
||||
{
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup settings file watcher
|
||||
/// </summary>
|
||||
private void SetupSettingsFileWatcher()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsPath = _settingsUtils.GetSettingsFilePath("PowerDisplay");
|
||||
var directory = Path.GetDirectoryName(settingsPath);
|
||||
var fileName = Path.GetFileName(settingsPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
// Ensure directory exists
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
_settingsWatcher = new FileSystemWatcher(directory, fileName)
|
||||
{
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime,
|
||||
EnableRaisingEvents = true,
|
||||
};
|
||||
|
||||
_settingsWatcher.Changed += OnSettingsFileChanged;
|
||||
_settingsWatcher.Created += OnSettingsFileChanged;
|
||||
|
||||
Logger.LogInfo($"Settings file watcher setup for: {settingsPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to setup settings file watcher: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle settings file changes - only monitors UI configuration changes from Settings UI
|
||||
/// (monitor_state.json is managed separately and doesn't trigger this)
|
||||
/// </summary>
|
||||
private void OnSettingsFileChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"Settings file changed by Settings UI: {e.FullPath}");
|
||||
|
||||
// Add small delay to ensure file write completion
|
||||
Task.Delay(200).ContinueWith(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Read updated settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
// Update feature visibility for each monitor (UI configuration only)
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
// Use HardwareId for lookup (unified identification)
|
||||
Logger.LogInfo($"[Settings Update] Looking for monitor settings with Hardware ID: '{monitorVm.HardwareId}'");
|
||||
|
||||
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
|
||||
m.HardwareId == monitorVm.HardwareId);
|
||||
|
||||
if (monitorSettings != null)
|
||||
{
|
||||
Logger.LogInfo($"[Settings Update] Found monitor settings for Hardware ID '{monitorVm.HardwareId}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}");
|
||||
|
||||
// Update visibility flags based on Settings UI toggles
|
||||
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
|
||||
monitorVm.ShowContrast = monitorSettings.EnableContrast;
|
||||
monitorVm.ShowVolume = monitorSettings.EnableVolume;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[Settings Update] No monitor settings found for Hardware ID '{monitorVm.HardwareId}'");
|
||||
Logger.LogInfo($"[Settings Update] Available monitors in settings:");
|
||||
foreach (var availableMonitor in settings.Properties.Monitors)
|
||||
{
|
||||
Logger.LogInfo($" - Hardware: '{availableMonitor.HardwareId}', Name: '{availableMonitor.Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger UI refresh for configuration changes
|
||||
UIRefreshRequested?.Invoke(this, EventArgs.Empty);
|
||||
});
|
||||
|
||||
Logger.LogInfo($"Settings UI configuration reloaded, monitor count: {settings.Properties.Monitors.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to reload settings: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error handling settings file change: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safe wrapper for initializing color temperature asynchronously
|
||||
/// </summary>
|
||||
private async Task InitializeColorTemperatureSafeAsync(string monitorId, MonitorViewModel vm)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _monitorManager.InitializeColorTemperatureAsync(monitorId);
|
||||
|
||||
// Update UI on dispatcher thread - get the monitor from manager
|
||||
var monitor = _monitorManager.GetMonitor(monitorId);
|
||||
if (monitor != null)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
// Update color temperature without triggering hardware write
|
||||
vm.UpdatePropertySilently(nameof(vm.ColorTemperature), monitor.CurrentColorTemperature);
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// INotifyPropertyChanged
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload monitor settings from configuration
|
||||
/// </summary>
|
||||
/// <param name="colorTempInitTasks">Optional tasks for color temperature initialization to wait for</param>
|
||||
public async Task ReloadMonitorSettingsAsync(List<Task>? colorTempInitTasks = null)
|
||||
{
|
||||
// Prevent duplicate calls
|
||||
if (IsLoading)
|
||||
{
|
||||
Logger.LogInfo("[Startup] ReloadMonitorSettingsAsync already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Set loading state to block UI interactions
|
||||
IsLoading = true;
|
||||
StatusText = "Loading settings...";
|
||||
|
||||
// Read current settings
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>("PowerDisplay");
|
||||
|
||||
if (settings.Properties.RestoreSettingsOnStartup)
|
||||
{
|
||||
// Restore saved settings from configuration file
|
||||
Logger.LogInfo("[Startup] RestoreSettingsOnStartup enabled - applying saved settings");
|
||||
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
var hardwareId = monitorVm.HardwareId;
|
||||
Logger.LogInfo($"[Startup] Processing monitor: '{monitorVm.Name}', HardwareId: '{hardwareId}'");
|
||||
|
||||
// Find and apply corresponding saved settings from state file using stable HardwareId
|
||||
var savedState = _stateManager.GetMonitorParameters(hardwareId);
|
||||
if (savedState.HasValue)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Restoring state for HardwareId '{hardwareId}': Brightness={savedState.Value.Brightness}, ColorTemp={savedState.Value.ColorTemperature}");
|
||||
|
||||
// Validate and apply saved values (skip invalid values)
|
||||
// Use UpdatePropertySilently to avoid triggering hardware updates during initialization
|
||||
if (savedState.Value.Brightness >= monitorVm.MinBrightness && savedState.Value.Brightness <= monitorVm.MaxBrightness)
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Brightness), savedState.Value.Brightness);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[Startup] Invalid brightness value {savedState.Value.Brightness} for HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
|
||||
// Color temperature must be valid and within range
|
||||
if (savedState.Value.ColorTemperature > 0 &&
|
||||
savedState.Value.ColorTemperature >= monitorVm.MinColorTemperature &&
|
||||
savedState.Value.ColorTemperature <= monitorVm.MaxColorTemperature)
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.ColorTemperature), savedState.Value.ColorTemperature);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[Startup] Invalid color temperature value {savedState.Value.ColorTemperature} for HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
|
||||
// Contrast validation - only apply if hardware supports it
|
||||
if (monitorVm.ShowContrast &&
|
||||
savedState.Value.Contrast >= monitorVm.MinContrast &&
|
||||
savedState.Value.Contrast <= monitorVm.MaxContrast)
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Contrast), savedState.Value.Contrast);
|
||||
}
|
||||
else if (!monitorVm.ShowContrast)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Contrast not supported on HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
|
||||
// Volume validation - only apply if hardware supports it
|
||||
if (monitorVm.ShowVolume &&
|
||||
savedState.Value.Volume >= monitorVm.MinVolume &&
|
||||
savedState.Value.Volume <= monitorVm.MaxVolume)
|
||||
{
|
||||
monitorVm.UpdatePropertySilently(nameof(monitorVm.Volume), savedState.Value.Volume);
|
||||
}
|
||||
else if (!monitorVm.ShowVolume)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Volume not supported on HardwareId '{hardwareId}', skipping");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo($"[Startup] No saved state for HardwareId '{hardwareId}' - keeping current hardware values");
|
||||
}
|
||||
|
||||
// Apply feature visibility settings using HardwareId
|
||||
ApplyFeatureVisibility(monitorVm, settings);
|
||||
}
|
||||
|
||||
StatusText = "Saved settings restored successfully";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Save current hardware values to configuration file
|
||||
Logger.LogInfo("[Startup] RestoreSettingsOnStartup disabled - saving current hardware values");
|
||||
|
||||
// Wait for color temperature initialization to complete (if any)
|
||||
if (colorTempInitTasks != null && colorTempInitTasks.Count > 0)
|
||||
{
|
||||
Logger.LogInfo("[Startup] Waiting for color temperature initialization to complete...");
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(colorTempInitTasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[Startup] Some color temperature initializations failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var monitorVm in Monitors)
|
||||
{
|
||||
// Save current hardware values to settings
|
||||
SaveMonitorSettingDirect(monitorVm.HardwareId, "Brightness", monitorVm.Brightness);
|
||||
SaveMonitorSettingDirect(monitorVm.HardwareId, "ColorTemperature", monitorVm.ColorTemperature);
|
||||
SaveMonitorSettingDirect(monitorVm.HardwareId, "Contrast", monitorVm.Contrast);
|
||||
SaveMonitorSettingDirect(monitorVm.HardwareId, "Volume", monitorVm.Volume);
|
||||
|
||||
Logger.LogInfo($"[Startup] Saved current values for Hardware ID '{monitorVm.HardwareId}': Brightness={monitorVm.Brightness}, ColorTemp={monitorVm.ColorTemperature}");
|
||||
|
||||
// Apply feature visibility settings
|
||||
ApplyFeatureVisibility(monitorVm, settings);
|
||||
}
|
||||
|
||||
// No need to flush - MonitorStateManager now saves directly!
|
||||
StatusText = "Current monitor values saved to state file";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to reload/save settings: {ex.Message}");
|
||||
StatusText = $"Failed to process settings: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Clear loading state to enable UI interactions
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply feature visibility settings to a monitor ViewModel
|
||||
/// </summary>
|
||||
private void ApplyFeatureVisibility(MonitorViewModel monitorVm, PowerDisplaySettings settings)
|
||||
{
|
||||
var monitorSettings = settings.Properties.Monitors.FirstOrDefault(m =>
|
||||
m.HardwareId == monitorVm.HardwareId);
|
||||
|
||||
if (monitorSettings != null)
|
||||
{
|
||||
Logger.LogInfo($"[Startup] Applying feature visibility for Hardware ID '{monitorVm.HardwareId}': ColorTemp={monitorSettings.EnableColorTemperature}, Contrast={monitorSettings.EnableContrast}, Volume={monitorSettings.EnableVolume}");
|
||||
|
||||
monitorVm.ShowColorTemperature = monitorSettings.EnableColorTemperature;
|
||||
monitorVm.ShowContrast = monitorSettings.EnableContrast;
|
||||
monitorVm.ShowVolume = monitorSettings.EnableVolume;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"[Startup] No feature settings found for Hardware ID '{monitorVm.HardwareId}' - using defaults");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe save method that can be called from background threads.
|
||||
/// Does not access UI collections or update UI properties.
|
||||
/// </summary>
|
||||
public void SaveMonitorSettingDirect(string hardwareId, string property, int value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// This is thread-safe - _stateManager has internal locking
|
||||
// No UI thread operations, no ObservableCollection access
|
||||
_stateManager.UpdateMonitorParameter(hardwareId, property, value);
|
||||
|
||||
Logger.LogTrace($"[State] Queued setting change for HardwareId '{hardwareId}': {property}={value}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Only log, don't update UI from background thread
|
||||
Logger.LogError($"Failed to queue setting save for HardwareId '{hardwareId}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset a monitor to default values
|
||||
/// </summary>
|
||||
public void ResetMonitor(string monitorId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var monitorVm = GetMonitorViewModel(monitorId);
|
||||
if (monitorVm != null)
|
||||
{
|
||||
// Apply default values
|
||||
monitorVm.Brightness = 30;
|
||||
monitorVm.ColorTemperature = 6500;
|
||||
monitorVm.Contrast = 50;
|
||||
monitorVm.Volume = 50;
|
||||
|
||||
StatusText = $"Monitor {monitorVm.Name} reset to default values";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Failed to reset monitor: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send monitor information to Settings UI via IPC (using standard Model)
|
||||
/// </summary>
|
||||
private void SendMonitorInfoToSettingsUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Monitors.Count == 0)
|
||||
{
|
||||
Logger.LogInfo("[IPC] No monitors to send to Settings UI");
|
||||
return;
|
||||
}
|
||||
|
||||
// Build monitor data list
|
||||
var monitorsData = new List<MonitorInfoData>();
|
||||
|
||||
foreach (var vm in Monitors)
|
||||
{
|
||||
var monitorData = new MonitorInfoData
|
||||
{
|
||||
Name = vm.Name,
|
||||
InternalName = vm.Id,
|
||||
HardwareId = vm.HardwareId,
|
||||
CommunicationMethod = GetCommunicationMethodString(vm.Type),
|
||||
MonitorType = vm.Type.ToString(),
|
||||
CurrentBrightness = vm.Brightness,
|
||||
ColorTemperature = vm.ColorTemperature,
|
||||
};
|
||||
|
||||
monitorsData.Add(monitorData);
|
||||
}
|
||||
|
||||
// Use standard IPC Response Model with JsonSerializer (source-generated for AOT)
|
||||
var response = new PowerDisplayMonitorsIPCResponse(monitorsData);
|
||||
string jsonMessage = System.Text.Json.JsonSerializer.Serialize(response, AppJsonContext.Default.PowerDisplayMonitorsIPCResponse);
|
||||
|
||||
// Send to Settings UI via IPC
|
||||
App.SendIPCMessage(jsonMessage);
|
||||
|
||||
Logger.LogInfo($"[IPC] Sent {Monitors.Count} monitors to Settings UI");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[IPC] Failed to send monitor info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get communication method string based on monitor type
|
||||
/// </summary>
|
||||
private string GetCommunicationMethodString(MonitorType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MonitorType.External => "DDC/CI",
|
||||
MonitorType.Internal => "WMI",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
// IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Cancel all async operations first
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
// Stop file monitoring immediately
|
||||
_settingsWatcher?.Dispose();
|
||||
_settingsWatcher = null;
|
||||
|
||||
// No need to flush state - MonitorStateManager now saves directly on each update!
|
||||
// State is already persisted, no pending changes to wait for.
|
||||
|
||||
// Quick cleanup of monitor view models
|
||||
try
|
||||
{
|
||||
foreach (var vm in Monitors)
|
||||
{
|
||||
vm?.Dispose();
|
||||
}
|
||||
|
||||
Monitors.Clear();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
}
|
||||
|
||||
// Release monitor manager
|
||||
try
|
||||
{
|
||||
_monitorManager?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
}
|
||||
|
||||
// Release state manager
|
||||
try
|
||||
{
|
||||
_stateManager?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
}
|
||||
|
||||
// Finally release cancellation token
|
||||
try
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* Ignore cleanup errors */
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ensure Dispose doesn't throw exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
using PowerDisplay.Commands;
|
||||
using PowerDisplay.Core;
|
||||
using PowerDisplay.Core.Models;
|
||||
using PowerDisplay.Helpers;
|
||||
using Monitor = PowerDisplay.Core.Models.Monitor;
|
||||
|
||||
namespace PowerDisplay.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for individual monitor
|
||||
/// </summary>
|
||||
public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private readonly Monitor _monitor;
|
||||
private readonly MonitorManager _monitorManager;
|
||||
private readonly MainViewModel? _mainViewModel;
|
||||
|
||||
// Simple debouncers for each property (KISS principle - simpler than complex queue)
|
||||
private readonly SimpleDebouncer _brightnessDebouncer = new(300);
|
||||
private readonly SimpleDebouncer _colorTempDebouncer = new(300);
|
||||
private readonly SimpleDebouncer _contrastDebouncer = new(300);
|
||||
private readonly SimpleDebouncer _volumeDebouncer = new(300);
|
||||
|
||||
private int _brightness;
|
||||
private int _colorTemperature;
|
||||
private int _contrast;
|
||||
private int _volume;
|
||||
private bool _isAvailable;
|
||||
|
||||
// Visibility settings (controlled by Settings UI)
|
||||
private bool _showColorTemperature;
|
||||
private bool _showContrast;
|
||||
private bool _showVolume;
|
||||
|
||||
/// <summary>
|
||||
/// Updates a property value directly without triggering hardware updates.
|
||||
/// Used during initialization to update UI from saved state.
|
||||
/// </summary>
|
||||
internal void UpdatePropertySilently(string propertyName, int value)
|
||||
{
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(ColorTemperature):
|
||||
_colorTemperature = value;
|
||||
OnPropertyChanged(nameof(ColorTemperature));
|
||||
OnPropertyChanged(nameof(ColorTemperaturePercent));
|
||||
break;
|
||||
case nameof(Brightness):
|
||||
_brightness = value;
|
||||
OnPropertyChanged(nameof(Brightness));
|
||||
break;
|
||||
case nameof(Contrast):
|
||||
_contrast = value;
|
||||
OnPropertyChanged(nameof(Contrast));
|
||||
OnPropertyChanged(nameof(ContrastPercent));
|
||||
break;
|
||||
case nameof(Volume):
|
||||
_volume = value;
|
||||
OnPropertyChanged(nameof(Volume));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Conversion function for x:Bind (AOT-compatible alternative to converters)
|
||||
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// Property to access IsInteractionEnabled from parent ViewModel
|
||||
public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
|
||||
|
||||
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
|
||||
{
|
||||
_monitor = monitor;
|
||||
_monitorManager = monitorManager;
|
||||
_mainViewModel = mainViewModel;
|
||||
|
||||
// Subscribe to MainViewModel property changes to update IsInteractionEnabled
|
||||
if (_mainViewModel != null)
|
||||
{
|
||||
_mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
// Initialize Show properties based on hardware capabilities
|
||||
_showColorTemperature = monitor.SupportsColorTemperature; // Only show for DDC/CI monitors that support it
|
||||
_showContrast = monitor.SupportsContrast;
|
||||
_showVolume = monitor.SupportsVolume;
|
||||
|
||||
// Try to get current color temperature via DDC/CI, use default if failed
|
||||
try
|
||||
{
|
||||
// For DDC/CI monitors that support color temperature, use 6500K as default
|
||||
// The actual temperature will be loaded asynchronously after construction
|
||||
if (monitor.SupportsColorTemperature)
|
||||
{
|
||||
_colorTemperature = 6500; // Default neutral temperature for DDC monitors
|
||||
}
|
||||
else
|
||||
{
|
||||
_colorTemperature = 6500; // Default for unsupported monitors
|
||||
}
|
||||
|
||||
monitor.CurrentColorTemperature = _colorTemperature;
|
||||
Logger.LogDebug($"Initialized {monitor.Id} with default color temperature {_colorTemperature}K");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to initialize color temperature for {monitor.Id}: {ex.Message}");
|
||||
_colorTemperature = 6500; // Default neutral temperature
|
||||
monitor.CurrentColorTemperature = 6500;
|
||||
}
|
||||
|
||||
// Initialize basic properties from monitor
|
||||
_brightness = monitor.CurrentBrightness;
|
||||
_contrast = monitor.CurrentContrast;
|
||||
_volume = monitor.CurrentVolume;
|
||||
_isAvailable = monitor.IsAvailable;
|
||||
}
|
||||
|
||||
public string Id => _monitor.Id;
|
||||
|
||||
public string HardwareId => _monitor.HardwareId;
|
||||
|
||||
public string Name => _monitor.Name;
|
||||
|
||||
public string Manufacturer => _monitor.Manufacturer;
|
||||
|
||||
public MonitorType Type => _monitor.Type;
|
||||
|
||||
public string TypeDisplay => Type == MonitorType.Internal ? "Internal" : "External";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon glyph based on monitor type
|
||||
/// </summary>
|
||||
public string MonitorIconGlyph => Type == MonitorType.Internal ? "\uEA37" : "\uE7F4";
|
||||
|
||||
// Monitor property ranges
|
||||
public int MinBrightness => _monitor.MinBrightness;
|
||||
|
||||
public int MaxBrightness => _monitor.MaxBrightness;
|
||||
|
||||
public int MinColorTemperature => _monitor.MinColorTemperature;
|
||||
|
||||
public int MaxColorTemperature => _monitor.MaxColorTemperature;
|
||||
|
||||
public int MinContrast => _monitor.MinContrast;
|
||||
|
||||
public int MaxContrast => _monitor.MaxContrast;
|
||||
|
||||
public int MinVolume => _monitor.MinVolume;
|
||||
|
||||
public int MaxVolume => _monitor.MaxVolume;
|
||||
|
||||
// Advanced control display logic
|
||||
public bool HasAdvancedControls => ShowColorTemperature || ShowContrast || ShowVolume;
|
||||
|
||||
public bool ShowColorTemperature
|
||||
{
|
||||
get => _showColorTemperature;
|
||||
set
|
||||
{
|
||||
if (_showColorTemperature != value)
|
||||
{
|
||||
_showColorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasAdvancedControls));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowContrast
|
||||
{
|
||||
get => _showContrast;
|
||||
set
|
||||
{
|
||||
if (_showContrast != value)
|
||||
{
|
||||
_showContrast = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasAdvancedControls));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowVolume
|
||||
{
|
||||
get => _showVolume;
|
||||
set
|
||||
{
|
||||
if (_showVolume != value)
|
||||
{
|
||||
_showVolume = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasAdvancedControls));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Brightness
|
||||
{
|
||||
get => _brightness;
|
||||
set
|
||||
{
|
||||
if (_brightness != value)
|
||||
{
|
||||
// Update UI state immediately - keep slider smooth
|
||||
_brightness = value;
|
||||
OnPropertyChanged(); // UI responds immediately
|
||||
|
||||
// Debounce hardware update - much simpler than complex queue!
|
||||
var capturedValue = value; // Capture value for async closure
|
||||
_brightnessDebouncer.Debounce(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _monitorManager.SetBrightnessAsync(Id, capturedValue);
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Brightness", capturedValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set brightness for {Id}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorTemperature
|
||||
{
|
||||
get => _colorTemperature;
|
||||
set
|
||||
{
|
||||
if (_colorTemperature != value)
|
||||
{
|
||||
_colorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
// Debounce hardware update - simple and clean!
|
||||
var capturedValue = value;
|
||||
_colorTempDebouncer.Debounce(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _monitorManager.SetColorTemperatureAsync(Id, capturedValue);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_monitor.CurrentColorTemperature = capturedValue;
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "ColorTemperature", capturedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError($"[{Id}] Failed to set color temperature: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set color temperature for {Id}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Contrast
|
||||
{
|
||||
get => _contrast;
|
||||
set
|
||||
{
|
||||
if (_contrast != value)
|
||||
{
|
||||
_contrast = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
// Debounce hardware update
|
||||
var capturedValue = value;
|
||||
_contrastDebouncer.Debounce(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _monitorManager.SetContrastAsync(Id, capturedValue);
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Contrast", capturedValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set contrast for {Id}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Volume
|
||||
{
|
||||
get => _volume;
|
||||
set
|
||||
{
|
||||
if (_volume != value)
|
||||
{
|
||||
_volume = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
// Debounce hardware update
|
||||
var capturedValue = value;
|
||||
_volumeDebouncer.Debounce(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _monitorManager.SetVolumeAsync(Id, capturedValue);
|
||||
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, "Volume", capturedValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set volume for {Id}: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAvailable
|
||||
{
|
||||
get => _isAvailable;
|
||||
set
|
||||
{
|
||||
_isAvailable = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SetBrightnessCommand => new RelayCommand<int?>((brightness) =>
|
||||
{
|
||||
if (brightness.HasValue)
|
||||
{
|
||||
Brightness = brightness.Value;
|
||||
}
|
||||
});
|
||||
|
||||
public ICommand SetColorTemperatureCommand => new RelayCommand<int?>((temperature) =>
|
||||
{
|
||||
if (temperature.HasValue && _monitor.SupportsColorTemperature)
|
||||
{
|
||||
Logger.LogDebug($"[{Id}] Color temperature command: {temperature.Value}K (DDC/CI)");
|
||||
ColorTemperature = temperature.Value;
|
||||
}
|
||||
else if (temperature.HasValue && !_monitor.SupportsColorTemperature)
|
||||
{
|
||||
Logger.LogWarning($"[{Id}] Color temperature not supported on this monitor");
|
||||
}
|
||||
});
|
||||
|
||||
public ICommand SetContrastCommand => new RelayCommand<int?>((contrast) =>
|
||||
{
|
||||
if (contrast.HasValue)
|
||||
{
|
||||
Contrast = contrast.Value;
|
||||
}
|
||||
});
|
||||
|
||||
public ICommand SetVolumeCommand => new RelayCommand<int?>((volume) =>
|
||||
{
|
||||
if (volume.HasValue)
|
||||
{
|
||||
Volume = volume.Value;
|
||||
}
|
||||
});
|
||||
|
||||
// Percentage-based properties for uniform slider behavior
|
||||
public int ColorTemperaturePercent
|
||||
{
|
||||
get => MapToPercent(_colorTemperature, MinColorTemperature, MaxColorTemperature);
|
||||
set
|
||||
{
|
||||
var actualValue = MapFromPercent(value, MinColorTemperature, MaxColorTemperature);
|
||||
ColorTemperature = actualValue;
|
||||
}
|
||||
}
|
||||
|
||||
public int ContrastPercent
|
||||
{
|
||||
get => MapToPercent(_contrast, MinContrast, MaxContrast);
|
||||
set
|
||||
{
|
||||
var actualValue = MapFromPercent(value, MinContrast, MaxContrast);
|
||||
Contrast = actualValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Mapping functions for percentage conversion
|
||||
private int MapToPercent(int value, int min, int max)
|
||||
{
|
||||
if (max <= min)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)Math.Round((value - min) * 100.0 / (max - min));
|
||||
}
|
||||
|
||||
private int MapFromPercent(int percent, int min, int max)
|
||||
{
|
||||
if (max <= min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
|
||||
percent = Math.Clamp(percent, 0, 100);
|
||||
return min + (int)Math.Round(percent * (max - min) / 100.0);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
|
||||
// Notify percentage properties when actual values change
|
||||
if (propertyName == nameof(ColorTemperature))
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ColorTemperaturePercent)));
|
||||
}
|
||||
else if (propertyName == nameof(Contrast))
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent)));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsInteractionEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Unsubscribe from MainViewModel events
|
||||
if (_mainViewModel != null)
|
||||
{
|
||||
_mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
// Dispose all debouncers
|
||||
_brightnessDebouncer?.Dispose();
|
||||
_colorTempDebouncer?.Dispose();
|
||||
_contrastDebouncer?.Dispose();
|
||||
_volumeDebouncer?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace PowerDisplay.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the current state of a ViewModel
|
||||
/// </summary>
|
||||
public enum ViewModelState
|
||||
{
|
||||
/// <summary>
|
||||
/// Initial state - ViewModel is being initialized
|
||||
/// </summary>
|
||||
Initializing,
|
||||
|
||||
/// <summary>
|
||||
/// Loading state - data is being reloaded or refreshed
|
||||
/// </summary>
|
||||
Loading,
|
||||
|
||||
/// <summary>
|
||||
/// Ready state - ViewModel is ready for user interaction
|
||||
/// </summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>
|
||||
/// Error state - ViewModel encountered an error
|
||||
/// </summary>
|
||||
Error,
|
||||
}
|
||||
}
|
||||
32
src/modules/powerdisplay/PowerDisplay/app.manifest
Normal file
32
src/modules/powerdisplay/PowerDisplay/app.manifest
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="1.0.0.0" name="PowerDisplay.app"/>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
<!-- Windows 11 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- The combination of below two tags have the following effect:
|
||||
1) Per-Monitor for >= Windows 10 Anniversary Update
|
||||
2) System < Windows 10 Anniversary Update
|
||||
-->
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,7 @@
|
||||
#include <string>
|
||||
|
||||
namespace PowerDisplayConstants
|
||||
{
|
||||
// Name of the powertoy module.
|
||||
inline const std::wstring ModuleKey = L"PowerDisplay";
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include <windows.h>
|
||||
#include "resource.h"
|
||||
#include "../../../common/version/version.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_DLL
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", FILE_DESCRIPTION
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", INTERNAL_NAME
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", ORIGINAL_FILENAME
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
|
||||
END
|
||||
END
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (United States) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
#pragma code_page(1252)
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{D1234567-8901-2345-6789-ABCDEF012345}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>PowerDisplayModuleInterface</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup>
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\modules\PowerDisplay\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\PowerDisplayModuleInterface\</IntDir>
|
||||
<TargetName>PowerToys.PowerDisplayModuleInterface</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Constants.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="PowerDisplayProcessManager.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PowerDisplayProcessManager.cpp" />
|
||||
<ClCompile Include="trace.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
|
||||
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
|
||||
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="PowerDisplayModuleInterface.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<Import Project="..\..\..\..\deps\spdlog.props" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Constants.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Trace.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Trace.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="RegistryPreviewExt.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
#include "PowerDisplayProcessManager.h"
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/interop/shared_constants.h>
|
||||
#include <atlstr.h>
|
||||
#include <format>
|
||||
|
||||
namespace
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate a pipe name with UUID suffix
|
||||
/// </summary>
|
||||
std::optional<std::wstring> get_pipe_uuid()
|
||||
{
|
||||
UUID temp_uuid;
|
||||
wchar_t* uuid_chars = nullptr;
|
||||
|
||||
if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
|
||||
{
|
||||
const auto val = get_last_error_message(GetLastError());
|
||||
Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
|
||||
return std::nullopt;
|
||||
}
|
||||
else if (UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) != RPC_S_OK)
|
||||
{
|
||||
const auto val = get_last_error_message(GetLastError());
|
||||
Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto uuid_str = std::wstring(uuid_chars);
|
||||
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
|
||||
|
||||
return uuid_str;
|
||||
}
|
||||
}
|
||||
|
||||
PowerDisplayProcessManager::~PowerDisplayProcessManager()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::start()
|
||||
{
|
||||
m_enabled = true;
|
||||
submit_task([this]() { refresh(); });
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::stop()
|
||||
{
|
||||
m_enabled = false;
|
||||
submit_task([this]() { refresh(); });
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::send_message_to_powerdisplay(const std::wstring& message)
|
||||
{
|
||||
submit_task([this, message]() {
|
||||
if (m_write_pipe)
|
||||
{
|
||||
try
|
||||
{
|
||||
const auto formatted = std::format(L"{}\r\n", message);
|
||||
const CString msg(formatted.c_str());
|
||||
const DWORD bytes_to_write = static_cast<DWORD>(msg.GetLength() * sizeof(TCHAR));
|
||||
|
||||
DWORD bytes_written = 0;
|
||||
if (FAILED(m_write_pipe->Write(msg.GetString(), bytes_to_write, &bytes_written)))
|
||||
{
|
||||
Logger::error(L"Failed to write message to PowerDisplay pipe");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"Sent message to PowerDisplay: {}", message);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Exception while sending message to PowerDisplay");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"Cannot send message to PowerDisplay: pipe not connected");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::submit_task(std::function<void()> task)
|
||||
{
|
||||
m_thread_executor.submit(OnThreadExecutor::task_t{ task });
|
||||
}
|
||||
|
||||
bool PowerDisplayProcessManager::is_process_running() const
|
||||
{
|
||||
return m_hProcess != nullptr && WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::terminate_process()
|
||||
{
|
||||
// Close pipe
|
||||
m_write_pipe.reset();
|
||||
|
||||
// Terminate process
|
||||
if (m_hProcess != nullptr)
|
||||
{
|
||||
TerminateProcess(m_hProcess, 1);
|
||||
CloseHandle(m_hProcess);
|
||||
m_hProcess = nullptr;
|
||||
}
|
||||
|
||||
Logger::trace(L"PowerDisplay process terminated");
|
||||
}
|
||||
|
||||
HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_uuid)
|
||||
{
|
||||
const unsigned long powertoys_pid = GetCurrentProcessId();
|
||||
|
||||
// Pass both runner PID and pipe UUID to PowerDisplay.exe
|
||||
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_uuid);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
||||
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = executable_args.data();
|
||||
|
||||
if (ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::trace(L"Successfully started PowerDisplay process with UUID: {}", pipe_uuid);
|
||||
terminate_process(); // Clean up old process if any
|
||||
m_hProcess = sei.hProcess;
|
||||
return S_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"PowerDisplay process failed to start. {}", get_last_error_or_default(GetLastError()));
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT PowerDisplayProcessManager::start_command_pipe(const std::wstring& pipe_uuid)
|
||||
{
|
||||
const constexpr DWORD BUFSIZE = 4096 * 4;
|
||||
|
||||
// Create pipe for writing to PowerDisplay (OUT)
|
||||
m_pipe_name_out = std::format(L"\\\\.\\pipe\\powertoys_powerdisplay_{}_out", pipe_uuid);
|
||||
|
||||
HANDLE hWritePipe = CreateNamedPipe(
|
||||
m_pipe_name_out.c_str(),
|
||||
PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED,
|
||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||
1, // max instances
|
||||
BUFSIZE, // out buffer size
|
||||
0, // in buffer size (not used for outbound)
|
||||
0, // client timeout
|
||||
NULL // default security
|
||||
);
|
||||
|
||||
if (hWritePipe == NULL || hWritePipe == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
Logger::error(L"Error creating write pipe for PowerDisplay");
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
// Create overlapped event for waiting for client to connect
|
||||
OVERLAPPED write_overlapped = { 0 };
|
||||
write_overlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||
|
||||
if (!write_overlapped.hEvent)
|
||||
{
|
||||
Logger::error(L"Error creating overlapped event for PowerDisplay pipe");
|
||||
CloseHandle(hWritePipe);
|
||||
return E_FAIL;
|
||||
}
|
||||
|
||||
// Connect write pipe
|
||||
if (!ConnectNamedPipe(hWritePipe, &write_overlapped))
|
||||
{
|
||||
const auto lastError = GetLastError();
|
||||
if (lastError != ERROR_IO_PENDING && lastError != ERROR_PIPE_CONNECTED)
|
||||
{
|
||||
Logger::error(L"Error connecting to write pipe");
|
||||
CloseHandle(write_overlapped.hEvent);
|
||||
CloseHandle(hWritePipe);
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for pipe to connect (with timeout)
|
||||
const constexpr DWORD client_timeout_millis = 5000;
|
||||
DWORD wait_result = WaitForSingleObject(write_overlapped.hEvent, client_timeout_millis);
|
||||
|
||||
if (wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
// Pipe connected successfully
|
||||
m_write_pipe = std::make_unique<CAtlFile>(hWritePipe);
|
||||
CloseHandle(write_overlapped.hEvent);
|
||||
|
||||
Logger::trace(L"PowerDisplay command pipe connected successfully");
|
||||
return S_OK;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Timeout waiting for PowerDisplay to connect to command pipe");
|
||||
CloseHandle(write_overlapped.hEvent);
|
||||
CloseHandle(hWritePipe);
|
||||
return E_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
void PowerDisplayProcessManager::refresh()
|
||||
{
|
||||
if (m_enabled == is_process_running())
|
||||
{
|
||||
// Already in correct state
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_enabled)
|
||||
{
|
||||
// Start PowerDisplay process
|
||||
Logger::trace(L"Starting PowerDisplay process");
|
||||
|
||||
const auto pipe_uuid = get_pipe_uuid();
|
||||
if (!pipe_uuid)
|
||||
{
|
||||
Logger::error(L"Failed to generate pipe UUID");
|
||||
return;
|
||||
}
|
||||
|
||||
if (start_command_pipe(pipe_uuid.value()) != S_OK)
|
||||
{
|
||||
Logger::error(L"Failed to initialize command pipe");
|
||||
return;
|
||||
}
|
||||
|
||||
if (start_process(pipe_uuid.value()) != S_OK)
|
||||
{
|
||||
Logger::error(L"Failed to start PowerDisplay process, cleaning up pipes");
|
||||
terminate_process();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop PowerDisplay process
|
||||
Logger::trace(L"Stopping PowerDisplay process");
|
||||
|
||||
// Send terminate message
|
||||
send_message_to_powerdisplay(L"{\"action\":\"terminate\"}");
|
||||
|
||||
// Wait for graceful exit
|
||||
if (m_hProcess != nullptr)
|
||||
{
|
||||
WaitForSingleObject(m_hProcess, 2000);
|
||||
}
|
||||
|
||||
if (is_process_running())
|
||||
{
|
||||
Logger::warn(L"PowerDisplay process failed to gracefully exit, terminating");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::trace(L"PowerDisplay process successfully exited");
|
||||
}
|
||||
|
||||
terminate_process();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <atlfile.h>
|
||||
#include <common/utils/OnThreadExecutor.h>
|
||||
|
||||
/// <summary>
|
||||
/// Manages PowerDisplay.exe process lifecycle and bidirectional IPC communication
|
||||
/// </summary>
|
||||
class PowerDisplayProcessManager
|
||||
{
|
||||
private:
|
||||
HANDLE m_hProcess = nullptr;
|
||||
std::unique_ptr<CAtlFile> m_write_pipe; // Write to PowerDisplay (OUT)
|
||||
OnThreadExecutor m_thread_executor;
|
||||
bool m_enabled = false;
|
||||
|
||||
// Pipe name for this session
|
||||
std::wstring m_pipe_name_out;
|
||||
|
||||
public:
|
||||
PowerDisplayProcessManager() = default;
|
||||
~PowerDisplayProcessManager();
|
||||
|
||||
/// <summary>
|
||||
/// Start PowerDisplay.exe process
|
||||
/// </summary>
|
||||
void start();
|
||||
|
||||
/// <summary>
|
||||
/// Stop PowerDisplay.exe process
|
||||
/// </summary>
|
||||
void stop();
|
||||
|
||||
/// <summary>
|
||||
/// Send message to PowerDisplay.exe
|
||||
/// </summary>
|
||||
void send_message_to_powerdisplay(const std::wstring& message);
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// Submit task to thread executor
|
||||
/// </summary>
|
||||
void submit_task(std::function<void()> task);
|
||||
|
||||
/// <summary>
|
||||
/// Check if PowerDisplay.exe is running
|
||||
/// </summary>
|
||||
bool is_process_running() const;
|
||||
|
||||
/// <summary>
|
||||
/// Terminate PowerDisplay.exe process
|
||||
/// </summary>
|
||||
void terminate_process();
|
||||
|
||||
/// <summary>
|
||||
/// Start PowerDisplay.exe with command line arguments
|
||||
/// </summary>
|
||||
HRESULT start_process(const std::wstring& pipe_uuid);
|
||||
|
||||
/// <summary>
|
||||
/// Create named pipe for sending commands to PowerDisplay
|
||||
/// </summary>
|
||||
HRESULT start_command_pipe(const std::wstring& pipe_uuid);
|
||||
|
||||
/// <summary>
|
||||
/// Refresh - start or stop process based on m_enabled state
|
||||
/// </summary>
|
||||
void refresh();
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "pch.h"
|
||||
#include "trace.h"
|
||||
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
TRACELOGGING_DEFINE_PROVIDER(
|
||||
g_hProvider,
|
||||
"Microsoft.PowerToys",
|
||||
// {38e8889b-9731-53f5-e901-e8a7c1753074}
|
||||
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
|
||||
TraceLoggingOptionProjectTelemetry());
|
||||
|
||||
// Log if the user has enabled or disabled the app
|
||||
void Trace::EnablePowerDisplay(_In_ bool enabled) noexcept
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"PowerDisplay_EnablePowerDisplay",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
|
||||
TraceLoggingBoolean(enabled, "Enabled"));
|
||||
}
|
||||
|
||||
// Log that the user tried to activate the app
|
||||
void Trace::ActivatePowerDisplay() noexcept
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"PowerDisplay_Activate",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
13
src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h
Normal file
13
src/modules/powerdisplay/PowerDisplayModuleInterface/Trace.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <common/Telemetry/TraceBase.h>
|
||||
|
||||
class Trace : public telemetry::TraceBase
|
||||
{
|
||||
public:
|
||||
// Log if the user has enabled or disabled the app
|
||||
static void EnablePowerDisplay(const bool enabled) noexcept;
|
||||
|
||||
// Log that the user tried to activate the app
|
||||
static void ActivatePowerDisplay() noexcept;
|
||||
};
|
||||
245
src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
Normal file
245
src/modules/powerdisplay/PowerDisplayModuleInterface/dllmain.cpp
Normal file
@@ -0,0 +1,245 @@
|
||||
// dllmain.cpp : Defines the entry point for the DLL Application.
|
||||
#include "pch.h"
|
||||
#include <interface/powertoy_module_interface.h>
|
||||
#include <common/SettingsAPI/settings_objects.h>
|
||||
#include "trace.h"
|
||||
#include <common/interop/shared_constants.h>
|
||||
#include <common/utils/string_utils.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/utils/resources.h>
|
||||
|
||||
#include "resource.h"
|
||||
#include "Constants.h"
|
||||
#include "PowerDisplayProcessManager.h"
|
||||
|
||||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
|
||||
{
|
||||
switch (ul_reason_for_call)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
Trace::RegisterProvider();
|
||||
break;
|
||||
case DLL_THREAD_ATTACH:
|
||||
case DLL_THREAD_DETACH:
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
Trace::UnregisterProvider();
|
||||
break;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
const static wchar_t* MODULE_NAME = L"PowerDisplay";
|
||||
const static wchar_t* MODULE_DESC = L"A utility to manage display brightness and color temperature across multiple monitors.";
|
||||
|
||||
namespace
|
||||
{
|
||||
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
|
||||
const wchar_t JSON_KEY_ENABLED[] = L"enabled";
|
||||
const wchar_t JSON_KEY_HOTKEY_ENABLED[] = L"hotkey_enabled";
|
||||
}
|
||||
|
||||
class PowerDisplayModule : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
bool m_hotkey_enabled = false;
|
||||
|
||||
// Process manager for handling PowerDisplay.exe lifecycle and IPC
|
||||
PowerDisplayProcessManager m_process_manager;
|
||||
|
||||
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
|
||||
{
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
|
||||
{
|
||||
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
m_hotkey_enabled = properties.GetNamedBoolean(JSON_KEY_HOTKEY_ENABLED, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info("Properties object not found in settings, using defaults");
|
||||
m_hotkey_enabled = false;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::info("Failed to parse hotkey settings, using defaults");
|
||||
m_hotkey_enabled = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info("Power Display settings are empty");
|
||||
m_hotkey_enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void init_settings()
|
||||
{
|
||||
try
|
||||
{
|
||||
PowerToysSettings::PowerToyValues settings =
|
||||
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
|
||||
|
||||
parse_hotkey_settings(settings);
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error("Invalid json when trying to load the Power Display settings json from file.");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
PowerDisplayModule()
|
||||
{
|
||||
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "PowerDisplay");
|
||||
Logger::info("Power Display object is constructing");
|
||||
|
||||
init_settings();
|
||||
|
||||
// Note: PowerDisplay.exe will send messages directly to runner via named pipes
|
||||
// The runner's message_receiver_thread will handle routing to Settings UI
|
||||
// No need to set a callback here - the process manager just manages lifecycle
|
||||
}
|
||||
|
||||
~PowerDisplayModule()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
m_process_manager.stop();
|
||||
}
|
||||
m_enabled = false;
|
||||
}
|
||||
|
||||
virtual void destroy() override
|
||||
{
|
||||
Logger::trace("PowerDisplay::destroy()");
|
||||
if (m_enabled)
|
||||
{
|
||||
m_process_manager.stop();
|
||||
}
|
||||
delete this;
|
||||
}
|
||||
|
||||
virtual const wchar_t* get_name() override
|
||||
{
|
||||
return MODULE_NAME;
|
||||
}
|
||||
|
||||
virtual const wchar_t* get_key() override
|
||||
{
|
||||
return MODULE_NAME;
|
||||
}
|
||||
|
||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
||||
{
|
||||
return powertoys_gpo::gpo_rule_configured_not_configured;
|
||||
}
|
||||
|
||||
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
||||
{
|
||||
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
|
||||
PowerToysSettings::Settings settings(hinstance, get_name());
|
||||
settings.set_description(MODULE_DESC);
|
||||
|
||||
return settings.serialize_to_buffer(buffer, buffer_size);
|
||||
}
|
||||
|
||||
virtual void call_custom_action(const wchar_t* action) override
|
||||
{
|
||||
try
|
||||
{
|
||||
PowerToysSettings::CustomActionObject action_object =
|
||||
PowerToysSettings::CustomActionObject::from_json_string(action);
|
||||
|
||||
if (action_object.get_name() == L"Launch")
|
||||
{
|
||||
Logger::trace(L"Launch action received, sending show_window command");
|
||||
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"show_window\"}");
|
||||
Trace::ActivatePowerDisplay();
|
||||
}
|
||||
else if (action_object.get_name() == L"RefreshMonitors")
|
||||
{
|
||||
Logger::trace(L"RefreshMonitors action received");
|
||||
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"refresh_monitors\"}");
|
||||
}
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error(L"Failed to parse action. {}", action);
|
||||
}
|
||||
}
|
||||
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
{
|
||||
try
|
||||
{
|
||||
PowerToysSettings::PowerToyValues values =
|
||||
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
|
||||
|
||||
parse_hotkey_settings(values);
|
||||
values.save_to_settings_file();
|
||||
|
||||
// Notify PowerDisplay.exe that settings have been updated
|
||||
auto message = std::format(L"{{\"action\":\"settings_updated\",\"config\":{}}}", config);
|
||||
m_process_manager.send_message_to_powerdisplay(message);
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
Logger::error(L"Invalid json when trying to parse Power Display settings json.");
|
||||
}
|
||||
}
|
||||
|
||||
virtual void enable() override
|
||||
{
|
||||
m_enabled = true;
|
||||
Trace::EnablePowerDisplay(true);
|
||||
|
||||
Logger::trace(L"PowerDisplay enabled, starting process manager");
|
||||
m_process_manager.start();
|
||||
}
|
||||
|
||||
virtual void disable() override
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
Logger::trace(L"Disabling Power Display...");
|
||||
m_process_manager.stop();
|
||||
}
|
||||
|
||||
m_enabled = false;
|
||||
Trace::EnablePowerDisplay(false);
|
||||
}
|
||||
|
||||
virtual bool is_enabled() override
|
||||
{
|
||||
return m_enabled;
|
||||
}
|
||||
|
||||
virtual bool on_hotkey(size_t /*hotkeyId*/) override
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
Logger::trace(L"Power Display hotkey pressed");
|
||||
// Send toggle window command
|
||||
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"toggle_window\"}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new PowerDisplayModule();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
15
src/modules/powerdisplay/PowerDisplayModuleInterface/pch.h
Normal file
15
src/modules/powerdisplay/PowerDisplayModuleInterface/pch.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
//#include <winrt/Windows.Foundation.h>
|
||||
#include <strsafe.h>
|
||||
#include <hIdUsage.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
//#include <Shlwapi.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include <common/logger/logger.h>
|
||||
@@ -0,0 +1,13 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by PowerDisplayExt.rc
|
||||
|
||||
//////////////////////////////
|
||||
// Non-localizable
|
||||
|
||||
#define FILE_DESCRIPTION "PowerToys PowerDisplay Module"
|
||||
#define INTERNAL_NAME "PowerToys.PowerDisplay"
|
||||
#define ORIGINAL_FILENAME "PowerToys.PowerDisplay.dll"
|
||||
|
||||
// Non-localizable
|
||||
//////////////////////////////
|
||||
@@ -178,6 +178,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
L"PowerToys.CmdPalModuleInterface.dll",
|
||||
L"PowerToys.ZoomItModuleInterface.dll",
|
||||
L"PowerToys.LightSwitchModuleInterface.dll",
|
||||
L"PowerToys.PowerDisplayExt.dll",
|
||||
};
|
||||
|
||||
for (auto moduleSubdir : knownModules)
|
||||
|
||||
@@ -323,6 +323,48 @@ void dispatch_received_json(const std::wstring& json_to_parse)
|
||||
Logger::error(L"Failed to process get all hotkey conflicts request");
|
||||
}
|
||||
}
|
||||
else if (name == L"powerdisplay_response")
|
||||
{
|
||||
try
|
||||
{
|
||||
// Forward PowerDisplay response messages to Settings UI
|
||||
// PowerDisplay sends monitor information via IPC
|
||||
std::unique_lock lock{ ipc_mutex };
|
||||
if (current_settings_ipc)
|
||||
{
|
||||
current_settings_ipc->send(value.Stringify().c_str());
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to forward PowerDisplay response to Settings");
|
||||
}
|
||||
}
|
||||
else if (name == L"powerdisplay_command")
|
||||
{
|
||||
try
|
||||
{
|
||||
// Forward command from Settings UI to PowerDisplay module
|
||||
Logger::trace(L"Received command from Settings UI to PowerDisplay");
|
||||
|
||||
// Find PowerDisplay module and send the command
|
||||
auto moduleIt = modules().find(L"PowerDisplay");
|
||||
if (moduleIt != modules().end())
|
||||
{
|
||||
// Use call_custom_action to send the command
|
||||
// The command should contain an action field
|
||||
moduleIt->second->call_custom_action(value.Stringify().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"PowerDisplay module not found, cannot send command");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::error(L"Failed to forward command to PowerDisplay");
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -809,6 +851,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value)
|
||||
return "CmdPal";
|
||||
case ESettingsWindowNames::ZoomIt:
|
||||
return "ZoomIt";
|
||||
case ESettingsWindowNames::PowerDisplay:
|
||||
return "PowerDisplay";
|
||||
default:
|
||||
{
|
||||
Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast<int>(value));
|
||||
@@ -948,6 +992,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
|
||||
{
|
||||
return ESettingsWindowNames::ZoomIt;
|
||||
}
|
||||
else if (value == "PowerDisplay")
|
||||
{
|
||||
return ESettingsWindowNames::PowerDisplay;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value));
|
||||
@@ -956,3 +1004,29 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value)
|
||||
|
||||
return ESettingsWindowNames::Dashboard;
|
||||
}
|
||||
|
||||
// Global function for PowerDisplay module to send messages to Settings UI
|
||||
void send_powerdisplay_message_to_settings_ui(const wchar_t* message)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger::trace(L"Sending PowerDisplay message to Settings UI");
|
||||
|
||||
std::unique_lock lock{ ipc_mutex };
|
||||
if (current_settings_ipc)
|
||||
{
|
||||
// Wrap the message in powerdisplay_response format
|
||||
json::JsonObject wrapper;
|
||||
wrapper.SetNamedValue(L"powerdisplay_response", json::JsonValue::Parse(message));
|
||||
current_settings_ipc->send(wrapper.Stringify().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"current_settings_ipc is null, cannot send to Settings UI");
|
||||
}
|
||||
}
|
||||
catch (const std::exception&)
|
||||
{
|
||||
Logger::error(L"Exception while sending PowerDisplay message to Settings UI");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ enum class ESettingsWindowNames
|
||||
NewPlus,
|
||||
CmdPal,
|
||||
ZoomIt,
|
||||
PowerDisplay,
|
||||
};
|
||||
|
||||
std::string ESettingsWindowNames_to_string(ESettingsWindowNames value);
|
||||
@@ -47,3 +48,6 @@ void close_settings_window();
|
||||
void open_oobe_window();
|
||||
void open_scoobe_window();
|
||||
void open_flyout();
|
||||
|
||||
// PowerDisplay IPC support
|
||||
void send_powerdisplay_message_to_settings_ui(const wchar_t* message);
|
||||
|
||||
@@ -58,6 +58,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.AdvancedPasteProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,33 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all PowerToys module settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><strong>IMPORTANT for Native AOT compatibility:</strong></para>
|
||||
/// <para>When creating a new class that inherits from <see cref="BasePTModuleSettings"/>,
|
||||
/// you MUST register it in <see cref="SettingsSerializationContext"/> by adding a
|
||||
/// <c>[JsonSerializable(typeof(YourNewSettingsClass))]</c> attribute.</para>
|
||||
/// <para>Failure to register the type will cause <see cref="ToJsonString"/> to throw
|
||||
/// <see cref="InvalidOperationException"/> at runtime.</para>
|
||||
/// <para>See <see cref="SettingsSerializationContext"/> for registration instructions.</para>
|
||||
/// </remarks>
|
||||
public abstract class BasePTModuleSettings
|
||||
{
|
||||
// Cached JsonSerializerOptions for Native AOT compatibility
|
||||
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
TypeInfoResolver = SettingsSerializationContext.Default,
|
||||
};
|
||||
|
||||
// Gets or sets name of the powertoy module.
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
@@ -17,11 +37,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; set; }
|
||||
|
||||
// converts the current to a json string.
|
||||
/// <summary>
|
||||
/// Converts the current settings object to a JSON string.
|
||||
/// </summary>
|
||||
/// <returns>JSON string representation of this settings object.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when the runtime type is not registered in <see cref="SettingsSerializationContext"/>.
|
||||
/// All derived types must be registered with <c>[JsonSerializable(typeof(YourType))]</c> attribute.
|
||||
/// </exception>
|
||||
/// <remarks>
|
||||
/// This method uses Native AOT-compatible JSON serialization. The runtime type must be
|
||||
/// registered in <see cref="SettingsSerializationContext"/> for serialization to work.
|
||||
/// </remarks>
|
||||
public virtual string ToJsonString()
|
||||
{
|
||||
// By default JsonSerializer will only serialize the properties in the base class. This can be avoided by passing the object type (more details at https://stackoverflow.com/a/62498888)
|
||||
return JsonSerializer.Serialize(this, GetType());
|
||||
var runtimeType = GetType();
|
||||
|
||||
// For Native AOT compatibility, get JsonTypeInfo from the TypeInfoResolver
|
||||
var typeInfo = _jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(runtimeType, _jsonSerializerOptions);
|
||||
|
||||
if (typeInfo == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Type {runtimeType.FullName} is not registered in SettingsSerializationContext. Please add it to the [JsonSerializable] attributes.");
|
||||
}
|
||||
|
||||
// Use AOT-friendly serialization
|
||||
return JsonSerializer.Serialize(this, typeInfo);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.BoolProperty);
|
||||
}
|
||||
|
||||
public bool TryToCmdRepresentable(out string result)
|
||||
|
||||
@@ -12,14 +12,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var boolProperty = JsonSerializer.Deserialize<BoolProperty>(ref reader, options);
|
||||
var boolProperty = JsonSerializer.Deserialize(ref reader, SettingsSerializationContext.Default.BoolProperty);
|
||||
return boolProperty.Value;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
var boolProperty = new BoolProperty(value);
|
||||
JsonSerializer.Serialize(writer, boolProperty, options);
|
||||
JsonSerializer.Serialize(writer, boolProperty, SettingsSerializationContext.Default.BoolProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
|
||||
{
|
||||
Hotkey = JsonSerializer.Deserialize<HotkeySettings>(hotkeyElement.GetRawText());
|
||||
Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText(), SettingsSerializationContext.Default.HotkeySettings);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
|
||||
@@ -87,6 +87,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public bool ShowColorName { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public bool ShowColorName { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerPropertiesVersion1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// Returns a JSON version of the class settings configuration class.
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.DoubleProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,6 +530,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
}
|
||||
|
||||
private bool powerDisplay;
|
||||
|
||||
[JsonPropertyName("PowerDisplay")]
|
||||
public bool PowerDisplay
|
||||
{
|
||||
get => powerDisplay;
|
||||
set
|
||||
{
|
||||
if (powerDisplay != value)
|
||||
{
|
||||
LogTelemetryEvent(value);
|
||||
powerDisplay = value;
|
||||
NotifyChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyChange()
|
||||
{
|
||||
notifyEnabledChangedAction?.Invoke();
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithLocalProperties);
|
||||
}
|
||||
|
||||
// This function is required to implement the ISettingsConfig interface and obtain the settings configurations.
|
||||
|
||||
@@ -17,6 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("bool_show_extended_menu")]
|
||||
public BoolProperty ExtendedContextMenuOnly { get; set; }
|
||||
|
||||
public override string ToString() => JsonSerializer.Serialize(this);
|
||||
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// converts the current to a json string.
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettings);
|
||||
}
|
||||
|
||||
private static string DefaultPowertoysVersion()
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettingsCustomAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
||||
{
|
||||
public struct SunTimes
|
||||
{
|
||||
public int SunriseHour;
|
||||
public int SunriseMinute;
|
||||
public int SunsetHour;
|
||||
public int SunsetMinute;
|
||||
public string Text;
|
||||
public int SunriseHour { get; set; }
|
||||
|
||||
public bool HasSunrise;
|
||||
public bool HasSunset;
|
||||
public int SunriseMinute { get; set; }
|
||||
|
||||
public int SunsetHour { get; set; }
|
||||
|
||||
public int SunsetMinute { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public bool HasSunrise { get; set; }
|
||||
|
||||
public bool HasSunset { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public override string ToJsonString()
|
||||
{
|
||||
var options = _serializerOptions;
|
||||
return JsonSerializer.Serialize(this, options);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerSettings);
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// Returns a JSON version of the class settings configuration class.
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.IntProperty);
|
||||
}
|
||||
|
||||
public static implicit operator IntProperty(int v)
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.KeyboardManagerProfile);
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
|
||||
@@ -46,6 +46,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public IntProperty DefaultMeasureStyle { get; set; }
|
||||
|
||||
public override string ToString() => JsonSerializer.Serialize(this);
|
||||
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.MeasureToolProperties);
|
||||
}
|
||||
}
|
||||
|
||||
201
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
201
src/settings-ui/Settings.UI.Library/MonitorInfo.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class MonitorInfo : Observable
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
private string _internalName = string.Empty;
|
||||
private string _hardwareId = string.Empty;
|
||||
private string _communicationMethod = string.Empty;
|
||||
private string _monitorType = string.Empty;
|
||||
private int _currentBrightness;
|
||||
private int _colorTemperature = 6500;
|
||||
private bool _isHidden;
|
||||
private bool _enableColorTemperature;
|
||||
private bool _enableContrast;
|
||||
private bool _enableVolume;
|
||||
|
||||
public MonitorInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public MonitorInfo(string name, string internalName, string communicationMethod)
|
||||
{
|
||||
Name = name;
|
||||
InternalName = internalName;
|
||||
CommunicationMethod = communicationMethod;
|
||||
}
|
||||
|
||||
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, string monitorType, int currentBrightness, int colorTemperature)
|
||||
{
|
||||
Name = name;
|
||||
InternalName = internalName;
|
||||
HardwareId = hardwareId;
|
||||
CommunicationMethod = communicationMethod;
|
||||
MonitorType = monitorType;
|
||||
CurrentBrightness = currentBrightness;
|
||||
ColorTemperature = colorTemperature;
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (_name != value)
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("internalName")]
|
||||
public string InternalName
|
||||
{
|
||||
get => _internalName;
|
||||
set
|
||||
{
|
||||
if (_internalName != value)
|
||||
{
|
||||
_internalName = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("communicationMethod")]
|
||||
public string CommunicationMethod
|
||||
{
|
||||
get => _communicationMethod;
|
||||
set
|
||||
{
|
||||
if (_communicationMethod != value)
|
||||
{
|
||||
_communicationMethod = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("hardwareId")]
|
||||
public string HardwareId
|
||||
{
|
||||
get => _hardwareId;
|
||||
set
|
||||
{
|
||||
if (_hardwareId != value)
|
||||
{
|
||||
_hardwareId = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("monitorType")]
|
||||
public string MonitorType
|
||||
{
|
||||
get => _monitorType;
|
||||
set
|
||||
{
|
||||
if (_monitorType != value)
|
||||
{
|
||||
_monitorType = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("currentBrightness")]
|
||||
public int CurrentBrightness
|
||||
{
|
||||
get => _currentBrightness;
|
||||
set
|
||||
{
|
||||
if (_currentBrightness != value)
|
||||
{
|
||||
_currentBrightness = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int ColorTemperature
|
||||
{
|
||||
get => _colorTemperature;
|
||||
set
|
||||
{
|
||||
if (_colorTemperature != value)
|
||||
{
|
||||
_colorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("isHidden")]
|
||||
public bool IsHidden
|
||||
{
|
||||
get => _isHidden;
|
||||
set
|
||||
{
|
||||
if (_isHidden != value)
|
||||
{
|
||||
_isHidden = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("enableColorTemperature")]
|
||||
public bool EnableColorTemperature
|
||||
{
|
||||
get => _enableColorTemperature;
|
||||
set
|
||||
{
|
||||
if (_enableColorTemperature != value)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[MonitorInfo] EnableColorTemperature changing from {_enableColorTemperature} to {value} for monitor {Name}");
|
||||
_enableColorTemperature = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("enableContrast")]
|
||||
public bool EnableContrast
|
||||
{
|
||||
get => _enableContrast;
|
||||
set
|
||||
{
|
||||
if (_enableContrast != value)
|
||||
{
|
||||
_enableContrast = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("enableVolume")]
|
||||
public bool EnableVolume
|
||||
{
|
||||
get => _enableVolume;
|
||||
set
|
||||
{
|
||||
if (_enableVolume != value)
|
||||
{
|
||||
_enableVolume = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/settings-ui/Settings.UI.Library/MonitorInfoData.cs
Normal file
35
src/settings-ui/Settings.UI.Library/MonitorInfoData.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitor information data for IPC
|
||||
/// </summary>
|
||||
public class MonitorInfoData
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("internalName")]
|
||||
public string InternalName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("hardwareId")]
|
||||
public string HardwareId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("communicationMethod")]
|
||||
public string CommunicationMethod { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("monitorType")]
|
||||
public string MonitorType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("currentBrightness")]
|
||||
public int CurrentBrightness { get; set; }
|
||||
|
||||
[JsonPropertyName("colorTemperature")]
|
||||
public int ColorTemperature { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
public struct ConnectionRequest
|
||||
#pragma warning restore SA1649 // File name should match first type name
|
||||
{
|
||||
public string PCName;
|
||||
public string SecurityKey;
|
||||
public string PCName { get; set; }
|
||||
|
||||
public string SecurityKey { get; set; }
|
||||
}
|
||||
|
||||
public struct NewKeyGenerationRequest
|
||||
|
||||
@@ -33,6 +33,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("ReplaceVariables")]
|
||||
public BoolProperty ReplaceVariables { get; set; }
|
||||
|
||||
public override string ToString() => JsonSerializer.Serialize(this);
|
||||
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.NewPlusProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingGeneralSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingLanguageSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Settings.UI.Library
|
||||
|
||||
public string ToJsonString()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
return JsonSerializer.Serialize(this, Microsoft.PowerToys.Settings.UI.Library.SettingsSerializationContext.Default.PeekPreviewSettings);
|
||||
}
|
||||
|
||||
public string GetModuleName()
|
||||
|
||||
@@ -32,6 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
public BoolProperty EnableSpaceToActivate { get; set; }
|
||||
|
||||
public override string ToString() => JsonSerializer.Serialize(this);
|
||||
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PeekProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Message for PowerDisplay module actions
|
||||
/// </summary>
|
||||
public class PowerDisplayActionMessage
|
||||
{
|
||||
[JsonPropertyName("action")]
|
||||
public ActionData Action { get; set; }
|
||||
|
||||
public class ActionData
|
||||
{
|
||||
[JsonPropertyName("PowerDisplay")]
|
||||
public PowerDisplayAction PowerDisplay { get; set; }
|
||||
}
|
||||
|
||||
public class PowerDisplayAction
|
||||
{
|
||||
[JsonPropertyName("action_name")]
|
||||
public string ActionName { get; set; }
|
||||
|
||||
[JsonPropertyName("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user