Introduce PowerDisplay

This commit is contained in:
Yu Leng
2025-10-20 16:22:47 +08:00
parent b1985bc8d1
commit e2774eff2d
121 changed files with 11869 additions and 43 deletions

View File

@@ -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" />

View File

@@ -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}

View 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>

View 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>

View File

@@ -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" />

View File

@@ -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;

View File

@@ -29,6 +29,7 @@ namespace ManagedCommon
PowerRename,
PowerLauncher,
PowerAccent,
PowerDisplay,
RegistryPreview,
MeasureTool,
ShortcutGuide,

View File

@@ -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;
}
}

View File

@@ -52,6 +52,8 @@ namespace winrt::PowerToys::Interop::implementation
static hstring WorkspacesHotkeyEvent();
static hstring PowerToysRunnerTerminateSettingsEvent();
static hstring ShowCmdPalEvent();
static hstring ShowPowerDisplayEvent();
static hstring TerminatePowerDisplayEvent();
};
}

View File

@@ -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";

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View 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;
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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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";
}
}
}

View 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;
}
}
}
}

View File

@@ -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,
}
}

View File

@@ -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}";
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
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,
}
}

View 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;
}
}
}
}

View File

@@ -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",
};
}
}
}

View 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]

View File

@@ -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);
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.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}");
}
}
}
}

View 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);
}
}
}

View 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");
}
}
}
}

View File

@@ -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.
}
}
}

View File

@@ -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;
}
}
}
}

View 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; }
}
}

View File

@@ -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";
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View 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;
}
}

View 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;
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);
}

View 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
}
}
}
}

View 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();
}
}

View File

@@ -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;
}
}
}
}

View 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);
}
}
}

View 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>

View File

@@ -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>

View File

@@ -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);
}
}
}
}

View File

@@ -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="&#xE7F4;" />
</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="&#xE793;"
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="&#xE790;"
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="&#xE7A6;"
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="&#xE767;"
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="&#xE71B;" FontSize="16" />
</Button>
<Button x:Name="DisableButton"
Width="40"
Height="40"
CornerRadius="4"
Background="Transparent"
BorderThickness="0"
ToolTipService.ToolTip="Toggle control">
<FontIcon Glyph="&#xE7E8;" FontSize="16" />
</Button>
<Button x:Name="RefreshButton"
Width="40"
Height="40"
CornerRadius="4"
Background="Transparent"
BorderThickness="0"
ToolTipService.ToolTip="Refresh monitors">
<FontIcon Glyph="&#xE72C;" FontSize="16" />
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -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);
}
}
}

View 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.");
}
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
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,
}
}

View 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>

View File

@@ -0,0 +1,7 @@
#include <string>
namespace PowerDisplayConstants
{
// Name of the powertoy module.
inline const std::wstring ModuleKey = L"PowerDisplay";
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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();
};

View File

@@ -0,0 +1,32 @@
#include "pch.h"
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
// Log if the user has enabled or disabled the app
void Trace::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));
}

View 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;
};

View 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();
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View 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>

View File

@@ -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
//////////////////////////////

View File

@@ -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)

View File

@@ -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");
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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()

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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);
}
}

View 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();
}
}
}
}
}

View 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; }
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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()

View File

@@ -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);
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
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