Compare commits

...

35 Commits

Author SHA1 Message Date
Niels Laute
9c23cbd448 More changes to the Settings page 2025-11-24 20:22:36 +01:00
Niels Laute
2992907142 More changes 2025-11-24 16:28:51 +01:00
Niels Laute
a7e006332a Fixes 2025-11-24 15:12:10 +01:00
Niels Laute
d85d109c78 Bunch of UX improvements to the flyout 2025-11-24 14:51:32 +01:00
Niels Laute
fe5edd9c5d First set of UX changes to flyout 2025-11-24 12:23:59 +01:00
Yu Leng
8806e4ef2e Remove deprecated Power Display event methods
Removed `ShowPowerDisplayEvent`, `TerminatePowerDisplayEvent`,
`SettingsUpdatedPowerDisplayEvent`, and
`ApplyColorTemperaturePowerDisplayEvent` methods from the
`Constants` class in `Constants.cpp`, `Constants.h`, and
`Constants.idl`. These methods were associated with Power
Display functionality that is no longer needed.

This change simplifies the codebase by removing unused
constants and methods related to Power Display events.
2025-11-21 17:08:50 +08:00
Yu Leng
b4ccac5ec2 Update InfoBar and resource strings for PowerDisplay
Updated the `InfoBar` in `LightSwitchPage.xaml` to improve
accessibility and visual consistency, including changes to
`Severity`, `Background`, and `HyperlinkButton` alignment.

Added new resource entries in `Resources.resw` for monitor
settings and updated the `PowerDisplayDisabledWarningBar`
title to clarify its purpose. Introduced additional resource
strings to support the updated messaging.
2025-11-21 16:45:15 +08:00
Yu Leng
b381472bf7 Refactor LightSwitch and PowerDisplay integration
Simplify theme change notification logic in `LightSwitchService.cpp` by consolidating redundant checks and improving error handling. Remove the `applyMonitorSettings` setting and associated logic from `LightSwitchSettings`.

Introduce `PowerDisplayProfilesHelper` to centralize profile management, ensuring thread safety and simplifying file operations. Update UI in `LightSwitchPage.xaml` to replace `ApplyMonitorSettings` with separate dark and light mode profile settings, adding navigation to PowerDisplay settings.

Enhance `LightSwitchViewModel` with nullable annotations, new profile selection properties, and improved property synchronization. Refactor `PowerDisplayViewModel` to use `PowerDisplayProfilesHelper` for profile management.

Update localization strings for new UI elements. Perform general code cleanup, including null safety annotations, improved logging, and removal of legacy code.
2025-11-21 15:36:56 +08:00
Yu Leng
925c97a7f9 Add PowerDisplay integration for theme-based profiles
Integrated PowerDisplay with LightSwitch to enable automatic monitor profile switching based on theme changes. Added event signaling to notify PowerDisplay of theme updates.

Enhanced `LightSwitchService` to support this integration and improved logging for better traceability. Updated `LightSwitchSettings` and `LightSwitchConfig` to include new settings for monitor profile management.

Introduced a background thread in `MainViewModel` to listen for theme change events and apply the appropriate PowerDisplay profile. Added UI elements for managing monitor settings and profiles in `LightSwitchPage`.

Implemented methods in `LightSwitchViewModel` to load PowerDisplay profiles, check module status, and manage new settings. Added localized strings for the new UI elements and warnings.

Improved backward compatibility for old profiles and enhanced error handling throughout the codebase.
2025-11-20 17:30:10 +08:00
Yu Leng
336234d05b Add InternalName for monitor identification
Enhanced monitor identification by introducing the `InternalName`
property as a unique identifier, with fallback to `HardwareId`
for backward compatibility. Updated `MainViewModel` logic,
logging, and UI bindings to use `InternalName`.

Extended `ProfileMonitorSetting` to include `MonitorInternalName`
for serialization and profile management. Adjusted profile
creation and pre-fill logic to support the new property.

These changes improve robustness, maintain compatibility with
older profiles, and enhance clarity in logging and the UI.
2025-11-20 16:36:37 +08:00
Yu Leng
8aec939c9d Enhance profile settings and UI for monitor controls
Refactor logic to support optional inclusion of brightness, contrast, volume, and color temperature in monitor profiles. Updated `Brightness` and `ColorTemperature` to nullable types and adjusted related logic in `MainViewModel.cs` and `ProfileMonitorSetting.cs`.

Improved the UI in `PowerDisplayPage.xaml` and `ProfileEditorDialog.xaml`:
- Added toggle switches for selectively including settings in profiles.
- Enhanced layout and styling for better user experience.
- Updated context menu and monitor selection visuals.

Enhanced `MonitorSelectionItem.cs` with new `Include` flags and auto-selection suppression. Updated `ProfileEditorViewModel.cs` to validate profiles and ensure at least one setting is included for selected monitors.

Performed general code cleanup for readability and maintainability.
2025-11-20 15:13:27 +08:00
Yu Leng
d64bb78727 Refactor PowerDisplay profile management
Simplified profile management by removing the concept of "Custom profiles" and "current profile" tracking. Profiles are now treated as templates for quick application of monitor settings, rather than persistent states.

Key changes include:
- Replaced `ObservableCollection<string>` with `ObservableCollection<PowerDisplayProfile>` to manage profile objects directly.
- Removed redundant properties and methods related to "selected" and "current" profiles.
- Refactored methods for creating, updating, and deleting profiles to operate on `PowerDisplayProfile` objects.
- Updated `PowerDisplayViewModel` and `ProfileManager` to streamline profile loading, saving, and application logic.
- Updated the UI to replace the profile dropdown with buttons for quick application, along with context menu options for managing profiles.
- Improved logging and error handling for profile operations.
- Updated resource strings and removed references to "Custom profiles" and "current profile."

These changes simplify the codebase, improve maintainability, and align the application with the new design philosophy of treating profiles as templates.
2025-11-20 04:40:36 +08:00
Yu Leng
b8abff02ac Add profile management system to PowerDisplay
Introduced a comprehensive profile management system for PowerDisplay, enabling users to create, edit, delete, and apply predefined monitor settings. Key changes include:

- Added `ProfileManager` for handling profile storage and retrieval.
- Introduced `PowerDisplayProfile`, `PowerDisplayProfiles`, and related data models for profile representation.
- Enhanced `MainViewModel` and `MonitorViewModel` to support profile application and parameter change detection.
- Created `ProfileEditorDialog` for editing and creating profiles via the UI.
- Updated `PowerDisplayViewModel` to manage profiles, including commands for adding, deleting, renaming, and saving profiles.
- Added new events (`ApplyProfileEvent`) and constants for profile application.
- Updated `PowerDisplayPage` UI to include a "Profiles" section for managing profiles.
- Added serialization support for profile-related classes.
- Updated `dllmain.cpp` and `App.xaml.cs` to handle profile-related events.

These changes improve user experience by allowing quick switching between tailored monitor configurations.
2025-11-19 17:18:01 +08:00
Yu Leng
fc54172e13 Refactor color temperature operation handling
Introduced `ColorTemperatureOperation` class to manage pending color temperature changes. Updated `MainViewModel` to process operations for specific monitors, improving efficiency and separation of concerns.

Added `PendingColorTemperatureOperation` property to `PowerDisplayProperties` for tracking operations. Enhanced IPC messaging with `MonitorId` and `ColorTemperature` in `PowerDisplayActionMessage`.

Refactored `PowerDisplayViewModel` and `PowerDisplayPage` to directly apply color temperature to specified monitors. Improved logging for better traceability.
2025-11-19 16:16:04 +08:00
Yu Leng
a48e999963 Improve error handling, debouncing, and code cleanup
Enhanced exception handling in RelayCommand to improve robustness.
Standardized slider debounce delays using a new constant
`SliderDebounceDelayMs`. Improved resource management in
SimpleDebouncer with proper disposal of CancellationTokenSource
and added support for synchronous actions. Refactored event
handling in App.xaml.cs for clarity and consistency. Removed
redundant logging in MonitorStateManager and MainViewModel to
reduce verbosity. Updated namespaces and dependencies for better
organization. General code cleanup to improve readability and
maintainability.
2025-11-19 15:26:35 +08:00
Yu Leng
ad83b5e67f Improve performance, thread safety, and resource handling
Enhanced monitor initialization with parallelism in `MonitorManager.cs` for better performance. Added cancellation support to `NativeEventWaiter.cs` with `CancellationToken` and timeout handling. Introduced thread safety in `PhysicalMonitorHandleManager.cs` using locks to prevent race conditions.

Updated `PowerDisplayViewModel.cs` to include proper resource cleanup with `CancellationTokenSource` and improved memory management. Added necessary namespaces for threading and asynchronous operations. General code improvements for readability, maintainability, and reliability.
2025-11-19 15:08:00 +08:00
Yu Leng
f10c9f49e9 Add color temperature support to PowerDisplay
Enhanced PowerDisplay with support for applying color temperature settings.

- Added `APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT` and event handling logic.
- Introduced `ApplyColorTemperatureFromSettings` in `MainViewModel` for explicit hardware updates.
- Refactored `MonitorInfo` to dynamically compute and cache color temperature presets.
- Updated `ReloadMonitorsFromSettings` to preserve object references and improve UI responsiveness.
- Simplified UI bindings and removed redundant properties like `MonitorType`.
- Improved event handling in `dllmain.cpp` for the new color temperature action.
- Enhanced logging for better debugging and traceability.
- Updated JSON serialization context to include new types for color temperature.
- Removed unused code and improved documentation for maintainability.
2025-11-18 20:03:36 +08:00
Yu Leng
3f84ccc603 Refactor and modernize codebase for maintainability
Refactored code to improve performance, readability, and scalability:
- Removed color temperature constants and obsolete VCP codes.
- Converted `MonitorStateManager` methods to async for non-blocking I/O.
- Added retry logic for physical monitor discovery in `DdcCiController`.
- Simplified UI logic in `MainWindow.xaml.cs` by removing animations.
- Streamlined `MainViewModel` initialization and reduced excessive logging.
- Enhanced error handling during disposal and initialization processes.
- Removed deprecated methods and unused features for cleaner code.
- Consolidated repetitive code into reusable helper methods.
- Replaced hardcoded UI constants with configurable values in `AppConstants`.

These changes align the application with modern coding practices.
2025-11-18 01:42:10 +08:00
Yu Leng
55cd6c95b8 Fix settings issue 2025-11-17 18:21:46 +08:00
Yu Leng
15e6a762d3 fix settings ui issue 2025-11-17 16:13:52 +08:00
Yu Leng
f05740b0cb Fix color temperature doesn't work issue 2025-11-17 15:39:36 +08:00
Yu Leng
5f97f7f222 Fix UI issue 2025-11-17 14:53:43 +08:00
Yu Leng
94bc13e703 Refactor PowerDisplay for dynamic monitor capabilities
Removed reliance on static `MonitorType` enumeration, replacing it with dynamic `CommunicationMethod` for better flexibility. Updated `IMonitorController` and `MonitorManager` to dynamically determine monitor control capabilities.

Refactored `Monitor` model to streamline properties and improve color temperature handling. Enhanced `MonitorViewModel` with unified methods for brightness, contrast, volume, and color temperature updates, improving UI responsiveness and hardware synchronization.

Improved settings handling by adding support for hidden monitors, preserving user preferences, and separating UI configuration from hardware parameter updates. Updated the PowerDisplay Settings UI with warnings, confirmation dialogs, and better VCP capabilities formatting.

Removed legacy IPC code in favor of event-driven settings updates. Conducted general code cleanup, improving logging, error handling, and documentation for maintainability.
2025-11-14 16:45:22 +08:00
Yu Leng
e645a19629 Refactor color temperature handling to use VCP presets
Transitioned color temperature handling from Kelvin-based values to VCP code `0x14` (Select Color Preset). Removed legacy Kelvin-to-VCP conversion logic and deprecated unused VCP codes. Updated `Monitor` and `MonitorViewModel` to reflect this change, making `ColorTemperature` read-only in the flyout UI and configurable via the Settings UI.

Enhanced monitor capabilities detection by relying on reported VCP codes instead of trial-and-error probing. Introduced `CapabilitiesStatus` to indicate feature availability and dynamically populated color temperature presets from VCP code `0x14`.

Streamlined the UI by replacing the color temperature slider with a ComboBox in the Settings UI. Added tooltips, warnings for unavailable capabilities, and improved logging for brightness and color temperature operations.

Removed obsolete code, simplified feature detection logic, and improved code documentation. Fixed issues with unsupported VCP values and ensured consistent ordering of color presets.
2025-11-14 13:17:55 +08:00
Yu Leng
4c799b61fc Update 2025-11-14 02:51:43 +08:00
Yu Leng
83410f1bc8 Add acknowledgment of Twinkle Tray in NOTICE.md
Acknowledged the use of techniques from the "Twinkle Tray"
project in the "PowerDisplay" utility's DDC/CI implementation.
Included a reference to the Twinkle Tray GitHub repository
and added the full text of its MIT License.
2025-11-13 14:29:40 +08:00
Yu Leng
33d5ff26c6 Refactor logging, state management, and IPC
Improved logging consistency by replacing verbose debug logs with concise warnings and errors where appropriate. Introduced a debounced-save strategy in `MonitorStateManager` to optimize disk I/O during rapid updates. Removed the `PowerDisplayProcessManager` class and named pipe-based IPC, indicating a significant architectural shift.

Translated all comments from Chinese to English for better readability. Simplified and refactored initialization logic in `MainWindow.xaml.cs` for better maintainability. Removed unused code, including system tray-related structures and imports, and improved overall code clarity and consistency.
2025-11-13 14:14:49 +08:00
Yu Leng
d822745c98 Remove tray icon functionality from PowerDisplay
The tray icon functionality has been completely removed from the
PowerDisplay application. This includes:

- Deletion of the `PowerDisplay.ico` file.
- Removal of the `TrayIconHelper.cs` class, which managed the tray
  icon's creation, updates, and interactions.
- Elimination of all references to `TrayIconHelper` in
  `MainWindow.xaml.cs`, including tray icon initialization, event
  handling, and disposal logic.
- Removal of the `<ApplicationIcon>` property in `PowerDisplay.csproj`.

These changes simplify the application by reducing its responsibilities
and dependencies, potentially aligning with a new design direction.
2025-11-13 13:06:37 +08:00
Yu Leng
0b7109dee4 Add toggle event for PowerDisplay window visibility
Introduced `TOGGLE_POWER_DISPLAY_EVENT` to enable toggling the
PowerDisplay window's visibility. Updated `App.xaml.cs` to handle
the new event and added the `ToggleWindow` method in
`MainWindow.xaml.cs` to manage window visibility.

Enhanced `MainWindow` with auto-hide functionality when the
window loses focus. Updated `dllmain.cpp` to integrate the
toggle event, including creating, signaling, and cleaning up
the event handle. Replaced `SHOW_POWER_DISPLAY_EVENT` with
`TOGGLE_POWER_DISPLAY_EVENT` for improved functionality.

Improved logging across the codebase for better traceability
and debugging. Performed general refactoring and ensured proper
resource management for event handles.
2025-11-13 12:56:12 +08:00
Yu Leng
ac9fd27095 Refactor PowerDisplay to use Windows Named Events
Replaced IPC-based communication with Windows Named Events for
simpler and more reliable process interaction. Introduced the
`NativeEventWaiter` helper class to handle event signaling and
callbacks. Removed the `PowerDisplayProcessManager` class and
refactored process lifecycle management to use direct process
launching and event signaling.

Simplified `App.xaml.cs` by removing IPC logic and adding event-
based handling for window visibility, monitor refresh, settings
updates, and termination. Enhanced `MainWindow` initialization
and show logic with detailed logging and error handling.

Updated `dllmain.cpp` to manage persistent event handles and
refactored the `enable` and `disable` methods to use event-based
communication. Improved process termination logic with additional
checks and logging.

Performed general cleanup, including removing unused code,
improving readability, and enhancing error handling throughout
the codebase.
2025-11-13 12:40:56 +08:00
Yu Leng
753fecbe9f Update PowerDisplay hotkey and add module launch support
Updated `JSON_KEY_ACTIVATION_SHORTCUT` to use lowercase for
consistency. Added `isShown` flag to `m_activation_hotkey`
to indicate visibility, setting it in relevant scenarios.

Added support for launching the PowerDisplay module via
an IPC message in `LaunchPage.xaml.cs`. Improved robustness
by adding a `default` case to handle unexpected `ModuleType`
values in the switch statement.
2025-11-12 23:18:14 +08:00
Yu Leng
c24b5d97c5 Use UTF-16 for pipe communication and simplify signaling
Refactored `send_message_to_powerdisplay` to use UTF-16
encoding for pipe communication, aligning with WinUI's
`Encoding.Unicode`. Removed UTF-8 conversion logic and
streamlined payload handling with `CString`.

Simplified the signaling mechanism in `dllmain.cpp` for
notifying `PowerDisplay.exe` of settings updates. The
message now excludes `config` data, relying on
`PowerDisplay.exe` to read the updated `settings.json`
file directly. Updated comments to reflect the new
behavior.
2025-11-12 17:08:30 +08:00
Yu Leng
5d63ca7a9c Refactor IPC and enhance PowerDisplay functionality
Refactored IPC communication by introducing a `NamedPipeProcessor` utility for unidirectional named pipe handling, replacing the old bidirectional implementation. Simplified application lifecycle management by removing mutex usage and relying on `AppInstance` for single-instance enforcement.

Replaced IPC-based monitor updates with file-based updates, saving monitor data to `settings.json` for the Settings UI. Added an `ActivationShortcut` property to PowerDisplay settings and updated the settings page UI to support shortcut configuration.

Simplified named pipe creation in `PowerDisplayProcessManager` by removing bidirectional pipe logic and focusing on unidirectional communication. Improved `MainWindow` initialization with an `EnsureInitializedAsync` method.

Updated localization resources and integrated PowerDisplay into the launcher menu. Removed redundant code, improved logging, and streamlined resource cleanup for better maintainability.
2025-11-12 16:36:43 +08:00
Yu Leng
e90c4273f7 Refactor PowerDisplay IPC and add hotkey support
Refactored IPC initialization to handle window visibility based on
launch mode (standalone or IPC). Added `IsWindowVisible` P/Invoke
method and implemented IPC commands for window control, monitor
refresh, and settings updates.

Fixed bidirectional pipe creation and adjusted process startup
order in `PowerDisplayProcessManager`. Made `ShowWindow` and
`HideWindow` methods public and added `IsWindowVisible` to
`MainWindow.xaml.cs`.

Introduced activation hotkey parsing and configuration with a
default of `Win+Alt+M`. Exposed hotkey to PowerToys runner and
integrated it into the dashboard with localization and a launch
button. Renamed module DLL for consistency.
2025-11-12 13:18:36 +08:00
Yu Leng
e2774eff2d Introduce PowerDisplay 2025-10-20 16:22:47 +08:00
142 changed files with 20840 additions and 60 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

@@ -1537,4 +1537,34 @@ SOFTWARE.
- UTF.Unknown
- WinUIEx
- WPF-UI
- WyHash
- WyHash
## Utility: PowerDisplay
### Twinkle Tray
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
**Source**: https://github.com/xanderfrangos/twinkle-tray
MIT License
Copyright © 2020 Xander Frangos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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::RefreshPowerDisplayMonitorsEvent()
{
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
}
hstring Constants::ApplyProfilePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_PROFILE_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 RefreshPowerDisplayMonitorsEvent();
static hstring ApplyProfilePowerDisplayEvent();
};
}

View File

@@ -49,6 +49,8 @@ namespace PowerToys
static String WorkspacesHotkeyEvent();
static String PowerToysRunnerTerminateSettingsEvent();
static String ShowCmdPalEvent();
static String RefreshPowerDisplayMonitorsEvent();
static String ApplyProfilePowerDisplayEvent();
}
}
}

View File

@@ -131,6 +131,15 @@ namespace CommonSharedConstants
const wchar_t ZOOMIT_REFRESH_SETTINGS_EVENT[] = L"Local\\PowerToysZoomIt-RefreshSettingsEvent-f053a563-d519-4b0d-8152-a54489c13324";
const wchar_t ZOOMIT_EXIT_EVENT[] = L"Local\\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220";
// Path to the events used by PowerDisplay
const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
const wchar_t TOGGLE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
const wchar_t APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
const wchar_t APPLY_PROFILE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";

View File

@@ -196,6 +196,44 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
if (settings.changeApps && isAppsCurrentlyLight)
SetAppsTheme(false);
}
// Notify PowerDisplay about theme change if any profile is enabled
bool shouldNotify = false;
if (isLightActive && settings.enableLightModeProfile && !settings.lightModeProfile.empty())
{
shouldNotify = true;
}
else if (!isLightActive && settings.enableDarkModeProfile && !settings.darkModeProfile.empty())
{
shouldNotify = true;
}
if (shouldNotify)
{
try
{
// Signal PowerDisplay to check LightSwitch settings and apply appropriate profile
// PowerDisplay will read LightSwitch settings to determine which profile to apply
Logger::info(L"[LightSwitch] Notifying PowerDisplay about theme change (isLight: {})", isLightActive);
HANDLE hThemeChangedEvent = CreateEventW(nullptr, FALSE, FALSE, L"Local\\PowerToys_LightSwitch_ThemeChanged");
if (hThemeChangedEvent)
{
SetEvent(hThemeChangedEvent);
CloseHandle(hThemeChangedEvent);
Logger::info(L"[LightSwitch] Theme change event signaled");
}
else
{
Logger::warn(L"[LightSwitch] Failed to create theme change event");
}
}
catch (...)
{
Logger::error(L"[LightSwitch] Failed to notify PowerDisplay");
}
}
};
// --- At service start: immediately honor the schedule ---
@@ -278,12 +316,12 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
if (wait == WAIT_OBJECT_0)
{
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
Logger::info(L"[LightSwitchService] Stop event triggered <20> exiting worker loop.");
break;
}
if (hParent && wait == WAIT_OBJECT_0 + 1) // parent process exited
{
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
Logger::info(L"[LightSwitchService] Parent process exited <20> stopping service.");
break;
}

View File

@@ -159,6 +159,46 @@ void LightSwitchSettings::LoadSettings()
NotifyObservers(SettingId::ChangeApps);
}
}
// EnableDarkModeProfile
if (const auto jsonVal = values.get_bool_value(L"enableDarkModeProfile"))
{
auto val = *jsonVal;
if (m_settings.enableDarkModeProfile != val)
{
m_settings.enableDarkModeProfile = val;
}
}
// EnableLightModeProfile
if (const auto jsonVal = values.get_bool_value(L"enableLightModeProfile"))
{
auto val = *jsonVal;
if (m_settings.enableLightModeProfile != val)
{
m_settings.enableLightModeProfile = val;
}
}
// DarkModeProfile
if (const auto jsonVal = values.get_string_value(L"darkModeProfile"))
{
auto val = *jsonVal;
if (m_settings.darkModeProfile != val)
{
m_settings.darkModeProfile = val;
}
}
// LightModeProfile
if (const auto jsonVal = values.get_string_value(L"lightModeProfile"))
{
auto val = *jsonVal;
if (m_settings.lightModeProfile != val)
{
m_settings.lightModeProfile = val;
}
}
}
catch (...)
{

View File

@@ -56,6 +56,11 @@ struct LightSwitchConfig
bool changeSystem = false;
bool changeApps = false;
bool enableDarkModeProfile = false;
bool enableLightModeProfile = false;
std::wstring darkModeProfile = L"";
std::wstring lightModeProfile = L"";
};
class LightSwitchSettings

View File

@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Windows.Input;
using ManagedCommon;
namespace PowerDisplay.Commands
{
/// <summary>
/// Basic relay command implementation for parameterless actions
/// </summary>
public partial class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool>? _canExecute;
public RelayCommand(Action execute, Func<bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
if (_canExecute == null)
{
return true;
}
try
{
return _canExecute.Invoke();
}
catch (Exception ex)
{
Logger.LogError($"CanExecute failed: {ex.Message}");
return false;
}
}
public void Execute(object? parameter)
{
try
{
_execute();
}
catch (Exception ex)
{
Logger.LogError($"Command execution failed: {ex.Message}");
}
}
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Generic relay command implementation for parameterized actions
/// </summary>
/// <typeparam name="T">Type of the command parameter</typeparam>
public partial class RelayCommand<T> : ICommand
{
private readonly Action<T?> _execute;
private readonly Func<T?, bool>? _canExecute;
public RelayCommand(Action<T?> execute, Func<T?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
if (_canExecute == null)
{
return true;
}
try
{
return _canExecute.Invoke((T?)parameter);
}
catch (Exception ex)
{
Logger.LogError($"CanExecute<T> failed: {ex.Message}");
return false;
}
}
public void Execute(object? parameter)
{
try
{
_execute((T?)parameter);
}
catch (Exception ex)
{
Logger.LogError($"Command<T> execution failed: {ex.Message}");
}
}
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerDisplay.Configuration
{
/// <summary>
/// Application-wide constants and configuration values
/// </summary>
public static class AppConstants
{
/// <summary>
/// State management configuration
/// </summary>
public static class State
{
/// <summary>
/// Interval in milliseconds to check for pending state changes to save
/// </summary>
public const int SaveIntervalMs = 2000;
/// <summary>
/// Name of the state file for monitor parameters
/// </summary>
public const string StateFileName = "monitor_state.json";
}
/// <summary>
/// Monitor parameter defaults and ranges
/// </summary>
public static class MonitorDefaults
{
// Brightness
public const int MinBrightness = 0;
public const int MaxBrightness = 100;
public const int DefaultBrightness = 50;
// Contrast
public const int MinContrast = 0;
public const int MaxContrast = 100;
public const int DefaultContrast = 50;
// Volume
public const int MinVolume = 0;
public const int MaxVolume = 100;
public const int DefaultVolume = 50;
}
/// <summary>
/// UI layout and timing constants
/// </summary>
public static class UI
{
// Window dimensions
public const int WindowWidth = 362;
public const int MaxWindowHeight = 650;
public const int WindowRightMargin = 12;
// Animation and layout update delays (milliseconds)
public const int LayoutUpdateDelayMs = 50;
public const int MonitorDiscoveryDelayMs = 200;
/// <summary>
/// Debounce delay for slider controls in milliseconds
/// </summary>
public const int SliderDebounceDelayMs = 300;
}
/// <summary>
/// Application lifecycle timing constants
/// </summary>
public static class Lifetime
{
/// <summary>
/// Normal shutdown timeout in milliseconds
/// </summary>
public const int NormalShutdownTimeoutMs = 1000;
/// <summary>
/// Emergency shutdown timeout in milliseconds
/// </summary>
public const int EmergencyShutdownTimeoutMs = 500;
}
}
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using PowerDisplay.Core.Models;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.Core.Interfaces
{
/// <summary>
/// Monitor controller interface
/// </summary>
public interface IMonitorController
{
/// <summary>
/// Controller name
/// </summary>
string Name { get; }
/// <summary>
/// Checks whether the specified monitor can be controlled
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Whether the monitor can be controlled</returns>
Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor brightness
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Brightness information</returns>
Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor brightness
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="brightness">Brightness value (0-100)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default);
/// <summary>
/// Discovers supported monitors
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>List of monitors</returns>
Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Validates monitor connection status
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Whether the monitor is connected</returns>
Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor contrast
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Contrast information</returns>
Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor contrast
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="contrast">Contrast value (0-100)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor volume
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Volume information</returns>
Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor volume
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="volume">Volume value (0-100)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor color temperature
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Color temperature information</returns>
Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor color temperature
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="colorTemperature">Color temperature value (2000-10000K)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default);
/// <summary>
/// Gets monitor capabilities string (DDC/CI)
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Capabilities string</returns>
Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Saves current settings to monitor
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Releases resources
/// </summary>
void Dispose();
}
// IMonitorManager interface removed - YAGNI principle
// Only one implementation exists (MonitorManager), so interface abstraction is unnecessary
// This simplifies the codebase and eliminates maintenance overhead
}

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,258 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using PowerDisplay.Configuration;
using PowerDisplay.Core.Utils;
namespace PowerDisplay.Core.Models
{
/// <summary>
/// Monitor model that implements property change notification
/// </summary>
public partial class Monitor : INotifyPropertyChanged
{
private int _currentBrightness;
private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
private bool _isAvailable = true;
/// <summary>
/// Unique identifier (based on hardware ID)
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Hardware ID (EDID format like GSM5C6D)
/// </summary>
public string HardwareId { get; set; } = string.Empty;
/// <summary>
/// Display name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Current brightness (0-100)
/// </summary>
public int CurrentBrightness
{
get => _currentBrightness;
set
{
var clamped = Math.Clamp(value, MinBrightness, MaxBrightness);
if (_currentBrightness != clamped)
{
_currentBrightness = clamped;
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum brightness value
/// </summary>
public int MinBrightness { get; set; }
/// <summary>
/// Maximum brightness value
/// </summary>
public int MaxBrightness { get; set; } = 100;
/// <summary>
/// Current color temperature VCP preset value (from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K), not Kelvin temperature.
/// Use ColorTemperaturePresetName to get human-readable name.
/// </summary>
public int CurrentColorTemperature
{
get => _currentColorTemperature;
set
{
if (_currentColorTemperature != value)
{
_currentColorTemperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorTemperaturePresetName));
}
}
}
/// <summary>
/// Human-readable color temperature preset name (e.g., "6500K (0x05)", "sRGB (0x01)")
/// </summary>
public string ColorTemperaturePresetName =>
VcpValueNames.GetFormattedName(0x14, CurrentColorTemperature);
/// <summary>
/// Whether supports color temperature adjustment via VCP 0x14
/// </summary>
public bool SupportsColorTemperature { get; set; }
/// <summary>
/// Capabilities detection status: "available", "unavailable", or "unknown"
/// </summary>
public string CapabilitiesStatus { get; set; } = "unknown";
/// <summary>
/// Whether supports contrast adjustment
/// </summary>
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
/// <summary>
/// Whether supports volume adjustment (for audio-capable monitors)
/// </summary>
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
private int _currentContrast = 50;
private int _currentVolume = 50;
/// <summary>
/// Current contrast (0-100)
/// </summary>
public int CurrentContrast
{
get => _currentContrast;
set
{
var clamped = Math.Clamp(value, MinContrast, MaxContrast);
if (_currentContrast != clamped)
{
_currentContrast = clamped;
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum contrast value
/// </summary>
public int MinContrast { get; set; }
/// <summary>
/// Maximum contrast value
/// </summary>
public int MaxContrast { get; set; } = 100;
/// <summary>
/// Current volume (0-100)
/// </summary>
public int CurrentVolume
{
get => _currentVolume;
set
{
var clamped = Math.Clamp(value, MinVolume, MaxVolume);
if (_currentVolume != clamped)
{
_currentVolume = clamped;
OnPropertyChanged();
}
}
}
/// <summary>
/// Minimum volume value
/// </summary>
public int MinVolume { get; set; }
/// <summary>
/// Maximum volume value
/// </summary>
public int MaxVolume { get; set; } = 100;
/// <summary>
/// Whether available/online
/// </summary>
public bool IsAvailable
{
get => _isAvailable;
set
{
if (_isAvailable != value)
{
_isAvailable = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Physical monitor handle (for DDC/CI)
/// </summary>
public IntPtr Handle { get; set; } = IntPtr.Zero;
/// <summary>
/// Device key - unique identifier part of device path
/// </summary>
public string DeviceKey { get; set; } = string.Empty;
/// <summary>
/// Instance name (used by WMI)
/// </summary>
public string InstanceName { get; set; } = string.Empty;
/// <summary>
/// Manufacturer information
/// </summary>
public string Manufacturer { get; set; } = string.Empty;
/// <summary>
/// Connection type (HDMI, DP, VGA, etc.)
/// </summary>
public string ConnectionType { get; set; } = string.Empty;
/// <summary>
/// Communication method (DDC/CI, WMI, HDR API, etc.)
/// </summary>
public string CommunicationMethod { get; set; } = string.Empty;
/// <summary>
/// Supported control methods
/// </summary>
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
/// <summary>
/// Raw DDC/CI capabilities string (MCCS format)
/// </summary>
public string? CapabilitiesRaw { get; set; }
/// <summary>
/// Parsed VCP capabilities information
/// </summary>
public VcpCapabilities? VcpCapabilitiesInfo { get; set; }
/// <summary>
/// Last update time
/// </summary>
public DateTime LastUpdate { get; set; } = DateTime.Now;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public override string ToString()
{
return $"{Name} ({CommunicationMethod}) - {CurrentBrightness}%";
}
/// <summary>
/// Update monitor status
/// </summary>
public void UpdateStatus(int brightness, bool isAvailable = true)
{
IsAvailable = isAvailable;
if (isAvailable)
{
CurrentBrightness = brightness;
LastUpdate = DateTime.Now;
}
}
}
}

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,134 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
namespace PowerDisplay.Core.Models
{
/// <summary>
/// DDC/CI VCP capabilities information
/// </summary>
public class VcpCapabilities
{
/// <summary>
/// Raw capabilities string (MCCS format)
/// </summary>
public string Raw { get; set; } = string.Empty;
/// <summary>
/// Monitor model name from capabilities
/// </summary>
public string? Model { get; set; }
/// <summary>
/// Monitor type from capabilities (e.g., "LCD")
/// </summary>
public string? Type { get; set; }
/// <summary>
/// MCCS protocol version
/// </summary>
public string? Protocol { get; set; }
/// <summary>
/// Supported command codes
/// </summary>
public List<byte> SupportedCommands { get; set; } = new();
/// <summary>
/// Supported VCP codes with their information
/// </summary>
public Dictionary<byte, VcpCodeInfo> SupportedVcpCodes { get; set; } = new();
/// <summary>
/// Check if a specific VCP code is supported
/// </summary>
public bool SupportsVcpCode(byte code) => SupportedVcpCodes.ContainsKey(code);
/// <summary>
/// Get VCP code information
/// </summary>
public VcpCodeInfo? GetVcpCodeInfo(byte code)
{
return SupportedVcpCodes.TryGetValue(code, out var info) ? info : null;
}
/// <summary>
/// Check if a VCP code supports discrete values
/// </summary>
public bool HasDiscreteValues(byte code)
{
var info = GetVcpCodeInfo(code);
return info?.HasDiscreteValues ?? false;
}
/// <summary>
/// Get supported values for a VCP code
/// </summary>
public IReadOnlyList<int>? GetSupportedValues(byte code)
{
return GetVcpCodeInfo(code)?.SupportedValues;
}
/// <summary>
/// Creates an empty capabilities object
/// </summary>
public static VcpCapabilities Empty => new();
public override string ToString()
{
return $"Model: {Model}, VCP Codes: {SupportedVcpCodes.Count}";
}
}
/// <summary>
/// Information about a single VCP code
/// </summary>
public readonly struct VcpCodeInfo
{
/// <summary>
/// VCP code (e.g., 0x10 for brightness)
/// </summary>
public byte Code { get; }
/// <summary>
/// Human-readable name of the VCP code
/// </summary>
public string Name { get; }
/// <summary>
/// Supported discrete values (empty if continuous range)
/// </summary>
public IReadOnlyList<int> SupportedValues { get; }
/// <summary>
/// Whether this VCP code has discrete values
/// </summary>
public bool HasDiscreteValues => SupportedValues.Count > 0;
/// <summary>
/// Whether this VCP code supports a continuous range
/// </summary>
public bool IsContinuous => SupportedValues.Count == 0;
public VcpCodeInfo(byte code, string name, IReadOnlyList<int>? supportedValues = null)
{
Code = code;
Name = name;
SupportedValues = supportedValues ?? Array.Empty<int>();
}
public override string ToString()
{
if (HasDiscreteValues)
{
return $"0x{Code:X2} ({Name}): {string.Join(", ", SupportedValues)}";
}
return $"0x{Code:X2} ({Name}): Continuous";
}
}
}

View File

@@ -0,0 +1,527 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using PowerDisplay.Core.Utils;
using PowerDisplay.Native;
using PowerDisplay.Native.DDC;
using PowerDisplay.Native.WMI;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.Core
{
/// <summary>
/// Monitor manager for unified control of all monitors
/// No interface abstraction - KISS principle (only one implementation needed)
/// </summary>
public partial class MonitorManager : IDisposable
{
private readonly List<Monitor> _monitors = new();
private readonly List<IMonitorController> _controllers = new();
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
private bool _disposed;
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
public event EventHandler<MonitorListChangedEventArgs>? MonitorsChanged;
public MonitorManager()
{
// Initialize controllers
InitializeControllers();
}
/// <summary>
/// Initialize controllers
/// </summary>
private void InitializeControllers()
{
try
{
// DDC/CI controller (external monitors)
_controllers.Add(new DdcCiController());
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
}
try
{
// WMI controller (internal monitors)
// First check if WMI is available
if (WmiController.IsWmiAvailable())
{
_controllers.Add(new WmiController());
}
else
{
Logger.LogWarning("WMI brightness control not available on this system");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
}
}
/// <summary>
/// Discover all monitors
/// </summary>
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
await _discoveryLock.WaitAsync(cancellationToken);
try
{
var oldMonitors = _monitors.ToList();
var newMonitors = new List<Monitor>();
// Discover monitors supported by all controllers in parallel
var discoveryTasks = _controllers.Select(async controller =>
{
try
{
var monitors = await controller.DiscoverMonitorsAsync(cancellationToken);
return (Controller: controller, Monitors: monitors.ToList());
}
catch (Exception ex)
{
// If a controller fails, log the error and return empty list
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
return (Controller: controller, Monitors: new List<Monitor>());
}
});
var results = await Task.WhenAll(discoveryTasks);
// Collect all discovered monitors
var allMonitors = new List<Monitor>();
foreach (var (controller, monitors) in results)
{
// Initialize monitors in parallel
var initTasks = monitors.Select(async monitor =>
{
// Verify if monitor can be controlled
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
{
// Get current brightness
try
{
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
if (brightnessInfo.IsValid)
{
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
monitor.MinBrightness = brightnessInfo.Minimum;
monitor.MaxBrightness = brightnessInfo.Maximum;
}
}
catch (Exception ex)
{
// If unable to get brightness, use default values
Logger.LogWarning($"Failed to get brightness for monitor {monitor.Id}: {ex.Message}");
}
// Get capabilities for DDC/CI monitors
// Check by CommunicationMethod instead of Type
if (monitor.CommunicationMethod?.Contains("DDC", StringComparison.OrdinalIgnoreCase) == true)
{
try
{
Logger.LogInfo($"Getting capabilities for monitor {monitor.Id}");
var capsString = await controller.GetCapabilitiesStringAsync(monitor, cancellationToken);
if (!string.IsNullOrEmpty(capsString))
{
monitor.CapabilitiesRaw = capsString;
// Parse capabilities
monitor.VcpCapabilitiesInfo = Utils.VcpCapabilitiesParser.Parse(capsString);
Logger.LogInfo($"Successfully parsed capabilities for {monitor.Id}: {monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count} VCP codes");
// Update capability flags based on parsed VCP codes
if (monitor.VcpCapabilitiesInfo.SupportedVcpCodes.Count > 0)
{
UpdateMonitorCapabilitiesFromVcp(monitor);
}
}
else
{
Logger.LogWarning($"Got empty capabilities string for monitor {monitor.Id}");
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to get capabilities for monitor {monitor.Id}: {ex.Message}");
// Continue without capabilities - not critical
}
}
return monitor;
}
return null;
});
var initializedMonitors = await Task.WhenAll(initTasks);
var validMonitors = initializedMonitors.Where(m => m != null).Cast<Monitor>();
newMonitors.AddRange(validMonitors);
}
// Update monitor list
_monitors.Clear();
_monitors.AddRange(newMonitors);
// Trigger change events
var addedMonitors = newMonitors.Where(m => !oldMonitors.Any(o => o.Id == m.Id)).ToList();
var removedMonitors = oldMonitors.Where(o => !newMonitors.Any(m => m.Id == o.Id)).ToList();
if (addedMonitors.Count > 0 || removedMonitors.Count > 0)
{
MonitorsChanged?.Invoke(this, new MonitorListChangedEventArgs(
addedMonitors.AsReadOnly(),
removedMonitors.AsReadOnly(),
_monitors.AsReadOnly()));
}
return _monitors.AsReadOnly();
}
finally
{
_discoveryLock.Release();
}
}
/// <summary>
/// Get brightness of the specified monitor
/// </summary>
public async Task<BrightnessInfo> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return BrightnessInfo.Invalid;
}
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
if (controller == null)
{
return BrightnessInfo.Invalid;
}
try
{
var brightnessInfo = await controller.GetBrightnessAsync(monitor, cancellationToken);
// Update cached brightness value
if (brightnessInfo.IsValid)
{
monitor.UpdateStatus(brightnessInfo.ToPercentage(), true);
}
return brightnessInfo;
}
catch (Exception ex)
{
// Mark monitor as unavailable
Logger.LogError($"Failed to get brightness for monitor {monitorId}: {ex.Message}");
monitor.IsAvailable = false;
return BrightnessInfo.Invalid;
}
}
/// <summary>
/// Set brightness of the specified monitor
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
Logger.LogError($"Monitor not found: {monitorId}");
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
if (controller == null)
{
Logger.LogError($"No controller available for monitor {monitorId}");
return MonitorOperationResult.Failure("No controller available for this monitor");
}
try
{
var result = await controller.SetBrightnessAsync(monitor, brightness, cancellationToken);
if (result.IsSuccess)
{
// Update monitor status
monitor.UpdateStatus(brightness, true);
}
else
{
// If setting fails, monitor may be unavailable
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
monitor.IsAvailable = false;
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
}
}
/// <summary>
/// Set brightness of all monitors
/// </summary>
public async Task<IEnumerable<MonitorOperationResult>> SetAllBrightnessAsync(int brightness, CancellationToken cancellationToken = default)
{
var tasks = _monitors
.Where(m => m.IsAvailable)
.Select(async monitor =>
{
try
{
return await SetBrightnessAsync(monitor.Id, brightness, cancellationToken);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Failed to set brightness for {monitor.Name}: {ex.Message}");
}
});
return await Task.WhenAll(tasks);
}
/// <summary>
/// Set contrast of the specified monitor
/// </summary>
public Task<MonitorOperationResult> SetContrastAsync(string monitorId, int contrast, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
contrast,
(ctrl, mon, val, ct) => ctrl.SetContrastAsync(mon, val, ct),
(mon, val) => mon.CurrentContrast = val,
cancellationToken);
/// <summary>
/// Set volume of the specified monitor
/// </summary>
public Task<MonitorOperationResult> SetVolumeAsync(string monitorId, int volume, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
volume,
(ctrl, mon, val, ct) => ctrl.SetVolumeAsync(mon, val, ct),
(mon, val) => mon.CurrentVolume = val,
cancellationToken);
/// <summary>
/// Get monitor color temperature
/// </summary>
public async Task<BrightnessInfo> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return BrightnessInfo.Invalid;
}
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
if (controller == null)
{
return BrightnessInfo.Invalid;
}
try
{
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
}
catch (Exception)
{
return BrightnessInfo.Invalid;
}
}
/// <summary>
/// Set monitor color temperature
/// </summary>
public Task<MonitorOperationResult> SetColorTemperatureAsync(string monitorId, int colorTemperature, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
colorTemperature,
(ctrl, mon, val, ct) => ctrl.SetColorTemperatureAsync(mon, val, ct),
(mon, val) => mon.CurrentColorTemperature = val,
cancellationToken);
/// <summary>
/// Initialize color temperature for a monitor (async operation)
/// </summary>
public async Task InitializeColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
{
try
{
var tempInfo = await GetColorTemperatureAsync(monitorId, cancellationToken);
if (tempInfo.IsValid)
{
var monitor = GetMonitor(monitorId);
if (monitor != null)
{
// Store raw VCP 0x14 preset value (e.g., 0x05 for 6500K)
// No Kelvin conversion - we use discrete presets
monitor.CurrentColorTemperature = tempInfo.Current;
}
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize color temperature for {monitorId}: {ex.Message}");
}
}
/// <summary>
/// Get monitor by ID
/// </summary>
public Monitor? GetMonitor(string monitorId)
{
return _monitors.FirstOrDefault(m => m.Id == monitorId);
}
/// <summary>
/// Get controller for the monitor
/// </summary>
private async Task<IMonitorController?> GetControllerForMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
// WMI monitors use WmiController, DDC/CI monitors use DdcCiController
foreach (var controller in _controllers)
{
if (await controller.CanControlMonitorAsync(monitor, cancellationToken))
{
return controller;
}
}
return null;
}
/// <summary>
/// Generic helper to execute monitor operations with common error handling.
/// Eliminates code duplication across Set* methods.
/// </summary>
private async Task<MonitorOperationResult> ExecuteMonitorOperationAsync<T>(
string monitorId,
T value,
Func<IMonitorController, Monitor, T, CancellationToken, Task<MonitorOperationResult>> operation,
Action<Monitor, T> onSuccess,
CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
Logger.LogError($"[MonitorManager] Monitor not found: {monitorId}");
return MonitorOperationResult.Failure("Monitor not found");
}
var controller = await GetControllerForMonitorAsync(monitor, cancellationToken);
if (controller == null)
{
Logger.LogError($"[MonitorManager] No controller available for monitor {monitorId}");
return MonitorOperationResult.Failure("No controller available for this monitor");
}
try
{
var result = await operation(controller, monitor, value, cancellationToken);
if (result.IsSuccess)
{
onSuccess(monitor, value);
monitor.LastUpdate = DateTime.Now;
}
else
{
monitor.IsAvailable = false;
}
return result;
}
catch (Exception ex)
{
monitor.IsAvailable = false;
Logger.LogError($"[MonitorManager] Operation failed for {monitorId}: {ex.Message}");
return MonitorOperationResult.Failure($"Exception: {ex.Message}");
}
}
/// <summary>
/// Update monitor capability flags based on parsed VCP capabilities
/// </summary>
private void UpdateMonitorCapabilitiesFromVcp(Monitor monitor)
{
var vcpCaps = monitor.VcpCapabilitiesInfo;
if (vcpCaps == null)
{
return;
}
// Check for Contrast support (VCP 0x12)
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeContrast))
{
monitor.Capabilities |= MonitorCapabilities.Contrast;
Logger.LogDebug($"[{monitor.Id}] Contrast support detected via VCP 0x12");
}
// Check for Volume support (VCP 0x62)
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeVolume))
{
monitor.Capabilities |= MonitorCapabilities.Volume;
Logger.LogDebug($"[{monitor.Id}] Volume support detected via VCP 0x62");
}
// Check for Color Temperature support (VCP 0x14)
if (vcpCaps.SupportsVcpCode(NativeConstants.VcpCodeSelectColorPreset))
{
monitor.SupportsColorTemperature = true;
Logger.LogDebug($"[{monitor.Id}] Color temperature support detected via VCP 0x14");
}
Logger.LogInfo($"[{monitor.Id}] Capabilities updated: Contrast={monitor.SupportsContrast}, Volume={monitor.SupportsVolume}, ColorTemp={monitor.SupportsColorTemperature}");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_discoveryLock?.Dispose();
// Release all controllers
foreach (var controller in _controllers)
{
controller?.Dispose();
}
_controllers.Clear();
_monitors.Clear();
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,315 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using ManagedCommon;
using PowerDisplay.Core.Models;
namespace PowerDisplay.Core.Utils
{
/// <summary>
/// Parser for DDC/CI MCCS capabilities strings
/// </summary>
public static class VcpCapabilitiesParser
{
private static readonly char[] SpaceSeparator = new[] { ' ' };
private static readonly char[] ValueSeparators = new[] { ' ', '(', ')' };
/// <summary>
/// Parse a capabilities string into structured VcpCapabilities
/// </summary>
/// <param name="capabilitiesString">Raw MCCS capabilities string</param>
/// <returns>Parsed capabilities object, or Empty if parsing fails</returns>
public static VcpCapabilities Parse(string? capabilitiesString)
{
if (string.IsNullOrWhiteSpace(capabilitiesString))
{
return VcpCapabilities.Empty;
}
try
{
var capabilities = new VcpCapabilities
{
Raw = capabilitiesString,
};
// Extract model, type, protocol
capabilities.Model = ExtractValue(capabilitiesString, "model");
capabilities.Type = ExtractValue(capabilitiesString, "type");
capabilities.Protocol = ExtractValue(capabilitiesString, "prot");
// Extract supported commands
capabilities.SupportedCommands = ParseCommandList(capabilitiesString);
// Extract and parse VCP codes
capabilities.SupportedVcpCodes = ParseVcpCodes(capabilitiesString);
Logger.LogInfo($"Parsed capabilities: Model={capabilities.Model}, VCP Codes={capabilities.SupportedVcpCodes.Count}");
return capabilities;
}
catch (Exception ex)
{
Logger.LogError($"Failed to parse capabilities string: {ex.Message}");
return VcpCapabilities.Empty;
}
}
/// <summary>
/// Extract a simple value from capabilities string
/// Example: "model(PD3220U)" -> "PD3220U"
/// </summary>
private static string? ExtractValue(string capabilities, string key)
{
try
{
var pattern = $@"{key}\(([^)]+)\)";
var match = Regex.Match(capabilities, pattern, RegexOptions.IgnoreCase);
return match.Success ? match.Groups[1].Value : null;
}
catch
{
return null;
}
}
/// <summary>
/// Parse command list from capabilities string
/// Example: "cmds(01 02 03 07 0C)" -> [0x01, 0x02, 0x03, 0x07, 0x0C]
/// </summary>
private static List<byte> ParseCommandList(string capabilities)
{
var commands = new List<byte>();
try
{
var match = Regex.Match(capabilities, @"cmds\(([^)]+)\)", RegexOptions.IgnoreCase);
if (match.Success)
{
var cmdString = match.Groups[1].Value;
var cmdTokens = cmdString.Split(SpaceSeparator, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in cmdTokens)
{
if (byte.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var cmd))
{
commands.Add(cmd);
}
}
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to parse command list: {ex.Message}");
}
return commands;
}
/// <summary>
/// Parse VCP codes section from capabilities string
/// </summary>
private static Dictionary<byte, VcpCodeInfo> ParseVcpCodes(string capabilities)
{
var vcpCodes = new Dictionary<byte, VcpCodeInfo>();
try
{
// Find the "vcp(" section
var vcpStart = capabilities.IndexOf("vcp(", StringComparison.OrdinalIgnoreCase);
if (vcpStart < 0)
{
Logger.LogWarning("No 'vcp(' section found in capabilities string");
return vcpCodes;
}
// Extract the complete VCP section by matching parentheses
var vcpSection = ExtractVcpSection(capabilities, vcpStart + 4); // Skip "vcp("
if (string.IsNullOrEmpty(vcpSection))
{
return vcpCodes;
}
Logger.LogDebug($"Extracted VCP section: {vcpSection.Substring(0, Math.Min(100, vcpSection.Length))}...");
// Parse VCP codes from the section
ParseVcpCodesFromSection(vcpSection, vcpCodes);
}
catch (Exception ex)
{
Logger.LogError($"Failed to parse VCP codes: {ex.Message}");
}
return vcpCodes;
}
/// <summary>
/// Extract VCP section by matching parentheses
/// </summary>
private static string ExtractVcpSection(string capabilities, int startIndex)
{
var depth = 1;
var result = string.Empty;
for (int i = startIndex; i < capabilities.Length && depth > 0; i++)
{
var ch = capabilities[i];
if (ch == '(')
{
depth++;
}
else if (ch == ')')
{
depth--;
if (depth == 0)
{
break;
}
}
result += ch;
}
return result;
}
/// <summary>
/// Parse VCP codes from the extracted VCP section
/// </summary>
private static void ParseVcpCodesFromSection(string vcpSection, Dictionary<byte, VcpCodeInfo> vcpCodes)
{
var i = 0;
while (i < vcpSection.Length)
{
// Skip whitespace
while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i]))
{
i++;
}
if (i >= vcpSection.Length)
{
break;
}
// Read VCP code (2 hex digits)
if (i + 1 < vcpSection.Length &&
IsHexDigit(vcpSection[i]) &&
IsHexDigit(vcpSection[i + 1]))
{
var codeStr = vcpSection.Substring(i, 2);
if (byte.TryParse(codeStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var code))
{
i += 2;
// Check if there are supported values (followed by '(')
while (i < vcpSection.Length && char.IsWhiteSpace(vcpSection[i]))
{
i++;
}
var supportedValues = new List<int>();
if (i < vcpSection.Length && vcpSection[i] == '(')
{
// Extract supported values
i++; // Skip '('
var valuesSection = ExtractVcpValuesSection(vcpSection, i);
i += valuesSection.Length + 1; // +1 for closing ')'
// Parse values
ParseVcpValues(valuesSection, supportedValues);
}
// Get VCP code name
var name = VcpCodeNames.GetName(code);
// Store VCP code info
vcpCodes[code] = new VcpCodeInfo(code, name, supportedValues);
Logger.LogDebug($"Parsed VCP code: 0x{code:X2} ({name}), Values: {supportedValues.Count}");
}
else
{
i++;
}
}
else
{
i++;
}
}
}
/// <summary>
/// Extract VCP values section by matching parentheses
/// </summary>
private static string ExtractVcpValuesSection(string section, int startIndex)
{
var depth = 1;
var result = string.Empty;
for (int i = startIndex; i < section.Length && depth > 0; i++)
{
var ch = section[i];
if (ch == '(')
{
depth++;
result += ch;
}
else if (ch == ')')
{
depth--;
if (depth == 0)
{
break;
}
result += ch;
}
else
{
result += ch;
}
}
return result;
}
/// <summary>
/// Parse VCP values from the values section
/// </summary>
private static void ParseVcpValues(string valuesSection, List<int> supportedValues)
{
var tokens = valuesSection.Split(ValueSeparators, StringSplitOptions.RemoveEmptyEntries);
foreach (var token in tokens)
{
// Try to parse as hex
if (int.TryParse(token, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
{
supportedValues.Add(value);
}
}
}
/// <summary>
/// Check if a character is a hex digit
/// </summary>
private static bool IsHexDigit(char c)
{
return (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
}
}
}

View File

@@ -0,0 +1,239 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace PowerDisplay.Core.Utils
{
/// <summary>
/// VCP code to friendly name mapping based on MCCS v2.2a specification
/// </summary>
public static class VcpCodeNames
{
/// <summary>
/// VCP code to name mapping
/// </summary>
private static readonly Dictionary<byte, string> CodeNames = new()
{
// Control codes
{ 0x01, "Degauss" },
{ 0x02, "New Control Value" },
{ 0x03, "Soft Controls" },
// Geometry codes
{ 0x04, "Restore Factory Defaults" },
{ 0x05, "Restore Brightness and Contrast" },
{ 0x06, "Restore Factory Geometry" },
{ 0x08, "Restore Color Defaults" },
{ 0x0A, "Restore Factory TV Defaults" },
// Color temperature codes
{ 0x0B, "Color Temperature Increment" },
{ 0x0C, "Color Temperature Request" },
{ 0x0E, "Clock" },
{ 0x0F, "Color Saturation" },
// Image adjustment codes
{ 0x10, "Brightness" },
{ 0x11, "Flesh Tone Enhancement" },
{ 0x12, "Contrast" },
{ 0x13, "Backlight Control" },
{ 0x14, "Select Color Preset" },
{ 0x16, "Video Gain: Red" },
{ 0x17, "User Color Vision Compensation" },
{ 0x18, "Video Gain: Green" },
{ 0x1A, "Video Gain: Blue" },
{ 0x1C, "Focus" },
{ 0x1E, "Auto Setup" },
{ 0x1F, "Auto Color Setup" },
// Geometry codes
{ 0x20, "Horizontal Position" },
{ 0x22, "Horizontal Size" },
{ 0x24, "Horizontal Pincushion" },
{ 0x26, "Horizontal Pincushion Balance" },
{ 0x28, "Horizontal Convergence R/B" },
{ 0x29, "Horizontal Convergence M/G" },
{ 0x2A, "Horizontal Linearity" },
{ 0x2C, "Horizontal Linearity Balance" },
{ 0x30, "Vertical Position" },
{ 0x32, "Vertical Size" },
{ 0x34, "Vertical Pincushion" },
{ 0x36, "Vertical Pincushion Balance" },
{ 0x38, "Vertical Convergence R/B" },
{ 0x39, "Vertical Convergence M/G" },
{ 0x3A, "Vertical Linearity" },
{ 0x3C, "Vertical Linearity Balance" },
{ 0x3E, "Clock Phase" },
// Miscellaneous codes
{ 0x40, "Horizontal Parallelogram" },
{ 0x41, "Vertical Parallelogram" },
{ 0x42, "Horizontal Keystone" },
{ 0x43, "Vertical Keystone" },
{ 0x44, "Rotation" },
{ 0x46, "Top Corner Flare" },
{ 0x48, "Top Corner Hook" },
{ 0x4A, "Bottom Corner Flare" },
{ 0x4C, "Bottom Corner Hook" },
// Advanced codes
{ 0x52, "Active Control" },
{ 0x54, "Performance Preservation" },
{ 0x56, "Horizontal Moire" },
{ 0x58, "Vertical Moire" },
{ 0x59, "6 Axis Saturation: Red" },
{ 0x5A, "6 Axis Saturation: Yellow" },
{ 0x5B, "6 Axis Saturation: Green" },
{ 0x5C, "6 Axis Saturation: Cyan" },
{ 0x5D, "6 Axis Saturation: Blue" },
{ 0x5E, "6 Axis Saturation: Magenta" },
// Input source codes
{ 0x60, "Input Source" },
{ 0x62, "Audio Speaker Volume" },
{ 0x63, "Speaker Select" },
{ 0x64, "Audio: Microphone Volume" },
{ 0x66, "Ambient Light Sensor" },
{ 0x6B, "Backlight Level: White" },
{ 0x6C, "Video Black Level: Red" },
{ 0x6D, "Backlight Level: Red" },
{ 0x6E, "Video Black Level: Green" },
{ 0x6F, "Backlight Level: Green" },
{ 0x70, "Video Black Level: Blue" },
{ 0x71, "Backlight Level: Blue" },
{ 0x72, "Gamma" },
{ 0x73, "LUT Size" },
{ 0x74, "Single Point LUT Operation" },
{ 0x75, "Block LUT Operation" },
// Color calibration codes
{ 0x86, "Display Scaling" },
{ 0x87, "Sharpness" },
{ 0x88, "Velocity Scan Modulation" },
{ 0x8A, "Color Saturation" },
{ 0x8C, "TV Sharpness" },
{ 0x8D, "Audio Mute/Screen Blank" },
{ 0x8E, "TV Contrast" },
{ 0x8F, "Audio Treble" },
{ 0x90, "Hue" },
{ 0x91, "Audio Bass" },
{ 0x92, "TV Black Level/Luminance" },
{ 0x93, "Audio Balance L/R" },
{ 0x94, "Audio Processor Mode" },
{ 0x95, "Window Position(TL_X)" },
{ 0x96, "Window Position(TL_Y)" },
{ 0x97, "Window Position(BR_X)" },
{ 0x98, "Window Position(BR_Y)" },
{ 0x99, "Window Background" },
{ 0x9A, "6 Axis Hue Control: Red" },
{ 0x9B, "6 Axis Hue Control: Yellow" },
{ 0x9C, "6 Axis Hue Control: Green" },
{ 0x9D, "6 Axis Hue Control: Cyan" },
{ 0x9E, "6 Axis Hue Control: Blue" },
{ 0x9F, "6 Axis Hue Control: Magenta" },
// Window control codes
{ 0xA0, "Auto Setup On/Off" },
{ 0xA2, "Auto Color Setup On/Off" },
{ 0xA4, "Window Mask Control" },
{ 0xA5, "Window Select" },
{ 0xA6, "Window Size" },
{ 0xA7, "Window Transparency" },
{ 0xAA, "Screen Orientation" },
{ 0xAC, "Horizontal Frequency" },
{ 0xAE, "Vertical Frequency" },
// Misc advanced codes
{ 0xB0, "Settings" },
{ 0xB2, "Flat Panel Sub-Pixel Layout" },
{ 0xB4, "Source Timing Mode" },
{ 0xB6, "Display Technology Type" },
{ 0xB7, "Monitor Status" },
{ 0xB8, "Packet Count" },
{ 0xB9, "Monitor X Origin" },
{ 0xBA, "Monitor Y Origin" },
{ 0xBB, "Header Error Count" },
{ 0xBC, "Body CRC Error Count" },
{ 0xBD, "Client ID" },
{ 0xBE, "Link Control" },
// Display controller codes
{ 0xC0, "Display Usage Time" },
{ 0xC2, "Display Firmware Level" },
{ 0xC4, "Display Descriptor Length" },
{ 0xC5, "Transmit Display Descriptor" },
{ 0xC6, "Enable Display of 'Display Descriptor'" },
{ 0xC8, "Display Controller Type" },
{ 0xC9, "Display Firmware Level" },
{ 0xCA, "OSD" },
{ 0xCC, "OSD Language" },
{ 0xD0, "Output Select" },
{ 0xD2, "Asset Tag" },
{ 0xD4, "Stereo Video Mode" },
{ 0xD6, "Power Mode" },
{ 0xD7, "Auxiliary Power Output" },
{ 0xD8, "Scan Mode" },
{ 0xD9, "Image Mode" },
{ 0xDA, "On Screen Display" },
{ 0xDC, "Display Application" },
{ 0xDE, "Scratch Pad" },
// Information codes
{ 0xDF, "VCP Version" },
{ 0xE0, "Manufacturer Specific" },
{ 0xE1, "Manufacturer Specific" },
{ 0xE2, "Manufacturer Specific" },
{ 0xE3, "Manufacturer Specific" },
{ 0xE4, "Manufacturer Specific" },
{ 0xE5, "Manufacturer Specific" },
{ 0xE6, "Manufacturer Specific" },
{ 0xE7, "Manufacturer Specific" },
{ 0xE8, "Manufacturer Specific" },
{ 0xE9, "Manufacturer Specific" },
{ 0xEA, "Manufacturer Specific" },
{ 0xEB, "Manufacturer Specific" },
{ 0xEC, "Manufacturer Specific" },
{ 0xED, "Manufacturer Specific" },
{ 0xEE, "Manufacturer Specific" },
{ 0xEF, "Manufacturer Specific" },
{ 0xF0, "Manufacturer Specific" },
{ 0xF1, "Manufacturer Specific" },
{ 0xF2, "Manufacturer Specific" },
{ 0xF3, "Manufacturer Specific" },
{ 0xF4, "Manufacturer Specific" },
{ 0xF5, "Manufacturer Specific" },
{ 0xF6, "Manufacturer Specific" },
{ 0xF7, "Manufacturer Specific" },
{ 0xF8, "Manufacturer Specific" },
{ 0xF9, "Manufacturer Specific" },
{ 0xFA, "Manufacturer Specific" },
{ 0xFB, "Manufacturer Specific" },
{ 0xFC, "Manufacturer Specific" },
{ 0xFD, "Manufacturer Specific" },
{ 0xFE, "Manufacturer Specific" },
{ 0xFF, "Manufacturer Specific" },
};
/// <summary>
/// Get the friendly name for a VCP code
/// </summary>
/// <param name="code">VCP code (e.g., 0x10)</param>
/// <returns>Friendly name, or hex representation if unknown</returns>
public static string GetName(byte code)
{
return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
}
/// <summary>
/// Check if a VCP code has a known name
/// </summary>
public static bool HasName(byte code) => CodeNames.ContainsKey(code);
/// <summary>
/// Get all known VCP codes
/// </summary>
public static IEnumerable<byte> GetAllKnownCodes() => CodeNames.Keys;
}
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace PowerDisplay.Core.Utils
{
/// <summary>
/// Provides human-readable names for VCP code values based on MCCS standard
/// </summary>
public static class VcpValueNames
{
// Dictionary<VcpCode, Dictionary<Value, Name>>
private static readonly Dictionary<byte, Dictionary<int, string>> ValueNames = new()
{
// 0x14: Select Color Preset
[0x14] = new Dictionary<int, string>
{
[0x01] = "sRGB",
[0x02] = "Display Native",
[0x03] = "4000K",
[0x04] = "5000K",
[0x05] = "6500K",
[0x06] = "7500K",
[0x08] = "9300K",
[0x09] = "10000K",
[0x0A] = "11500K",
[0x0B] = "User 1",
[0x0C] = "User 2",
[0x0D] = "User 3",
},
// 0x60: Input Source
[0x60] = new Dictionary<int, string>
{
[0x01] = "VGA-1",
[0x02] = "VGA-2",
[0x03] = "DVI-1",
[0x04] = "DVI-2",
[0x05] = "Composite Video 1",
[0x06] = "Composite Video 2",
[0x07] = "S-Video-1",
[0x08] = "S-Video-2",
[0x09] = "Tuner-1",
[0x0A] = "Tuner-2",
[0x0B] = "Tuner-3",
[0x0C] = "Component Video 1",
[0x0D] = "Component Video 2",
[0x0E] = "Component Video 3",
[0x0F] = "DisplayPort-1",
[0x10] = "DisplayPort-2",
[0x11] = "HDMI-1",
[0x12] = "HDMI-2",
[0x1B] = "USB-C",
},
// 0xD6: Power Mode
[0xD6] = new Dictionary<int, string>
{
[0x01] = "On",
[0x02] = "Standby",
[0x03] = "Suspend",
[0x04] = "Off (DPM)",
[0x05] = "Off (Hard)",
},
// 0x8D: Audio Mute
[0x8D] = new Dictionary<int, string>
{
[0x01] = "Muted",
[0x02] = "Unmuted",
},
// 0xDC: Display Application
[0xDC] = new Dictionary<int, string>
{
[0x00] = "Standard/Default",
[0x01] = "Productivity",
[0x02] = "Mixed",
[0x03] = "Movie",
[0x04] = "User Defined",
[0x05] = "Games",
[0x06] = "Sports",
[0x07] = "Professional (calibration)",
[0x08] = "Standard/Default with intermediate power consumption",
[0x09] = "Standard/Default with low power consumption",
[0x0A] = "Demonstration",
[0xF0] = "Dynamic Contrast",
},
// 0xCC: OSD Language
[0xCC] = new Dictionary<int, string>
{
[0x01] = "Chinese (traditional, Hantai)",
[0x02] = "English",
[0x03] = "French",
[0x04] = "German",
[0x05] = "Italian",
[0x06] = "Japanese",
[0x07] = "Korean",
[0x08] = "Portuguese (Portugal)",
[0x09] = "Russian",
[0x0A] = "Spanish",
[0x0B] = "Swedish",
[0x0C] = "Turkish",
[0x0D] = "Chinese (simplified, Kantai)",
[0x0E] = "Portuguese (Brazil)",
[0x0F] = "Arabic",
[0x10] = "Bulgarian",
[0x11] = "Croatian",
[0x12] = "Czech",
[0x13] = "Danish",
[0x14] = "Dutch",
[0x15] = "Estonian",
[0x16] = "Finnish",
[0x17] = "Greek",
[0x18] = "Hebrew",
[0x19] = "Hindi",
[0x1A] = "Hungarian",
[0x1B] = "Latvian",
[0x1C] = "Lithuanian",
[0x1D] = "Norwegian",
[0x1E] = "Polish",
[0x1F] = "Romanian",
[0x20] = "Serbian",
[0x21] = "Slovak",
[0x22] = "Slovenian",
[0x23] = "Thai",
[0x24] = "Ukrainian",
[0x25] = "Vietnamese",
},
// 0x62: Audio Speaker Volume
[0x62] = new Dictionary<int, string>
{
[0x00] = "Mute",
// Other values are continuous
},
// 0xDB: Image Mode (Dell monitors)
[0xDB] = new Dictionary<int, string>
{
[0x00] = "Standard",
[0x01] = "Multimedia",
[0x02] = "Movie",
[0x03] = "Game",
[0x04] = "Sports",
[0x05] = "Color Temperature",
[0x06] = "Custom Color",
[0x07] = "ComfortView",
},
};
/// <summary>
/// Get human-readable name for a VCP value
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <returns>Name string like "sRGB" or null if unknown</returns>
public static string? GetName(byte vcpCode, int value)
{
if (ValueNames.TryGetValue(vcpCode, out var codeValues))
{
if (codeValues.TryGetValue(value, out var name))
{
return name;
}
}
return null;
}
/// <summary>
/// Get formatted display name for a VCP value (with hex value in parentheses)
/// </summary>
/// <param name="vcpCode">VCP code (e.g., 0x14)</param>
/// <param name="value">Value to translate</param>
/// <returns>Formatted string like "sRGB (0x01)" or "0x01" if unknown</returns>
public static string GetFormattedName(byte vcpCode, int value)
{
var name = GetName(vcpCode, value);
if (name != null)
{
return $"{name} (0x{value:X2})";
}
return $"0x{value:X2}";
}
/// <summary>
/// Check if a VCP code has value name mappings
/// </summary>
/// <param name="vcpCode">VCP code to check</param>
/// <returns>True if value names are available</returns>
public static bool HasValueNames(byte vcpCode) => ValueNames.ContainsKey(vcpCode);
}
}

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,378 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Configuration;
using PowerDisplay.Serialization;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Manages monitor parameter state in a separate file from main settings.
/// This avoids FileSystemWatcher feedback loops by separating read-only config (settings.json)
/// from frequently-updated state (monitor_state.json).
/// Simplified to use direct save strategy for reliability and simplicity (KISS principle).
/// </summary>
public partial class MonitorStateManager : IDisposable
{
private readonly string _stateFilePath;
private readonly Dictionary<string, MonitorState> _states = new();
private readonly object _lock = new object();
private readonly Timer _saveTimer;
private bool _disposed;
private bool _isDirty;
private const int SaveDebounceMs = 2000; // Save 2 seconds after last update
/// <summary>
/// Monitor state data (internal tracking, not serialized)
/// </summary>
private sealed class MonitorState
{
public int Brightness { get; set; }
public int ColorTemperature { get; set; }
public int Contrast { get; set; }
public int Volume { get; set; }
public string? CapabilitiesRaw { get; set; }
}
public MonitorStateManager()
{
// Store state file in same location as settings.json but with different name
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
if (!Directory.Exists(powerToysPath))
{
Directory.CreateDirectory(powerToysPath);
}
_stateFilePath = Path.Combine(powerToysPath, AppConstants.State.StateFileName);
// Initialize debounce timer (disabled initially)
_saveTimer = new Timer(OnSaveTimerElapsed, null, Timeout.Infinite, Timeout.Infinite);
// Load existing state if available
LoadStateFromDisk();
Logger.LogInfo($"MonitorStateManager initialized with debounced-save strategy (debounce: {SaveDebounceMs}ms), state file: {_stateFilePath}");
}
/// <summary>
/// Timer callback to save state when dirty
/// </summary>
private async void OnSaveTimerElapsed(object? state)
{
bool shouldSave = false;
lock (_lock)
{
if (_isDirty && !_disposed)
{
shouldSave = true;
_isDirty = false;
}
}
if (shouldSave)
{
await SaveStateToDiskAsync();
}
}
/// <summary>
/// Update monitor parameter and schedule debounced save to disk.
/// Uses HardwareId as the stable key.
/// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
/// </summary>
public void UpdateMonitorParameter(string hardwareId, string property, int value)
{
try
{
if (string.IsNullOrEmpty(hardwareId))
{
Logger.LogWarning($"Cannot update monitor parameter: HardwareId is empty");
return;
}
lock (_lock)
{
// Get or create state entry using HardwareId
if (!_states.TryGetValue(hardwareId, out var state))
{
state = new MonitorState();
_states[hardwareId] = state;
}
// Update the specific property
switch (property)
{
case "Brightness":
state.Brightness = value;
break;
case "ColorTemperature":
state.ColorTemperature = value;
break;
case "Contrast":
state.Contrast = value;
break;
case "Volume":
state.Volume = value;
break;
default:
Logger.LogWarning($"Unknown property: {property}");
return;
}
// Mark dirty and schedule debounced save
_isDirty = true;
}
// Reset timer to debounce rapid updates (e.g., during slider drag)
_saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
}
catch (Exception ex)
{
Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
}
}
/// <summary>
/// Update monitor capabilities and schedule save.
/// Capabilities are saved separately to avoid frequent writes.
/// </summary>
public void UpdateMonitorCapabilities(string hardwareId, string? capabilitiesRaw)
{
try
{
if (string.IsNullOrEmpty(hardwareId))
{
Logger.LogWarning($"Cannot update capabilities: HardwareId is empty");
return;
}
lock (_lock)
{
// Get or create state entry
if (!_states.TryGetValue(hardwareId, out var state))
{
state = new MonitorState();
_states[hardwareId] = state;
}
// Update capabilities
state.CapabilitiesRaw = capabilitiesRaw;
// Mark dirty and schedule save
_isDirty = true;
}
// Schedule save
_saveTimer.Change(SaveDebounceMs, Timeout.Infinite);
Logger.LogInfo($"[State] Updated capabilities for monitor HardwareId='{hardwareId}' (length: {capabilitiesRaw?.Length ?? 0})");
}
catch (Exception ex)
{
Logger.LogError($"Failed to update monitor capabilities: {ex.Message}");
}
}
/// <summary>
/// Get saved parameters for a monitor using HardwareId
/// </summary>
public (int Brightness, int ColorTemperature, int Contrast, int Volume)? GetMonitorParameters(string hardwareId)
{
if (string.IsNullOrEmpty(hardwareId))
{
return null;
}
lock (_lock)
{
if (_states.TryGetValue(hardwareId, out var state))
{
return (state.Brightness, state.ColorTemperature, state.Contrast, state.Volume);
}
}
return null;
}
/// <summary>
/// Get saved capabilities for a monitor using HardwareId
/// </summary>
public string? GetMonitorCapabilities(string hardwareId)
{
if (string.IsNullOrEmpty(hardwareId))
{
return null;
}
lock (_lock)
{
if (_states.TryGetValue(hardwareId, out var state))
{
return state.CapabilitiesRaw;
}
}
return null;
}
/// <summary>
/// Check if state exists for a monitor (by HardwareId)
/// </summary>
public bool HasMonitorState(string hardwareId)
{
if (string.IsNullOrEmpty(hardwareId))
{
return false;
}
lock (_lock)
{
return _states.ContainsKey(hardwareId);
}
}
/// <summary>
/// Load state from disk
/// </summary>
private void LoadStateFromDisk()
{
try
{
if (!File.Exists(_stateFilePath))
{
Logger.LogInfo("[State] No existing state file found, starting fresh");
return;
}
var json = File.ReadAllText(_stateFilePath);
var stateFile = JsonSerializer.Deserialize(json, AppJsonContext.Default.MonitorStateFile);
if (stateFile?.Monitors != null)
{
lock (_lock)
{
foreach (var kvp in stateFile.Monitors)
{
var monitorKey = kvp.Key; // Should be HardwareId (e.g., "GSM5C6D")
var entry = kvp.Value;
_states[monitorKey] = new MonitorState
{
Brightness = entry.Brightness,
ColorTemperature = entry.ColorTemperature,
Contrast = entry.Contrast,
Volume = entry.Volume,
CapabilitiesRaw = entry.CapabilitiesRaw,
};
}
}
Logger.LogInfo($"[State] Loaded state for {stateFile.Monitors.Count} monitors from {_stateFilePath}");
Logger.LogInfo($"[State] Monitor keys in state file: {string.Join(", ", stateFile.Monitors.Keys)}");
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to load monitor state: {ex.Message}");
}
}
/// <summary>
/// Save current state to disk immediately (async).
/// Called by timer after debounce period or on dispose to flush pending changes.
/// </summary>
private async Task SaveStateToDiskAsync()
{
try
{
if (_disposed)
{
return;
}
// Build state file
var stateFile = new MonitorStateFile
{
LastUpdated = DateTime.Now,
};
var now = DateTime.Now;
lock (_lock)
{
foreach (var kvp in _states)
{
var monitorId = kvp.Key;
var state = kvp.Value;
stateFile.Monitors[monitorId] = new MonitorStateEntry
{
Brightness = state.Brightness,
ColorTemperature = state.ColorTemperature,
Contrast = state.Contrast,
Volume = state.Volume,
CapabilitiesRaw = state.CapabilitiesRaw,
LastUpdated = now,
};
}
}
// Write to disk asynchronously
var json = JsonSerializer.Serialize(stateFile, AppJsonContext.Default.MonitorStateFile);
await File.WriteAllTextAsync(_stateFilePath, json);
Logger.LogDebug($"[State] Saved state for {stateFile.Monitors.Count} monitors");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save monitor state: {ex.Message}");
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
// Stop the timer first
_saveTimer?.Change(Timeout.Infinite, Timeout.Infinite);
bool wasDirty = false;
lock (_lock)
{
wasDirty = _isDirty;
_disposed = true;
_isDirty = false;
}
// Flush any pending changes before disposing
if (wasDirty)
{
Logger.LogInfo("Flushing pending state changes before dispose");
SaveStateToDiskAsync().GetAwaiter().GetResult();
}
_saveTimer?.Dispose();
Logger.LogInfo("MonitorStateManager disposed");
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using Microsoft.UI.Dispatching;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Helper class for waiting on Windows Named Events (Awake pattern)
/// Based on Peek.UI implementation
/// </summary>
public static class NativeEventWaiter
{
/// <summary>
/// Wait for a Windows Event in a background thread and invoke callback on UI thread when signaled
/// </summary>
/// <param name="eventName">Name of the Windows Event to wait for</param>
/// <param name="callback">Callback to invoke when event is signaled</param>
/// <param name="cancellationToken">Token to cancel the wait loop</param>
public static void WaitForEventLoop(string eventName, Action callback, CancellationToken cancellationToken)
{
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
var t = new Thread(() =>
{
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
while (!cancellationToken.IsCancellationRequested)
{
if (eventHandle.WaitOne(500))
{
dispatcherQueue.TryEnqueue(() => callback());
}
}
});
t.IsBackground = true;
t.Start();
}
}
}

View File

@@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Configuration;
using PowerDisplay.Serialization;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Manages PowerDisplay profiles storage and retrieval
/// </summary>
public class ProfileManager
{
private readonly string _profilesFilePath;
private readonly object _lock = new object();
private PowerDisplayProfiles? _cachedProfiles;
public ProfileManager()
{
var settingsPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var powerToysPath = Path.Combine(settingsPath, "Microsoft", "PowerToys", "PowerDisplay");
if (!Directory.Exists(powerToysPath))
{
Directory.CreateDirectory(powerToysPath);
}
_profilesFilePath = Path.Combine(powerToysPath, "profiles.json");
Logger.LogInfo($"ProfileManager initialized, profiles file: {_profilesFilePath}");
}
/// <summary>
/// Loads profiles from disk
/// </summary>
public PowerDisplayProfiles LoadProfiles()
{
lock (_lock)
{
try
{
if (File.Exists(_profilesFilePath))
{
var json = File.ReadAllText(_profilesFilePath);
var profiles = JsonSerializer.Deserialize(json, AppJsonContext.Default.PowerDisplayProfiles);
if (profiles != null)
{
// Clean up any legacy Custom profiles
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
_cachedProfiles = profiles;
Logger.LogInfo($"Loaded {profiles.Profiles.Count} profiles");
return profiles;
}
}
Logger.LogInfo("No profiles file found, creating default");
_cachedProfiles = new PowerDisplayProfiles();
return _cachedProfiles;
}
catch (Exception ex)
{
Logger.LogError($"Failed to load profiles: {ex.Message}");
_cachedProfiles = new PowerDisplayProfiles();
return _cachedProfiles;
}
}
}
/// <summary>
/// Saves profiles to disk
/// </summary>
public void SaveProfiles(PowerDisplayProfiles profiles)
{
lock (_lock)
{
try
{
// Clean up any Custom profiles before saving
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
profiles.LastUpdated = DateTime.UtcNow;
var json = JsonSerializer.Serialize(profiles, AppJsonContext.Default.PowerDisplayProfiles);
File.WriteAllText(_profilesFilePath, json);
_cachedProfiles = profiles;
Logger.LogInfo($"Saved {profiles.Profiles.Count} profiles");
}
catch (Exception ex)
{
Logger.LogError($"Failed to save profiles: {ex.Message}");
}
}
}
/// <summary>
/// Adds or updates a profile
/// </summary>
public void AddOrUpdateProfile(PowerDisplayProfile profile)
{
lock (_lock)
{
if (profile == null || !profile.IsValid())
{
Logger.LogWarning("Cannot add invalid profile");
return;
}
var profiles = LoadProfiles();
profiles.SetProfile(profile);
SaveProfiles(profiles);
Logger.LogInfo($"Profile '{profile.Name}' added/updated with {profile.MonitorSettings.Count} monitors");
}
}
/// <summary>
/// Removes a profile by name
/// </summary>
public bool RemoveProfile(string profileName)
{
lock (_lock)
{
var profiles = LoadProfiles();
bool removed = profiles.RemoveProfile(profileName);
if (removed)
{
SaveProfiles(profiles);
Logger.LogInfo($"Profile '{profileName}' removed");
}
else
{
Logger.LogWarning($"Profile '{profileName}' not found or cannot be removed");
}
return removed;
}
}
/// <summary>
/// Gets all profiles
/// </summary>
public List<PowerDisplayProfile> GetAllProfiles()
{
var profiles = LoadProfiles();
return profiles.Profiles.ToList();
}
}
}

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,127 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Simple debouncer that delays execution of an action until a quiet period.
/// Replaces the complex PropertyUpdateQueue with a much simpler approach (KISS principle).
/// </summary>
public partial class SimpleDebouncer : IDisposable
{
private readonly int _delayMs;
private readonly object _lock = new object();
private CancellationTokenSource? _cts;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="SimpleDebouncer"/> class.
/// Create a debouncer with specified delay
/// </summary>
/// <param name="delayMs">Delay in milliseconds before executing action</param>
public SimpleDebouncer(int delayMs = 300)
{
_delayMs = delayMs;
}
/// <summary>
/// Debounce an async action. Cancels previous invocation if still pending.
/// </summary>
/// <param name="action">Async action to execute after delay</param>
public void Debounce(Func<Task> action)
{
_ = DebounceAsync(action);
}
/// <summary>
/// Debounce a synchronous action
/// </summary>
public void Debounce(Action action)
{
_ = DebounceAsync(() =>
{
action();
return Task.CompletedTask;
});
}
private async Task DebounceAsync(Func<Task> action)
{
if (_disposed)
{
return;
}
CancellationTokenSource cts;
CancellationTokenSource? oldCts = null;
lock (_lock)
{
// Store old CTS to dispose later
oldCts = _cts;
// Create new CTS
_cts = new CancellationTokenSource();
cts = _cts;
}
// Dispose old CTS outside the lock to avoid blocking
if (oldCts != null)
{
try
{
oldCts.Cancel();
oldCts.Dispose();
}
catch
{
// Ignore disposal errors
}
}
try
{
// Wait for quiet period
await Task.Delay(_delayMs, cts.Token).ConfigureAwait(false);
// Execute action if not cancelled
if (!cts.Token.IsCancellationRequested)
{
await action().ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// Expected when debouncing - a newer call cancelled this one
}
catch (Exception ex)
{
Logger.LogError($"Debounced action failed: {ex.Message}");
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
lock (_lock)
{
_disposed = true;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
GC.SuppressFinalize(this);
}
}
}

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,718 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using PowerDisplay.Core.Utils;
using PowerDisplay.Helpers;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates;
using static PowerDisplay.Native.PInvoke;
using Monitor = PowerDisplay.Core.Models.Monitor;
// Type aliases matching Windows API naming conventions for better readability when working with native structures.
// These uppercase aliases are used consistently throughout this file to match Win32 API documentation.
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native.DDC
{
/// <summary>
/// DDC/CI monitor controller for controlling external monitors
/// </summary>
public partial class DdcCiController : IMonitorController, IDisposable
{
private readonly PhysicalMonitorHandleManager _handleManager = new();
private readonly MonitorDiscoveryHelper _discoveryHelper;
private bool _disposed;
public DdcCiController()
{
_discoveryHelper = new MonitorDiscoveryHelper();
}
public string Name => "DDC/CI Monitor Controller";
/// <summary>
/// Check if the specified monitor can be controlled
/// </summary>
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
var physicalHandle = GetPhysicalHandle(monitor);
return physicalHandle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(physicalHandle);
},
cancellationToken);
}
/// <summary>
/// Get monitor brightness using VCP code 0x10
/// </summary>
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
var physicalHandle = GetPhysicalHandle(monitor);
if (physicalHandle == IntPtr.Zero)
{
Logger.LogDebug($"[{monitor.Id}] Invalid physical handle");
return BrightnessInfo.Invalid;
}
// First try high-level API
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint minBrightness, out uint currentBrightness, out uint maxBrightness))
{
Logger.LogDebug($"[{monitor.Id}] Brightness via high-level API: {currentBrightness}/{maxBrightness}");
return new BrightnessInfo((int)currentBrightness, (int)minBrightness, (int)maxBrightness);
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out uint current, out uint max))
{
Logger.LogDebug($"[{monitor.Id}] Brightness via 0x10: {current}/{max}");
return new BrightnessInfo((int)current, 0, (int)max);
}
Logger.LogWarning($"[{monitor.Id}] Failed to read brightness");
return BrightnessInfo.Invalid;
},
cancellationToken);
}
/// <summary>
/// Set monitor brightness using VCP code 0x10
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
{
brightness = Math.Clamp(brightness, 0, 100);
return await Task.Run(
() =>
{
var physicalHandle = GetPhysicalHandle(monitor);
if (physicalHandle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("No physical handle found");
}
try
{
var currentInfo = GetBrightnessInfo(monitor, physicalHandle);
if (!currentInfo.IsValid)
{
Logger.LogWarning($"[{monitor.Id}] Cannot read current brightness");
return MonitorOperationResult.Failure("Cannot read current brightness");
}
uint targetValue = (uint)currentInfo.FromPercentage(brightness);
// First try high-level API
if (DdcCiNative.TrySetMonitorBrightness(physicalHandle, targetValue))
{
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via high-level API");
return MonitorOperationResult.Success();
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TrySetVCPFeature(physicalHandle, VcpCodeBrightness, targetValue))
{
Logger.LogInfo($"[{monitor.Id}] Set brightness to {brightness}% via 0x10");
return MonitorOperationResult.Success();
}
var lastError = GetLastError();
Logger.LogError($"[{monitor.Id}] Failed to set brightness, error: {lastError}");
return MonitorOperationResult.Failure($"Failed to set brightness via DDC/CI", (int)lastError);
}
catch (Exception ex)
{
Logger.LogError($"[{monitor.Id}] Exception setting brightness: {ex.Message}");
return MonitorOperationResult.Failure($"Exception setting brightness: {ex.Message}");
}
},
cancellationToken);
}
/// <summary>
/// Get monitor contrast
/// </summary>
public Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
=> GetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, cancellationToken);
/// <summary>
/// Set monitor contrast
/// </summary>
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, 0, 100, cancellationToken);
/// <summary>
/// Get monitor volume
/// </summary>
public Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
=> GetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, cancellationToken);
/// <summary>
/// Set monitor volume
/// </summary>
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, 0, 100, cancellationToken);
/// <summary>
/// Get monitor color temperature using VCP code 0x14 (Select Color Preset)
/// Returns the raw VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature
/// </summary>
public async Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
Logger.LogDebug($"[{monitor.Id}] Invalid handle for color temperature read");
return BrightnessInfo.Invalid;
}
// Try VCP code 0x14 (Select Color Preset)
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, out uint current, out uint max))
{
var presetName = VcpValueNames.GetFormattedName(0x14, (int)current);
Logger.LogInfo($"[{monitor.Id}] Color temperature via 0x14: {presetName}");
return new BrightnessInfo((int)current, 0, (int)max);
}
Logger.LogWarning($"[{monitor.Id}] Failed to read color temperature (0x14 not supported)");
return BrightnessInfo.Invalid;
},
cancellationToken);
}
/// <summary>
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
/// </summary>
/// <param name="monitor">Monitor to control</param>
/// <param name="colorTemperature">VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature</param>
/// <param name="cancellationToken">Cancellation token</param>
public async Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("Invalid monitor handle");
}
try
{
// Validate value is in supported list if capabilities available
var capabilities = monitor.VcpCapabilitiesInfo;
if (capabilities != null && capabilities.SupportsVcpCode(0x14))
{
var supportedValues = capabilities.GetSupportedValues(0x14);
if (supportedValues?.Count > 0 && !supportedValues.Contains(colorTemperature))
{
var supportedList = string.Join(", ", supportedValues.Select(v => $"0x{v:X2}"));
Logger.LogWarning($"[{monitor.Id}] Color preset 0x{colorTemperature:X2} not in supported list: [{supportedList}]");
return MonitorOperationResult.Failure($"Color preset 0x{colorTemperature:X2} not supported by monitor");
}
}
// Set VCP 0x14 value
var presetName = VcpValueNames.GetFormattedName(0x14, colorTemperature);
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, VcpCodeSelectColorPreset, (uint)colorTemperature))
{
Logger.LogInfo($"[{monitor.Id}] Set color temperature to {presetName} via 0x14");
return MonitorOperationResult.Success();
}
var lastError = GetLastError();
Logger.LogError($"[{monitor.Id}] Failed to set color temperature, error: {lastError}");
return MonitorOperationResult.Failure($"Failed to set color temperature via DDC/CI", (int)lastError);
}
catch (Exception ex)
{
Logger.LogError($"[{monitor.Id}] Exception setting color temperature: {ex.Message}");
return MonitorOperationResult.Failure($"Exception setting color temperature: {ex.Message}");
}
},
cancellationToken);
}
/// <summary>
/// Get monitor capabilities string with retry logic
/// </summary>
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return string.Empty;
}
try
{
// Step 1: Get capabilities string length (retry up to 3 times)
uint length = 0;
const int lengthMaxRetries = 3;
for (int i = 0; i < lengthMaxRetries; i++)
{
if (GetCapabilitiesStringLength(monitor.Handle, out length) && length > 0)
{
Logger.LogDebug($"Got capabilities length: {length} (attempt {i + 1})");
break;
}
if (i < lengthMaxRetries - 1)
{
Thread.Sleep(100); // 100ms delay between retries
}
}
if (length == 0)
{
Logger.LogWarning("Failed to get capabilities string length after retries");
return string.Empty;
}
// Step 2: Get actual capabilities string (retry up to 5 times)
const int capsMaxRetries = 5;
for (int i = 0; i < capsMaxRetries; i++)
{
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
try
{
if (CapabilitiesRequestAndCapabilitiesReply(monitor.Handle, buffer, length))
{
var capsString = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer) ?? string.Empty;
if (!string.IsNullOrEmpty(capsString))
{
Logger.LogInfo($"Got capabilities string (length: {capsString.Length}, attempt: {i + 1})");
return capsString;
}
}
}
finally
{
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
}
if (i < capsMaxRetries - 1)
{
Thread.Sleep(100); // 100ms delay between retries
}
}
Logger.LogWarning("Failed to get capabilities string after retries");
}
catch (Exception ex)
{
Logger.LogError($"Exception getting capabilities string: {ex.Message}");
}
return string.Empty;
},
cancellationToken);
}
/// <summary>
/// Save current settings
/// </summary>
public async Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("Invalid monitor handle");
}
try
{
if (SaveCurrentSettings(monitor.Handle))
{
return MonitorOperationResult.Success();
}
var lastError = GetLastError();
return MonitorOperationResult.Failure($"Failed to save settings", (int)lastError);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Exception saving settings: {ex.Message}");
}
},
cancellationToken);
}
/// <summary>
/// Discover supported monitors
/// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
return await Task.Run(
async () =>
{
var monitors = new List<Monitor>();
var newHandleMap = new Dictionary<string, IntPtr>();
try
{
// Get all display devices with stable device IDs
var displayDevices = DdcCiNative.GetAllDisplayDevices();
// Also get hardware info for friendly names
var monitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
// Enumerate all monitors
var monitorHandles = new List<IntPtr>();
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
{
monitorHandles.Add(hMonitor);
return true;
}
bool enumResult = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero);
if (!enumResult)
{
Logger.LogWarning($"DDC: EnumDisplayMonitors failed");
return monitors;
}
// Get physical handles for each monitor
foreach (var hMonitor in monitorHandles)
{
var adapterName = _discoveryHelper.GetMonitorDeviceId(hMonitor);
if (string.IsNullOrEmpty(adapterName))
{
continue;
}
// Get physical monitors with retry logic for NULL handle workaround
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
if (physicalMonitors == null || physicalMonitors.Length == 0)
{
Logger.LogWarning($"DDC: Failed to get physical monitors for hMonitor 0x{hMonitor:X} after retries");
continue;
}
// Match physical monitors with DisplayDeviceInfo
// For each physical monitor on this adapter, find the corresponding DisplayDeviceInfo
for (int i = 0; i < physicalMonitors.Length; i++)
{
var physicalMonitor = physicalMonitors[i];
if (physicalMonitor.HPhysicalMonitor == IntPtr.Zero)
{
continue;
}
// Find matching DisplayDeviceInfo for this physical monitor
DisplayDeviceInfo? matchedDevice = null;
int foundCount = 0;
foreach (var displayDevice in displayDevices)
{
if (displayDevice.AdapterName == adapterName)
{
if (foundCount == i)
{
matchedDevice = displayDevice;
break;
}
foundCount++;
}
}
// Determine device key for handle reuse logic
string deviceKey = matchedDevice?.DeviceKey ?? $"{adapterName}_{i}";
// Use HandleManager to reuse or create handle
var (handleToUse, reusingOldHandle) = _handleManager.ReuseOrCreateHandle(deviceKey, physicalMonitor.HPhysicalMonitor);
// Validate DDC/CI connection for the handle we're going to use
if (!reusingOldHandle && !DdcCiNative.ValidateDdcCiConnection(handleToUse))
{
Logger.LogWarning($"DDC: New handle 0x{handleToUse:X} failed DDC/CI validation, skipping");
continue;
}
// Update physical monitor handle to use the correct one
var monitorToCreate = physicalMonitor;
monitorToCreate.HPhysicalMonitor = handleToUse;
var monitor = _discoveryHelper.CreateMonitorFromPhysical(monitorToCreate, adapterName, i, monitorDisplayInfo, matchedDevice);
if (monitor != null)
{
monitors.Add(monitor);
// Store in new map for cleanup
newHandleMap[monitor.DeviceKey] = handleToUse;
}
}
}
// Update handle manager with new mapping
_handleManager.UpdateHandleMap(newHandleMap);
}
catch (Exception ex)
{
Logger.LogError($"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}");
}
finally
{
}
return monitors;
},
cancellationToken);
}
/// <summary>
/// Validate monitor connection status
/// </summary>
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() => monitor.Handle != IntPtr.Zero && DdcCiNative.ValidateDdcCiConnection(monitor.Handle),
cancellationToken);
}
/// <summary>
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles
/// </summary>
/// <param name="hMonitor">Handle to the monitor</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Array of physical monitors, or null if failed after retries</returns>
private async Task<PHYSICAL_MONITOR[]?> GetPhysicalMonitorsWithRetryAsync(
IntPtr hMonitor,
CancellationToken cancellationToken)
{
const int maxRetries = 3;
const int retryDelayMs = 200;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
if (attempt > 0)
{
await Task.Delay(retryDelayMs, cancellationToken);
}
var monitors = _discoveryHelper.GetPhysicalMonitors(hMonitor);
var validationResult = ValidatePhysicalMonitors(monitors, attempt, maxRetries);
if (validationResult.IsValid)
{
return monitors;
}
if (validationResult.ShouldRetry)
{
continue;
}
// Last attempt failed, return what we have
return monitors;
}
return null;
}
/// <summary>
/// Validate physical monitors array for null handles
/// </summary>
/// <returns>Tuple indicating if valid and if should retry</returns>
private (bool IsValid, bool ShouldRetry) ValidatePhysicalMonitors(
PHYSICAL_MONITOR[]? monitors,
int attempt,
int maxRetries)
{
if (monitors == null || monitors.Length == 0)
{
if (attempt < maxRetries - 1)
{
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null/empty on attempt {attempt + 1}, will retry");
}
return (false, true);
}
bool hasNullHandle = HasAnyNullHandles(monitors, out int nullIndex);
if (!hasNullHandle)
{
return (true, false); // Valid, don't retry
}
if (attempt < maxRetries - 1)
{
Logger.LogWarning($"DDC: Physical monitor [{nullIndex}] has NULL handle on attempt {attempt + 1}, will retry");
return (false, true); // Invalid, should retry
}
Logger.LogWarning($"DDC: NULL handle still present after {maxRetries} attempts, continuing anyway");
return (false, false); // Invalid but no more retries
}
/// <summary>
/// Check if any physical monitor has a NULL handle
/// </summary>
/// <param name="monitors">Array of physical monitors to check</param>
/// <param name="nullIndex">Output index of first NULL handle found, or -1 if none</param>
/// <returns>True if any NULL handle found</returns>
private bool HasAnyNullHandles(PHYSICAL_MONITOR[] monitors, out int nullIndex)
{
for (int i = 0; i < monitors.Length; i++)
{
if (monitors[i].HPhysicalMonitor == IntPtr.Zero)
{
nullIndex = i;
return true;
}
}
nullIndex = -1;
return false;
}
/// <summary>
/// Generic method to get VCP feature value
/// </summary>
private async Task<BrightnessInfo> GetVcpFeatureAsync(
Monitor monitor,
byte vcpCode,
CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return BrightnessInfo.Invalid;
}
if (DdcCiNative.TryGetVCPFeature(monitor.Handle, vcpCode, out uint current, out uint max))
{
return new BrightnessInfo((int)current, 0, (int)max);
}
return BrightnessInfo.Invalid;
},
cancellationToken);
}
/// <summary>
/// Generic method to set VCP feature value
/// </summary>
private async Task<MonitorOperationResult> SetVcpFeatureAsync(
Monitor monitor,
byte vcpCode,
int value,
int min = 0,
int max = 100,
CancellationToken cancellationToken = default)
{
value = Math.Clamp(value, min, max);
return await Task.Run(
async () =>
{
if (monitor.Handle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("Invalid monitor handle");
}
try
{
// Get current value to determine range
var currentInfo = await GetVcpFeatureAsync(monitor, vcpCode);
if (!currentInfo.IsValid)
{
return MonitorOperationResult.Failure($"Cannot read current value for VCP 0x{vcpCode:X2}");
}
uint targetValue = (uint)currentInfo.FromPercentage(value);
if (DdcCiNative.TrySetVCPFeature(monitor.Handle, vcpCode, targetValue))
{
return MonitorOperationResult.Success();
}
var lastError = GetLastError();
return MonitorOperationResult.Failure($"Failed to set VCP 0x{vcpCode:X2}", (int)lastError);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Exception setting VCP 0x{vcpCode:X2}: {ex.Message}");
}
},
cancellationToken);
}
/// <summary>
/// Get brightness information using VCP code 0x10 only
/// </summary>
private BrightnessInfo GetBrightnessInfo(Monitor monitor, IntPtr physicalHandle)
{
if (physicalHandle == IntPtr.Zero)
{
return BrightnessInfo.Invalid;
}
// First try high-level API
if (DdcCiNative.TryGetMonitorBrightness(physicalHandle, out uint min, out uint current, out uint max))
{
return new BrightnessInfo((int)current, (int)min, (int)max);
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TryGetVCPFeature(physicalHandle, VcpCodeBrightness, out current, out max))
{
return new BrightnessInfo((int)current, 0, (int)max);
}
return BrightnessInfo.Invalid;
}
/// <summary>
/// Get physical handle for monitor using stable deviceKey
/// </summary>
private IntPtr GetPhysicalHandle(Monitor monitor)
{
return _handleManager.GetPhysicalHandle(monitor);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_handleManager?.Dispose();
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,498 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using ManagedCommon;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.NativeDelegates;
using static PowerDisplay.Native.PInvoke;
// Type aliases for Windows API naming conventions compatibility
using DISPLAY_DEVICE = PowerDisplay.Native.DisplayDevice;
using DISPLAYCONFIG_DEVICE_INFO_HEADER = PowerDisplay.Native.DISPLAYCONFIG_DEVICE_INFO_HEADER;
using DISPLAYCONFIG_MODE_INFO = PowerDisplay.Native.DISPLAYCONFIG_MODE_INFO;
using DISPLAYCONFIG_PATH_INFO = PowerDisplay.Native.DISPLAYCONFIG_PATH_INFO;
using DISPLAYCONFIG_TARGET_DEVICE_NAME = PowerDisplay.Native.DISPLAYCONFIG_TARGET_DEVICE_NAME;
using LUID = PowerDisplay.Native.Luid;
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
#pragma warning disable SA1649 // File name should match first type name - Multiple related types for DDC/CI
#pragma warning disable SA1402 // File may only contain a single type - Related DDC/CI types grouped together
namespace PowerDisplay.Native.DDC
{
/// <summary>
/// Display device information class
/// </summary>
public class DisplayDeviceInfo
{
public string DeviceName { get; set; } = string.Empty;
public string AdapterName { get; set; } = string.Empty;
public string DeviceID { get; set; } = string.Empty;
public string DeviceKey { get; set; } = string.Empty;
}
/// <summary>
/// DDC/CI native API wrapper
/// </summary>
public static class DdcCiNative
{
// Display Configuration constants
public const uint QdcAllPaths = 0x00000001;
public const uint QdcOnlyActivePaths = 0x00000002;
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
// Helper Methods
/// <summary>
/// Safe wrapper for getting VCP feature value
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="vcpCode">VCP code</param>
/// <param name="currentValue">Current value</param>
/// <param name="maxValue">Maximum value</param>
/// <returns>True if successful</returns>
public static bool TryGetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, out uint currentValue, out uint maxValue)
{
currentValue = 0;
maxValue = 0;
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return GetVCPFeatureAndVCPFeatureReply(hPhysicalMonitor, vcpCode, IntPtr.Zero, out currentValue, out maxValue);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Safe wrapper for setting VCP feature value
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="vcpCode">VCP code</param>
/// <param name="value">New value</param>
/// <returns>True if successful</returns>
public static bool TrySetVCPFeature(IntPtr hPhysicalMonitor, byte vcpCode, uint value)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return SetVCPFeature(hPhysicalMonitor, vcpCode, value);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// Safe wrapper for getting advanced brightness information
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <param name="minBrightness">Minimum brightness</param>
/// <param name="currentBrightness">Current brightness</param>
/// <param name="maxBrightness">Maximum brightness</param>
/// <returns>True if successful</returns>
public static bool TryGetMonitorBrightness(IntPtr hPhysicalMonitor, out uint minBrightness, out uint currentBrightness, out uint maxBrightness)
{
minBrightness = 0;
currentBrightness = 0;
maxBrightness = 0;
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return GetMonitorBrightness(hPhysicalMonitor, out minBrightness, out currentBrightness, out maxBrightness);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 设置高级亮度的安全包装
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <param name="brightness">亮度值</param>
/// <returns>是否成功</returns>
public static bool TrySetMonitorBrightness(IntPtr hPhysicalMonitor, uint brightness)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
try
{
return SetMonitorBrightness(hPhysicalMonitor, brightness);
}
catch (Exception)
{
return false;
}
}
/// <summary>
/// 检查 DDC/CI 连接的有效性
/// </summary>
/// <param name="hPhysicalMonitor">物理显示器句柄</param>
/// <returns>是否连接有效</returns>
public static bool ValidateDdcCiConnection(IntPtr hPhysicalMonitor)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return false;
}
// 尝试读取基本的 VCP 代码来验证连接
var testCodes = new byte[] { NativeConstants.VcpCodeBrightness, NativeConstants.VcpCodeNewControlValue, NativeConstants.VcpCodeVcpVersion };
foreach (var code in testCodes)
{
if (TryGetVCPFeature(hPhysicalMonitor, code, out _, out _))
{
return true;
}
}
return false;
}
/// <summary>
/// 获取显示器友好名称
/// </summary>
/// <param name="adapterId">适配器 ID</param>
/// <param name="targetId">目标 ID</param>
/// <returns>显示器友好名称,如果获取失败返回 null</returns>
public static unsafe string? GetMonitorFriendlyName(LUID adapterId, uint targetId)
{
try
{
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
{
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
{
Type = DisplayconfigDeviceInfoGetTargetName,
Size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME),
AdapterId = adapterId,
Id = targetId,
},
};
var result = DisplayConfigGetDeviceInfo(ref deviceName);
if (result == 0)
{
return deviceName.GetMonitorFriendlyDeviceName();
}
}
catch
{
// 忽略错误
}
return null;
}
/// <summary>
/// 通过枚举显示配置获取所有显示器友好名称
/// </summary>
/// <returns>设备路径到友好名称的映射</returns>
public static Dictionary<string, string> GetAllMonitorFriendlyNames()
{
var friendlyNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
// 获取缓冲区大小
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
if (result != 0)
{
return friendlyNames;
}
// 分配缓冲区
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// 查询显示配置
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
if (result != 0)
{
return friendlyNames;
}
// 获取每个路径的友好名称
for (int i = 0; i < pathCount; i++)
{
var path = paths[i];
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
if (!string.IsNullOrEmpty(friendlyName))
{
// 使用适配器和目标 ID 作为键
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
friendlyNames[key] = friendlyName;
}
}
}
catch
{
// 忽略错误
}
return friendlyNames;
}
/// <summary>
/// 获取显示器的EDID硬件ID信息
/// </summary>
/// <param name="adapterId">适配器ID</param>
/// <param name="targetId">目标ID</param>
/// <returns>硬件ID字符串格式为制造商代码+产品代码</returns>
public static unsafe string? GetMonitorHardwareId(LUID adapterId, uint targetId)
{
try
{
var deviceName = new DISPLAYCONFIG_TARGET_DEVICE_NAME
{
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
{
Type = DisplayconfigDeviceInfoGetTargetName,
Size = (uint)sizeof(DISPLAYCONFIG_TARGET_DEVICE_NAME),
AdapterId = adapterId,
Id = targetId,
},
};
var result = DisplayConfigGetDeviceInfo(ref deviceName);
if (result == 0)
{
// 将制造商ID转换为3字符字符串
var manufacturerId = deviceName.EdidManufactureId;
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
// 将产品ID转换为4位十六进制字符串
var productCode = deviceName.EdidProductCodeId.ToString("X4", System.Globalization.CultureInfo.InvariantCulture);
var hardwareId = $"{manufactureCode}{productCode}";
Logger.LogDebug($"GetMonitorHardwareId - ManufacturerId: 0x{manufacturerId:X4}, Code: '{manufactureCode}', ProductCode: '{productCode}', Result: '{hardwareId}'");
return hardwareId;
}
else
{
Logger.LogError($"GetMonitorHardwareId - DisplayConfigGetDeviceInfo failed with result: {result}");
}
}
catch
{
// 忽略错误
}
return null;
}
/// <summary>
/// 将制造商ID转换为3字符制造商代码
/// </summary>
/// <param name="manufacturerId">制造商ID</param>
/// <returns>3字符制造商代码</returns>
private static string ConvertManufactureIdToString(ushort manufacturerId)
{
// EDID制造商ID需要先进行字节序交换
manufacturerId = (ushort)(((manufacturerId & 0xff00) >> 8) | ((manufacturerId & 0x00ff) << 8));
// 提取3个5位字符每个字符是A-Z其中A=1, B=2, ..., Z=26
var char1 = (char)('A' - 1 + ((manufacturerId >> 0) & 0x1f));
var char2 = (char)('A' - 1 + ((manufacturerId >> 5) & 0x1f));
var char3 = (char)('A' - 1 + ((manufacturerId >> 10) & 0x1f));
// 按正确顺序组合字符
return $"{char3}{char2}{char1}";
}
/// <summary>
/// 获取所有显示器的完整信息包括友好名称和硬件ID
/// </summary>
/// <returns>包含显示器信息的字典</returns>
public static Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
{
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
try
{
// 获取缓冲区大小
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
if (result != 0)
{
return monitorInfo;
}
// 分配缓冲区
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// 查询显示配置
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero);
if (result != 0)
{
return monitorInfo;
}
// 获取每个路径的信息
for (int i = 0; i < pathCount; i++)
{
var path = paths[i];
var friendlyName = GetMonitorFriendlyName(path.TargetInfo.AdapterId, path.TargetInfo.Id);
var hardwareId = GetMonitorHardwareId(path.TargetInfo.AdapterId, path.TargetInfo.Id);
if (!string.IsNullOrEmpty(friendlyName) || !string.IsNullOrEmpty(hardwareId))
{
var key = $"{path.TargetInfo.AdapterId}_{path.TargetInfo.Id}";
monitorInfo[key] = new MonitorDisplayInfo
{
FriendlyName = friendlyName ?? string.Empty,
HardwareId = hardwareId ?? string.Empty,
AdapterId = path.TargetInfo.AdapterId,
TargetId = path.TargetInfo.Id,
};
}
}
}
catch
{
// 忽略错误
}
return monitorInfo;
}
/// <summary>
/// Get all display device information using EnumDisplayDevices API
/// </summary>
/// <returns>List of display device information</returns>
public static unsafe List<DisplayDeviceInfo> GetAllDisplayDevices()
{
var devices = new List<DisplayDeviceInfo>();
try
{
// 枚举所有适配器
uint adapterIndex = 0;
var adapter = default(DISPLAY_DEVICE);
adapter.Cb = (uint)sizeof(DisplayDevice);
while (EnumDisplayDevices(null, adapterIndex, ref adapter, EddGetDeviceInterfaceName))
{
// 跳过镜像驱动程序
if ((adapter.StateFlags & DisplayDeviceMirroringDriver) != 0)
{
adapterIndex++;
adapter = default(DISPLAY_DEVICE);
adapter.Cb = (uint)sizeof(DisplayDevice);
continue;
}
// 只处理已连接到桌面的适配器
if ((adapter.StateFlags & DisplayDeviceAttachedToDesktop) != 0)
{
// 枚举该适配器上的所有显示器
uint displayIndex = 0;
var display = default(DISPLAY_DEVICE);
display.Cb = (uint)sizeof(DisplayDevice);
string adapterDeviceName = adapter.GetDeviceName();
while (EnumDisplayDevices(adapterDeviceName, displayIndex, ref display, EddGetDeviceInterfaceName))
{
string displayDeviceID = display.GetDeviceID();
// 只处理活动的显示器
if ((display.StateFlags & DisplayDeviceAttachedToDesktop) != 0 &&
!string.IsNullOrEmpty(displayDeviceID))
{
var deviceInfo = new DisplayDeviceInfo
{
DeviceName = display.GetDeviceName(),
AdapterName = adapterDeviceName,
DeviceID = displayDeviceID,
};
// 提取 DeviceKey移除 GUID 部分(#{...} 及之后的内容)
// 例如:\\?\DISPLAY#GSM5C6D#5&1234&0&UID#{GUID} -> \\?\DISPLAY#GSM5C6D#5&1234&0&UID
int guidIndex = deviceInfo.DeviceID.IndexOf("#{", StringComparison.Ordinal);
if (guidIndex >= 0)
{
deviceInfo.DeviceKey = deviceInfo.DeviceID.Substring(0, guidIndex);
}
else
{
deviceInfo.DeviceKey = deviceInfo.DeviceID;
}
devices.Add(deviceInfo);
Logger.LogDebug($"Found display device - Name: {deviceInfo.DeviceName}, Adapter: {deviceInfo.AdapterName}, DeviceKey: {deviceInfo.DeviceKey}");
}
displayIndex++;
display = default(DISPLAY_DEVICE);
display.Cb = (uint)sizeof(DisplayDevice);
}
}
adapterIndex++;
adapter = default(DISPLAY_DEVICE);
adapter.Cb = (uint)sizeof(DisplayDevice);
}
Logger.LogInfo($"GetAllDisplayDevices found {devices.Count} display devices");
}
catch (Exception ex)
{
Logger.LogError($"GetAllDisplayDevices exception: {ex.Message}");
}
return devices;
}
}
/// <summary>
/// 显示器显示信息结构
/// </summary>
public struct MonitorDisplayInfo
{
public string FriendlyName { get; set; }
public string HardwareId { get; set; }
public LUID AdapterId { get; set; }
public uint TargetId { get; set; }
}
}

View File

@@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ManagedCommon;
using PowerDisplay.Core.Models;
using static PowerDisplay.Native.NativeConstants;
using static PowerDisplay.Native.PInvoke;
using MONITORINFOEX = PowerDisplay.Native.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Native.PhysicalMonitor;
using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native.DDC
{
/// <summary>
/// Helper class for discovering and creating monitor objects
/// </summary>
public class MonitorDiscoveryHelper
{
public MonitorDiscoveryHelper()
{
}
/// <summary>
/// Get monitor device ID
/// </summary>
public unsafe string GetMonitorDeviceId(IntPtr hMonitor)
{
try
{
var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MonitorInfoEx) };
if (GetMonitorInfo(hMonitor, ref monitorInfo))
{
return monitorInfo.GetDeviceName() ?? string.Empty;
}
}
catch
{
// Silent failure
}
return string.Empty;
}
/// <summary>
/// Get physical monitors for a logical monitor
/// </summary>
internal PHYSICAL_MONITOR[]? GetPhysicalMonitors(IntPtr hMonitor)
{
try
{
Logger.LogDebug($"GetPhysicalMonitors: hMonitor=0x{hMonitor:X}");
if (!GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint numMonitors))
{
Logger.LogWarning($"GetPhysicalMonitors: GetNumberOfPhysicalMonitorsFromHMONITOR failed for 0x{hMonitor:X}");
return null;
}
Logger.LogDebug($"GetPhysicalMonitors: numMonitors={numMonitors}");
if (numMonitors == 0)
{
Logger.LogWarning($"GetPhysicalMonitors: numMonitors is 0");
return null;
}
var physicalMonitors = new PHYSICAL_MONITOR[numMonitors];
bool apiResult;
unsafe
{
fixed (PHYSICAL_MONITOR* ptr = physicalMonitors)
{
apiResult = GetPhysicalMonitorsFromHMONITOR(hMonitor, numMonitors, ptr);
}
}
Logger.LogDebug($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR returned {apiResult}");
if (!apiResult)
{
Logger.LogWarning($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR failed");
return null;
}
// Log each physical monitor
for (int i = 0; i < numMonitors; i++)
{
string desc = physicalMonitors[i].GetDescription() ?? string.Empty;
IntPtr handle = physicalMonitors[i].HPhysicalMonitor;
Logger.LogDebug($"GetPhysicalMonitors: [{i}] Handle=0x{handle:X}, Desc='{desc}'");
if (handle == IntPtr.Zero)
{
Logger.LogWarning($"GetPhysicalMonitors: Monitor [{i}] has NULL handle despite successful API call!");
}
}
return physicalMonitors;
}
catch (Exception ex)
{
Logger.LogError($"GetPhysicalMonitors: Exception: {ex.Message}");
return null;
}
}
/// <summary>
/// Create Monitor object from physical monitor
/// </summary>
internal Monitor? CreateMonitorFromPhysical(
PHYSICAL_MONITOR physicalMonitor,
string adapterName,
int index,
Dictionary<string, MonitorDisplayInfo> monitorDisplayInfo,
DisplayDeviceInfo? displayDevice)
{
try
{
// Get hardware ID and friendly name from the display info
string hardwareId = string.Empty;
string name = physicalMonitor.GetDescription() ?? string.Empty;
// Try to find matching monitor info
foreach (var kvp in monitorDisplayInfo.Values)
{
if (!string.IsNullOrEmpty(kvp.HardwareId))
{
hardwareId = kvp.HardwareId;
if (!string.IsNullOrEmpty(kvp.FriendlyName) && !kvp.FriendlyName.Contains("Generic"))
{
name = kvp.FriendlyName;
}
break;
}
}
// Use stable device IDs from DisplayDeviceInfo
string deviceKey;
string monitorId;
if (displayDevice != null && !string.IsNullOrEmpty(displayDevice.DeviceKey))
{
// Use stable device key from EnumDisplayDevices
deviceKey = displayDevice.DeviceKey;
monitorId = $"DDC_{deviceKey.Replace(@"\\?\", string.Empty, StringComparison.Ordinal).Replace("#", "_", StringComparison.Ordinal).Replace("&", "_", StringComparison.Ordinal)}";
}
else
{
// Fallback: create device ID without handle in the key
var baseDevice = adapterName.Replace(@"\\.\", string.Empty, StringComparison.Ordinal);
deviceKey = $"{baseDevice}_{index}";
monitorId = $"DDC_{deviceKey}";
}
// If still no good name, use default value
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
{
name = $"External Display {index + 1}";
}
// Get current brightness
var brightnessInfo = GetCurrentBrightness(physicalMonitor.HPhysicalMonitor);
var monitor = new Monitor
{
Id = monitorId,
HardwareId = hardwareId,
Name = name.Trim(),
CurrentBrightness = brightnessInfo.IsValid ? brightnessInfo.ToPercentage() : 50,
MinBrightness = 0,
MaxBrightness = 100,
IsAvailable = true,
Handle = physicalMonitor.HPhysicalMonitor,
DeviceKey = deviceKey,
Capabilities = MonitorCapabilities.DdcCi,
ConnectionType = "External",
CommunicationMethod = "DDC/CI",
Manufacturer = ExtractManufacturer(name),
CapabilitiesStatus = "unknown",
};
// Note: Feature detection (brightness, contrast, color temp, volume) is now done
// in MonitorManager after capabilities string is retrieved and parsed.
// This ensures we rely on capabilities data rather than trial-and-error probing.
return monitor;
}
catch (Exception ex)
{
Logger.LogError($"DDC: CreateMonitorFromPhysical exception: {ex.Message}");
return null;
}
}
/// <summary>
/// Get current brightness using VCP code 0x10 only
/// </summary>
private BrightnessInfo GetCurrentBrightness(IntPtr handle)
{
// Try high-level API first
if (DdcCiNative.TryGetMonitorBrightness(handle, out uint min, out uint current, out uint max))
{
return new BrightnessInfo((int)current, (int)min, (int)max);
}
// Try VCP code 0x10 (standard brightness)
if (DdcCiNative.TryGetVCPFeature(handle, VcpCodeBrightness, out current, out max))
{
return new BrightnessInfo((int)current, 0, (int)max);
}
return BrightnessInfo.Invalid;
}
/// <summary>
/// Extract manufacturer from name
/// </summary>
private string ExtractManufacturer(string name)
{
if (string.IsNullOrEmpty(name))
{
return "Unknown";
}
// Common manufacturer prefixes
var manufacturers = new[] { "DELL", "HP", "LG", "Samsung", "ASUS", "Acer", "BenQ", "AOC", "ViewSonic" };
var upperName = name.ToUpperInvariant();
foreach (var manufacturer in manufacturers)
{
if (upperName.Contains(manufacturer))
{
return manufacturer;
}
}
// Return first word as manufacturer
var firstWord = name.Split(' ')[0];
return firstWord.Length > 2 ? firstWord : "Unknown";
}
}
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using ManagedCommon;
using PowerDisplay.Core.Models;
using static PowerDisplay.Native.PInvoke;
namespace PowerDisplay.Native.DDC
{
/// <summary>
/// Manages physical monitor handles - reuse, cleanup, and validation
/// </summary>
public partial class PhysicalMonitorHandleManager : IDisposable
{
// Mapping: deviceKey -> physical handle
private readonly Dictionary<string, IntPtr> _deviceKeyToHandleMap = new();
private readonly object _lock = new();
private bool _disposed;
/// <summary>
/// Get physical handle for monitor using stable deviceKey
/// </summary>
public IntPtr GetPhysicalHandle(Monitor monitor)
{
lock (_lock)
{
// Primary lookup: use stable deviceKey from EnumDisplayDevices
if (!string.IsNullOrEmpty(monitor.DeviceKey) &&
_deviceKeyToHandleMap.TryGetValue(monitor.DeviceKey, out var handle))
{
return handle;
}
}
// Fallback: use direct handle from monitor object
if (monitor.Handle != IntPtr.Zero)
{
return monitor.Handle;
}
return IntPtr.Zero;
}
/// <summary>
/// Try to reuse existing handle if valid, otherwise use new handle
/// Returns the handle to use and whether it was reused
/// </summary>
public (IntPtr Handle, bool WasReused) ReuseOrCreateHandle(string deviceKey, IntPtr newHandle)
{
if (string.IsNullOrEmpty(deviceKey))
{
return (newHandle, false);
}
lock (_lock)
{
// Try to reuse existing handle if it's still valid
if (_deviceKeyToHandleMap.TryGetValue(deviceKey, out var existingHandle) &&
existingHandle != IntPtr.Zero &&
DdcCiNative.ValidateDdcCiConnection(existingHandle))
{
// Destroy the newly created handle since we're using the old one
if (newHandle != existingHandle && newHandle != IntPtr.Zero)
{
DestroyPhysicalMonitor(newHandle);
}
return (existingHandle, true);
}
}
return (newHandle, false);
}
/// <summary>
/// Update the handle mapping with new handles
/// </summary>
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
{
lock (_lock)
{
// Clean up unused handles before updating
CleanupUnusedHandles(newHandleMap);
// Update the device key map
_deviceKeyToHandleMap.Clear();
foreach (var kvp in newHandleMap)
{
_deviceKeyToHandleMap[kvp.Key] = kvp.Value;
}
}
}
/// <summary>
/// Clean up handles that are no longer in use
/// </summary>
private void CleanupUnusedHandles(Dictionary<string, IntPtr> newHandles)
{
if (_deviceKeyToHandleMap.Count == 0)
{
return;
}
var handlesToDestroy = new List<IntPtr>();
// Find handles that are in old map but not being reused
foreach (var oldMapping in _deviceKeyToHandleMap)
{
bool found = false;
foreach (var newMapping in newHandles)
{
// If the same handle is being reused, don't destroy it
if (oldMapping.Value == newMapping.Value)
{
found = true;
break;
}
}
if (!found && oldMapping.Value != IntPtr.Zero)
{
handlesToDestroy.Add(oldMapping.Value);
}
}
// Destroy unused handles
foreach (var handle in handlesToDestroy)
{
try
{
DestroyPhysicalMonitor(handle);
Logger.LogDebug($"DDC: Cleaned up unused handle 0x{handle:X}");
}
catch (Exception ex)
{
Logger.LogWarning($"DDC: Failed to destroy handle 0x{handle:X}: {ex.Message}");
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
// Release all physical monitor handles
foreach (var handle in _deviceKeyToHandleMap.Values)
{
if (handle != IntPtr.Zero)
{
try
{
DestroyPhysicalMonitor(handle);
Logger.LogDebug($"Released physical monitor handle 0x{handle:X}");
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to destroy physical monitor handle 0x{handle:X}: {ex.Message}");
}
}
}
_deviceKeyToHandleMap.Clear();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,291 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace PowerDisplay.Native
{
/// <summary>
/// Windows API constant definitions
/// </summary>
public static class NativeConstants
{
/// <summary>
/// VCP code: Brightness (0x10)
/// Standard VESA MCCS brightness control.
/// This is the ONLY brightness code used by PowerDisplay.
/// </summary>
public const byte VcpCodeBrightness = 0x10;
/// <summary>
/// VCP code: Contrast (0x12)
/// Standard VESA MCCS contrast control.
/// </summary>
public const byte VcpCodeContrast = 0x12;
/// <summary>
/// VCP code: Audio Speaker Volume (0x62)
/// Standard VESA MCCS volume control for monitors with built-in speakers.
/// </summary>
public const byte VcpCodeVolume = 0x62;
/// <summary>
/// VCP code: Audio mute (0x8D)
/// </summary>
public const byte VcpCodeMute = 0x8D;
/// <summary>
/// VCP code: Gamma correction (0x72)
/// </summary>
public const byte VcpCodeGamma = 0x72;
/// <summary>
/// VCP code: Select Color Preset (0x14)
/// Standard VESA MCCS color temperature preset selection.
/// Supports discrete values like: 0x01=sRGB, 0x04=5000K, 0x05=6500K, 0x08=9300K.
/// This is the standard method for color temperature control.
/// </summary>
public const byte VcpCodeSelectColorPreset = 0x14;
/// <summary>
/// VCP code: VCP version
/// </summary>
public const byte VcpCodeVcpVersion = 0xDF;
/// <summary>
/// VCP code: New control value
/// </summary>
public const byte VcpCodeNewControlValue = 0x02;
/// <summary>
/// Display device attached to desktop
/// </summary>
public const uint DisplayDeviceAttachedToDesktop = 0x00000001;
/// <summary>
/// Multi-monitor primary display
/// </summary>
public const uint DisplayDeviceMultiDriver = 0x00000002;
/// <summary>
/// Primary device
/// </summary>
public const uint DisplayDevicePrimaryDevice = 0x00000004;
/// <summary>
/// Mirroring driver
/// </summary>
public const uint DisplayDeviceMirroringDriver = 0x00000008;
/// <summary>
/// VGA compatible
/// </summary>
public const uint DisplayDeviceVgaCompatible = 0x00000010;
/// <summary>
/// Removable device
/// </summary>
public const uint DisplayDeviceRemovable = 0x00000020;
/// <summary>
/// Get device interface name
/// </summary>
public const uint EddGetDeviceInterfaceName = 0x00000001;
/// <summary>
/// Primary monitor
/// </summary>
public const uint MonitorinfoFPrimary = 0x00000001;
/// <summary>
/// Query display config: only active paths
/// </summary>
public const uint QdcOnlyActivePaths = 0x00000002;
/// <summary>
/// Query display config: all paths
/// </summary>
public const uint QdcAllPaths = 0x00000001;
/// <summary>
/// Set display config: apply
/// </summary>
public const uint SdcApply = 0x00000080;
/// <summary>
/// Set display config: use supplied display config
/// </summary>
public const uint SdcUseSuppliedDisplayConfig = 0x00000020;
/// <summary>
/// Set display config: save to database
/// </summary>
public const uint SdcSaveToDatabase = 0x00000200;
/// <summary>
/// Set display config: topology supplied
/// </summary>
public const uint SdcTopologySupplied = 0x00000010;
/// <summary>
/// Set display config: allow path order changes
/// </summary>
public const uint SdcAllowPathOrderChanges = 0x00002000;
/// <summary>
/// Get target name
/// </summary>
public const uint DisplayconfigDeviceInfoGetTargetName = 1;
/// <summary>
/// Get SDR white level
/// </summary>
public const uint DisplayconfigDeviceInfoGetSdrWhiteLevel = 7;
/// <summary>
/// Get advanced color information
/// </summary>
public const uint DisplayconfigDeviceInfoGetAdvancedColorInfo = 9;
/// <summary>
/// Set SDR white level (custom)
/// </summary>
public const uint DisplayconfigDeviceInfoSetSdrWhiteLevel = 0xFFFFFFEE;
/// <summary>
/// Path active
/// </summary>
public const uint DisplayconfigPathActive = 0x00000001;
/// <summary>
/// Path mode index invalid
/// </summary>
public const uint DisplayconfigPathModeIdxInvalid = 0xFFFFFFFF;
/// <summary>
/// COM initialization: multithreaded
/// </summary>
public const uint CoinitMultithreaded = 0x0;
/// <summary>
/// RPC authentication level: connect
/// </summary>
public const uint RpcCAuthnLevelConnect = 2;
/// <summary>
/// RPC impersonation level: impersonate
/// </summary>
public const uint RpcCImpLevelImpersonate = 3;
/// <summary>
/// RPC authentication service: Win NT
/// </summary>
public const uint RpcCAuthnWinnt = 10;
/// <summary>
/// RPC authorization service: none
/// </summary>
public const uint RpcCAuthzNone = 0;
/// <summary>
/// RPC authentication level: call
/// </summary>
public const uint RpcCAuthnLevelCall = 3;
/// <summary>
/// EOAC: none
/// </summary>
public const uint EoacNone = 0;
/// <summary>
/// WMI flag: forward only
/// </summary>
public const long WbemFlagForwardOnly = 0x20;
/// <summary>
/// WMI flag: return immediately
/// </summary>
public const long WbemFlagReturnImmediately = 0x10;
/// <summary>
/// WMI flag: connect use max wait
/// </summary>
public const long WbemFlagConnectUseMaxWait = 0x80;
/// <summary>
/// Success
/// </summary>
public const int ErrorSuccess = 0;
/// <summary>
/// Insufficient buffer
/// </summary>
public const int ErrorInsufficientBuffer = 122;
/// <summary>
/// Invalid parameter
/// </summary>
public const int ErrorInvalidParameter = 87;
/// <summary>
/// Access denied
/// </summary>
public const int ErrorAccessDenied = 5;
/// <summary>
/// General failure
/// </summary>
public const int ErrorGenFailure = 31;
/// <summary>
/// Unsupported VCP code
/// </summary>
public const int ErrorGraphicsDdcciVcpNotSupported = -1071243251;
/// <summary>
/// Infinite wait
/// </summary>
public const uint Infinite = 0xFFFFFFFF;
/// <summary>
/// User message
/// </summary>
public const uint WmUser = 0x0400;
/// <summary>
/// Output technology: HDMI
/// </summary>
public const uint DisplayconfigOutputTechnologyHdmi = 5;
/// <summary>
/// Output technology: DVI
/// </summary>
public const uint DisplayconfigOutputTechnologyDvi = 4;
/// <summary>
/// Output technology: DisplayPort
/// </summary>
public const uint DisplayconfigOutputTechnologyDisplayportExternal = 6;
/// <summary>
/// Output technology: internal
/// </summary>
public const uint DisplayconfigOutputTechnologyInternal = 0x80000000;
/// <summary>
/// HDR minimum SDR white level (nits)
/// </summary>
public const int HdrMinSdrWhiteLevel = 80;
/// <summary>
/// HDR maximum SDR white level (nits)
/// </summary>
public const int HdrMaxSdrWhiteLevel = 480;
/// <summary>
/// SDR white level conversion factor
/// </summary>
public const int SdrWhiteLevelFactor = 80;
}
}

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;
// Type aliases for Windows API naming conventions compatibility
using RECT = PowerDisplay.Native.Rect;
namespace PowerDisplay.Native;
/// <summary>
/// Native delegate type definitions
/// </summary>
public static class NativeDelegates
{
/// <summary>
/// Monitor enumeration procedure delegate
/// </summary>
/// <param name="hMonitor">Monitor handle</param>
/// <param name="hdcMonitor">Monitor device context</param>
/// <param name="lprcMonitor">Pointer to monitor rectangle</param>
/// <param name="dwData">User data</param>
/// <returns>True to continue enumeration</returns>
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
/// <summary>
/// Thread start routine delegate
/// </summary>
/// <param name="lpParameter">Thread parameter</param>
/// <returns>Thread exit code</returns>
public delegate uint ThreadStartRoutine(IntPtr lpParameter);
}

View File

@@ -0,0 +1,445 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
#pragma warning disable SA1649 // File name should match first type name - Multiple related P/Invoke structures
namespace PowerDisplay.Native
{
/// <summary>
/// Physical monitor structure for DDC/CI
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct PhysicalMonitor
{
/// <summary>
/// Physical monitor handle
/// </summary>
public IntPtr HPhysicalMonitor;
/// <summary>
/// Physical monitor description string - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort SzPhysicalMonitorDescription[128];
/// <summary>
/// Helper method to get description as string
/// </summary>
public readonly string GetDescription()
{
fixed (ushort* ptr = SzPhysicalMonitorDescription)
{
return new string((char*)ptr);
}
}
}
/// <summary>
/// Rectangle structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
public int Width => Right - Left;
public int Height => Bottom - Top;
public Rect(int left, int top, int right, int bottom)
{
Left = left;
Top = top;
Right = right;
Bottom = bottom;
}
}
/// <summary>
/// Monitor information extended structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct MonitorInfoEx
{
/// <summary>
/// Structure size
/// </summary>
public uint CbSize;
/// <summary>
/// Monitor rectangle area
/// </summary>
public Rect RcMonitor;
/// <summary>
/// Work area rectangle
/// </summary>
public Rect RcWork;
/// <summary>
/// Flags
/// </summary>
public uint DwFlags;
/// <summary>
/// Device name (e.g., "\\.\DISPLAY1") - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort SzDevice[32];
/// <summary>
/// Helper property to get device name as string
/// </summary>
public readonly string GetDeviceName()
{
fixed (ushort* ptr = SzDevice)
{
return new string((char*)ptr);
}
}
}
/// <summary>
/// Display device structure
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct DisplayDevice
{
/// <summary>
/// Structure size
/// </summary>
public uint Cb;
/// <summary>
/// Device name (e.g., "\\.\DISPLAY1\Monitor0") - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DeviceName[32];
/// <summary>
/// Device description string - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DeviceString[128];
/// <summary>
/// Status flags
/// </summary>
public uint StateFlags;
/// <summary>
/// Device ID - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DeviceID[128];
/// <summary>
/// Registry device key - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DeviceKey[128];
/// <summary>
/// Helper method to get device name as string
/// </summary>
public readonly string GetDeviceName()
{
fixed (ushort* ptr = DeviceName)
{
return new string((char*)ptr);
}
}
/// <summary>
/// Helper method to get device string as string
/// </summary>
public readonly string GetDeviceString()
{
fixed (ushort* ptr = DeviceString)
{
return new string((char*)ptr);
}
}
/// <summary>
/// Helper method to get device ID as string
/// </summary>
public readonly string GetDeviceID()
{
fixed (ushort* ptr = DeviceID)
{
return new string((char*)ptr);
}
}
/// <summary>
/// Helper method to get device key as string
/// </summary>
public readonly string GetDeviceKey()
{
fixed (ushort* ptr = DeviceKey)
{
return new string((char*)ptr);
}
}
}
/// <summary>
/// LUID (Locally Unique Identifier) structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Luid
{
public uint LowPart;
public int HighPart;
public override string ToString()
{
return $"{HighPart:X8}:{LowPart:X8}";
}
}
/// <summary>
/// Display configuration path information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_PATH_INFO
{
public DISPLAYCONFIG_PATH_SOURCE_INFO SourceInfo;
public DISPLAYCONFIG_PATH_TARGET_INFO TargetInfo;
public uint Flags;
}
/// <summary>
/// Display configuration path source information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_PATH_SOURCE_INFO
{
public Luid AdapterId;
public uint Id;
public uint ModeInfoIdx;
public uint StatusFlags;
}
/// <summary>
/// Display configuration path target information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_PATH_TARGET_INFO
{
public Luid AdapterId;
public uint Id;
public uint ModeInfoIdx;
public uint OutputTechnology;
public uint Rotation;
public uint Scaling;
public DISPLAYCONFIG_RATIONAL RefreshRate;
public uint ScanLineOrdering;
public bool TargetAvailable;
public uint StatusFlags;
}
/// <summary>
/// Display configuration rational number
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_RATIONAL
{
public uint Numerator;
public uint Denominator;
}
/// <summary>
/// Display configuration mode information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_MODE_INFO
{
public uint InfoType;
public uint Id;
public Luid AdapterId;
public DISPLAYCONFIG_MODE_INFO_UNION ModeInfo;
}
/// <summary>
/// Display configuration mode information union
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct DISPLAYCONFIG_MODE_INFO_UNION
{
[FieldOffset(0)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Native API structure field")]
public DISPLAYCONFIG_TARGET_MODE targetMode;
[FieldOffset(0)]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Native API structure field")]
public DISPLAYCONFIG_SOURCE_MODE sourceMode;
}
/// <summary>
/// Display configuration target mode
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_TARGET_MODE
{
public DISPLAYCONFIG_VIDEO_SIGNAL_INFO TargetVideoSignalInfo;
}
/// <summary>
/// Display configuration source mode
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SOURCE_MODE
{
public uint Width;
public uint Height;
public uint PixelFormat;
public DISPLAYCONFIG_POINT Position;
}
/// <summary>
/// Display configuration point
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_POINT
{
public int X;
public int Y;
}
/// <summary>
/// Display configuration video signal information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_VIDEO_SIGNAL_INFO
{
public ulong PixelRate;
public DISPLAYCONFIG_RATIONAL HSyncFreq;
public DISPLAYCONFIG_RATIONAL VSyncFreq;
public DISPLAYCONFIG_2DREGION ActiveSize;
public DISPLAYCONFIG_2DREGION TotalSize;
public uint VideoStandard;
public uint ScanLineOrdering;
}
/// <summary>
/// Display configuration 2D region
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_2DREGION
{
public uint Cx;
public uint Cy;
}
/// <summary>
/// Display configuration device information header
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_DEVICE_INFO_HEADER
{
public uint Type;
public uint Size;
public Luid AdapterId;
public uint Id;
}
/// <summary>
/// Display configuration target device name
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct DISPLAYCONFIG_TARGET_DEVICE_NAME
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint Flags;
public uint OutputTechnology;
public ushort EdidManufactureId;
public ushort EdidProductCodeId;
public uint ConnectorInstance;
/// <summary>
/// Monitor friendly name - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort MonitorFriendlyDeviceName[64];
/// <summary>
/// Monitor device path - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort MonitorDevicePath[128];
/// <summary>
/// Helper method to get monitor friendly name as string
/// </summary>
public readonly string GetMonitorFriendlyDeviceName()
{
fixed (ushort* ptr = MonitorFriendlyDeviceName)
{
return new string((char*)ptr);
}
}
/// <summary>
/// Helper method to get monitor device path as string
/// </summary>
public readonly string GetMonitorDevicePath()
{
fixed (ushort* ptr = MonitorDevicePath)
{
return new string((char*)ptr);
}
}
}
/// <summary>
/// Display configuration SDR white level
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SDR_WHITE_LEVEL
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint SDRWhiteLevel;
}
/// <summary>
/// Display configuration advanced color information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint AdvancedColorSupported;
public uint AdvancedColorEnabled;
public uint BitsPerColorChannel;
public uint ColorEncoding;
public uint FormatSupport;
}
/// <summary>
/// Custom structure for setting SDR white level
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SET_SDR_WHITE_LEVEL
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint SDRWhiteLevel;
public byte FinalValue;
}
/// <summary>
/// Point structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
}

View File

@@ -0,0 +1,272 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace PowerDisplay.Native
{
/// <summary>
/// P/Invoke declarations using LibraryImport source generator
/// </summary>
internal static partial class PInvoke
{
// ==================== User32.dll - Window Management ====================
// GetWindowLong: On 64-bit use GetWindowLongPtrW, on 32-bit use GetWindowLongW
#if WIN64
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
internal static partial IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
#else
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongW")]
internal static partial int GetWindowLong(IntPtr hWnd, int nIndex);
#endif
// SetWindowLong: On 64-bit use SetWindowLongPtrW, on 32-bit use SetWindowLongW
#if WIN64
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
internal static partial IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
#else
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongW")]
internal static partial int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
#endif
// SetWindowLongPtr: Always uses the Ptr variant (64-bit)
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
internal static partial IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetWindowPos(
IntPtr hWnd,
IntPtr hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint uFlags);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool ShowWindow(IntPtr hWnd, int nCmdShow);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetForegroundWindow(IntPtr hWnd);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool IsWindowVisible(IntPtr hWnd);
// ==================== User32.dll - Window Creation and Messaging ====================
[LibraryImport("user32.dll", EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial IntPtr CreateWindowEx(
uint dwExStyle,
[MarshalAs(UnmanagedType.LPWStr)] string lpClassName,
[MarshalAs(UnmanagedType.LPWStr)] string lpWindowName,
uint dwStyle,
int x,
int y,
int nWidth,
int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DestroyWindow(IntPtr hWnd);
[LibraryImport("user32.dll", EntryPoint = "DefWindowProcW")]
internal static partial IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[LibraryImport("user32.dll")]
internal static partial IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
[LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
internal static partial uint RegisterWindowMessage([MarshalAs(UnmanagedType.LPWStr)] string lpString);
// ==================== User32.dll - Menu Functions ====================
[LibraryImport("user32.dll")]
internal static partial IntPtr CreatePopupMenu();
[LibraryImport("user32.dll", EntryPoint = "AppendMenuW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool AppendMenu(
IntPtr hMenu,
uint uFlags,
uint uIDNewItem,
[MarshalAs(UnmanagedType.LPWStr)] string lpNewItem);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DestroyMenu(IntPtr hMenu);
[LibraryImport("user32.dll")]
internal static partial int TrackPopupMenu(
IntPtr hMenu,
uint uFlags,
int x,
int y,
int nReserved,
IntPtr hWnd,
IntPtr prcRect);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetCursorPos(out POINT lpPoint);
// ==================== User32.dll - Display Configuration ====================
[LibraryImport("user32.dll")]
internal static partial int GetDisplayConfigBufferSizes(
uint flags,
out uint numPathArrayElements,
out uint numModeInfoArrayElements);
// With DisableRuntimeMarshalling, LibraryImport can handle struct arrays
[LibraryImport("user32.dll")]
internal static partial int QueryDisplayConfig(
uint flags,
ref uint numPathArrayElements,
[Out] DISPLAYCONFIG_PATH_INFO[] pathArray,
ref uint numModeInfoArrayElements,
[Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray,
IntPtr currentTopologyId);
[LibraryImport("user32.dll")]
internal static partial int DisplayConfigGetDeviceInfo(
ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName);
// ==================== User32.dll - Monitor Enumeration ====================
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool EnumDisplayMonitors(
IntPtr hdc,
IntPtr lprcClip,
NativeDelegates.MonitorEnumProc lpfnEnum,
IntPtr dwData);
[LibraryImport("user32.dll", EntryPoint = "GetMonitorInfoW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorInfo(
IntPtr hMonitor,
ref MonitorInfoEx lpmi);
[LibraryImport("user32.dll", EntryPoint = "EnumDisplayDevicesW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool EnumDisplayDevices(
[MarshalAs(UnmanagedType.LPWStr)] string? lpDevice,
uint iDevNum,
ref DisplayDevice lpDisplayDevice,
uint dwFlags);
[LibraryImport("user32.dll")]
internal static partial IntPtr MonitorFromWindow(
IntPtr hwnd,
uint dwFlags);
[LibraryImport("user32.dll")]
internal static partial IntPtr MonitorFromPoint(
POINT pt,
uint dwFlags);
// ==================== Dxva2.dll - DDC/CI Monitor Control ====================
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetNumberOfPhysicalMonitorsFromHMONITOR(
IntPtr hMonitor,
out uint pdwNumberOfPhysicalMonitors);
// Use unsafe pointer to avoid ArraySubType limitation
[LibraryImport("Dxva2.dll", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool GetPhysicalMonitorsFromHMONITOR(
IntPtr hMonitor,
uint dwPhysicalMonitorArraySize,
PhysicalMonitor* pPhysicalMonitorArray);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool DestroyPhysicalMonitor(IntPtr hMonitor);
// Use unsafe pointer to avoid LPArray limitation
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool DestroyPhysicalMonitors(
uint dwPhysicalMonitorArraySize,
PhysicalMonitor* pPhysicalMonitorArray);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetVCPFeatureAndVCPFeatureReply(
IntPtr hPhysicalMonitor,
byte bVCPCode,
IntPtr pvct,
out uint pdwCurrentValue,
out uint pdwMaximumValue);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetVCPFeature(
IntPtr hPhysicalMonitor,
byte bVCPCode,
uint dwNewValue);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SaveCurrentSettings(IntPtr hPhysicalMonitor);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetCapabilitiesStringLength(
IntPtr hPhysicalMonitor,
out uint pdwCapabilitiesStringLengthInCharacters);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CapabilitiesRequestAndCapabilitiesReply(
IntPtr hPhysicalMonitor,
IntPtr pszASCIICapabilitiesString,
uint dwCapabilitiesStringLengthInCharacters);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorBrightness(
IntPtr hPhysicalMonitor,
out uint pdwMinimumBrightness,
out uint pdwCurrentBrightness,
out uint pdwMaximumBrightness);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetMonitorBrightness(
IntPtr hPhysicalMonitor,
uint dwNewBrightness);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorContrast(
IntPtr hPhysicalMonitor,
out uint pdwMinimumContrast,
out uint pdwCurrentContrast,
out uint pdwMaximumContrast);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetMonitorContrast(
IntPtr hPhysicalMonitor,
uint dwNewContrast);
[LibraryImport("Dxva2.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool GetMonitorCapabilities(
IntPtr hPhysicalMonitor,
out uint pdwMonitorCapabilities,
out uint pdwSupportedColorTemperatures);
// ==================== Kernel32.dll ====================
[LibraryImport("kernel32.dll")]
internal static partial uint GetLastError();
}
}

View File

@@ -0,0 +1,448 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using WmiLight;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.Native.WMI
{
/// <summary>
/// WMI monitor controller for controlling internal laptop displays.
/// Rewritten to use WmiLight library for Native AOT compatibility.
/// </summary>
public partial class WmiController : IMonitorController, IDisposable
{
private const string WmiNamespace = @"root\WMI";
private const string BrightnessQueryClass = "WmiMonitorBrightness";
private const string BrightnessMethodClass = "WmiMonitorBrightnessMethods";
private const string MonitorIdClass = "WmiMonitorID";
private bool _disposed;
public string Name => "WMI Monitor Controller (WmiLight)";
/// <summary>
/// Check if the specified monitor can be controlled
/// </summary>
public async Task<bool> CanControlMonitorAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
if (monitor.CommunicationMethod != "WMI")
{
return false;
}
return await Task.Run(
() =>
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = $"SELECT * FROM {BrightnessQueryClass}";
var results = connection.CreateQuery(query).ToList();
return results.Count > 0;
}
catch (Exception ex)
{
Logger.LogWarning($"WMI CanControlMonitor check failed: {ex.Message}");
return false;
}
},
cancellationToken);
}
/// <summary>
/// Get monitor brightness
/// </summary>
public async Task<BrightnessInfo> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = $"SELECT CurrentBrightness FROM {BrightnessQueryClass}";
var results = connection.CreateQuery(query);
foreach (var obj in results)
{
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
return new BrightnessInfo(currentBrightness, 0, 100);
}
}
catch (WmiException ex)
{
Logger.LogWarning($"WMI GetBrightness failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
}
catch (Exception ex)
{
Logger.LogWarning($"WMI GetBrightness failed: {ex.Message}");
}
return BrightnessInfo.Invalid;
},
cancellationToken);
}
/// <summary>
/// Set monitor brightness
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
{
// Validate brightness range
brightness = Math.Clamp(brightness, 0, 100);
return await Task.Run(
() =>
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = $"SELECT * FROM {BrightnessMethodClass}";
var results = connection.CreateQuery(query);
foreach (var obj in results)
{
// Call WmiSetBrightness method
// Parameters: Timeout (uint32), Brightness (byte)
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
using (WmiMethodParameters inParams = method.CreateInParameters())
{
inParams.SetPropertyValue("Timeout", 0u);
inParams.SetPropertyValue("Brightness", (byte)brightness);
uint result = obj.ExecuteMethod<uint>(
method,
inParams,
out WmiMethodParameters outParams);
// Check return value (0 indicates success)
if (result == 0)
{
return MonitorOperationResult.Success();
}
else
{
return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
}
}
}
return MonitorOperationResult.Failure("No WMI brightness methods found");
}
catch (UnauthorizedAccessException)
{
return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
}
catch (WmiException ex)
{
return MonitorOperationResult.Failure($"WMI error: {ex.Message}", ex.HResult);
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Unexpected error: {ex.Message}");
}
},
cancellationToken);
}
/// <summary>
/// Discover supported monitors
/// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
var monitors = new List<Monitor>();
try
{
using var connection = new WmiConnection(WmiNamespace);
// First check if WMI brightness support is available
var brightnessQuery = $"SELECT * FROM {BrightnessQueryClass}";
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
if (brightnessResults.Count == 0)
{
return monitors;
}
// Get monitor information
var idQuery = $"SELECT * FROM {MonitorIdClass}";
var idResults = connection.CreateQuery(idQuery).ToList();
var monitorInfos = new Dictionary<string, (string Name, string InstanceName)>();
foreach (var obj in idResults)
{
try
{
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
var userFriendlyName = GetUserFriendlyName(obj) ?? "Internal Display";
if (!string.IsNullOrEmpty(instanceName))
{
monitorInfos[instanceName] = (userFriendlyName, instanceName);
}
}
catch (Exception ex)
{
// Skip problematic entries
Logger.LogDebug($"Failed to parse WMI monitor info: {ex.Message}");
}
}
// Create monitor objects for each supported brightness instance
foreach (var obj in brightnessResults)
{
try
{
var instanceName = obj.GetPropertyValue<string>("InstanceName") ?? string.Empty;
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
var name = "Internal Display";
if (monitorInfos.TryGetValue(instanceName, out var info))
{
name = info.Name;
}
var monitor = new Monitor
{
Id = $"WMI_{instanceName}",
Name = name,
CurrentBrightness = currentBrightness,
MinBrightness = 0,
MaxBrightness = 100,
IsAvailable = true,
InstanceName = instanceName,
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
ConnectionType = "Internal",
CommunicationMethod = "WMI",
Manufacturer = "Internal",
SupportsColorTemperature = false,
};
monitors.Add(monitor);
}
catch (Exception ex)
{
// Skip problematic monitors
Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
}
}
}
catch (WmiException ex)
{
// Return empty list instead of throwing exception
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message} (HResult: 0x{ex.HResult:X})");
}
catch (Exception ex)
{
Logger.LogError($"WMI DiscoverMonitors failed: {ex.Message}");
}
return monitors;
},
cancellationToken);
}
/// <summary>
/// Validate monitor connection status
/// </summary>
public async Task<bool> ValidateConnectionAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
try
{
// Try to read current brightness to validate connection
using var connection = new WmiConnection(WmiNamespace);
var query = $"SELECT CurrentBrightness FROM {BrightnessQueryClass} WHERE InstanceName='{monitor.InstanceName}'";
var results = connection.CreateQuery(query).ToList();
return results.Count > 0;
}
catch (Exception ex)
{
Logger.LogWarning($"WMI ValidateConnection failed for {monitor.InstanceName}: {ex.Message}");
return false;
}
},
cancellationToken);
}
/// <summary>
/// Get user-friendly name from WMI object
/// </summary>
private static string? GetUserFriendlyName(WmiObject monitorObject)
{
try
{
// WmiLight returns arrays as object arrays
var userFriendlyNameObj = monitorObject.GetPropertyValue<object>("UserFriendlyName");
if (userFriendlyNameObj is ushort[] userFriendlyName && userFriendlyName.Length > 0)
{
// Convert UINT16 array to string
var chars = userFriendlyName
.Where(c => c != 0)
.Select(c => (char)c)
.ToArray();
if (chars.Length > 0)
{
return new string(chars).Trim();
}
}
}
catch (Exception ex)
{
// Ignore conversion errors
Logger.LogDebug($"Failed to parse UserFriendlyName: {ex.Message}");
}
return null;
}
/// <summary>
/// Check WMI service availability
/// </summary>
public static bool IsWmiAvailable()
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = $"SELECT * FROM {BrightnessQueryClass}";
var results = connection.CreateQuery(query).ToList();
return results.Count > 0;
}
catch (WmiException ex) when (ex.HResult == 0x1068)
{
// Expected on systems without WMI brightness support (desktops, some laptops)
Logger.LogInfo("WMI brightness control not supported on this system (expected for desktops)");
return false;
}
catch (Exception ex)
{
// Unexpected error during WMI check
Logger.LogDebug($"WMI availability check failed: {ex.Message}");
return false;
}
}
/// <summary>
/// Check if administrator privileges are required
/// </summary>
public static bool RequiresElevation()
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = $"SELECT * FROM {BrightnessMethodClass}";
var results = connection.CreateQuery(query).ToList();
foreach (var obj in results)
{
// Try to call method to check permissions
try
{
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
using (WmiMethodParameters inParams = method.CreateInParameters())
{
inParams.SetPropertyValue("Timeout", 0u);
inParams.SetPropertyValue("Brightness", (byte)50);
obj.ExecuteMethod<uint>(method, inParams, out WmiMethodParameters outParams);
return false; // If successful, no elevation required
}
}
catch (UnauthorizedAccessException)
{
return true; // Administrator privileges required
}
catch (Exception ex)
{
// Other errors may not be permission issues
Logger.LogDebug($"WMI RequiresElevation check error: {ex.Message}");
return false;
}
}
}
catch (Exception ex)
{
// Cannot determine, assume privileges required
Logger.LogWarning($"WMI RequiresElevation check failed: {ex.Message}");
return true;
}
return false;
}
// Extended features not supported by WMI
public Task<BrightnessInfo> GetContrastAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return Task.FromResult(BrightnessInfo.Invalid);
}
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
{
return Task.FromResult(MonitorOperationResult.Failure("Contrast control not supported via WMI"));
}
public Task<BrightnessInfo> GetVolumeAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return Task.FromResult(BrightnessInfo.Invalid);
}
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
{
return Task.FromResult(MonitorOperationResult.Failure("Volume control not supported via WMI"));
}
public Task<BrightnessInfo> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return Task.FromResult(BrightnessInfo.Invalid);
}
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
{
return Task.FromResult(MonitorOperationResult.Failure("Color temperature control not supported via WMI"));
}
public Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return Task.FromResult(string.Empty);
}
public Task<MonitorOperationResult> SaveCurrentSettingsAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return Task.FromResult(MonitorOperationResult.Failure("Save settings not supported via WMI"));
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
// WmiLight objects are automatically cleaned up, no specific cleanup needed here
_disposed = true;
}
}
}
}

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>
/// Disable window moving and resizing functionality
/// </summary>
public static void DisableWindowMovingAndResizing(IntPtr hWnd)
{
// Get current window style
#if WIN64
int style = (int)GetWindowLong(hWnd, GwlStyle);
#else
int style = GetWindowLong(hWnd, GwlStyle);
#endif
// Remove resizable borders, title bar, and system menu
style &= ~WsThickframe;
style &= ~WsMaximizebox;
style &= ~WsMinimizebox;
style &= ~WsCaption; // Remove entire title bar
style &= ~WsSysmenu; // Remove system menu
// Set new window style
#if WIN64
_ = SetWindowLong(hWnd, GwlStyle, new IntPtr(style));
#else
_ = SetWindowLong(hWnd, GwlStyle, style);
#endif
// Get extended style and remove related borders
#if WIN64
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
#else
int exStyle = GetWindowLong(hWnd, GwlExstyle);
#endif
exStyle &= ~WsExDlgmodalframe;
exStyle &= ~WsExWindowedge;
exStyle &= ~WsExClientedge;
exStyle &= ~WsExStaticedge;
#if WIN64
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
#else
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
#endif
// Refresh window frame
SetWindowPos(
hWnd,
IntPtr.Zero,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
/// <summary>
/// Set whether window is topmost
/// </summary>
public static void SetWindowTopmost(IntPtr hWnd, bool topmost)
{
SetWindowPos(
hWnd,
topmost ? HwndTopmost : HwndNotopmost,
0,
0,
0,
0,
SwpNomove | SwpNosize);
}
/// <summary>
/// Show or hide window
/// </summary>
public static void ShowWindow(IntPtr hWnd, bool show)
{
PInvoke.ShowWindow(hWnd, show ? SwShow : SwHide);
}
/// <summary>
/// Minimize window
/// </summary>
public static void MinimizeWindow(IntPtr hWnd)
{
PInvoke.ShowWindow(hWnd, SwMinimize);
}
/// <summary>
/// Restore window
/// </summary>
public static void RestoreWindow(IntPtr hWnd)
{
PInvoke.ShowWindow(hWnd, SwRestore);
}
/// <summary>
/// Hide window from taskbar
/// </summary>
public static void HideFromTaskbar(IntPtr hWnd)
{
// Get current extended style
#if WIN64
int exStyle = (int)GetWindowLong(hWnd, GwlExstyle);
#else
int exStyle = GetWindowLong(hWnd, GwlExstyle);
#endif
// Add WS_EX_TOOLWINDOW style to hide window from taskbar
exStyle |= WsExToolwindow;
// Set new extended style
#if WIN64
_ = SetWindowLong(hWnd, GwlExstyle, new IntPtr(exStyle));
#else
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
#endif
// Refresh window frame
SetWindowPos(
hWnd,
IntPtr.Zero,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
}
}

View File

@@ -0,0 +1,87 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>PowerDisplay</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x64;ARM64</Platforms>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<AssemblyName>PowerToys.PowerDisplay</AssemblyName>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.PowerDisplay.pri</ProjectPriFileName>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<!-- Disable XAML-generated Main method, use custom Program.cs -->
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
</PropertyGroup>
<!-- Native AOT Configuration -->
<PropertyGroup>
<PublishSingleFile>false</PublishSingleFile>
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
<PublishAot>true</PublishAot>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<Page Remove="PowerDisplayXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="PowerDisplayXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<Folder Include="PowerDisplayXAML\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="WmiLight" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Sizers" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally -->
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup>
<!-- Copy Assets folder to output directory -->
<ItemGroup>
<Content Include="Assets\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="PowerDisplay.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:toolkit="using:CommunityToolkit.WinUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- WinUI 3 System Resources -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,334 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppLifecycle;
using PowerDisplay.Helpers;
using PowerDisplay.Serialization;
using PowerToys.Interop;
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay application main class
/// </summary>
#pragma warning disable CA1001 // CancellationTokenSource is disposed in Shutdown/ForceExit methods
public partial class App : Application
#pragma warning restore CA1001
{
// Windows Event names (from shared_constants.h)
private const string ShowPowerDisplayEvent = "Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
private const string TogglePowerDisplayEvent = "Local\\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c";
private const string TerminatePowerDisplayEvent = "Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
private const string RefreshMonitorsEvent = "Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
private const string SettingsUpdatedEvent = "Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
private const string ApplyColorTemperatureEvent = "Local\\PowerToysPowerDisplay-ApplyColorTemperatureEvent-4b7e9f2a-3c6d-5a8e-7f1b-9d2e4c6a8b0d";
private const string ApplyProfileEvent = "Local\\PowerToysPowerDisplay-ApplyProfileEvent-6e8a3c9d-4f7b-5d2e-8a1c-3e9f7b6d2a5c";
private Window? _mainWindow;
private int _powerToysRunnerPid;
public App(int runnerPid)
{
_powerToysRunnerPid = runnerPid;
this.InitializeComponent();
// Ensure types used in XAML are preserved for AOT compilation
TypePreservation.PreserveTypes();
// Initialize Logger
Logger.InitializeLogger("\\PowerDisplay\\Logs");
// Initialize PowerToys telemetry
try
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
}
catch
{
// Telemetry errors should not crash the app
}
// Initialize language settings
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
// Handle unhandled exceptions
this.UnhandledException += OnUnhandledException;
}
/// <summary>
/// Handle unhandled exceptions
/// </summary>
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// Try to display error information
ShowStartupError(e.Exception);
// Mark exception as handled to prevent app crash
e.Handled = true;
}
/// <summary>
/// Called when the application is launched
/// </summary>
/// <param name="args">Launch arguments</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
try
{
// Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs
// PID is already parsed in Program.cs and passed to constructor
// Set up Windows Events monitoring (Awake pattern)
NativeEventWaiter.WaitForEventLoop(
ShowPowerDisplayEvent,
() =>
{
Logger.LogInfo("[EVENT] Show event received");
Logger.LogInfo($"[EVENT] _mainWindow is null: {_mainWindow == null}");
Logger.LogInfo($"[EVENT] _mainWindow type: {_mainWindow?.GetType().Name}");
Logger.LogInfo($"[EVENT] Current thread ID: {Environment.CurrentManagedThreadId}");
// Direct call - NativeEventWaiter already marshalled to UI thread
// No need for double DispatcherQueue.TryEnqueue
if (_mainWindow is MainWindow mainWindow)
{
Logger.LogInfo("[EVENT] Calling ShowWindow directly");
mainWindow.ShowWindow();
Logger.LogInfo("[EVENT] ShowWindow returned");
}
else
{
Logger.LogError($"[EVENT] _mainWindow type mismatch, actual type: {_mainWindow?.GetType().Name}");
}
},
CancellationToken.None);
NativeEventWaiter.WaitForEventLoop(
TogglePowerDisplayEvent,
() =>
{
Logger.LogInfo("[EVENT] Toggle event received");
if (_mainWindow is MainWindow mainWindow)
{
Logger.LogInfo("[EVENT] Calling ToggleWindow");
mainWindow.ToggleWindow();
}
else
{
Logger.LogError($"[EVENT] _mainWindow type mismatch for toggle");
}
},
CancellationToken.None);
NativeEventWaiter.WaitForEventLoop(
TerminatePowerDisplayEvent,
() =>
{
Logger.LogInfo("Received terminate event - exiting immediately");
Environment.Exit(0);
},
CancellationToken.None);
// Note: PowerDisplay.exe should NOT listen to RefreshMonitorsEvent
// That event is sent BY PowerDisplay TO Settings UI for one-way notification
// Listening to our own event would create an infinite refresh loop
NativeEventWaiter.WaitForEventLoop(
SettingsUpdatedEvent,
() =>
{
Logger.LogInfo("Received settings updated event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.ApplySettingsFromUI();
}
});
},
CancellationToken.None);
NativeEventWaiter.WaitForEventLoop(
ApplyColorTemperatureEvent,
() =>
{
Logger.LogInfo("Received apply color temperature event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.ApplyColorTemperatureFromSettings();
}
});
},
CancellationToken.None);
NativeEventWaiter.WaitForEventLoop(
ApplyProfileEvent,
() =>
{
Logger.LogInfo("Received apply profile event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.ApplyProfileFromSettings();
}
});
},
CancellationToken.None);
// Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0)
{
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
Environment.Exit(0);
});
}
else
{
Logger.LogInfo("PowerDisplay started in standalone mode");
}
// Create main window
_mainWindow = new MainWindow();
// Window visibility depends on launch mode
bool isStandaloneMode = _powerToysRunnerPid <= 0;
if (isStandaloneMode)
{
// Standalone mode - activate and show window immediately
_mainWindow.Activate();
Logger.LogInfo("Window activated (standalone mode)");
}
else
{
// PowerToys mode - window remains hidden until show event received
Logger.LogInfo("Window created, waiting for show event (PowerToys mode)");
// Start background initialization to scan monitors even when hidden
_ = Task.Run(async () =>
{
// Give window a moment to finish construction
await Task.Delay(500);
// Trigger initialization on UI thread
_mainWindow?.DispatcherQueue.TryEnqueue(async () =>
{
if (_mainWindow is MainWindow mainWindow)
{
await mainWindow.EnsureInitializedAsync();
Logger.LogInfo("Background initialization completed");
}
});
});
}
}
catch (Exception ex)
{
ShowStartupError(ex);
}
}
/// <summary>
/// Show startup error
/// </summary>
private void ShowStartupError(Exception ex)
{
try
{
Logger.LogError($"PowerDisplay startup failed: {ex.Message}");
var errorWindow = new Window { Title = "PowerDisplay - Startup Error" };
var panel = new StackPanel { Margin = new Thickness(20), Spacing = 16 };
panel.Children.Add(new TextBlock
{
Text = "PowerDisplay Startup Failed",
FontSize = 20,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
});
panel.Children.Add(new TextBlock
{
Text = $"Error: {ex.Message}",
FontSize = 14,
TextWrapping = TextWrapping.Wrap,
});
panel.Children.Add(new TextBlock
{
Text = $"Details:\n{ex}",
FontSize = 12,
TextWrapping = TextWrapping.Wrap,
Foreground = new SolidColorBrush(Microsoft.UI.Colors.Gray),
Margin = new Thickness(0, 10, 0, 0),
});
var closeButton = new Button
{
Content = "Close",
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 10, 0, 0),
};
closeButton.Click += (_, _) => errorWindow.Close();
panel.Children.Add(closeButton);
errorWindow.Content = new ScrollViewer
{
Content = panel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
MaxHeight = 600,
MaxWidth = 800,
};
errorWindow.Activate();
}
catch
{
Environment.Exit(1);
}
}
/// <summary>
/// Gets the main window instance
/// </summary>
public Window? MainWindow => _mainWindow;
/// <summary>
/// Check if running standalone (not launched from PowerToys Runner)
/// </summary>
public bool IsRunningDetachedFromPowerToys()
{
return _powerToysRunnerPid == -1;
}
/// <summary>
/// Shutdown application (Awake pattern - simple and clean)
/// </summary>
public void Shutdown()
{
Logger.LogInfo("PowerDisplay shutting down");
Environment.Exit(0);
}
}
}

View File

@@ -0,0 +1,273 @@
<winuiex:WindowEx
x:Class="PowerDisplay.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedVisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:PowerDisplay.ViewModels"
xmlns:winuiex="using:WinUIEx"
IsAlwaysOnTop="True"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False">
<winuiex:WindowEx.Backdrop>
<winuiex:AcrylicSystemBackdrop
DarkFallbackColor="#1c1c1c"
DarkLuminosityOpacity="0.96"
DarkTintColor="#202020"
DarkTintOpacity="0.5"
LightFallbackColor="#EEEEEE"
LightLuminosityOpacity="0.90"
LightTintColor="#F3F3F3"
LightTintOpacity="0" />
</winuiex:WindowEx.Backdrop>
<Grid x:Name="RootGrid">
<Grid.Resources>
<Style
x:Key="FlyoutButtonStyle"
BasedOn="{StaticResource SubtleButtonStyle}"
TargetType="Button">
<Setter Property="Padding" Value="6" />
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="32" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
</Style>
</Grid.Resources>
<Border x:Name="MainContainer">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="48" />
</Grid.RowDefinitions>
<!-- Main Container with modern design -->
<Grid
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<StackPanel
Margin="0,16,0,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="16"
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.IsScanning), Mode=OneWay}">
<ProgressRing
Width="24"
Height="24"
Foreground="{ThemeResource AccentFillColorDefaultBrush}"
IsActive="True" />
<TextBlock
x:Name="ScanningMonitorsTextBlock"
x:Uid="ScanningMonitorsText"
HorizontalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextAlignment="Center" />
</StackPanel>
<!-- No Monitors State with InfoBar -->
<InfoBar
x:Name="NoMonitorsInfoBar"
x:Uid="NoMonitorsText"
IconSource="{ui:FontIconSource Glyph=&#xE7F4;}"
IsClosable="False"
IsOpen="{x:Bind ViewModel.ShowNoMonitorsMessage, Mode=OneWay}"
Severity="Informational" />
<!-- Content Area -->
<ScrollViewer
Padding="16,16,16,16"
HorizontalScrollBarVisibility="Disabled"
HorizontalScrollMode="Disabled"
VerticalScrollBarVisibility="Auto"
ZoomMode="Disabled">
<!-- Monitors List with modern card design -->
<ItemsRepeater
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
Visibility="{x:Bind ConvertBoolToVisibility(ViewModel.HasMonitors), Mode=OneWay}">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" Spacing="32" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="vm:MonitorViewModel">
<StackPanel HorizontalAlignment="Stretch" Spacing="4">
<!-- Monitor Name with Icon -->
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="MonitorTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="14"
Glyph="{x:Bind MonitorIconGlyph, Mode=OneWay}" />
<TextBlock
Grid.Column="2"
VerticalAlignment="Center"
FontWeight="SemiBold"
Text="{x:Bind Name, Mode=OneWay}" />
</Grid>
<!-- Brightness Control -->
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="BrightnessTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="14"
Glyph="&#xE706;" />
<Slider
x:Uid="BrightnessAutomation"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Maximum="{x:Bind MaxBrightness, Mode=OneWay}"
Minimum="{x:Bind MinBrightness, Mode=OneWay}"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Brightness"
ValueChanged="Slider_ValueChanged"
Value="{x:Bind Brightness, Mode=OneWay}" />
</Grid>
<!-- Visibility="{x:Bind ConvertBoolToVisibility(ShowContrast), Mode=OneWay}" -->
<!-- Contrast Control -->
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="ContrastTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="14"
Glyph="&#xE7A1;" />
<Slider
x:Uid="ContrastAutomation"
Grid.Column="2"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Maximum="100"
Minimum="0"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Contrast"
ValueChanged="Slider_ValueChanged"
Value="{x:Bind ContrastPercent, Mode=OneWay}" />
</Grid>
<!-- Visibility="{x:Bind ConvertBoolToVisibility(ShowVolume), Mode=OneWay}"> -->
<!-- Volume Control -->
<Grid Margin="0,8,0,0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="VolumeTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="14"
Glyph="&#xE767;" />
<Slider
x:Uid="VolumeAutomation"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Maximum="{x:Bind MaxVolume, Mode=OneWay}"
Minimum="{x:Bind MinVolume, Mode=OneWay}"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Volume"
ValueChanged="Slider_ValueChanged"
Value="{x:Bind Volume, Mode=OneWay}" />
</Grid>
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
<!-- Status Bar with modern design -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Action Buttons -->
<StackPanel
Grid.Column="1"
Margin="0,0,12,0"
HorizontalAlignment="Right"
Orientation="Horizontal"
Spacing="8">
<Button
x:Name="LinkButton"
x:Uid="SyncAllMonitorsTooltip"
Click="OnLinkClick"
Content="{ui:FontIcon Glyph=&#xE71B;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
<Button
x:Name="DisableButton"
x:Uid="DisableControlTooltip"
Click="OnDisableClick"
Content="{ui:FontIcon Glyph=&#xE7E8;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
<Button
x:Name="RefreshButton"
x:Uid="RefreshTooltip"
Click="OnRefreshClick"
Content="{ui:FontIcon Glyph=&#xE72C;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
<Button
x:Name="SettingsBtn"
x:Uid="SettingsTooltip"
Padding="6"
Click="OnSettingsClick"
Style="{StaticResource FlyoutButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SettingsTooltip" />
</ToolTipService.ToolTip>
<AnimatedIcon x:Name="SearchAnimatedIcon">
<AnimatedIcon.Source>
<animatedVisuals:AnimatedSettingsVisualSource />
</AnimatedIcon.Source>
<AnimatedIcon.FallbackIconSource>
<SymbolIconSource Symbol="Setting" />
</AnimatedIcon.FallbackIconSource>
</AnimatedIcon>
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</winuiex:WindowEx>

View File

@@ -0,0 +1,654 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Animation;
using PowerDisplay.Configuration;
using PowerDisplay.Core;
using PowerDisplay.Core.Interfaces;
using PowerDisplay.Core.Models;
using PowerDisplay.Helpers;
using PowerDisplay.Native;
using PowerDisplay.ViewModels;
using Windows.Graphics;
using WinRT.Interop;
using WinUIEx;
using static PowerDisplay.Native.PInvoke;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay main window
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
public sealed partial class MainWindow : WindowEx, IDisposable
{
private readonly ISettingsUtils _settingsUtils = new SettingsUtils();
private MainViewModel _viewModel = null!;
private AppWindow _appWindow = null!;
private bool _isExiting;
// Expose ViewModel as property for x:Bind
public MainViewModel ViewModel => _viewModel;
// Conversion functions for x:Bind (AOT-compatible alternative to converters)
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
public MainWindow()
{
try
{
this.InitializeComponent();
// 1. Configure window immediately (synchronous, no data dependency)
ConfigureWindow();
// 2. Create ViewModel immediately (lightweight object, no scanning yet)
_viewModel = new MainViewModel();
RootGrid.DataContext = _viewModel;
Bindings.Update();
// 3. Register event handlers
RegisterEventHandlers();
// 4. Start background initialization (don't wait)
_ = Task.Run(async () =>
{
try
{
await InitializeAsync();
_hasInitialized = true;
}
catch (Exception ex)
{
Logger.LogError($"Background initialization failed: {ex.Message}");
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
}
});
}
catch (Exception ex)
{
Logger.LogError($"MainWindow initialization failed: {ex.Message}");
ShowError($"Unable to start main window: {ex.Message}");
}
}
/// <summary>
/// Register all event handlers for window and ViewModel
/// </summary>
private void RegisterEventHandlers()
{
// Window events
this.Closed += OnWindowClosed;
this.Activated += OnWindowActivated;
// ViewModel events
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
}
private bool _hasInitialized;
/// <summary>
/// Ensures the window is properly initialized with ViewModel and data
/// Can be called from external code (e.g., App startup) to pre-initialize in background
/// </summary>
public async Task EnsureInitializedAsync()
{
if (_hasInitialized)
{
return;
}
// Wait for background initialization to complete
// This is a no-op if initialization already completed
await InitializeAsync();
_hasInitialized = true;
}
private async Task InitializeAsync()
{
try
{
// Perform monitor scanning (which internally calls ReloadMonitorSettingsAsync)
await _viewModel.RefreshMonitorsAsync();
// Adjust window size after data is loaded (must run on UI thread)
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
}
catch (WmiLight.WmiException ex)
{
Logger.LogError($"WMI access failed: {ex.Message}");
DispatcherQueue.TryEnqueue(() => ShowError("Unable to access internal display control, administrator privileges may be required."));
}
catch (Exception ex)
{
Logger.LogError($"Initialization failed: {ex.Message}");
DispatcherQueue.TryEnqueue(() => ShowError($"Initialization failed: {ex.Message}"));
}
}
private void ShowError(string message)
{
if (_viewModel != null)
{
_viewModel.StatusText = $"Error: {message}";
}
else
{
Logger.LogError($"Error (ViewModel not yet initialized): {message}");
}
}
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
{
// Auto-hide window when it loses focus (deactivated)
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
HideWindow();
}
}
private void OnWindowClosed(object sender, WindowEventArgs args)
{
// Allow window to close if program is exiting
if (_isExiting)
{
// Clean up event subscriptions
if (_viewModel != null)
{
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
}
args.Handled = false;
return;
}
// If only user operation (although we hide close button), just hide window
args.Handled = true; // Prevent window closing
HideWindow();
}
public void ShowWindow()
{
try
{
// If not initialized, log warning but continue showing
if (!_hasInitialized)
{
Logger.LogWarning("Window not fully initialized yet, showing anyway");
}
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
AdjustWindowSizeToContent();
if (_appWindow != null)
{
PositionWindowAtBottomRight(_appWindow);
}
else
{
Logger.LogWarning("AppWindow is null, skipping window repositioning");
}
this.Activate();
WindowHelper.ShowWindow(hWnd, true);
WindowHelpers.BringToForeground(hWnd);
bool isVisible = IsWindowVisible();
if (!isVisible)
{
Logger.LogError("Window not visible after show attempt, forcing visibility");
this.Activate();
WindowHelpers.BringToForeground(hWnd);
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to show window: {ex.Message}");
throw;
}
}
public void HideWindow()
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
// Fallback: hide immediately if animation not found
WindowHelper.ShowWindow(hWnd, false);
}
/// <summary>
/// Check if window is currently visible
/// </summary>
/// <returns>True if window is visible, false otherwise</returns>
public bool IsWindowVisible()
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
return PInvoke.IsWindowVisible(hWnd);
}
/// <summary>
/// Toggle window visibility (show if hidden, hide if visible)
/// </summary>
public void ToggleWindow()
{
try
{
bool isVisible = IsWindowVisible();
if (isVisible)
{
HideWindow();
}
else
{
ShowWindow();
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to toggle window: {ex.Message}");
throw;
}
}
private void OnUIRefreshRequested(object? sender, EventArgs e)
{
// Adjust window size when UI configuration changes (feature visibility toggles)
DispatcherQueue.TryEnqueue(() => AdjustWindowSizeToContent());
}
private void OnMonitorsCollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// Adjust window size when monitors collection changes (event-driven!)
// The UI binding will update first, then we adjust size
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
AdjustWindowSizeToContent();
});
}
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
// Adjust window size when relevant properties change (event-driven!)
if (e.PropertyName == nameof(_viewModel.IsScanning) ||
e.PropertyName == nameof(_viewModel.HasMonitors) ||
e.PropertyName == nameof(_viewModel.ShowNoMonitorsMessage))
{
// Use Low priority to ensure UI bindings update first
DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
{
AdjustWindowSizeToContent();
});
}
}
/// <summary>
/// Set exit flag to allow window to close normally
/// </summary>
public void SetExiting()
{
_isExiting = true;
}
/// <summary>
/// Fast shutdown: skip animations and complex cleanup
/// </summary>
public void FastShutdown()
{
try
{
_isExiting = true;
// Quick cleanup of ViewModel
if (_viewModel != null)
{
// Unsubscribe from events
_viewModel.UIRefreshRequested -= OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged -= OnMonitorsCollectionChanged;
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
// Dispose immediately
_viewModel.Dispose();
}
// Close window directly without animations
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
WindowHelper.ShowWindow(hWnd, false);
}
catch (Exception ex)
{
// Ignore cleanup errors to ensure shutdown
Logger.LogWarning($"FastShutdown error: {ex.Message}");
}
}
private void ExitApplication()
{
try
{
// Use fast shutdown
FastShutdown();
// Call application shutdown directly
if (Application.Current is App app)
{
app.Shutdown();
}
// Ensure immediate exit
Environment.Exit(0);
}
catch (Exception ex)
{
// Ensure exit even on error
Logger.LogError($"ExitApplication error: {ex.Message}");
Environment.Exit(0);
}
}
private void OnRefreshClick(object sender, RoutedEventArgs e)
{
try
{
// Refresh monitor list
if (_viewModel?.RefreshCommand?.CanExecute(null) == true)
{
_viewModel.RefreshCommand.Execute(null);
// Window size will be adjusted automatically by OnMonitorsCollectionChanged event!
// No delay needed - event-driven design
}
}
catch (Exception ex)
{
Logger.LogError($"OnRefreshClick failed: {ex}");
if (_viewModel != null)
{
_viewModel.StatusText = "Refresh failed";
}
}
}
private void OnLinkClick(object sender, RoutedEventArgs e)
{
try
{
// Link all monitor brightness (synchronized adjustment)
if (_viewModel != null && _viewModel.Monitors.Count > 0)
{
// Get first monitor brightness as reference
var baseBrightness = _viewModel.Monitors.First().Brightness;
_ = _viewModel.SetAllBrightnessAsync(baseBrightness);
}
}
catch (Exception ex)
{
Logger.LogError($"OnLinkClick failed: {ex}");
}
}
private void OnDisableClick(object sender, RoutedEventArgs e)
{
try
{
// Disable/enable all monitor controls
if (_viewModel != null)
{
foreach (var monitor in _viewModel.Monitors)
{
monitor.IsAvailable = !monitor.IsAvailable;
}
_viewModel.StatusText = _viewModel.Monitors.Any(m => m.IsAvailable)
? "Display control enabled"
: "Display control disabled";
}
}
catch (Exception ex)
{
Logger.LogError($"OnDisableClick failed: {ex}");
}
}
private void OnSettingsClick(object sender, RoutedEventArgs e)
{
// TO DO: Open PowerDisplay settings screen
}
/// <summary>
/// Configure window properties (synchronous, no data dependency)
/// </summary>
private void ConfigureWindow()
{
try
{
// Get window handle
var hWnd = WindowNative.GetWindowHandle(this);
var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
_appWindow = AppWindow.GetFromWindowId(windowId);
if (_appWindow != null)
{
// Set initial window size - will be adjusted later based on content
_appWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = 480 });
// Position window at bottom right corner
PositionWindowAtBottomRight(_appWindow);
// Set window icon and title bar
_appWindow.Title = "PowerDisplay";
// Remove title bar and system buttons
var presenter = _appWindow.Presenter as OverlappedPresenter;
if (presenter != null)
{
// Disable resizing
presenter.IsResizable = false;
// Disable maximize button
presenter.IsMaximizable = false;
// Disable minimize button
presenter.IsMinimizable = false;
// Set borderless mode
presenter.SetBorderAndTitleBar(false, false);
}
// Custom title bar - completely remove all buttons
var titleBar = _appWindow.TitleBar;
if (titleBar != null)
{
// Extend content into title bar area
titleBar.ExtendsContentIntoTitleBar = true;
// Completely remove title bar height
titleBar.PreferredHeightOption = Microsoft.UI.Windowing.TitleBarHeightOption.Collapsed;
// Set all button colors to transparent
titleBar.ButtonBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonInactiveBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonHoverBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonHoverForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonPressedBackgroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonPressedForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
titleBar.ButtonInactiveForegroundColor = Windows.UI.Color.FromArgb(0, 0, 0, 0);
// Disable title bar interaction area
titleBar.SetDragRectangles(Array.Empty<Windows.Graphics.RectInt32>());
}
// Use Win32 API to further disable window moving
WindowHelper.DisableWindowMovingAndResizing(hWnd);
// Hide window from taskbar
WindowHelper.HideFromTaskbar(hWnd);
// Optional: set window topmost
// WindowHelper.SetWindowTopmost(hWnd, true);
}
}
catch (Exception ex)
{
// Ignore window setup errors
Logger.LogWarning($"Window configuration error: {ex.Message}");
}
}
private void AdjustWindowSizeToContent()
{
try
{
if (_appWindow == null || RootGrid == null)
{
return;
}
// Force layout update to ensure proper measurement
RootGrid.UpdateLayout();
// Get precise content height
var availableWidth = (double)AppConstants.UI.WindowWidth;
var contentHeight = GetContentHeight(availableWidth);
// Account for display scaling
var scale = RootGrid.XamlRoot?.RasterizationScale ?? 1.0;
var scaledHeight = (int)Math.Ceiling(contentHeight * scale);
// Only set maximum height for scrollable content
scaledHeight = Math.Min(scaledHeight, AppConstants.UI.MaxWindowHeight);
// Check if resize is needed
var currentSize = _appWindow.Size;
if (Math.Abs(currentSize.Height - scaledHeight) > 1)
{
_appWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = scaledHeight });
// Reposition to maintain bottom-right position
PositionWindowAtBottomRight(_appWindow);
}
}
catch (Exception ex)
{
Logger.LogError($"Error adjusting window size: {ex.Message}");
}
}
private double GetContentHeight(double availableWidth)
{
// Try to measure MainContainer directly for precise content size
if (RootGrid.FindName("MainContainer") is Border mainContainer)
{
mainContainer.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
return mainContainer.DesiredSize.Height;
}
// Fallback: Measure the root grid
RootGrid.Measure(new Windows.Foundation.Size(availableWidth, double.PositiveInfinity));
return RootGrid.DesiredSize.Height + 4; // Small padding for fallback method
}
private void PositionWindowAtBottomRight(AppWindow appWindow)
{
try
{
// Get display area
var displayArea = DisplayArea.GetFromWindowId(appWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea != null)
{
var workArea = displayArea.WorkArea;
var windowSize = appWindow.Size;
// Calculate bottom-right position, close to taskbar
// WorkArea already excludes taskbar area, so use WorkArea bottom directly
int rightMargin = AppConstants.UI.WindowRightMargin; // Small margin from right edge
int x = workArea.Width - windowSize.Width - rightMargin;
int y = workArea.Height - windowSize.Height; // Close to taskbar top, no gap
// Move window to bottom right
appWindow.Move(new PointInt32 { X = x, Y = y });
}
}
catch (Exception)
{
// Ignore errors when positioning window
}
}
/// <summary>
/// Slider ValueChanged event handler - does nothing during drag
/// This allows the slider UI to update smoothly without triggering hardware operations
/// </summary>
private void Slider_ValueChanged(object sender, Microsoft.UI.Xaml.Controls.Primitives.RangeBaseValueChangedEventArgs e)
{
// During drag, this event fires 60-120 times per second
// We intentionally do nothing here to keep UI smooth
// The actual ViewModel update happens in PointerCaptureLost after drag completes
}
/// <summary>
/// Slider PointerCaptureLost event handler - updates ViewModel when drag completes
/// This is the WinUI3 recommended way to detect drag completion
/// </summary>
private void Slider_PointerCaptureLost(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
var slider = sender as Slider;
if (slider == null)
{
return;
}
var propertyName = slider.Tag as string;
var monitorVm = slider.DataContext as MonitorViewModel;
if (monitorVm == null || propertyName == null)
{
return;
}
// Get final value after drag completes
int finalValue = (int)slider.Value;
// Now update the ViewModel, which will trigger hardware operation
switch (propertyName)
{
case "Brightness":
monitorVm.Brightness = finalValue;
break;
// ColorTemperature case removed - now controlled via Settings UI
case "Contrast":
monitorVm.ContrastPercent = finalValue;
break;
case "Volume":
monitorVm.Volume = finalValue;
break;
}
}
public void Dispose()
{
_viewModel?.Dispose();
GC.SuppressFinalize(this);
}
}
}

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 (Awake pattern)
int runnerPid = -1;
if (args.Length >= 1)
{
if (int.TryParse(args[0], out int parsedPid))
{
runnerPid = parsedPid;
Logger.LogInfo($"PowerDisplay started with runner_pid={runnerPid}");
}
else
{
Logger.LogWarning($"Failed to parse PID from args[0]: {args[0]}");
}
}
else
{
Logger.LogWarning("PowerDisplay started without runner PID. Running in standalone mode.");
}
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance");
if (instanceKey.IsCurrent)
{
Microsoft.UI.Xaml.Application.Start((p) =>
{
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
_ = new App(runnerPid);
});
}
else
{
Logger.LogWarning("Another instance of PowerDisplay is running. Exiting.");
}
}
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
#pragma warning disable SA1402 // File may only contain a single type - Related JSON serialization types grouped together
namespace PowerDisplay.Serialization
{
/// <summary>
/// JSON source generation context for AOT compatibility.
/// Eliminates reflection-based JSON serialization.
/// </summary>
[JsonSerializable(typeof(MonitorInfoData))]
[JsonSerializable(typeof(IPCMessageAction))]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(PowerDisplaySettings))]
[JsonSerializable(typeof(ColorTemperatureOperation))]
[JsonSerializable(typeof(ProfileOperation))]
[JsonSerializable(typeof(PowerDisplayProfiles))]
[JsonSerializable(typeof(PowerDisplayProfile))]
[JsonSerializable(typeof(ProfileMonitorSetting))]
// MonitorInfo and related types (Settings.UI.Library)
[JsonSerializable(typeof(MonitorInfo))]
[JsonSerializable(typeof(VcpCodeDisplayInfo))]
[JsonSerializable(typeof(VcpValueInfo))]
// Generic collection types
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(List<MonitorInfo>))]
[JsonSerializable(typeof(List<VcpCodeDisplayInfo>))]
[JsonSerializable(typeof(List<VcpValueInfo>))]
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.Never)]
internal sealed partial class AppJsonContext : JsonSerializerContext
{
}
/// <summary>
/// IPC message wrapper for parsing action-based messages.
/// Used in App.xaml.cs for dynamic IPC command handling.
/// </summary>
internal sealed class IPCMessageAction
{
[JsonPropertyName("action")]
public string? Action { get; set; }
}
/// <summary>
/// Monitor state file structure for JSON persistence.
/// Made internal (from private) to support source generation.
/// </summary>
internal sealed class MonitorStateFile
{
[JsonPropertyName("monitors")]
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
/// <summary>
/// Individual monitor state entry.
/// Made internal (from private) to support source generation.
/// </summary>
internal sealed class MonitorStateEntry
{
[JsonPropertyName("brightness")]
public int Brightness { get; set; }
[JsonPropertyName("colorTemperature")]
public int ColorTemperature { get; set; }
[JsonPropertyName("contrast")]
public int Contrast { get; set; }
[JsonPropertyName("volume")]
public int Volume { get; set; }
[JsonPropertyName("capabilitiesRaw")]
public string? CapabilitiesRaw { get; set; }
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ScanningMonitorsText.Text" xml:space="preserve">
<value>Scanning monitors..</value>
</data>
<data name="NoMonitorsText.Message" xml:space="preserve">
<value>No monitors detected</value>
</data>
<data name="SyncAllMonitorsTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Synchronize all monitors to the same brightness</value>
</data>
<data name="DisableControlTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Enable or disable brightness control</value>
</data>
<data name="RefreshTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Rescan connected monitors</value>
</data>
<data name="SettingsTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Settings</value>
</data>
<data name="NoMonitorsText.Message" xml:space="preserve">
<value>No monitors detected</value>
</data>
<data name="MonitorTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Monitor</value>
</data>
<data name="BrightnessTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Brightness</value>
</data>
<data name="ContrastTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Contrast</value>
</data>
<data name="VolumeTooltip.ToolTipService.ToolTip" xml:space="preserve">
<value>Volume</value>
</data>
<data name="VolumeAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Volume</value>
</data>
<data name="ContrastAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Contrast</value>
</data>
<data name="BrightnessAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Brightness</value>
</data>
</root>

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,548 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Input;
using ManagedCommon;
using Microsoft.UI.Xaml;
using PowerDisplay.Commands;
using PowerDisplay.Configuration;
using PowerDisplay.Core;
using PowerDisplay.Core.Models;
using PowerDisplay.Helpers;
using Monitor = PowerDisplay.Core.Models.Monitor;
namespace PowerDisplay.ViewModels;
/// <summary>
/// ViewModel for individual monitor
/// </summary>
public partial class MonitorViewModel : INotifyPropertyChanged, IDisposable
{
private readonly Monitor _monitor;
private readonly MonitorManager _monitorManager;
private readonly MainViewModel? _mainViewModel;
// Simple debouncers for each property (KISS principle - simpler than complex queue)
private readonly SimpleDebouncer _brightnessDebouncer = new(AppConstants.UI.SliderDebounceDelayMs);
private readonly SimpleDebouncer _contrastDebouncer = new(AppConstants.UI.SliderDebounceDelayMs);
private readonly SimpleDebouncer _volumeDebouncer = new(AppConstants.UI.SliderDebounceDelayMs);
private int _brightness;
private int _contrast;
private int _volume;
private bool _isAvailable;
// Visibility settings (controlled by Settings UI)
private bool _showContrast;
private bool _showVolume;
/// <summary>
/// Updates a property value directly without triggering hardware updates.
/// Used during initialization to update UI from saved state.
/// </summary>
internal void UpdatePropertySilently(string propertyName, int value)
{
switch (propertyName)
{
case nameof(Brightness):
_brightness = value;
OnPropertyChanged(nameof(Brightness));
break;
case nameof(Contrast):
_contrast = value;
OnPropertyChanged(nameof(Contrast));
OnPropertyChanged(nameof(ContrastPercent));
break;
case nameof(Volume):
_volume = value;
OnPropertyChanged(nameof(Volume));
break;
case nameof(ColorTemperature):
// Update underlying monitor model
_monitor.CurrentColorTemperature = value;
OnPropertyChanged(nameof(ColorTemperature));
OnPropertyChanged(nameof(ColorTemperaturePresetName));
break;
}
}
/// <summary>
/// Unified method to apply brightness with hardware update and state persistence.
/// Can be called from Flyout UI (with debounce) or Settings UI/IPC (immediate).
/// </summary>
/// <param name="brightness">Brightness value (0-100)</param>
/// <param name="immediate">If true, applies immediately; if false, debounces for smooth slider</param>
/// <param name="fromProfile">If true, skip profile change detection (avoid recursion)</param>
public async Task SetBrightnessAsync(int brightness, bool immediate = false, bool fromProfile = false)
{
brightness = Math.Clamp(brightness, MinBrightness, MaxBrightness);
// Update UI state immediately for smooth response
if (_brightness != brightness)
{
_brightness = brightness;
OnPropertyChanged(nameof(Brightness));
}
// Apply to hardware (with or without debounce)
if (immediate)
{
await ApplyBrightnessToHardwareAsync(brightness, fromProfile);
}
else
{
// Debounce for slider smoothness (always from user interaction, not from profile)
var capturedValue = brightness;
_brightnessDebouncer.Debounce(async () => await ApplyBrightnessToHardwareAsync(capturedValue, fromUserInteraction: true));
}
}
/// <summary>
/// Unified method to apply contrast with hardware update and state persistence.
/// </summary>
public async Task SetContrastAsync(int contrast, bool immediate = false, bool fromProfile = false)
{
contrast = Math.Clamp(contrast, MinContrast, MaxContrast);
if (_contrast != contrast)
{
_contrast = contrast;
OnPropertyChanged(nameof(Contrast));
OnPropertyChanged(nameof(ContrastPercent));
}
if (immediate)
{
await ApplyContrastToHardwareAsync(contrast, fromProfile);
}
else
{
var capturedValue = contrast;
_contrastDebouncer.Debounce(async () => await ApplyContrastToHardwareAsync(capturedValue, fromUserInteraction: true));
}
}
/// <summary>
/// Unified method to apply volume with hardware update and state persistence.
/// </summary>
public async Task SetVolumeAsync(int volume, bool immediate = false, bool fromProfile = false)
{
volume = Math.Clamp(volume, MinVolume, MaxVolume);
if (_volume != volume)
{
_volume = volume;
OnPropertyChanged(nameof(Volume));
}
if (immediate)
{
await ApplyVolumeToHardwareAsync(volume, fromProfile);
}
else
{
var capturedValue = volume;
_volumeDebouncer.Debounce(async () => await ApplyVolumeToHardwareAsync(capturedValue, fromUserInteraction: true));
}
}
/// <summary>
/// Unified method to apply color temperature with hardware update and state persistence.
/// Always immediate (no debouncing for discrete preset values).
/// </summary>
public async Task SetColorTemperatureAsync(int colorTemperature, bool fromProfile = false)
{
try
{
Logger.LogInfo($"[{HardwareId}] Setting color temperature to 0x{colorTemperature:X2}");
var result = await _monitorManager.SetColorTemperatureAsync(Id, colorTemperature);
if (result.IsSuccess)
{
_monitor.CurrentColorTemperature = colorTemperature;
OnPropertyChanged(nameof(ColorTemperature));
OnPropertyChanged(nameof(ColorTemperaturePresetName));
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
// Trigger profile change detection if from user interaction
if (!fromProfile)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(ColorTemperature), colorTemperature);
}
Logger.LogInfo($"[{HardwareId}] Color temperature applied successfully");
}
else
{
Logger.LogWarning($"[{HardwareId}] Failed to set color temperature: {result.ErrorMessage}");
}
}
catch (Exception ex)
{
Logger.LogError($"[{HardwareId}] Exception setting color temperature: {ex.Message}");
}
}
/// <summary>
/// Internal method - applies brightness to hardware and persists state.
/// Unified logic for all sources (Flyout, Settings, etc.).
/// </summary>
private async Task ApplyBrightnessToHardwareAsync(int brightness, bool fromUserInteraction = false)
{
try
{
Logger.LogDebug($"[{HardwareId}] Applying brightness: {brightness}%");
var result = await _monitorManager.SetBrightnessAsync(Id, brightness);
if (result.IsSuccess)
{
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Brightness), brightness);
// Trigger profile change detection if from user interaction
if (fromUserInteraction)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Brightness), brightness);
}
}
else
{
Logger.LogWarning($"[{HardwareId}] Failed to set brightness: {result.ErrorMessage}");
}
}
catch (Exception ex)
{
Logger.LogError($"[{HardwareId}] Exception setting brightness: {ex.Message}");
}
}
/// <summary>
/// Internal method - applies contrast to hardware and persists state.
/// </summary>
private async Task ApplyContrastToHardwareAsync(int contrast, bool fromUserInteraction = false)
{
try
{
Logger.LogDebug($"[{HardwareId}] Applying contrast: {contrast}%");
var result = await _monitorManager.SetContrastAsync(Id, contrast);
if (result.IsSuccess)
{
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Contrast), contrast);
// Trigger profile change detection if from user interaction
if (fromUserInteraction)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Contrast), contrast);
}
}
else
{
Logger.LogWarning($"[{HardwareId}] Failed to set contrast: {result.ErrorMessage}");
}
}
catch (Exception ex)
{
Logger.LogError($"[{HardwareId}] Exception setting contrast: {ex.Message}");
}
}
/// <summary>
/// Internal method - applies volume to hardware and persists state.
/// </summary>
private async Task ApplyVolumeToHardwareAsync(int volume, bool fromUserInteraction = false)
{
try
{
Logger.LogDebug($"[{HardwareId}] Applying volume: {volume}%");
var result = await _monitorManager.SetVolumeAsync(Id, volume);
if (result.IsSuccess)
{
_mainViewModel?.SaveMonitorSettingDirect(_monitor.HardwareId, nameof(Volume), volume);
// Trigger profile change detection if from user interaction
if (fromUserInteraction)
{
_mainViewModel?.OnMonitorParameterChanged(_monitor.HardwareId, nameof(Volume), volume);
}
}
else
{
Logger.LogWarning($"[{HardwareId}] Failed to set volume: {result.ErrorMessage}");
}
}
catch (Exception ex)
{
Logger.LogError($"[{HardwareId}] Exception setting volume: {ex.Message}");
}
}
// Conversion function for x:Bind (AOT-compatible alternative to converters)
public Visibility ConvertBoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
// Property to access IsInteractionEnabled from parent ViewModel
public bool IsInteractionEnabled => _mainViewModel?.IsInteractionEnabled ?? true;
public MonitorViewModel(Monitor monitor, MonitorManager monitorManager, MainViewModel mainViewModel)
{
_monitor = monitor;
_monitorManager = monitorManager;
_mainViewModel = mainViewModel;
// Subscribe to MainViewModel property changes to update IsInteractionEnabled
if (_mainViewModel != null)
{
_mainViewModel.PropertyChanged += OnMainViewModelPropertyChanged;
}
// Initialize Show properties based on hardware capabilities
_showContrast = monitor.SupportsContrast;
_showVolume = monitor.SupportsVolume;
// Color temperature initialization removed - now controlled via Settings UI
// The Monitor.CurrentColorTemperature stores VCP 0x14 preset value (e.g., 0x05 for 6500K)
// and will be initialized by MonitorManager based on capabilities
// Initialize basic properties from monitor
_brightness = monitor.CurrentBrightness;
_contrast = monitor.CurrentContrast;
_volume = monitor.CurrentVolume;
_isAvailable = monitor.IsAvailable;
}
public string Id => _monitor.Id;
public string HardwareId => _monitor.HardwareId;
public string InternalName => _monitor.Id;
public string Name => _monitor.Name;
public string Manufacturer => _monitor.Manufacturer;
public string CommunicationMethod => _monitor.CommunicationMethod;
public bool IsInternal => _monitor.CommunicationMethod == "WMI";
public string? CapabilitiesRaw => _monitor.CapabilitiesRaw;
public VcpCapabilities? VcpCapabilitiesInfo => _monitor.VcpCapabilitiesInfo;
/// <summary>
/// Gets the icon glyph based on communication method
/// WMI monitors (laptop internal displays) use laptop icon, others use external monitor icon
/// </summary>
public string MonitorIconGlyph => _monitor.CommunicationMethod?.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true
? "\uE7F8" // Laptop icon for WMI
: "\uE7F4"; // External monitor icon for DDC/CI and others
// Monitor property ranges
public int MinBrightness => _monitor.MinBrightness;
public int MaxBrightness => _monitor.MaxBrightness;
public int MinContrast => _monitor.MinContrast;
public int MaxContrast => _monitor.MaxContrast;
public int MinVolume => _monitor.MinVolume;
public int MaxVolume => _monitor.MaxVolume;
// Advanced control display logic
public bool HasAdvancedControls => ShowContrast || ShowVolume;
public bool ShowContrast
{
get => _showContrast;
set
{
if (_showContrast != value)
{
_showContrast = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public bool ShowVolume
{
get => _showVolume;
set
{
if (_showVolume != value)
{
_showVolume = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasAdvancedControls));
}
}
}
public int Brightness
{
get => _brightness;
set
{
if (_brightness != value)
{
// Use unified method with debouncing for smooth slider
_ = SetBrightnessAsync(value, immediate: false);
}
}
}
/// <summary>
/// Gets color temperature VCP preset value (from VCP code 0x14).
/// Read-only in flyout UI - controlled via Settings UI.
/// Returns the raw VCP value (e.g., 0x05 for 6500K).
/// </summary>
public int ColorTemperature => _monitor.CurrentColorTemperature;
/// <summary>
/// Gets human-readable color temperature preset name (e.g., "6500K", "sRGB")
/// </summary>
public string ColorTemperaturePresetName => _monitor.ColorTemperaturePresetName;
public int Contrast
{
get => _contrast;
set
{
if (_contrast != value)
{
// Use unified method with debouncing
_ = SetContrastAsync(value, immediate: false);
}
}
}
public int Volume
{
get => _volume;
set
{
if (_volume != value)
{
// Use unified method with debouncing
_ = SetVolumeAsync(value, immediate: false);
}
}
}
public bool IsAvailable
{
get => _isAvailable;
set
{
_isAvailable = value;
OnPropertyChanged();
}
}
public ICommand SetBrightnessCommand => new RelayCommand<int?>((brightness) =>
{
if (brightness.HasValue)
{
Brightness = brightness.Value;
}
});
// SetColorTemperatureCommand removed - now controlled via Settings UI
public ICommand SetContrastCommand => new RelayCommand<int?>((contrast) =>
{
if (contrast.HasValue)
{
Contrast = contrast.Value;
}
});
public ICommand SetVolumeCommand => new RelayCommand<int?>((volume) =>
{
if (volume.HasValue)
{
Volume = volume.Value;
}
});
// Percentage-based properties for uniform slider behavior
// ColorTemperaturePercent removed - now controlled via Settings UI
public int ContrastPercent
{
get => MapToPercent(_contrast, MinContrast, MaxContrast);
set
{
var actualValue = MapFromPercent(value, MinContrast, MaxContrast);
Contrast = actualValue;
}
}
// Mapping functions for percentage conversion
private int MapToPercent(int value, int min, int max)
{
if (max <= min)
{
return 0;
}
return (int)Math.Round((value - min) * 100.0 / (max - min));
}
private int MapFromPercent(int percent, int min, int max)
{
if (max <= min)
{
return min;
}
percent = Math.Clamp(percent, 0, 100);
return min + (int)Math.Round(percent * (max - min) / 100.0);
}
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// Notify percentage properties when actual values change
if (propertyName == nameof(Contrast))
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContrastPercent)));
}
}
private void OnMainViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsInteractionEnabled))
{
OnPropertyChanged(nameof(IsInteractionEnabled));
}
}
public void Dispose()
{
// Unsubscribe from MainViewModel events
if (_mainViewModel != null)
{
_mainViewModel.PropertyChanged -= OnMainViewModelPropertyChanged;
}
// Dispose all debouncers
_brightnessDebouncer?.Dispose();
_contrastDebouncer?.Dispose();
_volumeDebouncer?.Dispose();
GC.SuppressFinalize(this);
}
}

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,131 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{D1234567-8901-2345-6789-ABCDEF012345}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>PowerDisplayModuleInterface</RootNamespace>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup>
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\PowerDisplayModuleInterface\</IntDir>
<TargetName>PowerToys.PowerDisplayModuleInterface</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;POWERDISPLAYMODULEINTERFACE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Constants.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerDisplayModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

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,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,472 @@
// dllmain.cpp : Defines the entry point for the DLL Application.
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include <common/interop/shared_constants.h>
#include <common/utils/string_utils.h>
#include <common/utils/winapi_error.h>
#include <common/utils/logger_helper.h>
#include <common/utils/resources.h>
#include "resource.h"
#include "Constants.h"
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
const static wchar_t* MODULE_NAME = L"PowerDisplay";
const static wchar_t* MODULE_DESC = L"A utility to manage display brightness and color temperature across multiple monitors.";
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_ENABLED[] = L"enabled";
const wchar_t JSON_KEY_HOTKEY_ENABLED[] = L"hotkey_enabled";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
}
class PowerDisplayModule : public PowertoyModuleIface
{
private:
bool m_enabled = false;
bool m_hotkey_enabled = false;
Hotkey m_activation_hotkey = { .win = true, .ctrl = false, .shift = false, .alt = true, .key = 'M' };
// Windows Events for IPC (persistent handles - ColorPicker pattern)
HANDLE m_hProcess = nullptr;
HANDLE m_hInvokeEvent = nullptr;
HANDLE m_hToggleEvent = nullptr;
HANDLE m_hTerminateEvent = nullptr;
HANDLE m_hRefreshEvent = nullptr;
HANDLE m_hSettingsUpdatedEvent = nullptr;
HANDLE m_hApplyColorTemperatureEvent = nullptr;
HANDLE m_hApplyProfileEvent = nullptr;
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
m_hotkey_enabled = properties.GetNamedBoolean(JSON_KEY_HOTKEY_ENABLED, false);
}
else
{
Logger::info("Properties object not found in settings, using defaults");
m_hotkey_enabled = false;
}
}
catch (...)
{
Logger::info("Failed to parse hotkey settings, using defaults");
m_hotkey_enabled = false;
}
}
else
{
Logger::info("Power Display settings are empty");
m_hotkey_enabled = false;
}
}
void parse_activation_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
auto properties = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (properties.HasKey(JSON_KEY_ACTIVATION_SHORTCUT))
{
auto jsonHotkeyObject = properties.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
m_activation_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_activation_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_activation_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_activation_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_activation_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
m_activation_hotkey.isShown = true;
Logger::trace(L"Parsed activation hotkey: Win={} Ctrl={} Alt={} Shift={} Key={}",
m_activation_hotkey.win, m_activation_hotkey.ctrl, m_activation_hotkey.alt,
m_activation_hotkey.shift, m_activation_hotkey.key);
}
else
{
Logger::info("ActivationShortcut not found in settings, using default Win+Alt+M");
m_activation_hotkey.isShown = true;
}
}
}
catch (...)
{
Logger::error("Failed to parse PowerDisplay activation shortcut, using default Win+Alt+M");
m_activation_hotkey.isShown = true;
}
}
}
void init_settings()
{
try
{
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
parse_hotkey_settings(settings);
parse_activation_hotkey(settings);
}
catch (std::exception&)
{
Logger::error("Invalid json when trying to load the Power Display settings json from file.");
}
}
// Helper method to check if PowerDisplay.exe process is still running
bool is_process_running()
{
if (m_hProcess == nullptr)
{
return false;
}
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
// Helper method to launch PowerDisplay.exe process
void launch_process()
{
Logger::trace(L"Starting PowerDisplay process");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = std::to_wstring(powertoys_pid);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
Logger::trace(L"Successfully started PowerDisplay process");
m_hProcess = sei.hProcess;
}
else
{
Logger::error(L"PowerDisplay process failed to start. {}",
get_last_error_or_default(GetLastError()));
}
}
public:
PowerDisplayModule()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", "PowerDisplay");
Logger::info("Power Display object is constructing");
init_settings();
// Create all Windows Events (persistent handles - ColorPicker pattern)
m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT);
m_hToggleEvent = CreateDefaultEvent(CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT);
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT);
m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT);
m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT);
m_hApplyColorTemperatureEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT);
m_hApplyProfileEvent = CreateDefaultEvent(CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT);
if (!m_hInvokeEvent || !m_hToggleEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent || !m_hApplyColorTemperatureEvent || !m_hApplyProfileEvent)
{
Logger::error(L"Failed to create one or more event handles");
}
}
~PowerDisplayModule()
{
if (m_enabled)
{
disable();
}
// Clean up all event handles
if (m_hInvokeEvent)
{
CloseHandle(m_hInvokeEvent);
m_hInvokeEvent = nullptr;
}
if (m_hToggleEvent)
{
CloseHandle(m_hToggleEvent);
m_hToggleEvent = nullptr;
}
if (m_hTerminateEvent)
{
CloseHandle(m_hTerminateEvent);
m_hTerminateEvent = nullptr;
}
if (m_hRefreshEvent)
{
CloseHandle(m_hRefreshEvent);
m_hRefreshEvent = nullptr;
}
if (m_hSettingsUpdatedEvent)
{
CloseHandle(m_hSettingsUpdatedEvent);
m_hSettingsUpdatedEvent = nullptr;
}
if (m_hApplyColorTemperatureEvent)
{
CloseHandle(m_hApplyColorTemperatureEvent);
m_hApplyColorTemperatureEvent = nullptr;
}
if (m_hApplyProfileEvent)
{
CloseHandle(m_hApplyProfileEvent);
m_hApplyProfileEvent = nullptr;
}
}
virtual void destroy() override
{
Logger::trace("PowerDisplay::destroy()");
delete this;
}
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
virtual const wchar_t* get_key() override
{
return MODULE_NAME;
}
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::gpo_rule_configured_not_configured;
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
return settings.serialize_to_buffer(buffer, buffer_size);
}
virtual void call_custom_action(const wchar_t* action) override
{
try
{
PowerToysSettings::CustomActionObject action_object =
PowerToysSettings::CustomActionObject::from_json_string(action);
if (action_object.get_name() == L"Launch")
{
Logger::trace(L"Launch action received");
// ColorPicker pattern: check if process is running, re-launch if needed
if (!is_process_running())
{
Logger::trace(L"PowerDisplay process not running, re-launching");
launch_process();
}
if (m_hToggleEvent)
{
Logger::trace(L"Signaling toggle event");
SetEvent(m_hToggleEvent);
}
Trace::ActivatePowerDisplay();
}
else if (action_object.get_name() == L"RefreshMonitors")
{
Logger::trace(L"RefreshMonitors action received, signaling refresh event");
if (m_hRefreshEvent)
{
SetEvent(m_hRefreshEvent);
}
else
{
Logger::warn(L"Refresh event handle is null");
}
}
else if (action_object.get_name() == L"ApplyColorTemperature")
{
Logger::trace(L"ApplyColorTemperature action received");
if (m_hApplyColorTemperatureEvent)
{
Logger::trace(L"Signaling apply color temperature event");
SetEvent(m_hApplyColorTemperatureEvent);
}
else
{
Logger::warn(L"Apply color temperature event handle is null");
}
}
else if (action_object.get_name() == L"ApplyProfile")
{
Logger::trace(L"ApplyProfile action received");
if (m_hApplyProfileEvent)
{
Logger::trace(L"Signaling apply profile event");
SetEvent(m_hApplyProfileEvent);
}
else
{
Logger::warn(L"Apply profile event handle is null");
}
}
}
catch (std::exception&)
{
Logger::error(L"Failed to parse action. {}", action);
}
}
virtual void set_config(const wchar_t* config) override
{
try
{
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey_settings(values);
parse_activation_hotkey(values);
// Signal settings updated event
if (m_hSettingsUpdatedEvent)
{
Logger::trace(L"Signaling settings updated event");
SetEvent(m_hSettingsUpdatedEvent);
}
else
{
Logger::warn(L"Settings updated event handle is null");
}
}
catch (std::exception&)
{
Logger::error(L"Invalid json when trying to parse Power Display settings json.");
}
}
virtual void enable() override
{
Logger::trace(L"PowerDisplay::enable()");
m_enabled = true;
Trace::EnablePowerDisplay(true);
// Launch PowerDisplay.exe with PID only (Awake pattern)
launch_process();
}
virtual void disable() override
{
Logger::trace(L"PowerDisplay::disable()");
if (m_enabled)
{
// Reset invoke event to prevent accidental activation during shutdown
if (m_hInvokeEvent)
{
ResetEvent(m_hInvokeEvent);
}
// Signal terminate event
if (m_hTerminateEvent)
{
Logger::trace(L"Signaling PowerDisplay to exit");
SetEvent(m_hTerminateEvent);
}
else
{
Logger::warn(L"Terminate event handle is null");
}
// Close process handle (don't wait, don't force terminate - Awake pattern)
if (m_hProcess)
{
CloseHandle(m_hProcess);
m_hProcess = nullptr;
}
}
m_enabled = false;
Trace::EnablePowerDisplay(false);
}
virtual bool is_enabled() override
{
return m_enabled;
}
virtual bool on_hotkey(size_t /*hotkeyId*/) override
{
if (m_enabled && m_hToggleEvent)
{
Logger::trace(L"Power Display hotkey pressed");
// ColorPicker pattern: check if process is running, re-launch if needed
if (!is_process_running())
{
Logger::trace(L"PowerDisplay process not running, re-launching");
launch_process();
}
Logger::trace(L"Signaling toggle event");
SetEvent(m_hToggleEvent);
return true;
}
return false;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (m_activation_hotkey.key != 0)
{
if (hotkeys && buffer_size >= 1)
{
hotkeys[0] = m_activation_hotkey;
}
return 1;
}
return 0;
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new PowerDisplayModule();
}

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

@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Represents a pending color temperature change operation
/// </summary>
public class ColorTemperatureOperation
{
[JsonPropertyName("monitor_id")]
public string MonitorId { get; set; }
[JsonPropertyName("color_temperature")]
public int ColorTemperature { get; set; }
}
}

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

@@ -17,6 +17,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public const string DefaultLatitude = "0.0";
public const string DefaultLongitude = "0.0";
public const string DefaultScheduleMode = "FixedHours";
public const bool DefaultEnableDarkModeProfile = false;
public const bool DefaultEnableLightModeProfile = false;
public const string DefaultDarkModeProfile = "";
public const string DefaultLightModeProfile = "";
public static readonly HotkeySettings DefaultToggleThemeHotkey = new HotkeySettings(true, true, false, true, 0x44); // Ctrl+Win+Shift+D
public LightSwitchProperties()
@@ -31,6 +35,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
SunsetOffset = new IntProperty(DefaultSunsetOffset);
ScheduleMode = new StringProperty(DefaultScheduleMode);
ToggleThemeHotkey = new KeyboardKeysProperty(DefaultToggleThemeHotkey);
EnableDarkModeProfile = new BoolProperty(DefaultEnableDarkModeProfile);
EnableLightModeProfile = new BoolProperty(DefaultEnableLightModeProfile);
DarkModeProfile = new StringProperty(DefaultDarkModeProfile);
LightModeProfile = new StringProperty(DefaultLightModeProfile);
}
[JsonPropertyName("changeSystem")]
@@ -62,5 +70,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("toggle-theme-hotkey")]
public KeyboardKeysProperty ToggleThemeHotkey { get; set; }
[JsonPropertyName("enableDarkModeProfile")]
public BoolProperty EnableDarkModeProfile { get; set; }
[JsonPropertyName("enableLightModeProfile")]
public BoolProperty EnableLightModeProfile { get; set; }
[JsonPropertyName("darkModeProfile")]
public StringProperty DarkModeProfile { get; set; }
[JsonPropertyName("lightModeProfile")]
public StringProperty LightModeProfile { get; set; }
}
}

View File

@@ -60,6 +60,10 @@ namespace Settings.UI.Library
Latitude = new StringProperty(Properties.Latitude.Value),
Longitude = new StringProperty(Properties.Longitude.Value),
ToggleThemeHotkey = new KeyboardKeysProperty(Properties.ToggleThemeHotkey.Value),
EnableDarkModeProfile = new BoolProperty(Properties.EnableDarkModeProfile.Value),
EnableLightModeProfile = new BoolProperty(Properties.EnableLightModeProfile.Value),
DarkModeProfile = new StringProperty(Properties.DarkModeProfile.Value),
LightModeProfile = new StringProperty(Properties.LightModeProfile.Value),
},
};
}

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,626 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MonitorInfo : Observable
{
private string _name = string.Empty;
private string _internalName = string.Empty;
private string _hardwareId = string.Empty;
private string _communicationMethod = string.Empty;
private int _currentBrightness;
private int _colorTemperature = 6500;
private bool _isHidden;
private bool _enableContrast;
private bool _enableVolume;
private string _capabilitiesRaw = string.Empty;
private List<string> _vcpCodes = new List<string>();
private List<VcpCodeDisplayInfo> _vcpCodesFormatted = new List<VcpCodeDisplayInfo>();
// Feature support status (determined from capabilities)
private bool _supportsBrightness = true; // Brightness always shown even if unsupported
private bool _supportsContrast;
private bool _supportsColorTemperature;
private bool _supportsVolume;
private string _capabilitiesStatus = "unknown"; // "available", "unavailable", or "unknown"
// Cached color temperature presets (computed from VcpCodesFormatted)
private ObservableCollection<ColorPresetItem> _availableColorPresetsCache;
public MonitorInfo()
{
}
public MonitorInfo(string name, string internalName, string communicationMethod)
{
Name = name;
InternalName = internalName;
CommunicationMethod = communicationMethod;
}
public MonitorInfo(string name, string internalName, string hardwareId, string communicationMethod, int currentBrightness, int colorTemperature)
{
Name = name;
InternalName = internalName;
HardwareId = hardwareId;
CommunicationMethod = communicationMethod;
CurrentBrightness = currentBrightness;
ColorTemperature = colorTemperature;
}
[JsonPropertyName("name")]
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged();
}
}
}
public string MonitorIconGlyph => CommunicationMethod.Contains("WMI", StringComparison.OrdinalIgnoreCase) == true
? "\uE7F8" // Laptop icon for WMI
: "\uE7F4"; // External monitor icon for DDC/CI and others
[JsonPropertyName("internalName")]
public string InternalName
{
get => _internalName;
set
{
if (_internalName != value)
{
_internalName = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("communicationMethod")]
public string CommunicationMethod
{
get => _communicationMethod;
set
{
if (_communicationMethod != value)
{
_communicationMethod = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("hardwareId")]
public string HardwareId
{
get => _hardwareId;
set
{
if (_hardwareId != value)
{
_hardwareId = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness
{
get => _currentBrightness;
set
{
if (_currentBrightness != value)
{
_currentBrightness = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("colorTemperature")]
public int ColorTemperature
{
get => _colorTemperature;
set
{
if (_colorTemperature != value)
{
_colorTemperature = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Update display list when current value changes
}
}
}
[JsonPropertyName("isHidden")]
public bool IsHidden
{
get => _isHidden;
set
{
if (_isHidden != value)
{
_isHidden = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableContrast")]
public bool EnableContrast
{
get => _enableContrast;
set
{
if (_enableContrast != value)
{
_enableContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("enableVolume")]
public bool EnableVolume
{
get => _enableVolume;
set
{
if (_enableVolume != value)
{
_enableVolume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("capabilitiesRaw")]
public string CapabilitiesRaw
{
get => _capabilitiesRaw;
set
{
if (_capabilitiesRaw != value)
{
_capabilitiesRaw = value ?? string.Empty;
OnPropertyChanged();
OnPropertyChanged(nameof(HasCapabilities));
}
}
}
[JsonPropertyName("vcpCodes")]
public List<string> VcpCodes
{
get => _vcpCodes;
set
{
if (_vcpCodes != value)
{
_vcpCodes = value ?? new List<string>();
OnPropertyChanged();
OnPropertyChanged(nameof(VcpCodesSummary));
}
}
}
[JsonPropertyName("vcpCodesFormatted")]
public List<VcpCodeDisplayInfo> VcpCodesFormatted
{
get => _vcpCodesFormatted;
set
{
if (_vcpCodesFormatted != value)
{
_vcpCodesFormatted = value ?? new List<VcpCodeDisplayInfo>();
_availableColorPresetsCache = null; // Clear cache when VCP codes change
OnPropertyChanged();
OnPropertyChanged(nameof(AvailableColorPresets));
}
}
}
[JsonIgnore]
public string VcpCodesSummary
{
get
{
if (_vcpCodes == null || _vcpCodes.Count == 0)
{
return "No VCP codes detected";
}
var count = _vcpCodes.Count;
var preview = string.Join(", ", _vcpCodes.Take(10));
return count > 10
? $"{count} VCP codes: {preview}..."
: $"{count} VCP codes: {preview}";
}
}
[JsonPropertyName("supportsBrightness")]
public bool SupportsBrightness
{
get => _supportsBrightness;
set
{
if (_supportsBrightness != value)
{
_supportsBrightness = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsContrast")]
public bool SupportsContrast
{
get => _supportsContrast;
set
{
if (_supportsContrast != value)
{
_supportsContrast = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("supportsColorTemperature")]
public bool SupportsColorTemperature
{
get => _supportsColorTemperature;
set
{
if (_supportsColorTemperature != value)
{
_supportsColorTemperature = value;
_availableColorPresetsCache = null; // Clear cache when support status changes
OnPropertyChanged();
OnPropertyChanged(nameof(AvailableColorPresets)); // Refresh computed property
OnPropertyChanged(nameof(ColorPresetsForDisplay)); // Refresh display list
}
}
}
[JsonPropertyName("supportsVolume")]
public bool SupportsVolume
{
get => _supportsVolume;
set
{
if (_supportsVolume != value)
{
_supportsVolume = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("capabilitiesStatus")]
public string CapabilitiesStatus
{
get => _capabilitiesStatus;
set
{
if (_capabilitiesStatus != value)
{
_capabilitiesStatus = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowCapabilitiesWarning));
}
}
}
/// <summary>
/// Available color temperature presets computed from VcpCodesFormatted (VCP code 0x14).
/// This is a computed property that parses the VCP capabilities data on-demand.
/// </summary>
[JsonIgnore]
public ObservableCollection<ColorPresetItem> AvailableColorPresets
{
get
{
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] GET called for monitor '{_name}'");
// Return cached value if available
if (_availableColorPresetsCache != null)
{
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] Cache HIT - returning {_availableColorPresetsCache.Count} items");
return _availableColorPresetsCache;
}
Logger.LogInfo("[MonitorInfo.AvailableColorPresets] Cache MISS - computing from VcpCodesFormatted");
// Compute from VcpCodesFormatted
_availableColorPresetsCache = ComputeAvailableColorPresets();
Logger.LogInfo($"[MonitorInfo.AvailableColorPresets] Computed {_availableColorPresetsCache.Count} items");
return _availableColorPresetsCache;
}
}
/// <summary>
/// Compute available color presets from VcpCodesFormatted (VCP code 0x14)
/// </summary>
private ObservableCollection<ColorPresetItem> ComputeAvailableColorPresets()
{
Logger.LogInfo($"[ComputeAvailableColorPresets] START for monitor '{_name}'");
Logger.LogInfo($" - SupportsColorTemperature: {_supportsColorTemperature}");
Logger.LogInfo($" - VcpCodesFormatted: {(_vcpCodesFormatted == null ? "NULL" : $"{_vcpCodesFormatted.Count} items")}");
// Check if color temperature is supported
if (!_supportsColorTemperature || _vcpCodesFormatted == null)
{
Logger.LogWarning($"[ComputeAvailableColorPresets] Color temperature not supported or no VCP codes - returning empty");
return new ObservableCollection<ColorPresetItem>();
}
// Find VCP code 0x14 (Color Temperature / Select Color Preset)
var colorTempVcp = _vcpCodesFormatted.FirstOrDefault(v =>
{
if (int.TryParse(v.Code?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int code))
{
return code == 0x14;
}
return false;
});
Logger.LogInfo($"[ComputeAvailableColorPresets] VCP 0x14 found: {colorTempVcp != null}");
if (colorTempVcp != null)
{
Logger.LogInfo($" - ValueList: {(colorTempVcp.ValueList == null ? "NULL" : $"{colorTempVcp.ValueList.Count} items")}");
}
// No VCP 0x14 or no values
if (colorTempVcp == null || colorTempVcp.ValueList == null || colorTempVcp.ValueList.Count == 0)
{
Logger.LogWarning($"[ComputeAvailableColorPresets] No VCP 0x14 or empty ValueList - returning empty");
return new ObservableCollection<ColorPresetItem>();
}
// Build preset list from supported values
var presetList = new List<ColorPresetItem>();
foreach (var valueInfo in colorTempVcp.ValueList)
{
if (int.TryParse(valueInfo.Value?.Replace("0x", string.Empty), System.Globalization.NumberStyles.HexNumber, null, out int vcpValue))
{
var displayName = FormatColorTemperatureDisplayName(valueInfo.Name, vcpValue);
presetList.Add(new ColorPresetItem(vcpValue, displayName));
Logger.LogDebug($"[ComputeAvailableColorPresets] Added: {displayName}");
}
}
// Sort by VCP value for consistent ordering
presetList = presetList.OrderBy(p => p.VcpValue).ToList();
Logger.LogInfo($"[ComputeAvailableColorPresets] COMPLETE - returning {presetList.Count} items");
Logger.LogInfo($"[ComputeAvailableColorPresets] Current ColorTemperature value: {_colorTemperature}");
return new ObservableCollection<ColorPresetItem>(presetList);
}
/// <summary>
/// Format color temperature display name
/// </summary>
private string FormatColorTemperatureDisplayName(string name, int vcpValue)
{
var hexValue = $"0x{vcpValue:X2}";
// Check if name is undefined (null or empty)
if (string.IsNullOrEmpty(name))
{
return $"Manufacturer Defined ({hexValue})";
}
// For predefined names, append the hex value in parentheses
return $"{name} ({hexValue})";
}
/// <summary>
/// Color presets for display in ComboBox, includes current value if not in preset list
/// </summary>
[JsonIgnore]
public ObservableCollection<ColorPresetItem> ColorPresetsForDisplay
{
get
{
var presets = AvailableColorPresets;
if (presets == null || presets.Count == 0)
{
return new ObservableCollection<ColorPresetItem>();
}
// Check if current value is in the preset list
var currentValueInList = presets.Any(p => p.VcpValue == _colorTemperature);
if (currentValueInList)
{
// Current value is in the list, return as-is
return presets;
}
// Current value is not in the preset list - add it at the beginning
var displayList = new List<ColorPresetItem>();
// Add current value with "Custom" indicator
var currentValueName = GetColorTemperatureName(_colorTemperature);
var displayName = string.IsNullOrEmpty(currentValueName)
? $"Custom (0x{_colorTemperature:X2})"
: $"{currentValueName} (0x{_colorTemperature:X2}) - Custom";
displayList.Add(new ColorPresetItem(_colorTemperature, displayName));
// Add all supported presets
displayList.AddRange(presets);
return new ObservableCollection<ColorPresetItem>(displayList);
}
}
/// <summary>
/// Get the name for a color temperature value from standard VCP naming
/// </summary>
private string GetColorTemperatureName(int vcpValue)
{
return vcpValue switch
{
0x04 => "5000K",
0x05 => "6500K",
0x06 => "7500K",
0x08 => "9300K",
0x09 => "10000K",
0x0A => "11500K",
0x0B => "User 1",
0x0C => "User 2",
0x0D => "User 3",
_ => null,
};
}
[JsonIgnore]
public bool HasColorPresets => AvailableColorPresets != null && AvailableColorPresets.Count > 0;
[JsonIgnore]
public bool HasCapabilities => !string.IsNullOrEmpty(_capabilitiesRaw);
[JsonIgnore]
public bool ShowCapabilitiesWarning => _capabilitiesStatus == "unavailable";
[JsonIgnore]
public string BrightnessTooltip => _supportsBrightness ? string.Empty : "Brightness control not supported by this monitor";
[JsonIgnore]
public string ContrastTooltip => _supportsContrast ? string.Empty : "Contrast control not supported by this monitor";
/// <summary>
/// Generate formatted text of all VCP codes for clipboard
/// </summary>
public string GetVcpCodesAsText()
{
if (_vcpCodesFormatted == null || _vcpCodesFormatted.Count == 0)
{
return "No VCP codes detected";
}
var lines = new List<string>();
lines.Add($"VCP Capabilities for {_name}");
lines.Add($"Monitor: {_name}");
lines.Add($"Hardware ID: {_hardwareId}");
lines.Add(string.Empty);
lines.Add("Detected VCP Codes:");
lines.Add(new string('-', 50));
foreach (var vcp in _vcpCodesFormatted)
{
lines.Add(string.Empty);
lines.Add(vcp.Title);
if (vcp.HasValues)
{
lines.Add($" {vcp.Values}");
}
}
lines.Add(string.Empty);
lines.Add(new string('-', 50));
lines.Add($"Total: {_vcpCodesFormatted.Count} VCP codes");
return string.Join(System.Environment.NewLine, lines);
}
/// <summary>
/// Update this monitor's properties from another MonitorInfo instance.
/// This preserves the object reference while updating all properties.
/// </summary>
/// <param name="other">The source MonitorInfo to copy properties from</param>
public void UpdateFrom(MonitorInfo other)
{
if (other == null)
{
return;
}
// Update all properties that can change
Name = other.Name;
InternalName = other.InternalName;
HardwareId = other.HardwareId;
CommunicationMethod = other.CommunicationMethod;
CurrentBrightness = other.CurrentBrightness;
ColorTemperature = other.ColorTemperature;
IsHidden = other.IsHidden;
EnableContrast = other.EnableContrast;
EnableVolume = other.EnableVolume;
CapabilitiesRaw = other.CapabilitiesRaw;
VcpCodes = other.VcpCodes;
VcpCodesFormatted = other.VcpCodesFormatted;
SupportsBrightness = other.SupportsBrightness;
SupportsContrast = other.SupportsContrast;
SupportsColorTemperature = other.SupportsColorTemperature;
SupportsVolume = other.SupportsVolume;
CapabilitiesStatus = other.CapabilitiesStatus;
}
/// <summary>
/// Represents a color temperature preset item for VCP code 0x14
/// </summary>
public class ColorPresetItem : Observable
{
private int _vcpValue;
private string _displayName = string.Empty;
[JsonPropertyName("vcpValue")]
public int VcpValue
{
get => _vcpValue;
set
{
if (_vcpValue != value)
{
_vcpValue = value;
OnPropertyChanged();
}
}
}
[JsonPropertyName("displayName")]
public string DisplayName
{
get => _displayName;
set
{
if (_displayName != value)
{
_displayName = value;
OnPropertyChanged();
}
}
}
public ColorPresetItem()
{
}
public ColorPresetItem(int vcpValue, string displayName)
{
VcpValue = vcpValue;
DisplayName = displayName;
}
}
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Monitor information data for IPC
/// </summary>
public class MonitorInfoData
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("internalName")]
public string InternalName { get; set; } = string.Empty;
[JsonPropertyName("hardwareId")]
public string HardwareId { get; set; } = string.Empty;
[JsonPropertyName("communicationMethod")]
public string CommunicationMethod { get; set; } = string.Empty;
[JsonPropertyName("currentBrightness")]
public int CurrentBrightness { get; set; }
[JsonPropertyName("colorTemperature")]
public int ColorTemperature { get; set; }
[JsonPropertyName("capabilitiesRaw")]
public string CapabilitiesRaw { get; set; } = string.Empty;
[JsonPropertyName("vcpCodes")]
public List<string> VcpCodes { get; set; } = new List<string>();
[JsonPropertyName("vcpCodesFormatted")]
public List<VcpCodeDisplayInfo> VcpCodesFormatted { get; set; } = new List<VcpCodeDisplayInfo>();
}
}

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

Some files were not shown because too many files have changed in this diff Show More