Compare commits

...

201 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
bf71ba32b0 Simplify log messages to avoid redundant information
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
2025-12-26 06:53:30 +00:00
copilot-swe-agent[bot]
fbfd57bd93 Use structured logging with Exception parameter for better formatting
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
2025-12-26 06:52:29 +00:00
copilot-swe-agent[bot]
e6b499487a Include stack traces in exception logging for better debugging
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
2025-12-26 06:51:02 +00:00
copilot-swe-agent[bot]
5d10bbcaf9 Add exception logging to SafeExecute and SafeDispose methods
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
2025-12-26 06:49:52 +00:00
copilot-swe-agent[bot]
2c8aca7dfe Initial plan 2025-12-26 06:45:16 +00:00
moooyo
598629e57b Update src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj.filters
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-26 14:43:51 +08:00
Yu Leng
1e3cec08e3 Optimize monitor restore: skip unchanged settings
RestoreMonitorSettingsAsync now only updates hardware values if
the saved state differs from the current value, reducing
unnecessary writes. Updated comments to reflect this logic.
2025-12-26 14:02:55 +08:00
Yu Leng
afaaa76c54 Improve flowchart layout in design.md for clarity
Updated the Mermaid flowchart in design.md to enforce a vertical layout by positioning PowerDisplay.Lib above Storage and Hardware. This enhances the diagram's readability and structural clarity without altering other elements or styles.
2025-12-25 17:20:16 +08:00
Yu Leng
d4d475d42a Update design docs: clarify monitor state restore flow
Expanded PowerDisplay design docs to detail monitor state persistence and restoration. Updated MonitorStateManager.cs comment, revised discovery flowchart to show conditional settings restore, added explanatory notes, and enhanced the sequence diagram to illustrate the restore process on startup.
2025-12-25 15:47:40 +08:00
Yu Leng
bdb47bf534 Refactor SettingsUtils usage, async monitor restore
Refactor to use SettingsUtils class instead of ISettingsUtils throughout the project. Make monitor settings restoration on startup fully asynchronous, ensuring hardware values are applied before initialization completes. Improve initialization flow and user feedback by keeping IsScanning true until restore finishes, and only firing InitializationCompleted after all settings are restored. Update comments and logging for clarity.
2025-12-24 16:43:17 +08:00
Yu Leng
003977a95d Switch monitor state key from HardwareId to Monitor.Id
Update codebase to use Monitor.Id as the key for monitor state management instead of HardwareId. This includes updating method parameters, documentation, and internal logic to ensure monitor state persistence and retrieval are based on the unique monitor identifier. Improves clarity and robustness of monitor state handling.
2025-12-24 16:23:27 +08:00
Yu Leng
a7d4324dda merge main 2025-12-24 15:41:15 +08:00
Yu Leng
f86dcb3863 merge main 2025-12-24 15:32:09 +08:00
Yu Leng
5b135b0654 change default hotkey 2025-12-22 11:03:47 +08:00
Yu Leng
98a54dba9b Implement in-process hotkey handling for PowerDisplay
Switch PowerDisplay to handle activation hotkeys in-process using a new HotkeyService and the Win32 RegisterHotKey API, following the CmdPal pattern. Hotkey registration and handling are now managed directly in PowerDisplay.exe, with settings changes propagated via a new HotkeyUpdatedPowerDisplayEvent. The Runner no longer registers or manages PowerDisplay hotkeys. Updated the default activation shortcut to Win+Alt+D. This improves reliability and avoids IPC timing issues with the previous centralized hotkey mechanism.
2025-12-19 15:45:30 +08:00
Yu Leng
ebb1758d73 Remove unused ShowPowerDisplayEvent from PowerDisplay
All references to ShowPowerDisplayEvent have been removed, including its constant definition, related methods, event handle management, and event registration. Error handling and shutdown logic were updated accordingly. Documentation was revised to clarify event naming and provide a Toggle event example. This cleans up legacy code for an event that is no longer used.
2025-12-19 11:52:32 +08:00
Yu Leng
9c35ac90f7 Reorder mc:Ignorable attribute in IdentifyWindow.xaml
Moved mc:Ignorable="d" to the end of the WindowEx attribute list for improved clarity and consistency. No functional changes were made.
2025-12-19 11:35:05 +08:00
Yu Leng
90be113e20 Change default RestoreSettingsOnStartup to false
Set RestoreSettingsOnStartup default to false in PowerDisplayProperties, so settings are not restored on startup by default. Also, clarify XML doc comments for pending operation properties to improve documentation accuracy.
2025-12-19 11:05:40 +08:00
Yu Leng
11565dbf1c Refactor window management to use WinUIEx WindowEx
Refactored IdentifyWindow and MainWindow to leverage WinUIEx's WindowEx for declarative window configuration. Moved window properties (resizability, minimizability, title bar, taskbar visibility) to XAML. Replaced manual AppWindow and Win32 interop logic with WinUIEx APIs, simplifying DPI scaling and window positioning. This reduces code complexity and improves maintainability.
2025-12-19 11:02:49 +08:00
Yu Leng
40a1245b86 Move IsAlwaysOnTop logic from XAML to code-behind
IsAlwaysOnTop is now set programmatically in MainWindow.xaml.cs instead of XAML. Added logging for when IsAlwaysOnTop is set. Removed BringToFront() calls and related logs, as setting IsAlwaysOnTop in code ensures the window stays on top.
2025-12-19 10:54:06 +08:00
Yu Leng
3fa044d44c Improve telemetry accuracy for hotkey and tray icon settings
Updated telemetry event to use ActivationShortcut.IsValid() for hotkey status and ShowSystemTrayIcon for tray icon status, replacing previous properties. This ensures more accurate and meaningful telemetry data collection.
2025-12-18 14:11:30 +08:00
Yu Leng
989d263091 Replace PowerDisplay.png with animated PowerDisplay.gif
Updated OobePowerDisplay.xaml to use PowerDisplay.gif as the HeroImage, replacing the previous static PNG. Added the PowerDisplay.gif binary to the project to enhance the OOBE Power Display page with animation.
2025-12-18 14:05:24 +08:00
Yu Leng
d6a851535d Add PowerDisplay settings telemetry event support
Enables Runner to signal PowerDisplay to send a settings telemetry event. Introduces a new named event for this purpose, updates interop constants, and implements logic in PowerDisplay to gather and log current settings and profile info to telemetry. Also improves process shutdown handling in Runner. This enhances diagnostics and observability for the PowerDisplay module.
2025-12-18 14:00:35 +08:00
Yu Leng
03088b8a2e Refactor PowerDisplay init; remove WiX installer logic
Refactored initialization so MainViewModel handles all async startup and signals MainWindow via a new InitializationCompleted event. Removed redundant initialization code from MainWindow. Updated process termination list to include PowerToys.PowerDisplay.exe. Removed PowerDisplay.wxs, shifting asset installation logic out of the WiX setup project. Improves separation of concerns and UI readiness handling.
2025-12-18 00:22:35 +08:00
Yu Leng
7cc00fa99e Improve window sizing and tray icon reliability
- Enforce minimum window height and adjust sizing logic to respect both min/max limits
- Call Activate() before Show() to ensure window visibility
- Add fallback Activate() if window is not visible after show
- Enhance tray icon error handling and retry logic if Shell_NotifyIcon fails
- Add warning logs for missing monitors and tray icon failures
2025-12-17 17:41:43 +08:00
Yu Leng
3e203649f1 Ensure profile application runs on UI thread
Refactored profile application to dispatch to the UI thread using _dispatcherQueue, as MonitorViewModels are UI-bound. Introduced an async helper to manage completion signaling. Improved error logging to include exception types and inner exception details for better diagnostics.
2025-12-17 14:53:43 +08:00
Yu Leng
08c0944cca Improve LightSwitch profile sync and settings persistence
- Sync internal theme state and notify PowerDisplay on manual override (hotkey) in LightSwitchStateManager.
- Add SaveSettings() in LightSwitchViewModel and call it on relevant property changes to ensure immediate settings persistence.
- Clear profile selection and update settings if a configured profile no longer exists, preventing invalid references.
- Update expect.txt with new recognized keywords.
2025-12-17 14:48:52 +08:00
Yu Leng
30da716be3 Refactor single-instance logic to prevent extra log files
Rework startup to check for existing instance before logger
initialization, following the Command Palette pattern. Only the
primary instance now creates log files. Remove DecideRedirection()
and move redirection logic directly into Main. Update
RedirectActivationTo to avoid logging before logger is initialized.
Clarify log messages and ensure activation handler is only
registered for the primary instance.
2025-12-17 14:17:32 +08:00
Yu Leng
6f5477442b Improve logging and single-instance handling for PowerDisplay
- Add detailed logging throughout C# app and C++ module for lifecycle, event, and process actions
- Remove "process ready" event; switch to Command Palette single-instance pattern using AppInstance and RedirectActivationToAsync
- Refactor process launch and event registration logic for clarity and reliability
- Enhance error handling and diagnostics for telemetry, language, and hotkey parsing
- Make event handling and UI thread marshalling more robust and traceable
- Clean up obsolete code and improve comments for maintainability
2025-12-17 14:08:43 +08:00
Yu Leng
5e0909fa36 Position window on monitor under mouse cursor
Updated window positioning logic to use the monitor where the mouse cursor is currently located, instead of always using the primary monitor. Added GetMonitorAtCursor method leveraging Win32 GetCursorPos to determine cursor location and select the appropriate monitor. Updated documentation and added necessary P/Invoke support.
2025-12-17 13:21:59 +08:00
Yu Leng
bfc5765530 Remove custom exit logic and cleanup from MainWindow
Refactored MainWindow.xaml.cs to eliminate the _isExiting flag and all related shutdown/cleanup methods, including UnsubscribeFromViewModelEvents, SetExiting, FastShutdown, and ExitApplication. The OnWindowClosed handler now always hides the window instead of handling exit scenarios, simplifying window lifecycle management.
2025-12-17 10:59:34 +08:00
Yu Leng
6eeb18b4c8 Refactor window management to use WinUIEx APIs
Replaces direct Win32/AppWindow interop in MainWindow.xaml.cs with higher-level WinUIEx APIs for showing, hiding, and configuring the window. Refactors window configuration to use WindowEx properties and AppWindow.TitleBar, removes obsolete using directives, and simplifies window sizing and positioning. This results in cleaner, more maintainable, and idiomatic WinUI code.
2025-12-17 10:48:36 +08:00
Yu Leng
e1c443628a Refactor window sizing to prevent flicker on show
Adjust window size to content before showing to avoid flicker and ensure correct initial appearance. Set minimal initial height to prevent "shrinking" effect. Remove redundant post-show resize logic and clarify intent with comments. Focus clearing is now performed immediately after showing the window.
2025-12-17 10:37:40 +08:00
Yu Leng
734ef8816b Refactor window positioning and sizing logic
Simplify and improve window positioning by using WinUIEx MonitorInfo to handle multi-monitor setups, taskbar positions, and DPI scaling. Remove manual DPI calculations and obsolete methods, making window sizing and placement more robust and concise.
2025-12-17 10:26:54 +08:00
Yu Leng
6d032713aa Fix window positioning to respect WorkArea X/Y offsets
Previously, window coordinates were calculated without considering the WorkArea's X and Y offsets, leading to incorrect placement on multi-monitor setups or when the taskbar is at the top or left. This update adds the WorkArea's X and Y values to the position calculations, ensuring accurate window placement across all display configurations.
2025-12-17 10:12:52 +08:00
Yu Leng
6acd859d43 Sync LightSwitch event names, add monitor refresh delay
- Use shared constants for LightSwitch theme event names in both C++ and C# to ensure cross-module consistency.
- Replace "Brightness update rate" with "Monitor refresh delay" setting in PowerDisplay; user can now configure delay (1–30s) after display changes before monitor refresh.
- Update UI and resources to reflect new setting and remove old references.
- MainViewModel now uses the configurable delay instead of a hardcoded value.
- Improves user control and reliability of monitor detection after hot-plug events.
2025-12-15 13:53:23 +08:00
Yu Leng
4f4a724d35 Add PowerDisplay asset support and logger name constant
Added PowerDisplay asset group to generateAllFileComponents.ps1 for file/component generation. Introduced powerDisplayLoggerName constant in LogSettings and updated PowerDisplayModule to use it for logger initialization, improving consistency.
2025-12-15 12:01:59 +08:00
Yu Leng
da0b272fb3 Add new DLLs to ESRP signing list
Added PowerDisplay.Lib.dll, WmiLight.dll, and WmiLight.Native.dll to ESRPSigning_core.json to ensure these files are included in the code signing process. This enhances security and trust for the newly introduced components.
2025-12-12 17:41:58 +08:00
Yu Leng
2f4f079d97 Add PowerDisplay binaries to ESRP signing list
Included PowerToys.PowerDisplayModuleInterface.dll, WinUI3Apps\PowerToys.PowerDisplay.dll, and WinUI3Apps\PowerDisplay.exe in ESRPSigning_core.json for code signing. This supports integration of the new PowerDisplay module.
2025-12-12 15:12:03 +08:00
Copilot
dcf1767c23 Refactor: Extract duplicated boolean-to-visibility converter to shared helper (#44236)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Eliminates code duplication by extracting the `ConvertBoolToVisibility`
method that was duplicated across `MonitorViewModel.cs` and
`MainWindow.xaml.cs` into a shared `VisibilityConverter` helper class.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

**Before:** Identical `ConvertBoolToVisibility` methods existed in two
places for x:Bind visibility conversions.

**After:** Single `VisibilityConverter.BoolToVisibility()` static method
in `Helpers/` namespace.

### Changes
- Created `PowerDisplay.Helpers.VisibilityConverter` with static
`BoolToVisibility` method
- Removed duplicate methods from `MonitorViewModel` and `MainWindow`
- Updated 8 XAML bindings to use
`helpers:VisibilityConverter.BoolToVisibility()`

Maintains AOT-compatible x:Bind pattern while eliminating duplication.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Code review and security scan passed with no issues.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
2025-12-12 14:38:28 +08:00
Yu Leng
04de4b8357 fix spelling check 2025-12-12 14:30:06 +08:00
Copilot
53a6d45056 Extract EnsureProcessRunning helper to eliminate code duplication (#44235)
## Summary of the Pull Request

Addresses code review feedback on #42642 by extracting duplicated
process launching logic into a reusable helper method.

## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments

The `ApplyColorTemperature` and `ApplyProfile` custom action handlers
contained identical 6-line blocks for process state checking, launching,
and synchronization.

**Changes:**
- Added `EnsureProcessRunning()` helper encapsulating the pattern: check
if running → launch if needed → wait for ready signal
- Replaced duplicated blocks in both handlers with single helper call

**Before:**
```cpp
if (!is_process_running())
{
    Logger::trace(L"PowerDisplay process not running, launching before applying...");
    launch_process();
    wait_for_process_ready();
}
```

**After:**
```cpp
EnsureProcessRunning();
```

## Validation Steps Performed

Code review and security checks passed.

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
2025-12-12 14:27:10 +08:00
Yu Leng
ab1561bfc4 Update to use GetWindowLongPtr for 64-bit compatibility
Replaced all usages of GetWindowLong with GetWindowLongPtr and updated the P/Invoke declaration to use "GetWindowLongPtrW". This ensures correct window style handling on 64-bit Windows systems.
2025-12-12 14:25:49 +08:00
Yu Leng
39c10ad039 Update keywords, docs, and WMI controller imports
- Refreshed keyword lists in expect.txt to reflect recent changes.
- Fixed documentation link casing in design.md.
- Added Copilot attribution note to mccsParserDesign.md.
- Reordered and added using directives in WmiController.cs for clarity and dependency resolution.
2025-12-12 14:07:28 +08:00
Yu Leng
132ed2128e Expand and reorganize design.md for clarity and depth
- Majorly restructured Table of Contents, splitting "Goals" and "Future Considerations" and adding new technical sub-sections.
- Added rationale sections: "Why WmiLight Instead of System.Management" (AOT, memory, API) and "Why We Need an MCCS Capabilities String Parser" (recursive parsing, regex limitations).
- Rewrote Settings UI ↔ PowerDisplay architecture diagram for clarity; summarized UI data models in a table.
- Reformatted Windows Events for IPC section with a new table and event name explanation.
- Overhauled Monitor Discovery Flow with step-by-step breakdowns, new Mermaid diagrams, and a DDC/CI vs. WMI comparison.
- Removed the inlined Data Models class diagrams for brevity.
- Split "Future Considerations" into "Already Implemented" and a focused "Potential Future Enhancements" list.
- Improved formatting, terminology, and explanations throughout.
2025-12-12 13:03:44 +08:00
Yu Leng
06a72f3c54 Refactor: update docs, diagrams, and PnpIdHelper namespace
- Improved PowerDisplay design docs for clarity and technical accuracy
- Rewrote DDC/CI section to better explain real-world issues
- Updated architecture diagrams and terminology (Helpers → Utils)
- Replaced ASCII diagrams with Mermaid for better visualization
- Clarified monitor identification and MST/Clone mode handling
- Corrected PnP Manufacturer ID attribution to UEFI Forum
- Moved PnpIdHelper.cs to Utils namespace and updated all references
2025-12-12 12:40:54 +08:00
Yu Leng
2083fcd143 Refactor PowerDisplay design: VCP, windowing, class updates
Update design docs to reflect major refactoring and enhancements:
- Merge VcpCodeNames.cs and VcpValueNames.cs into VcpNames.cs
- Add GlobalUsings.cs for global using directives
- Expand DdcCiController/WmiController diagrams with high-level methods
- Extend VcpCapabilities with windowing support and new APIs
- Add WindowCapability, WindowArea, WindowSize structs
- Extend MonitorInfo with new properties and VCP formatting
- Add VcpCodeDisplayInfo and VcpValueInfo for richer VCP display
- Update class relationships and remove obsolete files (e.g., Styles)
2025-12-12 11:26:14 +08:00
Yu Leng
ef9c26dd50 Refactor CreateMonitorInfo to use object initializer
Refactored the CreateMonitorInfo method to use object initializer syntax instead of a parameterized constructor. This change improves code readability and maintainability, and removes the deprecated hardwareId parameter.
2025-12-12 11:14:50 +08:00
Yu Leng
a001d5cacd Remove unused MonitorInfo properties and constructors
Clean up MonitorInfo by removing obsolete constructors and several computed properties (ColorTemperatureDisplay, VcpCodesSummary, HasColorPresets, BrightnessTooltip, ContrastTooltip). Update XML docs for clarity and rely on remaining properties for display logic.
2025-12-12 11:10:20 +08:00
Yu Leng
dea4cbd045 Remove batch notification support from MonitorInfo
Removed SuspendNotifications, NotificationResumer, and the custom OnPropertyChanged override from MonitorInfo. Property changes now trigger immediate notifications instead of batching updates for UI refresh. This simplifies the class and notification logic.
2025-12-12 11:01:13 +08:00
Yu Leng
f47abb43e9 Simplify color temp display names and VCP hex parsing
Refactored color temperature display name formatting to remove
hexadecimal values from user-facing strings, returning only the
preset or custom name. Updated fallback and custom value handling
to be more concise. Replaced the TryParseHexCode helper with
direct string slicing and parsing for VCP codes. Updated
documentation comments to match new formatting. These changes
improve code clarity and provide a cleaner UI.
2025-12-12 10:59:18 +08:00
Yu Leng
4557c509e5 Refactor logging and VCP naming; simplify capabilities status
Removed excessive debug/info logging and consolidated VCP code/value naming into a single VcpNames utility. Eliminated CapabilitiesStatus property in favor of simpler logic. Cleaned up exception handling and removed non-essential UI and service logs. No changes to core functionality.
2025-12-12 10:36:46 +08:00
Yu Leng
73dca1b598 Clean up usings and update IPC exception handling
Removed unused using directives across multiple files to reduce clutter and unnecessary dependencies. Updated IPCResponseService.cs to suppress debug output in the exception handler for IPC message processing.
2025-12-12 09:57:48 +08:00
Yu Leng
4c93fe01d0 PowerDisplay: overhaul design docs, remove obsolete IPC code
Expanded and clarified PowerDisplay design documentation:
- Added detailed section on monitor identification (handles, IDs, MST, clone mode, etc.) with diagrams and tables.
- Updated architecture, flowcharts, and class diagrams to reflect new helpers, services, and improved data flows.
- Expanded internal structure details for PowerDisplay.Lib and app, listing new helpers, models, and utilities.
- Updated sequence diagrams for LightSwitch integration and Settings UI interaction.
- Revised future considerations, marking hot-plug and rotation as implemented, and adding new feature ideas.

Removed all obsolete PowerDisplay IPC forwarding code from settings_window.cpp/h, including the send_powerdisplay_message_to_settings_ui function and related JSON dispatch logic. This reflects a refactor or deprecation of the previous custom IPC mechanism.
2025-12-12 09:44:28 +08:00
Yu Leng
865dd60a83 Update expect.txt terms and fix NCP spelling in PnpIdHelper
Updated expect.txt with new and modified terms. Corrected the manufacturer name for "NCP" in PnpIdHelper.cs from "Najing CEC Panda" to "Nanjing CEC Panda" to fix a spelling error.
2025-12-11 13:18:00 +08:00
Yu Leng
2880b5afce Improve monitor orientation sync for mirror/clone mode
Refactor orientation tracking to use property change notifications in the Monitor model. Add GetCurrentOrientation to DisplayRotationService and a RefreshAllOrientations method in MonitorManager to ensure all monitors sharing a GdiDeviceName are updated after rotation. Update MonitorViewModel to subscribe to orientation changes and forward them to the UI, and clean up event subscriptions on dispose. These changes ensure accurate orientation state in both UI and data models, especially for mirrored displays.
2025-12-11 13:02:00 +08:00
Yu Leng
9a175df510 Improve WMI internal display naming and refresh logic
Enhance monitor discovery by extracting manufacturer IDs from WMI and mapping them to user-friendly names using a new PnpIdHelper. Remove unreliable WmiMonitorID name parsing. Add detailed logging and allow forced monitor refreshes during display changes. Update asset paths in project file for better organization.
2025-12-11 12:46:15 +08:00
Yu Leng
f07fa4db60 Improve monitor identification and display change handling
- Add detailed WMI monitor logging and fallback for missing names
- Update IdentifyWindow to support multi-monitor (mirrored) labels
- Map GDI device names to multiple monitor numbers for mirror mode
- Show pipe-separated monitor numbers in identify overlay
- Delay monitor refresh after display changes for hardware stability
- Enhance debug logging for monitor detection and mapping
2025-12-11 12:12:28 +08:00
Yu Leng
19eb78e696 Fix spelling issue 2025-12-11 11:18:09 +08:00
Yu Leng
ac94b3d9a5 Merge main into yuleng/display/pr/3
Resolve conflict in LightSwitchStateManager.cpp by keeping
NotifyPowerDisplay function for PowerDisplay integration.
2025-12-11 11:08:34 +08:00
Yu Leng
570cff4590 Remove redundant Logger initialization in App.xaml.cs
Logger initialization is now handled in Program.cs before the App constructor, so the duplicate initialization in App.xaml.cs has been removed. A clarifying comment was added to document this change and improve startup sequence clarity.
2025-12-11 11:04:41 +08:00
Yu Leng
d3ebebc24c Add "Vcpkg" variant; fix typo in IsValid property comment
Updated expect.txt to include the "Vcpkg" variant assignment. Fixed a duplicated word typo in the XML comment for the IsValid property in VcpFeatureValue.cs.
2025-12-11 10:57:40 +08:00
Yu Leng
5f15229691 Refactor: use Polly, ConcurrentDictionary, unify VCP parsing
- Replace custom retry logic with Polly.Core resilience pipelines for DDC/CI operations
- Remove LockedDictionary and RetryHelper; use ConcurrentDictionary for thread safety
- Add MonitorFeatureHelper to centralize VCP feature parsing
- Simplify monitor state management and update code for clarity
- Add Polly.Core dependency and update documentation
- Remove obsolete helper files
2025-12-11 10:52:51 +08:00
Yu Leng
75f57f53f2 Simplify ProfileService XML doc comments
Removed the <remarks> section from the ProfileService class XML documentation, eliminating detailed design notes and usage guidelines. Only the summary description is now retained for clarity and brevity.
2025-12-11 10:28:44 +08:00
Yu Leng
a4770a84cf Refactor LightSwitchService to use PowerToys settings API
Replaced manual JSON parsing with strongly-typed settings access via SettingsUtils and LightSwitchSettings. Updated logic to use EnableLightModeProfile and EnableDarkModeProfile flags. Changed namespace to PowerDisplay.Services and updated using directives accordingly. Removed obsolete helper methods and improved code clarity.
2025-12-11 10:27:17 +08:00
Yu Leng
87eb7cc07f Refactor LightSwitch integration: decouple event handling
Removed LightSwitchListener and ThemeChangedEventArgs. Added LightSwitchService to centralize theme/profile logic. Updated event registration and MainViewModel to handle LightSwitch theme changes via service, decoupling event listening from profile application. Event listening now handled externally.
2025-12-11 10:08:51 +08:00
Yu Leng
54006a8ef1 Remove Manufacturer/ConnectionType from Monitor model
Simplifies monitor metadata by removing Manufacturer and ConnectionType properties and related extraction logic. Refactors WMI controller to always instantiate, relying on monitor discovery for support. Updates model property summaries for clarity, adjusts LightSwitchListener log prefix, and removes Manufacturer from MonitorViewModel. Streamlines code and improves documentation consistency.
2025-12-11 09:52:43 +08:00
Yu Leng
0fc2fc42d3 Refactor DDC/CI brightness initialization logic
Move brightness setup from MonitorDiscoveryHelper to DdcCiController to avoid slow I2C operations during monitor discovery. Set default brightness to 50 and update after discovery. Remove unused brightness methods and type aliases. Update comments to clarify initialization responsibilities.
2025-12-11 09:27:10 +08:00
Yu Leng
cffdf72afb Fix spelling issue 2025-12-10 19:32:15 +08:00
Yu Leng
a05859efc8 Remove unused monitor helpers and simplify utilities
Removed obsolete helper methods and the MonitorFeatureHelper class, streamlining monitor feature parsing and value conversion logic. Cleaned up thread-safe dictionary utilities by dropping rarely used methods. Updated application icon path in project file. Documentation comments were clarified for consistency.
2025-12-10 19:30:17 +08:00
Yu Leng
762a6bce0c Remove GetPhysicalHandle from PhysicalMonitorHandleManager
Eliminated the GetPhysicalHandle method, which handled monitor handle lookup by Id or direct handle. This refactors PhysicalMonitorHandleManager to no longer provide handle retrieval logic.
2025-12-10 19:09:58 +08:00
Yu Leng
6aa7e2cdf6 Refactor: unify VCP feature handling with VcpFeatureValue
Replaced BrightnessInfo with generic VcpFeatureValue struct to represent any VCP feature (brightness, color temp, input source, etc.). Updated IMonitorController and all controller implementations to use VcpFeatureValue for relevant methods. Simplified and unified VCP set/get logic, removed redundant validation and obsolete DdcCiNative methods, and updated helper classes accordingly. Improved code clarity and maintainability by generalizing VCP feature handling throughout the codebase.
2025-12-10 19:04:19 +08:00
Yu Leng
ecd8331d51 Refactor controller selection and remove CanControlMonitorAsync
Refactored MonitorManager to use dedicated controller fields for
O(1) lookup based on CommunicationMethod, replacing the async
GetControllerForMonitorAsync with a synchronous method. Removed
CanControlMonitorAsync from IMonitorController and all
implementations, simplifying the interface and controller logic.
Updated controller discovery and disposal logic accordingly.
Improved performance and maintainability by eliminating
unnecessary async checks and streamlining controller management.
2025-12-10 17:17:13 +08:00
Yu Leng
e85797b449 Update PowerDisplay.wxs for WiX v4 and add to installer
Updated PowerDisplay.wxs to use WiX Toolset v4 schema URLs for compatibility. Added PowerDisplay.wxs to build and file restoration steps in PowerToysInstallerVNext.wixproj, ensuring it is included and managed in the installer package.
2025-12-10 17:03:29 +08:00
Yu Leng
b622e6249d Fix spelling issue 2025-12-10 14:27:11 +08:00
Yu Leng
5b2af47528 Move Twinkle Tray notice to PowerDisplay utility section
Relocated the Twinkle Tray license and attribution from after the NuGet packages list to a new "Utility: PowerDisplay" section in NOTICE.md, aligning it with the format used for other utilities. No changes were made to the content of the notice.
2025-12-10 14:18:26 +08:00
Yu Leng
77a7c04b2e Improve monitor rotation using GDI device name
Refactor monitor discovery and rotation logic to use the GdiDeviceName property for accurate display targeting, especially in multi-monitor setups. Update the Monitor model to include GdiDeviceName, adjust DisplayRotationService and MonitorManager to use it, and enhance logging for better traceability. Also, unify hardware property application in MonitorViewModel to reduce code duplication. These changes increase reliability and maintainability of monitor control operations.
2025-12-10 14:16:28 +08:00
Yu Leng
4817709fda Unify monitor identification using stable Id property
Refactor monitor matching and persistence to use a single, stable Id (format: "{Source}_{EdidId}_{MonitorNumber}") across all components. Remove HardwareId and DeviceKey properties from Monitor, update ProfileMonitorSetting to use MonitorId, and simplify MonitorMatchingHelper logic. Update DDC/CI, WMI, ViewModels, Settings UI, and unit tests to rely on Id for all lookups, state management, and handle mapping. Improves reliability for multi-monitor setups and simplifies codebase by removing legacy fallback logic.
2025-12-10 13:34:36 +08:00
Yu Leng
0965f814ce Refactor profile and color temp application logic
Simplify MainViewModel by removing legacy HardwareId matching and the ApplyColorTemperatureAsync method. Update ApplyProfileAsync to accept only monitor settings and match monitors by InternalName. Improve documentation in ThemeChangedEventArgs and ProfileService for clarity. Streamline method signatures and remove outdated compatibility code.
2025-12-10 12:42:01 +08:00
Yu Leng
d48438571e Improve monitor identification accuracy in IdentifyMonitors
Refactored IdentifyMonitors to use Windows API for mapping display areas to monitor numbers via HMONITOR and GDI device names. This ensures correct monitor numbering in identify windows, especially in complex setups. Added detailed logging and only counts successfully created windows.
2025-12-10 12:10:54 +08:00
Yu Leng
102077a29b Remove "Link all monitor brightness" feature
Eliminates the UI and backend logic for synchronizing all monitor brightness levels. Refactors monitor list update logic to distinguish between initial load and refresh, introducing RestoreMonitorSettings for startup state restoration. Streamlines feature visibility application and settings handling for improved maintainability.
2025-12-10 11:42:23 +08:00
Yu Leng
97560ea6c0 Refactor color temp init to be synchronous in DDC/CI
Simplifies color temperature initialization by moving it from async handling in MainViewModel to synchronous initialization during monitor enumeration in DdcCiController. Removes related async methods and UI update logic, reducing complexity and ensuring color temperature values are available immediately after enumeration.
2025-12-10 11:18:28 +08:00
Yu Leng
0a2b433697 Refactor monitor discovery and initialization flow
Move all monitor initialization (capabilities, input source) into the controller discovery phase, eliminating redundant async initialization steps from MonitorManager. Remove obsolete initialization methods from MonitorManager. Add helper methods in DdcCiController for capability and input source setup. Improve logging for monitor capabilities. This streamlines monitor setup, reduces redundant work, and improves performance.
2025-12-10 11:00:36 +08:00
Yu Leng
db15380fcf Improve WMI monitor name extraction using length property
Refactor GetUserFriendlyName to use UserFriendlyNameLength for accurate string extraction from the WMI UserFriendlyName buffer. This ensures only valid characters are included, improving reliability when parsing monitor names.
2025-12-10 10:15:40 +08:00
Yu Leng
095ae2bebd Refactor DDC monitor discovery; clarify interface docs
Refactored DiscoverMonitorsAsync in DdcCiController to remove Task.Run and simplify async flow and error handling. Updated XML doc for Name property in IMonitorController to clarify its purpose.
2025-12-10 08:43:44 +08:00
Yu Leng
c093332f84 Improve DDC/CI monitor matching and detection logic
Refactor monitor enumeration to use GDI device name and device path for accurate matching with Windows display config data. Expand MonitorDisplayInfo with new fields, add native interop for source device names, and enhance robustness for multi-monitor and mirror mode setups. Improve logging and remove index-based matching.
2025-12-10 08:40:44 +08:00
Yu Leng
725ac65450 Refactor DDC/CI monitor identification to use QueryDisplayConfig
Switch monitor discovery from EnumDisplayDevices to QueryDisplayConfig for stable identification using hardware ID and monitor number. Simplify CandidateMonitor structure and remove DisplayDeviceInfo and related matching logic. Update device key format to "{HardwareId}_{MonitorNumber}" for improved handle management. Rewrite CreateMonitorFromPhysical to use MonitorDisplayInfo directly. Update documentation and remove obsolete helpers for better reliability and maintainability.
2025-12-10 06:47:39 +08:00
Yu Leng
0bc59e7101 Refactor DDC/CI monitor discovery and control logic
- Split monitor discovery into three clear phases for readability and performance
- Introduce CandidateMonitor record for better data handling
- Fetch DDC/CI capabilities in parallel to speed up enumeration
- Filter out NULL physical monitor handles and retry up to 3 times
- Refactor color temperature and input source access to use generic VCP feature methods
- Add discrete VCP value validation for safer set operations
- Move input source verification to a dedicated method
- Improve error handling and logging throughout
- Remove obsolete tuple code and update documentation for maintainability
2025-12-10 06:21:50 +08:00
Yu Leng
b004fe1445 Remove StatusText from ViewModel; use logging for errors
StatusText property and all related UI updates have been removed
from MainViewModel and MainWindow. Status and error messages are
now logged via Logger instead of being shown in the UI. This
simplifies the ViewModel and centralizes error reporting. Only
IsScanning and IsLoading remain for UI state indication.
2025-12-10 05:01:09 +08:00
Yu Leng
a0f45c444f Refactor error handling, DPI scaling, and UI events
- Replace custom error UI with Logger.LogError for exceptions
- Remove acrylic backdrop; use semi-transparent black background
- Scale IdentifyWindow size for DPI using GetDpiForWindow
- Remove ValueChanged handlers from sliders; use PointerCaptureLost
- Delete unused event handlers and clean up using directives
- Simplify input source switching logic in MainWindow
2025-12-09 15:28:18 +08:00
Yu Leng
25db00ec45 Add WinUIEx to software list in NOTICE.md
Added "WinUIEx" to the list of referenced software/tools in the NOTICE.md file to ensure proper attribution.
2025-12-09 15:01:14 +08:00
Yu Leng
10bdb31a8a Use SettingsUtils.Default singleton for consistency
Updated PowerDisplayPage, DashboardViewModel, and LightSwitchViewModel to use the SettingsUtils.Default singleton instance instead of creating new SettingsUtils objects. This change ensures consistent settings utility usage and improves resource management.
2025-12-09 14:21:29 +08:00
Yu Leng
9654ffde06 fix spelling issue 2025-12-09 13:52:33 +08:00
Yu Leng
393b0af104 Fix typo: "relys" to "relies" in documentation
Corrected a grammatical error in the documentation by changing "PowerDisplay relys on" to "PowerDisplay relies on" for improved clarity and accuracy.
2025-12-09 11:24:23 +08:00
Yu Leng
b409f1e4bf Update expect.txt with new keywords across multiple sections
Expanded keyword lists in expect.txt for DBLCLKS, edid, MBR, MSIXCA, PATCOPY, and VERBW sections to include new identifiers and terminology. No removals; changes support new features and components.
2025-12-09 11:13:52 +08:00
Yu Leng
ab4ad5c940 Fix PR issue 2025-12-09 10:59:52 +08:00
Yu Leng
253b29bd11 Merge remote-tracking branch 'origin/main' into yuleng/display/pr/3 2025-12-09 10:55:14 +08:00
Yu Leng
bd9b66afa4 Refactor to use nint for window handles and P/Invoke
Replaces IntPtr with nint for window handles and native API calls in WindowHelper, removing 32/64-bit conditional logic. Cleans up unused minimize/restore methods. Updates TrayIconService constructor usage in App.xaml.cs.
2025-12-09 10:52:53 +08:00
Yu Leng
dd02ed54e0 Remove unused logo and add WindowsDesktop.App reference
Deleted obsolete PNG asset. Added Microsoft.WindowsDesktop.App as a framework reference in the project file to ensure consistent Microsoft.VisualBasic.dll versioning across WinUI3 apps, without enabling WPF or WinForms. No changes to log file exclusions or App.xaml handling.
2025-12-09 10:36:54 +08:00
Yu Leng
5bf0a610e8 #
Refactor VCP code handling and improve immutability

Refactored `WaitForColorTempAndSaveAsync` to use `IReadOnlyList<Task>` for improved immutability. Removed the `GetMonitorViewModel` method from `MainViewModel.Monitors.cs`.

Simplified VCP code handling in `MainViewModel.Settings.cs` by inlining the logic of `BuildVcpCodesList` and `BuildFormattedVcpCodesList` into the object initialization for `monitorInfo`. This reduces redundancy and improves code readability and maintainability.
2025-12-08 14:57:05 +08:00
Yu Leng
b75db43988 Improve thread safety and simplify monitor management
Refactored `DisplayChangeWatcher` to enhance thread safety by
introducing `_initialEnumerationComplete` and ensuring state
changes and event handling are dispatched to the UI thread.

Removed `MonitorListChangedEventArgs` and the `MonitorsChanged`
event from `MonitorManager`, simplifying monitor management
logic. Updated `MainViewModel` to remove its dependency on
`MonitorsChanged`.

Cleaned up `TrayIconService` by removing unused `_showWindowAction`
and simplifying resource fallback logic. Fixed minor wording
inconsistencies in XML documentation.

These changes improve maintainability, thread safety, and
performance.
2025-12-08 14:51:02 +08:00
Yu Leng
b624dd2b03 Make DisplayChangeWatcher a partial class; add fields
The `DisplayChangeWatcher` class is now a partial class, enabling its definition to be split across multiple files. Added two private fields: `_dispatcherQueue` for managing thread-safe operations and `_debounceDelay` for debouncing with a 1-second delay. Updated the `DeviceWatcher` field to use nullable reference types (`DeviceWatcher?`).
2025-12-08 14:17:05 +08:00
Yu Leng
ce9bd1e67e Refactor: Replace custom RelayCommand with CommunityToolkit
Replaced the custom RelayCommand implementation with the
[RelayCommand] attribute from the CommunityToolkit.Mvvm library
to simplify command creation and reduce boilerplate code.

- Removed RelayCommand and RelayCommand<T> classes.
- Added CommunityToolkit.Mvvm package to the project.
- Updated MainViewModel and MonitorViewModel to use
  [RelayCommand] for command generation.
- Cleaned up unused imports related to the removed RelayCommand.

This change improves maintainability and aligns the project
with modern MVVM practices.
2025-12-08 14:01:18 +08:00
Yu Leng
0655497762 Refactor SettingsUtils and update project ID
Refactored `SettingsUtils` initialization across multiple files
(`App.xaml.cs`, `MainWindow.xaml.cs`, `MainViewModel.cs`) to use
`SettingsUtils.Default` instead of creating new instances. This
improves consistency, reduces redundancy, and promotes better
resource management.

Updated the project ID for `PowerDisplayModuleInterface.vcxproj`
in `PowerToys.slnx` to reflect a configuration or structural
change in the project setup.
2025-12-08 13:37:06 +08:00
Yu Leng
430a41875e Merge branch 'main' into yuleng/display/pr/3
Resolved conflicts:
- PowerToys.sln: Deleted (migrated to .slnx format)
- LauncherViewModel.cs: Merged SettingsUtils.Default changes with PowerDisplay support
- Added PowerDisplay projects to PowerToys.slnx
2025-12-08 13:25:07 +08:00
Yu Leng
47638c5c6d Add DisplayChangeWatcher for monitor hot-plug detection
Introduced the `DisplayChangeWatcher` component to detect monitor
connect/disconnect events using the WinRT `DeviceWatcher` API.
Implemented 1-second debouncing to coalesce rapid changes and
trigger a `DisplayChanged` event for refreshing the monitor list.

Integrated `DisplayChangeWatcher` into `MainViewModel`, adding
lifecycle management methods (`StartDisplayWatching`,
`StopDisplayWatching`) and handling the `DisplayChanged` event
to refresh monitors dynamically.

Updated `Dispose` in `MainViewModel` to ensure proper cleanup
of the `DisplayChangeWatcher`. Enhanced logging and added
detailed comments for better traceability.

Modified `design.md` to document the new component, updated
flowcharts, and marked "Monitor Hot-Plug" as implemented.
Reflected changes in the `PowerDisplay` directory structure.
2025-12-05 00:10:26 +08:00
Yu Leng
cf727e8a92 Ensure WMI monitors support brightness control
Added a condition in `PowerDisplayViewModel` to ensure that monitors using the "WMI" communication method are always marked as supporting brightness control. This is achieved by explicitly setting `monitor.SupportsBrightness` to `true` for WMI monitors, as brightness is controlled through the Windows WMI interface. Updated comments to clarify this behavior, improving compatibility and accuracy for WMI-based monitors.
2025-12-04 19:26:44 +08:00
Yu Leng
fc0ae601a6 Refactor monitor matching logic
Replaced legacy parsing and matching methods with a streamlined approach using `QueryDisplayConfig` and `MonitorDisplayInfo` to align monitor numbering with Windows Display Settings' "Identify" feature. Removed redundant methods and tests for parsing display numbers and device paths. Introduced a `MonitorNumber` property in `MonitorDisplayInfo` for consistent numbering. Updated `MonitorDiscoveryHelper` and `WmiController` to use the new logic. Enhanced logging for better debugging and maintainability.
2025-12-04 19:12:49 +08:00
Yu Leng
6c7504d134 Add logs 2025-12-04 18:43:40 +08:00
Yu Leng
578a66734a Refactor monitor discovery and naming logic
Refactored `MonitorDiscoveryHelper.cs` to improve clarity and maintainability:
- Added `ExtractHardwareIdFromDeviceId` helper method to extract hardware IDs from `DeviceID` strings.
- Updated monitor matching logic to prioritize hardware ID matches, with a fallback to the first match for backward compatibility.
- Simplified default monitor naming by removing index-based names.

Enhanced `DisplayName` property in `MonitorViewModel.cs`:
- Included monitor numbers in display names for multi-monitor setups.
- Aligned naming logic with the Settings UI's `MonitorInfo.DisplayName`.

Improved code comments and documentation for better readability.
2025-12-04 12:32:13 +08:00
Yu Leng
6366fa7407 fix build issue 2025-12-04 11:52:07 +08:00
Yu Leng
f07b4942ef Refactor and clean up PowerDisplay codebase
- Removed `GetCurrentRotation` and related methods from `DisplayRotationService.cs` as they are no longer required.
- Removed Kelvin-to-VCP conversion logic and formatting utilities from `MonitorValueConverter.cs`.
- Refactored `ProfileHelper.cs` by consolidating profile name generation logic and removing unused validation methods.
- Removed unused constants from `AppConstants.cs`, including DDC/CI protocol and process synchronization constants.
- Reorganized namespaces, moving `MonitorListChangedEventArgs` and `MonitorManager` to `PowerDisplay.Helpers`.
- Replaced `PowerDisplay.Core` references with `PowerDisplay.Helpers` across multiple files.
- Performed general cleanup, including removing unused `using` directives and redundant code.

These changes simplify the codebase, improve maintainability, and enhance the overall structure.
2025-12-04 06:14:01 +08:00
Yu Leng
e2be06f387 Hide build log files from Solution Explorer
Added `<ItemGroup>` entries in `PowerDisplay.Lib.UnitTests.csproj`,
`PowerDisplay.Lib.csproj`, and `PowerDisplay.csproj` to exclude
build log files (`*.log` and `*.binlog`) from being displayed in
the Solution Explorer. This change declutters the project view
and ensures consistency across all project files, improving the
developer experience.
2025-12-04 05:50:47 +08:00
Yu Leng
f32ee3ea02 Add WindowParser and refactor theme and brightness logic
Introduced `WindowParser` to parse `windowN` segments for PIP/PBP
capabilities in MCCS strings. Added new data models (`WindowCapability`,
`WindowArea`, `WindowSize`) to represent parsed window data. Updated
`MccsCapabilitiesParser` to handle `windowN` segments and added unit
tests for various configurations.

Refactored brightness control in `DdcCiController` to exclusively use
VCP code `0x10`, removing high-level API methods. Updated `DdcCiNative`
to streamline brightness operations.

Revised `LightSwitchListener` and `LightSwitchStateManager` to use
separate light/dark theme events, eliminating race conditions. Removed
registry-based theme detection logic.

Enhanced `VcpCodeNames` with additional VCP codes and improved
categorization. Updated documentation and architecture diagrams to
reflect these changes. Removed unused legacy methods and improved
logging and error handling.
2025-12-04 05:44:59 +08:00
Yu Leng
d9584de585 Fix some WMI issues 2025-12-03 06:24:20 +08:00
Yu Leng
967ff78c93 Refactor PowerDisplay and cleanup resources
Added detailed XML documentation to the `Monitor` class in the `PowerDisplay.Common.Models` namespace, improving clarity and providing usage guidelines for properties like `Id`, `HardwareId`, and `DeviceKey`. Enhanced the `Ddc` class in `PowerDisplay.Configuration` with `<remarks>` referencing centralized VCP code definitions, and removed hardcoded VCP constants to eliminate redundancy.

Cleaned up `Resources.resw.bak` by removing unused or deprecated localized strings. This includes strings for shortcut conflict resolution, search results, the "Power Display" utility, and the "Light Switch" feature, suggesting deprecation or restructuring of these features.
2025-12-03 02:05:09 +08:00
Yu Leng
9413b7cc37 Replace regex parser with recursive descent parser
Introduce a new `MccsCapabilitiesParser` to replace the
regex-based `VcpCapabilitiesParser` for parsing MCCS capabilities
strings. The new parser uses a grammar-based recursive descent
approach, improving performance, extensibility, and error handling.

Key changes:
- Implement `MccsCapabilitiesParser` with `ref struct` for zero
  heap allocation and efficient parsing using `ReadOnlySpan<char>`.
- Add sub-parsers for `vcp()` and `vcpname()` segments.
- Accumulate errors in `ParseError` list and return partial results.
- Replace all references to `VcpCapabilitiesParser` with the new
  parser in `DdcCiNative.cs` and `MonitorManager.cs`.
- Enhance `VcpCapabilities` model with `MccsVersion` property.
- Add comprehensive unit tests in `MccsCapabilitiesParserTests.cs`
  to validate real-world examples and edge cases.
- Remove legacy `VcpCapabilitiesParser` and associated code.

Additional improvements:
- Optimize parsing for both space-separated and concatenated hex
  formats.
- Improve logging for parsing progress and error diagnostics.
- Adjust application initialization to prevent race conditions.
2025-12-03 01:32:06 +08:00
Yu Leng
ea75725ba7 Refactor monitor handling to use Id instead of HardwareId for hidden monitor checks 2025-12-02 17:21:57 +08:00
Yu Leng
3aabc0fcd1 Update ApplyFeatureVisibility to use InternalName instead of HardwareId for monitor settings 2025-12-02 17:19:18 +08:00
Yu Leng
c7ffb46f48 Merge main into yuleng/display/pr/3 - resolve conflicts in SettingsSerializationContext.cs and SettingsUtils.cs 2025-12-02 16:42:38 +08:00
Yu Leng
f729e4ab32 fix spelling issue 2025-12-02 15:00:50 +08:00
Yu Leng
a5ea44921c Update expect.txt keywords and csproj package references
Updated `expect.txt` to modify or replace specific keywords for
consistency. Added `System.CodeDom` and `System.Diagnostics.
EventLog` package references to `PowerDisplay.Lib.UnitTests.csproj`
with `ExcludeAssets` set to `runtime` to prevent conflicts with
.NET SDK-provided DLLs.
2025-12-02 14:51:05 +08:00
Yu Leng
4fae32cffe Update expect.txt for naming consistency and corrections
Updated multiple sections in `expect.txt` to reflect changes in naming conventions, corrected casing, and added new entries. Key updates include:

- `DBPROPSET`: Added "hardwareid", removed "HardwareId".
- `GWLSTYLE` and `INSTALLMESSAGE`: Added "internalname", removed "InternalName".
- `jpe`: Removed "jpnime", added "Jsons" and "jsonval".
- `olditem`: Added "ollama", removed "ollama-ollama".
- `Pokedex` and `vcamp`: Added "vcpcode" and "vcpcodes", removed "VcpCode" and "VcpCodes".

These changes improve consistency and accuracy across the file.
2025-12-02 06:21:10 +08:00
Yu Leng
700078259b Refactor and reformat XAML and related files
Improved code readability, consistency, and formatting across multiple files:
- Updated `expect.txt` placeholders and entries for `customaction`, `Dxva`, and `svchost`.
- Fixed a minor XML documentation formatting issue in `MonitorStateManager.cs`.
- Reformatted `TextBlock` in `IdentifyWindow.xaml` for better alignment.
- Cleaned up `MainWindow.xaml` by removing duplicate `xmlns` declarations, consolidating attributes, and improving clarity in `ProfilesButton`.
- Refactored `LightSwitchPage.xaml`:
  - Improved indentation and alignment in `SettingsExpander` and `SettingsCard`.
  - Reintroduced and reformatted "Force mode now" and location settings sections.
  - Enhanced `LocationResultPanel` with sunrise/sunset tooltips.
- Organized `PowerDisplayPage.xaml` by deduplicating `xmlns` declarations and consolidating `SettingsCard` attributes.

These changes enhance maintainability, readability, and adherence to coding standards.
2025-12-02 05:54:08 +08:00
Yu Leng
9957da6e0f Update expect.txt with new and modified entries
Updated the `expect.txt` file to include new keywords, constants, and identifiers across multiple sections. These changes add terms like `Backlight`, `CAuthn`, `Clientedge`, `Coinit`, `Ddcci`, `DREGION`, `Eoac`, `FPrimary`, `Hantai`, `Kantai`, `Maximizebox`, `Monitorinfo`, `Nosize`, `Notupdated`, `qdc`, `Staticedge`, `Thickframe`, `Vga`, `Windowedge`, and `Winhook`.

The updates reflect enhancements to the list of terms relevant to the system or application being developed or tested.
2025-12-02 05:21:31 +08:00
Yu Leng
9230ba198c Update expect.txt with new terms across multiple sections
Added new terms to various sections in `expect.txt`:
- `Backlight` under `azcliversion`
- `Displayport` under `DISPLAYCHANGE`
- `dvi` under `dto`
- `Dxva` under `DWMWCP`
- `HPhysical`, `HSpeed`, and `HSync` under `hotkeycontrol`
- `POWERDISPLAYMODULEINTERFACE` under `Pomodoro`
- `Sdr` under `screensaver`
- `VSync` under `vsetq`
2025-12-02 04:31:49 +08:00
Yu Leng
61e636d1ea Update expect.txt and improve code comments
Updated `expect.txt` to add new terms (e.g., `debouncer`, `onnx`, `PowerDisplay`) and remove outdated ones across multiple sections.

Refined comments in the codebase for grammatical accuracy, clarity, and consistency:
- Corrected verb forms (e.g., "fallback" to "fall back").
- Fixed spelling (e.g., "re-entrant" to "reentrant").
- Improved punctuation for better readability.

These changes enhance code maintainability and ensure up-to-date terminology.
2025-12-02 04:15:11 +08:00
Yu Leng
6f1b336040 Merge main 2025-12-02 03:55:09 +08:00
moooyo
391f61d4ed Fix some issues 2025-12-01 06:09:26 +08:00
Yu Leng
0bbfc8015a Add GPO rule and profile management for PowerDisplay
Introduced a new GPO rule to manage PowerDisplay's enabled state via Group Policy, including updates to `GPOWrapper` and policy files (`PowerToys.admx` and `PowerToys.adml`).

Enhanced the PowerDisplay UI with profile management features, including quick apply, add, edit, and delete functionality. Updated `MainWindow.xaml` and `PowerDisplayPage.xaml` to support these changes, and added localized strings for improved accessibility.

Refactored `MainViewModel` to include a `Profiles` collection, `HasProfiles` property, and `ApplyProfileCommand`. Added methods to load profiles from disk and signal updates to PowerDisplay.

Improved error handling in `DdcCiController` and `WmiController` with input validation and WMI error classification. Optimized handle cleanup in `PhysicalMonitorHandleManager` with a more efficient algorithm.

Refactored `dllmain.cpp` to prevent duplicate PowerDisplay process launches. Updated initialization logic in `MainWindow.xaml.cs` to ensure proper ViewModel setup.

Localized strings for tooltips, warnings, and dialogs. Improved async behavior, logging, and UI accessibility.
2025-12-01 04:32:29 +08:00
Yu Leng
fe36b62ec6 Refactor rotation button layout to use Grid
Replaced the `<StackPanel>` with a `<Grid>` for better layout control and flexibility. Added `<ColumnDefinition>` elements to structure the grid with alternating columns for buttons and spacing. Updated the rotation buttons to `<ToggleButton>` elements aligned to specific grid columns, each configured for rotation options (Normal, Left, Right, Inverted). Retained the `<FontIcon>` glyphs for visual consistency.
2025-11-28 16:20:58 +08:00
Yu Leng
ae9dd9970c Add EnableRotation to SignalSettingsUpdated trigger list
The `EnableRotation` property was added to the list of feature
visibility properties that trigger the `SignalSettingsUpdated()`
method when their values change. This ensures that the `PowerDisplay`
UI refreshes appropriately when the `EnableRotation` property is
updated. The change aligns with the existing behavior for other
properties like `EnableContrast`, `EnableVolume`, `EnableInputSource`,
and `IsHidden`. This update addresses the lack of UI refresh for
`EnableRotation` due to `set_config()` not signaling the
`SettingsUpdatedEvent`.
2025-11-28 16:17:39 +08:00
moooyo
bbeea7b2e6 Add display rotation feature with UI controls and settings integration 2025-11-28 05:08:55 +08:00
Yu Leng
589aaf6f3e Add PowerDisplay support to PowerToys Settings UI
Updated `OpenSettings` and `OnSettingsClick` methods in the
`PowerDisplay` namespace to handle the deployment structure
of PowerDisplay as a WinUI 3 app in a subfolder. The
`mainExecutableIsOnTheParentFolder` parameter is set to `true`
to reflect this structure.

Added a new case for `PowerDisplay` in the `Microsoft.PowerToys.Settings.UI`
namespace to enable navigation to the `PowerDisplayPage` in the
PowerToys Settings UI.
2025-11-28 00:27:49 +08:00
Yu Leng
a4e2fe18fe Improve focus handling and window initialization
Added `IsTabStop="True"` to `RootGrid` in `MainWindow.xaml` to make it focusable. Updated `DispatcherQueue.TryEnqueue` in `MainWindow.xaml.cs` to clear focus from interactive elements (e.g., sliders) on window open, preventing unwanted tooltips and ensuring a cleaner initial state.
2025-11-28 00:15:00 +08:00
Yu Leng
1f425f9540 Add input source control support to monitor settings
Introduced support for input source control across the app.

- Added `SupportsInputSource` and `EnableInputSource` properties to data models (`FeatureSupportResult`, `MonitorInfo`, etc.).
- Updated `MainViewModel` and `MonitorViewModel` to handle input source visibility (`ShowInputSource`) and initialization.
- Modified XAML bindings to reflect the new `ShowInputSource` property.
- Enhanced `PowerDisplayPage.xaml` with a checkbox for enabling/disabling input source control.
- Added localization for the input source control checkbox.
- Implemented signaling (`SignalSettingsUpdated`) to notify `PowerDisplay` of feature visibility changes.
- Improved logging to include input source feature status.
- Performed general code cleanup and added clarifying comments.

These changes ensure the input source control feature is configurable, persists user preferences, and integrates seamlessly with the existing application.
2025-11-28 00:05:48 +08:00
Yu Leng
12916deca0 Refactor: Simplify codebase and remove unused methods
Removed unused methods across multiple files, including `GetContrastAsync`, `GetVolumeAsync`, and `SaveCurrentSettingsAsync` in `DdcCiController.cs` and `WmiController.cs`. Simplified the `IMonitorController` interface by removing redundant methods.

Replaced `ProcessThemeChangeAsync` with a synchronous `ProcessThemeChange` in `LightSwitchListener.cs`. Changed `ParseVcpCodesToIntegers` visibility to private in `MonitorFeatureHelper.cs` and removed related helper methods.

Eliminated retry logic by removing `ExecuteWithRetryAsync` in `RetryHelper.cs`. Simplified `SetBrightnessAsync` in `MonitorManager.cs` using a generic helper. Streamlined `DisplayName` in `MonitorViewModel.cs` and removed unnecessary property change notifications.

These changes reduce complexity, improve maintainability, and streamline the codebase by removing unused or redundant functionality.
2025-11-27 22:43:28 +08:00
Yu Leng
1c33cf0348 Refactor TrayIconService to replace CsWin32 dependencies
Replaced CsWin32-generated types and P/Invoke methods with
custom `LibraryImport` declarations and primitive types (`nint`,
`nuint`) to improve accessibility and reduce reliance on
CsWin32. Updated `TrayIconService` to use `nint` for handles
and replaced safe handle types. Simplified `WindowProc` logic
and updated methods like `SetupTrayIcon` and `Destroy` to use
new interop methods.

Added custom constants, enums, and structs for window messages
and menu flags. Removed obsolete entries from `NativeMethods.txt`.
These changes enhance maintainability and ensure compatibility
with standard .NET interop practices.
2025-11-27 21:43:59 +08:00
Yu Leng
7c69874689 Add system tray icon support for PowerDisplay
Introduced a `TrayIconService` to manage the system tray icon, enabling quick access to settings and exit options. Added a new `ShowSystemTrayIcon` setting to control tray icon visibility, with UI integration in the settings page.

Implemented `SettingsDeepLink` to open PowerDisplay settings directly in the PowerToys Settings UI. Updated `App.xaml.cs` to integrate tray icon lifecycle management and refresh behavior.

Replaced `ManagedCsWin32` with `CsWin32` for Windows API interop. Added localized strings for tray menu options and updated default settings to enable the tray icon by default. Improved resilience by handling `WM_TASKBAR_RESTART` for tray icon recreation.
2025-11-27 20:57:54 +08:00
Yu Leng
9b86aef4b3 Fix WMI parameter type mismatch in WmiSetBrightness
Updated the `WmiSetBrightness` method to pass `Timeout` and
`Brightness` parameters as strings instead of numeric types
to ensure compatibility with WMI driver implementations
that require string values. Updated comments to reflect
this change and clarify the reasoning behind it.
2025-11-27 18:14:14 +08:00
Yu Leng
59d0ac58aa Fix WMI Brightness type mismatch issue
Updated the `Brightness` parameter in the `WmiSetBrightness` method to use `int` instead of casting to `byte`, addressing potential `WBEM_E_TYPE_MISMATCH` errors (0x80041005) caused by certain WMI driver implementations expecting `VT_I4` instead of `VT_UI1`.

Added comments to clarify the rationale for this change. Applied the fix in two sections of `WmiController.cs`, including both dynamic and hardcoded `Brightness` values.
2025-11-27 18:08:49 +08:00
Yu Leng
dac9a3de50 Refactor: Remove unused classes and constants
Simplified the codebase by removing unused or redundant functionality:
- Removed `State` and `Lifetime` nested classes from `AppConstants`.
- Deleted unused constants from the `UI` nested class in `AppConstants`.
- Removed the `MonitorStatusChangedEventArgs` class from `PowerDisplay.Core.Interfaces`.
- Deleted the `SettingsDeepLink` helper class from `PowerDisplay.Helpers`.
- Cleaned up `WindowHelper` by removing unused constants, P/Invoke declarations, and the `MakeWindowTransparent` method.

These changes improve maintainability and reduce code complexity.
2025-11-27 18:02:59 +08:00
Yu Leng
6b634ca0d3 Refactor DDC/CI logic and remove "Disable" functionality
Refactored `CanControlMonitorAsync` in `DdcCiController.cs`:
- Updated XML documentation to reflect that monitor capabilities
  are always cached during discovery, enabling quick connection
  checks exclusively.
- Removed fallback logic for full validation and obsolete code
  related to `ValidateDdcCiConnection`.

Removed the "Disable" button from `MainWindow.xaml` and its
associated `OnDisableClick` event handler in `MainWindow.xaml.cs`:
- Deleted the button and its properties from the XAML file.
- Removed the event handler logic for toggling monitor control
  availability and updating the status text.

These changes simplify the codebase, align with the updated
behavior of cached capabilities, and deprecate unused features.
2025-11-27 17:41:32 +08:00
Yu Leng
79c155e422 Optimize monitor discovery and validation process
Refactored monitor discovery to a two-phase process, enabling
parallel capability fetching and reducing total discovery time.
Introduced caching of monitor capabilities to avoid redundant
I2C operations, improving performance during initialization
and runtime validation.

Added `DdcCiValidationResult` to encapsulate validation status
and cached capabilities. Replaced `ValidateDdcCiConnection`
with `FetchCapabilities` for capability retrieval, marking the
former as obsolete. Introduced `QuickConnectionCheck` for fast
runtime validation.

Updated `CanControlMonitorAsync`, `GetCapabilitiesStringAsync`,
and `InitializeMonitorCapabilitiesAsync` to leverage cached
data. Improved logging for better insights into discovery and
validation processes.
2025-11-27 17:34:44 +08:00
Yu Leng
04c1a2cac9 Update VCP codes in testCodes array for validation
Replaced `VcpCodeNewControlValue` with `VcpCodeContrast` and
added `VcpCodeVolume` to the `testCodes` array in
`DdcCiNative.cs` within the `PowerDisplay.Common.Drivers.DDC`
namespace. This change enhances the connection validation
process by including additional VCP features such as contrast
and volume.
2025-11-27 16:41:10 +08:00
Yu Leng
42db185274 Validate DDC/CI for all handles and improve logging
Previously, DDC/CI validation was skipped for reused handles.
This change ensures validation is performed for all handles,
excluding monitors that do not support DDC/CI (e.g., internal
laptop displays).

Additionally, the log message for failed validation now
includes whether the handle was reused and uses `LogDebug`
instead of `LogWarning` to adjust the logging level.
2025-11-27 16:40:29 +08:00
Yu Leng
0c43859784 Enhance input source selection in UI
Added dynamic visibility binding to the input source button. Updated the `ListView` to bind to `AvailableInputSources` and replaced hardcoded items with a `DataTemplate` for better flexibility. Introduced `InputSourceListView_SelectionChanged` to handle selection changes, update the monitor's input source, and close the flyout after selection. Added logging for improved debugging and error handling.
2025-11-27 16:24:33 +08:00
Yu Leng
8bdd2ffdfd merge niels changes 2025-11-27 15:21:14 +08:00
Yu Leng
be23f2d7fd Add input source management via VCP code 0x60
Introduced functionality to get and set monitor input sources using VESA MCCS VCP code 0x60. This includes backend logic, UI integration, and error handling.

Backend changes:
- Added `GetInputSourceAsync` and `SetInputSourceAsync` in `DdcCiController.cs` for input source management.
- Defined `VcpCodeInputSource` constant in `NativeConstants.cs`.
- Updated `Monitor.cs` to include `CurrentInputSource`, `InputSourceName`, and support detection for input sources.
- Enhanced `MonitorManager.cs` to initialize and manage input source capabilities.

UI changes:
- Added a "More Actions" button in `MainWindow.xaml` for input source switching.
- Implemented a flyout menu to display and select available input sources.
- Added `InputSourceItem_Click` handler in `MainWindow.xaml.cs`.

ViewModel changes:
- Introduced `InputSourceItem` class for UI representation of input sources.
- Updated `MonitorViewModel.cs` to expose input source properties and handle switching.

Other improvements:
- Added detailed logging for input source operations.
- Implemented fallback mechanisms for robust behavior.
- Enhanced user experience with dynamic UI updates for input source changes.
2025-11-27 14:51:31 +08:00
Niels Laute
84f8c45733 UX tweaks 2025-11-26 18:07:20 +01:00
Yu Leng
3598c2c126 Refactor and enhance monitor matching logic
- Renamed project entries in `PowerToys.sln` for consistency.
- Added new projects: "runner," "NewShellExtensionContextMenu," and "BgcodePreviewHandlerCpp."
- Introduced `MonitorMatchingHelper` to centralize monitor identification logic.
- Added unit tests for `MonitorMatchingHelper` to validate parsing and matching.
- Updated `MonitorInfo` with `MonitorNumber`, `TotalMonitorCount`, and `DisplayName` for dynamic formatting.
- Enhanced WMI monitor matching with pre-fetched display devices.
- Updated UI components to use dynamic `DisplayName` for monitors.
- Added `PowerDisplay.Lib.UnitTests` project for testing.
- Improved serialization, logging, and null handling in `PowerDisplayViewModel`.
- Removed redundant parsing logic from `MonitorDiscoveryHelper`.
2025-11-26 19:08:01 +08:00
Yu Leng
65af62e77a Add monitor number and dynamic display name handling
Enhanced multi-monitor support by introducing a `MonitorNumber` property in the `Monitor` model to represent the Windows DISPLAY number. Updated the `MonitorViewModel` to include a `DisplayName` property that appends the monitor number to the name when multiple monitors are visible.

Modified `MainWindow.xaml` to bind to `DisplayName` instead of `Name`, ensuring the UI reflects the updated naming convention. Added logic to update `DisplayName` dynamically when the monitor count changes, improving clarity in multi-monitor setups.

Included comments in `MonitorViewModel` for better code readability and maintainability.
2025-11-26 14:50:57 +08:00
Yu Leng
a68f31aa59 Enhance "Identify Monitors" feature with transparency
Refactor and enhance the "Identify Monitors" feature:
- Added constants, P/Invoke methods, and `MakeWindowTransparent` in `WindowHelper.cs` to support layered windows and transparency.
- Redesigned `IdentifyWindow` with WinUI 3 features (`AppWindow`, `DesktopAcrylicController`) for a polished, transparent appearance.
- Simplified `IdentifyWindow.xaml` UI for a cleaner design.
- Fully implemented monitor identification in `MainViewModel.cs`:
  - Created and positioned windows for each monitor.
  - Handled DPI scaling and display coordinates.
  - Added detailed logging and error handling.
2025-11-26 06:52:53 +08:00
Yu Leng
4ff44b382b Add monitor identification feature with orientation support
Enhanced monitor discovery to include monitor numbers and orientations.
- Added `ParseMonitorNumber` and `GetMonitorOrientation` in `MonitorDiscoveryHelper.cs`.
- Introduced `DevMode` structure and `EnumDisplaySettings` P/Invoke for retrieving display settings.
- Updated `IMonitorData` and `Monitor.cs` to support new properties.
- Sorted monitors by number in `MonitorManager.cs`.

Implemented a new "Identify Monitors" feature:
- Added `IdentifyWindow.xaml` to display monitor numbers visually.
- Added `IdentifyMonitorsCommand` in `MainViewModel.cs` to trigger identification.
- Updated `MainWindow.xaml` to include an "Identify Monitors" button.

Improved code readability with comments and updated license headers.
2025-11-26 05:57:26 +08:00
Yu Leng
de44da04de Refactor and enhance monitor control logic
- Consolidated brightness and capabilities retrieval logic with `GetBrightnessInfoCore` and `RetryHelper` for improved modularity and resiliency.
- Introduced `LockedDictionary` for thread-safe dictionary operations, replacing manual locking in `PhysicalMonitorHandleManager` and `MonitorStateManager`.
- Refactored monitor discovery in `MonitorManager` to separate discovery, initialization, and validation steps for better maintainability.
- Simplified event registration in `App.xaml.cs` with helper methods to reduce repetitive code.
- Enhanced VCP code handling with new methods for formatted and sorted VCP code retrieval.
- Added `SuspendNotifications` in `MonitorInfo` to optimize batch property updates and improve UI performance.
- Simplified parameter update methods in `MonitorViewModel` by removing redundant `fromProfile` logic.
- Improved state management with synchronous save support and reusable JSON building logic in `MonitorStateManager`.
- Updated UI bindings in `ProfileEditorDialog.xaml` and improved VCP code display in `MainViewModel`.
- Cleaned up redundant code, improved logging, and standardized method naming for better readability and maintainability.
2025-11-26 05:02:49 +08:00
Yu Leng
f5a2235f53 Improve logging, synchronization, and process handling
Enhanced logging for event handling, settings updates, and process management to improve traceability and debugging.

- Added detailed logging and exception handling in `NativeEventWaiter.cs` for event listener setup and execution.
- Updated `MainViewModel.Settings.cs` to synchronize monitor settings and save changes after applying color temperature.
- Improved process management in `dllmain.cpp` to ensure `PowerDisplay` is running before signaling events, with added race condition prevention.
- Removed redundant settings update signaling in `dllmain.cpp` to avoid excessive UI refreshes and dropdown closures.
- Enhanced monitor synchronization in `PowerDisplayViewModel.cs` to handle pending operations and prevent stale data overwrites.
- General improvements in error handling and logging across all changes.
2025-11-25 16:45:06 +08:00
Yu Leng
738b6696c5 Refactor UI and improve monitor settings handling
Refactored `PowerDisplayPage.xaml` and `ProfileEditorDialog.xaml` to use `SettingsExpander` and `SettingsCard` components for a cleaner, modular design. Added dynamic monitor icons via the new `MonitorIconGlyph` property in `MonitorInfo.cs`.

Optimized `VcpCodesFormatted` property with a comparison method to prevent unnecessary updates. Enhanced color temperature handling with confirmation dialogs and improved event handling to prevent re-entrant logic.

Updated resource strings for consistency and improved layout, spacing, and visual hierarchy across the UI. Removed redundant code and improved maintainability.
2025-11-25 15:44:50 +08:00
Yu Leng
649cd8fd5a Switch to DesktopAcrylicBackdrop for system backdrop
Replaced `WindowEx.Backdrop` using `AcrylicSystemBackdrop` with
`WindowEx.SystemBackdrop` using `DesktopAcrylicBackdrop`.
Simplified configuration by removing detailed dark and light
theme properties such as fallback colors, luminosity opacity,
and tint settings.
2025-11-25 05:58:31 +08:00
Yu Leng
933ca71c6d Simplify window resizing and logging logic
Removed logic for resizing the window when height differences
are negligible (<1px), including associated debug logs and
DPI-aware height conversion. Eliminated the `LogActualSizes`
method, which logged detailed UI element sizes and layout
information. Simplified `GetContentHeight` by replacing
`RootGrid.FindName` with a direct `MainContainer` check and
removing debug logs. These changes streamline the code and
reduce logging verbosity.
2025-11-25 05:51:05 +08:00
Yu Leng
a59ea081b3 Improve DPI scaling, layout, and UI design
Refactored `WindowHelper.cs` to add DPI-aware utilities for scaling, positioning, and converting units. Updated `MainWindow.xaml` to modernize the UI with better structure, scrolling, and visibility bindings. Enhanced window resizing logic in `MainWindow.xaml.cs` with DPI scaling, logging, and layout diagnostics.

Simplified `AdjustWindowSizeToContent` and `GetContentHeight` methods for precise content measurement. Replaced redundant positioning logic with a unified DPI-aware approach. Improved settings handling in `MainViewModel.Settings.cs` by consolidating monitor filtering logic.

Added minimum size constraints to `MainWindow.xaml` for layout stability. Introduced detailed logging for debugging layout and scaling issues. Overall, these changes enhance maintainability, scalability, and user experience.
2025-11-25 05:48:33 +08:00
Niels Laute
9c23cbd448 More changes to the Settings page 2025-11-24 20:22:36 +01:00
Yu Leng
c1a82420ed Improve handling of hidden monitors in settings
Added detailed logging for skipped hidden monitors in `MainViewModel.Monitors.cs`.
Implemented functionality in `MainViewModel.Settings.cs` to remove hidden monitors
from the `Monitors` collection during settings updates. Triggered UI updates to
reflect changes in monitor visibility and ensure proper messaging when no monitors
are available. Enhanced maintainability and traceability through improved logging
and UI notifications.
2025-11-25 02:43:47 +08:00
Yu Leng
b0feeb1f9c Improve logging and add new Slider controls
Enhanced logging in Slider_PointerCaptureLost for better traceability and error handling. Added debug and warning logs to capture state and handle null checks for `slider`, `monitorVm`, and `propertyName`.

Introduced new Slider controls in MainWindow.xaml for `BrightnessAutomation`, `ContrastAutomation`, and `VolumeAutomation` with proper bindings, localization support, and layout adjustments. Improved user experience and dynamic control behavior.
2025-11-25 02:15:08 +08:00
Yu Leng
5fb1183027 Merge niels9001/powerdisplay-UX branch 2025-11-25 01:35:17 +08:00
Yu Leng
471cad659f Refactor and optimize profile and monitor handling
Refactored profile name generation by centralizing logic in `ProfileHelper` with overloads for flexibility. Simplified folder creation logic in `PathConstants` using a reusable helper method.

Improved profile loading and saving in `ProfileService` with internal helper methods for better error handling and reduced duplication. Optimized monitor key generation and lookup with concise expressions and dictionary-based retrieval for O(1) performance.

Introduced caching for color presets in `MonitorInfo` to avoid redundant computations and added a helper for range validation in `MainViewModel.Settings`. Centralized percentage formatting and property change handling to reduce duplication.

Removed redundant methods in `PowerDisplayViewModel` and streamlined event unsubscription in `MainWindow`. Enhanced logging, readability, and maintainability across the codebase.
2025-11-24 23:36:25 +08: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
Yu Leng
15746e8f45 Refactor and enhance monitor management system
Refactored namespaces to improve modularity, including moving `PowerDisplay.Native` to `PowerDisplay.Common.Drivers`. Introduced the `IMonitorData` interface for better abstraction of monitor hardware data. Replaced `ColorTemperature` with `ColorTemperatureVcp` for precise VCP-based color temperature control, adding utilities for Kelvin conversion.

Enhanced monitor state management with a new `MonitorStateFile` for JSON persistence and updated `MonitorStateManager` for debounced saves. Added `MonitorMatchingHelper` for consistent monitor identification and `ProfileHelper` for profile management operations.

Refactored P/Invoke declarations into helper classes, updated UI bindings for `ColorTemperatureVcp`, and improved logging for better runtime visibility. Removed redundant code, added new utility classes (`MonitorValueConverter`, `MonitorMatchingHelper`), and ensured backward compatibility.

These changes improve code organization, maintainability, and extensibility while aligning with hardware-level control standards.
2025-11-24 21:58:34 +08: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
580651b47a Refactor PowerDisplay module and add shared library
Introduced `PowerDisplay.Lib` to centralize shared logic and models, improving modularity and reusability. Refactored namespaces, moving classes to `PowerDisplay.Common`. Added utilities like `ColorTemperatureHelper` and `MonitorFeatureHelper` for consistent logic.

Replaced `ProfileManager` with `ProfileService` for centralized profile management. Enhanced event handling, monitor state management, and settings synchronization. Improved color temperature handling and feature detection.

Removed redundant code and converters. Updated `Settings.UI.Library` and XAML bindings to use shared models. Enhanced logging, serialization, and disposal logic. Updated project files and added documentation for better maintainability.
2025-11-24 18:08:11 +08: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
159 changed files with 20610 additions and 164 deletions

View File

@@ -11,6 +11,7 @@ ACCESSDENIED
ACCESSTOKEN
acfs
ACIE
ACR
AClient
AColumn
acrt
@@ -92,6 +93,7 @@ asf
Ashcraft
AShortcut
ASingle
ASUS
ASSOCCHANGED
ASSOCF
ASSOCSTR
@@ -102,6 +104,7 @@ ATRIOX
ATX
aumid
authenticode
AUO
AUTOBUDDY
AUTOCHECKBOX
AUTOHIDE
@@ -119,6 +122,10 @@ azureaiinference
azureinference
azureopenai
backticks
Backlight
Badflags
Badmode
Badparam
bbwe
BCIE
bck
@@ -190,7 +197,10 @@ CARETBLINKING
Carlseibert
CAtl
caub
CAuthn
CAuthz
CBN
Cds
cch
CCHDEVICENAME
CCHFORMNAME
@@ -209,9 +219,11 @@ changecursor
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
Chunghwa
cidl
CIELCh
cim
CImp
CImage
cla
CLASSDC
@@ -219,7 +231,7 @@ CLASSNOTAVAILABLE
CLEARTYPE
clickable
clickonce
CLIENTEDGE
clientedge
clientid
clientside
CLIPBOARDUPDATE
@@ -231,6 +243,7 @@ CLSCTX
clsids
Clusion
cmder
CMN
CMDNOTFOUNDMODULEINTERFACE
cmdpal
CMIC
@@ -246,7 +259,7 @@ codereview
Codespaces
Coen
cognitiveservices
COINIT
coinit
colid
colorconv
colorformat
@@ -285,6 +298,7 @@ Corpor
cotaskmem
COULDNOT
countof
Cowait
covrun
cpcontrols
cph
@@ -304,11 +318,13 @@ CRECT
CRH
critsec
cropandlock
crt
Crossdevice
csdevkit
CSearch
CSettings
cso
CSOT
CSRW
CStyle
cswin
@@ -349,11 +365,17 @@ DBPROP
DBPROPIDSET
DBPROPSET
DCBA
DCapabilities
DCOM
DComposition
DCR
ddc
Ddc
Ddcci
ddcutil
DDEIf
Deact
debouncer
debugbreak
decryptor
Dedup
@@ -370,6 +392,7 @@ DEFAULTTOPRIMARY
DEFERERASE
DEFPUSHBUTTON
deinitialization
DELA
DELETEDKEYIMAGE
DELETESCANS
DEMOTYPE
@@ -399,18 +422,21 @@ DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
DISPLAYCHANGE
DISPLAYCONFIG
displayconfig
DISPLAYFLAGS
DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
Displayport
diu
divyan
Dlg
DLGFRAME
DLGMODALFRAME
dlgmodalframe
dlib
dllhost
dllmain
Dmdo
DNLEN
DONOTROUND
DONTVALIDATEPATH
@@ -427,6 +453,7 @@ DRAWCLIPBOARD
DRAWFRAME
drawingcolor
dreamsofameaningfullife
DREGION
drivedetectionwarning
DROPFILES
DSTINVERT
@@ -438,6 +465,7 @@ dutil
DVASPECT
DVASPECTINFO
DVD
dvi
dvr
DVTARGETDEVICE
dwflags
@@ -457,15 +485,19 @@ DWMWINDOWMAXIMIZEDCHANGE
DWORDLONG
dworigin
dwrite
Dxva
dxgi
eab
EAccess
easeofaccess
ecount
Edid
edid
EDITKEYBOARD
EDITSHORTCUTS
EDITTEXT
EFile
EInvalid
eep
eku
emojis
ENABLEDELAYEDEXPANSION
@@ -475,14 +507,16 @@ ENABLETEMPLATE
encodedlaunch
encryptor
ENDSESSION
ENot
ENSUREVISIBLE
ENTERSIZEMOVE
ENTRYW
ENU
environmentvariables
EOAC
eoac
EPO
epu
EProvider
ERASEBKGND
EREOF
EResize
@@ -536,6 +570,7 @@ fdx
FErase
fesf
FFFF
FFh
FInc
Figma
FILEEXPLORER
@@ -577,7 +612,9 @@ FORMATDLGORD
formatetc
FORPARSING
foundrylocal
FPrimary
FRAMECHANGED
Framechanged
FRestore
frm
FROMTOUCH
@@ -637,6 +674,8 @@ gwl
GWLP
GWLSTYLE
hangeul
Hann
Hantai
Hanzi
Hardlines
hardlinks
@@ -658,6 +697,8 @@ HCRYPTPROV
hcursor
hcwhite
hdc
hdmi
HDMI
hdr
hdrop
hdwwiz
@@ -694,6 +735,7 @@ HKPD
HKU
HMD
hmenu
HMON
hmodule
hmonitor
homies
@@ -711,6 +753,7 @@ hotkeys
hotlight
hotspot
HPAINTBUFFER
HPhysical
HRAWINPUT
hredraw
hres
@@ -721,6 +764,7 @@ hsb
HSCROLL
hsi
HSpeed
HSync
HTCLIENT
hthumbnail
HTOUCHINPUT
@@ -730,6 +774,7 @@ HVal
HValue
Hvci
hwb
HWP
HWHEEL
HWINEVENTHOOK
hwnd
@@ -786,6 +831,7 @@ INITTOLOGFONTSTRUCT
INLINEPREFIX
inlines
Inno
Innolux
INPC
inproc
INPUTHARDWARE
@@ -827,6 +873,7 @@ istep
ith
ITHUMBNAIL
IUI
IVO
IUWP
IWIC
jfif
@@ -838,6 +885,7 @@ jpnime
Jsons
jsonval
jxr
Kantai
keybd
KEYBDDATA
KEYBDINPUT
@@ -859,6 +907,7 @@ KILLFOCUS
killrunner
kmph
kvp
KVM
Kybd
LARGEICON
lastcodeanalysissucceeded
@@ -878,6 +927,8 @@ LEFTTEXT
LError
LEVELID
LExit
Lenovo
LGD
lhwnd
LIBFUZZER
LIBID
@@ -982,6 +1033,7 @@ MAPTOSAMESHORTCUT
MAPVK
MARKDOWNPREVIEWHANDLERCPP
MAXIMIZEBOX
Maximizebox
MAXSHORTCUTSIZE
maxversiontested
mber
@@ -992,6 +1044,8 @@ MDL
mdtext
mdtxt
mdwn
Mccs
mccs
meme
mcp
memicmp
@@ -1013,6 +1067,7 @@ mikeclayton
mindaro
Minimizable
MINIMIZEBOX
Minimizebox
MINIMIZEEND
MINIMIZESTART
MINMAXINFO
@@ -1032,6 +1087,7 @@ MODALFRAME
MODESPRUNED
MONITORENUMPROC
MONITORINFO
Monitorinfo
MONITORINFOEX
MONITORINFOEXW
monitorinfof
@@ -1072,9 +1128,10 @@ MSLLHOOKSTRUCT
Mso
msrc
msstore
mstsc
mswhql
msvcp
MT
mstsc
MTND
MULTIPLEUSE
multizone
@@ -1090,6 +1147,7 @@ MVVMTK
MWBEx
MYICON
NAMECHANGE
Nanjing
namespaceanddescendants
nao
NCACTIVATE
@@ -1158,6 +1216,7 @@ NOMCX
NOMINMAX
NOMIRRORBITMAP
NOMOVE
Nomove
NONANTIALIASED
nonclient
NONCLIENTMETRICSW
@@ -1179,6 +1238,7 @@ NORMALUSER
NOSEARCH
NOSENDCHANGING
NOSIZE
Nosize
NOTHOUSANDS
NOTICKS
NOTIFICATIONSDLL
@@ -1186,9 +1246,11 @@ NOTIFYICONDATA
NOTIFYICONDATAW
NOTIMPL
NOTOPMOST
Notopmost
NOTRACK
NOTSRCCOPY
NOTSRCERASE
Notupdated
notwindows
NOTXORPEN
nowarn
@@ -1230,10 +1292,9 @@ OPENFILENAME
openrdp
opensource
openxmlformats
ollama
onnx
openurl
OPTIMIZEFORINVOKE
Optronics
ORPHANEDDIALOGTITLE
ORSCANS
oss
@@ -1269,6 +1330,7 @@ PATINVERT
PATPAINT
pbc
pbi
PBP
PBlob
pbrush
pcb
@@ -1283,6 +1345,7 @@ PDBs
PDEVMODE
pdisp
PDLL
pdmodels
pdo
pdto
pdtobj
@@ -1305,6 +1368,7 @@ pguid
phbm
phbmp
phicon
PHL
Photoshop
phwnd
pici
@@ -1336,6 +1400,8 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
powerdisplay
POWERDISPLAYMODULEINTERFACE
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1390,6 +1456,7 @@ projectname
PROPERTYKEY
Propset
PROPVARIANT
prot
PRTL
prvpane
psapi
@@ -1417,12 +1484,15 @@ PTOKEN
PToy
ptstr
pui
pvct
PWAs
pwcs
PWSTR
pwsz
pwtd
QDC
qdc
QDS
qit
QITAB
QITABENT
@@ -1447,7 +1517,6 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
remotedesktop
rdp
RDW
READMODE
@@ -1476,6 +1545,7 @@ remappings
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
remotedesktop
remoteip
Removelnk
renamable
@@ -1549,6 +1619,7 @@ scrollviewer
SDDL
SDKDDK
sdns
Sdr
searchterm
SEARCHUI
secondaryclickaction
@@ -1703,6 +1774,7 @@ STARTUPINFOW
startupscreen
STATFLAG
STATICEDGE
Staticedge
STATSTG
stdafx
STDAPI
@@ -1745,7 +1817,7 @@ SVGIO
svgz
SVSI
SWFO
SWP
swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
@@ -1765,6 +1837,7 @@ syskeydown
SYSKEYUP
SYSLIB
SYSMENU
Sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
@@ -1804,7 +1877,9 @@ THEMECHANGED
themeresources
THH
THICKFRAME
Thickframe
THISCOMPONENT
Tianma
throughs
TILEDWINDOW
TILLSON
@@ -1891,7 +1966,7 @@ unzoom
UOffset
UOI
UPDATENOW
UPDATEREGISTRY
updateregistry
updown
UPGRADINGPRODUCTCODE
upscaling
@@ -1918,6 +1993,9 @@ vcamp
vcenter
vcgtq
VCINSTALLDIR
Vcp
vcp
vcpname
Vcpkg
VCRT
vcruntime
@@ -1930,7 +2008,10 @@ VERIFYCONTEXT
VERSIONINFO
VERTRES
VERTSIZE
VESA
vesa
VFT
Vga
vget
vgetq
viewmodels
@@ -1960,6 +2041,7 @@ VSM
vso
vsonline
VSpeed
VSync
vstemplate
vstest
VSTHRD
@@ -2001,7 +2083,7 @@ winapi
winappsdk
windir
WINDOWCREATED
WINDOWEDGE
windowedge
WINDOWINFO
WINDOWNAME
WINDOWPLACEMENT
@@ -2025,7 +2107,7 @@ WINL
winlogon
winmd
winml
WINNT
winnt
winres
winrt
winsdk
@@ -2042,6 +2124,7 @@ WKSG
Wlkr
wmain
Wman
wmi
WMI
WMICIM
wmimgmt

View File

@@ -203,6 +203,11 @@
"PowerToys.PowerAccentModuleInterface.dll",
"PowerToys.PowerAccentKeyboardService.dll",
"PowerToys.PowerDisplayModuleInterface.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
"PowerDisplay.Lib.dll",
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
"WinUI3Apps\\PowerToys.PowerRename.exe",
"WinUI3Apps\\PowerToys.PowerRenameContextMenu.dll",
@@ -371,6 +376,8 @@
"UnitsNet.dll",
"UtfUnknown.dll",
"Wpf.Ui.dll",
"WmiLight.dll",
"WmiLight.Native.dll",
"Shmuelie.WinRTServer.dll",
"ToolGood.Words.Pinyin.dll"
],

View File

@@ -91,6 +91,7 @@
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.5.0" />
<PackageVersion Include="Polly.Core" Version="8.6.5" />
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
@@ -102,6 +103,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.10" />
<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.10" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
@@ -131,6 +133,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

@@ -10,6 +10,7 @@ This software incorporates material from third parties.
- Installer/Runner
- Measure tool
- Peek
- PowerDisplay
- Registry Preview
## Utility: Color Picker
@@ -1519,6 +1520,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## 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.
## NuGet Packages used by PowerToys
@@ -1557,6 +1587,7 @@ SOFTWARE.
- NLog.Extensions.Logging
- NLog.Schema
- OpenAI
- Polly.Core
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
@@ -1569,5 +1600,6 @@ SOFTWARE.
- UnitsNet
- UTF.Unknown
- WinUIEx
- WmiLight
- WPF-UI
- WyHash

View File

@@ -667,6 +667,23 @@
<Deploy />
</Project>
</Folder>
<Folder Name="/modules/PowerDisplay/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
</Folder>
<Folder Name="/modules/PowerDisplay/Tests/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/MeasureTool/">
<Project Path="src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj" Id="54a93af7-60c7-4f6c-99d2-fbb1f75f853a">
<BuildDependency Project="src/common/Display/Display.vcxproj" />

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
# MCCS Capabilities String Parser - Recursive Descent Design
## Overview
This document describes the recursive descent parser implementation for DDC/CI MCCS (Monitor Control Command Set) capabilities strings.
### Attention!
This document and the code implement are generated by Copilot.
## Grammar Definition (BNF)
```bnf
capabilities ::= ['('] segment* [')']
segment ::= identifier '(' segment_content ')'
segment_content ::= text | vcp_entries | hex_list
vcp_entries ::= vcp_entry*
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
hex_list ::= hex_byte*
hex_byte ::= [0-9A-Fa-f]{2}
identifier ::= [a-z_A-Z]+
text ::= [^()]+
```
## Example Input
```
(prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03 07)vcp(10 12 14(04 05 06) 16 60(11 12 0F) DC DF)mccs_ver(2.2)vcpname(F0(Custom Setting)))
```
## Parser Architecture
### Component Hierarchy
```
MccsCapabilitiesParser (main parser)
├── ParseCapabilities() → MccsParseResult
├── ParseSegment() → ParsedSegment?
├── ParseBalancedContent() → string
├── ParseIdentifier() → ReadOnlySpan<char>
├── ApplySegment() → void
│ ├── ParseHexList() → List<byte>
│ ├── ParseVcpEntries() → Dictionary<byte, VcpCodeInfo>
│ └── ParseVcpNames() → void
├── VcpEntryParser (sub-parser for vcp() content)
│ └── TryParseEntry() → VcpEntry
├── VcpNameParser (sub-parser for vcpname() content)
│ └── TryParseEntry() → (byte code, string name)
└── WindowParser (sub-parser for windowN() content)
├── Parse() → WindowCapability
└── ParseSubSegment() → (name, content)?
```
### Design Principles
1. **ref struct for Zero Allocation**
- Main parser uses `ref struct` to avoid heap allocation
- Works with `ReadOnlySpan<char>` for efficient string slicing
- No intermediate string allocations during parsing
2. **Recursive Descent Pattern**
- Each grammar rule has a corresponding parse method
- Methods call each other recursively for nested structures
- Single-character lookahead via `Peek()`
3. **Error Recovery**
- Errors are accumulated, not thrown
- Parser attempts to continue after errors
- Returns partial results when possible
4. **Sub-parsers for Specialized Content**
- `VcpEntryParser` for VCP code entries
- `VcpNameParser` for custom VCP names
- Each sub-parser handles its own grammar subset
## Parse Methods Detail
### ParseCapabilities()
Entry point. Handles optional outer parentheses and iterates through segments.
```csharp
private MccsParseResult ParseCapabilities()
{
// Handle optional outer parens
// while (!IsAtEnd()) { ParseSegment() }
// Return result with accumulated errors
}
```
### ParseSegment()
Parses a single `identifier(content)` segment.
```csharp
private ParsedSegment? ParseSegment()
{
// 1. ParseIdentifier()
// 2. Expect '('
// 3. ParseBalancedContent()
// 4. Expect ')'
}
```
### ParseBalancedContent()
Extracts content between balanced parentheses, handling nested parens.
```csharp
private string ParseBalancedContent()
{
int depth = 1;
while (depth > 0) {
if (char == '(') depth++;
if (char == ')') depth--;
}
}
```
### ParseVcpEntries()
Delegates to `VcpEntryParser` for the specialized VCP entry grammar.
```csharp
vcp_entry ::= hex_byte [ '(' hex_list ')' ]
Examples:
- "10" code=0x10, values=[]
- "14(04 05 06)" code=0x14, values=[4, 5, 6]
- "60(11 12 0F)" code=0x60, values=[0x11, 0x12, 0x0F]
```
## Comparison with Other Approaches
| Approach | Pros | Cons |
|----------|------|------|
| **Recursive Descent** (this) | Clear structure, handles nesting, extensible | More code |
| **Regex** (DDCSharp) | Concise | Hard to debug, limited nesting |
| **Mixed** (original) | Pragmatic | Inconsistent, hard to maintain |
## Performance Characteristics
- **Time Complexity**: O(n) where n = input length
- **Space Complexity**: O(1) for parsing + O(m) for output where m = number of VCP codes
- **Allocations**: Minimal - only for output structures
## Supported Segments
| Segment | Description | Parser |
|---------|-------------|--------|
| `prot(...)` | Protocol type | Direct assignment |
| `type(...)` | Display type (lcd/crt) | Direct assignment |
| `model(...)` | Model name | Direct assignment |
| `cmds(...)` | Supported commands | ParseHexList |
| `vcp(...)` | VCP code entries | VcpEntryParser |
| `mccs_ver(...)` | MCCS version | Direct assignment |
| `vcpname(...)` | Custom VCP names | VcpNameParser |
| `windowN(...)` | PIP/PBP window capabilities | WindowParser |
### Window Segment Format
The `windowN` segment (where N is 1, 2, 3, etc.) describes PIP/PBP window capabilities:
```
window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10))
```
| Sub-field | Format | Description |
|-----------|--------|-------------|
| `type` | `type(PIP)` or `type(PBP)` | Window type (Picture-in-Picture or Picture-by-Picture) |
| `area` | `area(x1 y1 x2 y2)` | Window area coordinates in pixels |
| `max` | `max(width height)` | Maximum window dimensions |
| `min` | `min(width height)` | Minimum window dimensions |
| `window` | `window(id)` | Window identifier |
All sub-fields are optional; missing fields default to zero values.
## Error Handling
```csharp
public readonly struct ParseError
{
public int Position { get; } // Character position
public string Message { get; } // Human-readable error
}
public sealed class MccsParseResult
{
public VcpCapabilities Capabilities { get; }
public IReadOnlyList<ParseError> Errors { get; }
public bool HasErrors => Errors.Count > 0;
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
}
```
## Usage Example
```csharp
// Parse capabilities string
var result = MccsCapabilitiesParser.Parse(capabilitiesString);
if (result.IsValid)
{
var caps = result.Capabilities;
Console.WriteLine($"Model: {caps.Model}");
Console.WriteLine($"MCCS Version: {caps.MccsVersion}");
Console.WriteLine($"VCP Codes: {caps.SupportedVcpCodes.Count}");
}
if (result.HasErrors)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Parse error at {error.Position}: {error.Message}");
}
}
```
## Edge Cases Handled
1. **Missing outer parentheses** (Apple Cinema Display)
2. **No spaces between hex bytes** (`010203` vs `01 02 03`)
3. **Nested parentheses** in VCP values
4. **Unknown segments** (logged but not fatal)
5. **Malformed input** (partial results returned)

View File

@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 42> processesToTerminate = {
std::array<std::wstring_view, 43> processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1565,6 +1565,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
L"PowerToys.LightSwitchService.exe",
L"PowerToys.PowerDisplay.exe",
L"PowerToys.GcodeThumbnailProvider.exe",
L"PowerToys.BgcodeThumbnailProvider.exe",
L"PowerToys.PdfThumbnailProvider.exe",

View File

@@ -0,0 +1,29 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util" >
<?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

@@ -47,6 +47,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="PowerDisplay.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />

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

@@ -176,6 +176,10 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
#PowerDisplay
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay"
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs

View File

@@ -45,6 +45,7 @@ namespace Common.UI
NewPlus,
CmdPal,
ZoomIt,
PowerDisplay,
}
private static string SettingsWindowNameToString(SettingsWindow value)
@@ -115,6 +116,8 @@ namespace Common.UI
return "CmdPal";
case SettingsWindow.ZoomIt:
return "ZoomIt";
case SettingsWindow.PowerDisplay:
return "PowerDisplay";
default:
{
return string.Empty;

View File

@@ -32,6 +32,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredPowerDisplayEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerDisplayEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredFancyZonesEnabledValue());

View File

@@ -14,6 +14,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

@@ -18,6 +18,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredPowerDisplayEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();

View File

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

View File

@@ -247,4 +247,36 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::CMDPAL_SHOW_EVENT;
}
hstring Constants::TogglePowerDisplayEvent()
{
return CommonSharedConstants::TOGGLE_POWER_DISPLAY_EVENT;
}
hstring Constants::TerminatePowerDisplayEvent()
{
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
}
hstring Constants::RefreshPowerDisplayMonitorsEvent()
{
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
}
hstring Constants::SettingsUpdatedPowerDisplayEvent()
{
return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT;
}
hstring Constants::ApplyColorTemperaturePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_COLOR_TEMPERATURE_POWER_DISPLAY_EVENT;
}
hstring Constants::ApplyProfilePowerDisplayEvent()
{
return CommonSharedConstants::APPLY_PROFILE_POWER_DISPLAY_EVENT;
}
hstring Constants::PowerDisplaySendSettingsTelemetryEvent()
{
return CommonSharedConstants::POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT;
}
hstring Constants::HotkeyUpdatedPowerDisplayEvent()
{
return CommonSharedConstants::HOTKEY_UPDATED_POWER_DISPLAY_EVENT;
}
}

View File

@@ -65,6 +65,14 @@ namespace winrt::PowerToys::Interop::implementation
static hstring WorkspacesHotkeyEvent();
static hstring PowerToysRunnerTerminateSettingsEvent();
static hstring ShowCmdPalEvent();
static hstring TogglePowerDisplayEvent();
static hstring TerminatePowerDisplayEvent();
static hstring RefreshPowerDisplayMonitorsEvent();
static hstring SettingsUpdatedPowerDisplayEvent();
static hstring ApplyColorTemperaturePowerDisplayEvent();
static hstring ApplyProfilePowerDisplayEvent();
static hstring PowerDisplaySendSettingsTelemetryEvent();
static hstring HotkeyUpdatedPowerDisplayEvent();
};
}

View File

@@ -62,6 +62,14 @@ namespace PowerToys
static String WorkspacesHotkeyEvent();
static String PowerToysRunnerTerminateSettingsEvent();
static String ShowCmdPalEvent();
static String TogglePowerDisplayEvent();
static String TerminatePowerDisplayEvent();
static String RefreshPowerDisplayMonitorsEvent();
static String SettingsUpdatedPowerDisplayEvent();
static String ApplyColorTemperaturePowerDisplayEvent();
static String ApplyProfilePowerDisplayEvent();
static String PowerDisplaySendSettingsTelemetryEvent();
static String HotkeyUpdatedPowerDisplayEvent();
}
}
}

View File

@@ -148,6 +148,20 @@ namespace CommonSharedConstants
const wchar_t ZOOMIT_SNIP_EVENT[] = L"Local\\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30";
const wchar_t ZOOMIT_RECORD_EVENT[] = L"Local\\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512";
// Path to the events used by PowerDisplay
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";
const wchar_t POWER_DISPLAY_SEND_SETTINGS_TELEMETRY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsTelemetryEvent-8c4f2a1d-5e3b-7f9c-1a6d-3b8e5f2c9a7d";
const wchar_t HOTKEY_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-HotkeyUpdatedEvent-9d5f3a2b-7e1c-4b8a-6f3d-2a9e5c7b1d4f";
// Path to the events used by LightSwitch to notify PowerDisplay of theme changes
const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
// 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

@@ -83,6 +83,7 @@ struct LogSettings
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
inline const static std::string zoomItLoggerName = "zoom-it";
inline const static std::string lightSwitchLoggerName = "light-switch";
inline const static std::string powerDisplayLoggerName = "powerdisplay";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();

View File

@@ -32,6 +32,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_DISPLAY = L"ConfigureEnabledUtilityPowerDisplay";
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
@@ -310,6 +311,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH);
}
inline gpo_rule_configured_t getConfiguredPowerDisplayEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_DISPLAY);
}
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);

View File

@@ -148,6 +148,16 @@
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />

View File

@@ -247,6 +247,7 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>

View File

@@ -241,6 +241,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

@@ -67,6 +67,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

@@ -4,6 +4,7 @@
#include <LightSwitchUtils.h>
#include "ThemeScheduler.h"
#include <ThemeHelper.h>
#include <common/interop/shared_constants.h>
void ApplyTheme(bool shouldBeLight);
@@ -37,7 +38,7 @@ void LightSwitchStateManager::OnTick(int currentMinutes)
}
}
// Called when manual override is triggered
// Called when manual override is triggered (via hotkey)
void LightSwitchStateManager::OnManualOverride()
{
std::lock_guard<std::mutex> lock(_stateMutex);
@@ -45,15 +46,19 @@ void LightSwitchStateManager::OnManualOverride()
_state.isManualOverride = !_state.isManualOverride;
// When entering manual override, sync internal theme state to match the current system
// The hotkey handler in ModuleInterface has already toggled the theme, so we read the new state
if (_state.isManualOverride)
{
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
// Notify PowerDisplay about the theme change triggered by hotkey
// The theme has already been applied by ModuleInterface, we just need to notify PowerDisplay
NotifyPowerDisplay(_state.isSystemLightActive);
}
EvaluateAndApplyIfNeeded();
@@ -264,7 +269,61 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
// Notify PowerDisplay to apply display profile if configured
NotifyPowerDisplay(shouldBeLight);
}
_state.lastTickMinutes = now;
}
// Notify PowerDisplay module about theme change to apply display profiles
void LightSwitchStateManager::NotifyPowerDisplay(bool isLight)
{
const auto& settings = LightSwitchSettings::settings();
// Check if any profile is enabled and configured
bool shouldNotify = false;
if (isLight && settings.enableLightModeProfile && !settings.lightModeProfile.empty())
{
shouldNotify = true;
}
else if (!isLight && settings.enableDarkModeProfile && !settings.darkModeProfile.empty())
{
shouldNotify = true;
}
if (!shouldNotify)
{
return;
}
try
{
// Signal PowerDisplay with the specific theme event
// Using separate events for light/dark eliminates race conditions where PowerDisplay
// might read the registry before LightSwitch has finished updating it
const wchar_t* eventName = isLight
? CommonSharedConstants::LIGHT_SWITCH_LIGHT_THEME_EVENT
: CommonSharedConstants::LIGHT_SWITCH_DARK_THEME_EVENT;
Logger::info(L"[LightSwitchStateManager] Notifying PowerDisplay about theme change (isLight: {})", isLight);
HANDLE hThemeEvent = CreateEventW(nullptr, FALSE, FALSE, eventName);
if (hThemeEvent)
{
SetEvent(hThemeEvent);
CloseHandle(hThemeEvent);
Logger::info(L"[LightSwitchStateManager] Theme event signaled to PowerDisplay: {}", eventName);
}
else
{
Logger::warn(L"[LightSwitchStateManager] Failed to create theme event (error: {})", GetLastError());
}
}
catch (...)
{
Logger::error(L"[LightSwitchStateManager] Failed to notify PowerDisplay");
}
}

View File

@@ -48,4 +48,7 @@ private:
void EvaluateAndApplyIfNeeded();
bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon);
// Notify PowerDisplay module about theme change to apply display profiles
void NotifyPowerDisplay(bool isLight);
};

View File

@@ -0,0 +1,741 @@
// 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.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Unit tests for MccsCapabilitiesParser class.
/// Tests parsing of DDC/CI MCCS capabilities strings using real-world examples.
/// Reference: https://www.ddcutil.com/cap_u3011_verbose_output/
/// </summary>
[TestClass]
public class MccsCapabilitiesParserTests
{
// Real capabilities string from Dell U3011 monitor
// Source: https://www.ddcutil.com/cap_u3011_verbose_output/
private const string DellU3011Capabilities =
"(prot(monitor)type(lcd)model(U3011)cmds(01 02 03 07 0C E3 F3)vcp(02 04 05 06 08 10 12 14(01 05 08 0B 0C) 16 18 1A 52 60(01 03 04 0C 0F 11 12) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 04 05) DF FD)mccs_ver(2.1)mswhql(1))";
// Real capabilities string from Dell P2416D monitor
private const string DellP2416DCapabilities =
"(prot(monitor)type(LCD)model(P2416D)cmds(01 02 03 07 0C E3 F3) vcp(02 04 05 08 10 12 14(05 08 0B 0C) 16 18 1A 52 60(01 11 0F) AA(01 02) AC AE B2 B6 C6 C8 C9 D6(01 04 05) DC(00 02 03 05) DF E0 E1 E2(00 01 02 04 0E 12 14 19) F0(00 08) F1(01 02) F2 FD) mswhql(1)asset_eep(40)mccs_ver(2.1))";
// Simple test string
private const string SimpleCapabilities =
"(prot(monitor)type(lcd)model(TestMonitor)vcp(10 12)mccs_ver(2.2))";
// Capabilities without outer parentheses (some monitors like Apple Cinema Display)
private const string NoOuterParensCapabilities =
"prot(monitor)type(lcd)model(TestMonitor)vcp(10 12)mccs_ver(2.0)";
// Concatenated hex format (no spaces between hex bytes)
private const string ConcatenatedHexCapabilities =
"(prot(monitor)cmds(01020307)vcp(101214)mccs_ver(2.1))";
[TestMethod]
public void Parse_NullInput_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(null);
// Assert
Assert.IsNotNull(result);
Assert.IsNotNull(result.Capabilities);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
Assert.IsFalse(result.HasErrors);
}
[TestMethod]
public void Parse_EmptyString_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(string.Empty);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_WhitespaceOnly_ReturnsEmptyCapabilities()
{
// Act
var result = MccsCapabilitiesParser.Parse(" \t\n ");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_DellU3011_ParsesProtocol()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
}
[TestMethod]
public void Parse_DellU3011_ParsesType()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("lcd", result.Capabilities.Type);
}
[TestMethod]
public void Parse_DellU3011_ParsesModel()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("U3011", result.Capabilities.Model);
}
[TestMethod]
public void Parse_DellU3011_ParsesMccsVersion()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.AreEqual("2.1", result.Capabilities.MccsVersion);
}
[TestMethod]
public void Parse_DellU3011_ParsesCommands()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
var cmds = result.Capabilities.SupportedCommands;
Assert.IsNotNull(cmds);
Assert.AreEqual(7, cmds.Count);
CollectionAssert.Contains(cmds, (byte)0x01);
CollectionAssert.Contains(cmds, (byte)0x02);
CollectionAssert.Contains(cmds, (byte)0x03);
CollectionAssert.Contains(cmds, (byte)0x07);
CollectionAssert.Contains(cmds, (byte)0x0C);
CollectionAssert.Contains(cmds, (byte)0xE3);
CollectionAssert.Contains(cmds, (byte)0xF3);
}
[TestMethod]
public void Parse_DellU3011_ParsesBrightnessVcpCode()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x10 is Brightness
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
var brightnessInfo = result.Capabilities.GetVcpCodeInfo(0x10);
Assert.IsNotNull(brightnessInfo);
Assert.AreEqual(0x10, brightnessInfo.Value.Code);
Assert.IsTrue(brightnessInfo.Value.IsContinuous);
}
[TestMethod]
public void Parse_DellU3011_ParsesContrastVcpCode()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x12 is Contrast
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_DellU3011_ParsesInputSourceWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x60 is Input Source with discrete values
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x60));
var inputSourceInfo = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsNotNull(inputSourceInfo);
Assert.IsTrue(inputSourceInfo.Value.HasDiscreteValues);
// Should have values: 01 03 04 0C 0F 11 12
var values = inputSourceInfo.Value.SupportedValues;
Assert.AreEqual(7, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x03));
Assert.IsTrue(values.Contains(0x04));
Assert.IsTrue(values.Contains(0x0C));
Assert.IsTrue(values.Contains(0x0F));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x12));
}
[TestMethod]
public void Parse_DellU3011_ParsesColorPresetWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0x14 is Color Preset
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
var colorPresetInfo = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsNotNull(colorPresetInfo);
Assert.IsTrue(colorPresetInfo.Value.HasDiscreteValues);
// Should have values: 01 05 08 0B 0C
var values = colorPresetInfo.Value.SupportedValues;
Assert.AreEqual(5, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x05));
Assert.IsTrue(values.Contains(0x08));
Assert.IsTrue(values.Contains(0x0B));
Assert.IsTrue(values.Contains(0x0C));
}
[TestMethod]
public void Parse_DellU3011_ParsesPowerModeWithDiscreteValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP 0xD6 is Power Mode
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xD6));
var powerModeInfo = result.Capabilities.GetVcpCodeInfo(0xD6);
Assert.IsNotNull(powerModeInfo);
Assert.IsTrue(powerModeInfo.Value.HasDiscreteValues);
// Should have values: 01 04 05
var values = powerModeInfo.Value.SupportedValues;
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x04));
Assert.IsTrue(values.Contains(0x05));
}
[TestMethod]
public void Parse_DellU3011_TotalVcpCodeCount()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert - VCP codes: 02 04 05 06 08 10 12 14 16 18 1A 52 60 AC AE B2 B6 C6 C8 C9 D6 DC DF FD
Assert.AreEqual(24, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_DellP2416D_ParsesModel()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert
Assert.AreEqual("P2416D", result.Capabilities.Model);
}
[TestMethod]
public void Parse_DellP2416D_ParsesTypeWithDifferentCase()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - Type is "LCD" (uppercase) in this monitor
Assert.AreEqual("LCD", result.Capabilities.Type);
}
[TestMethod]
public void Parse_DellP2416D_ParsesMccsVersion()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert
Assert.AreEqual("2.1", result.Capabilities.MccsVersion);
}
[TestMethod]
public void Parse_DellP2416D_ParsesInputSourceWithThreeValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - VCP 0x60 Input Source has values: 01 11 0F
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x60));
var inputSourceInfo = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsNotNull(inputSourceInfo);
var values = inputSourceInfo.Value.SupportedValues;
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x0F));
}
[TestMethod]
public void Parse_DellP2416D_ParsesE2WithManyValues()
{
// Act
var result = MccsCapabilitiesParser.Parse(DellP2416DCapabilities);
// Assert - VCP 0xE2 has values: 00 01 02 04 0E 12 14 19
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xE2));
var e2Info = result.Capabilities.GetVcpCodeInfo(0xE2);
Assert.IsNotNull(e2Info);
var values = e2Info.Value.SupportedValues;
Assert.AreEqual(8, values.Count);
}
[TestMethod]
public void Parse_NoOuterParentheses_StillParses()
{
// Act - Some monitors like Apple Cinema Display omit outer parens
var result = MccsCapabilitiesParser.Parse(NoOuterParensCapabilities);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual("TestMonitor", result.Capabilities.Model);
Assert.AreEqual("2.0", result.Capabilities.MccsVersion);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_ConcatenatedHexFormat_ParsesCorrectly()
{
// Act - Some monitors output hex without spaces: cmds(01020307)
var result = MccsCapabilitiesParser.Parse(ConcatenatedHexCapabilities);
// Assert
var cmds = result.Capabilities.SupportedCommands;
Assert.AreEqual(4, cmds.Count);
CollectionAssert.Contains(cmds, (byte)0x01);
CollectionAssert.Contains(cmds, (byte)0x02);
CollectionAssert.Contains(cmds, (byte)0x03);
CollectionAssert.Contains(cmds, (byte)0x07);
// VCP codes without spaces
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
}
[TestMethod]
public void Parse_NestedParenthesesInVcp_HandlesCorrectly()
{
// Arrange - VCP code 0x14 with nested discrete values
var input = "(vcp(14(01 05 08)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
var vcpInfo = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsNotNull(vcpInfo);
Assert.AreEqual(3, vcpInfo.Value.SupportedValues.Count);
}
[TestMethod]
public void Parse_MultipleVcpCodesWithMixedFormats_ParsesAll()
{
// Arrange - Mixed: some with values, some without
var input = "(vcp(10 12 14(01 05) 16 60(0F 11)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual(5, result.Capabilities.SupportedVcpCodes.Count);
// Continuous codes (no discrete values)
var brightness = result.Capabilities.GetVcpCodeInfo(0x10);
Assert.IsTrue(brightness?.IsContinuous ?? false);
var contrast = result.Capabilities.GetVcpCodeInfo(0x12);
Assert.IsTrue(contrast?.IsContinuous ?? false);
// Discrete codes (with values)
var colorPreset = result.Capabilities.GetVcpCodeInfo(0x14);
Assert.IsTrue(colorPreset?.HasDiscreteValues ?? false);
Assert.AreEqual(2, colorPreset?.SupportedValues.Count);
var inputSource = result.Capabilities.GetVcpCodeInfo(0x60);
Assert.IsTrue(inputSource?.HasDiscreteValues ?? false);
Assert.AreEqual(2, inputSource?.SupportedValues.Count);
}
[TestMethod]
public void Parse_UnknownSegments_DoesNotFail()
{
// Arrange - Contains unknown segments like mswhql and asset_eep
var input = "(prot(monitor)mswhql(1)asset_eep(40)vcp(10))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsFalse(result.HasErrors);
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
}
[TestMethod]
public void Parse_ExtraWhitespace_HandlesCorrectly()
{
// Arrange - Extra spaces everywhere
var input = "( prot( monitor ) type( lcd ) vcp( 10 12 14( 01 05 ) ) )";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual("monitor", result.Capabilities.Protocol);
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual(3, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_LowercaseHex_ParsesCorrectly()
{
// Arrange - All lowercase hex
var input = "(cmds(01 0c e3 f3)vcp(10 ac ae))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
CollectionAssert.Contains(result.Capabilities.SupportedCommands, (byte)0xE3);
CollectionAssert.Contains(result.Capabilities.SupportedCommands, (byte)0xF3);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAC));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAE));
}
[TestMethod]
public void Parse_MixedCaseHex_ParsesCorrectly()
{
// Arrange - Mixed case hex
var input = "(vcp(Aa Bb cC Dd))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xAA));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xBB));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xCC));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0xDD));
}
[TestMethod]
public void Parse_MalformedInput_ReturnsPartialResults()
{
// Arrange - Missing closing paren for vcp section
var input = "(prot(monitor)vcp(10 12";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should still parse what it can
Assert.AreEqual("monitor", result.Capabilities.Protocol);
// VCP codes should still be parsed
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void Parse_InvalidHexInVcp_SkipsAndContinues()
{
// Arrange - Contains invalid hex "GG"
var input = "(vcp(10 GG 12 14))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should skip invalid and parse valid codes
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x14));
Assert.AreEqual(3, result.Capabilities.SupportedVcpCodes.Count);
}
[TestMethod]
public void Parse_SingleCharacterHex_Skipped()
{
// Arrange - Single char "A" is not valid (need 2 chars)
var input = "(vcp(10 A 12))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert - Should only have 10 and 12
Assert.AreEqual(2, result.Capabilities.SupportedVcpCodes.Count);
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x10));
Assert.IsTrue(result.Capabilities.SupportsVcpCode(0x12));
}
[TestMethod]
public void GetVcpCodesAsHexStrings_ReturnsSortedList()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60 10 14 12))");
// Act
var hexStrings = result.Capabilities.GetVcpCodesAsHexStrings();
// Assert - Should be sorted
Assert.AreEqual(4, hexStrings.Count);
Assert.AreEqual("0x10", hexStrings[0]);
Assert.AreEqual("0x12", hexStrings[1]);
Assert.AreEqual("0x14", hexStrings[2]);
Assert.AreEqual("0x60", hexStrings[3]);
}
[TestMethod]
public void GetSortedVcpCodes_ReturnsSortedEnumerable()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60 10 14 12))");
// Act
var sortedCodes = result.Capabilities.GetSortedVcpCodes().ToList();
// Assert
Assert.AreEqual(0x10, sortedCodes[0].Code);
Assert.AreEqual(0x12, sortedCodes[1].Code);
Assert.AreEqual(0x14, sortedCodes[2].Code);
Assert.AreEqual(0x60, sortedCodes[3].Code);
}
[TestMethod]
public void HasDiscreteValues_ContinuousCode_ReturnsFalse()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(10))");
// Act & Assert
Assert.IsFalse(result.Capabilities.HasDiscreteValues(0x10));
}
[TestMethod]
public void HasDiscreteValues_DiscreteCode_ReturnsTrue()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60(01 11)))");
// Act & Assert
Assert.IsTrue(result.Capabilities.HasDiscreteValues(0x60));
}
[TestMethod]
public void GetSupportedValues_DiscreteCode_ReturnsValues()
{
// Arrange
var result = MccsCapabilitiesParser.Parse("(vcp(60(01 11 0F)))");
// Act
var values = result.Capabilities.GetSupportedValues(0x60);
// Assert
Assert.IsNotNull(values);
Assert.AreEqual(3, values.Count);
Assert.IsTrue(values.Contains(0x01));
Assert.IsTrue(values.Contains(0x11));
Assert.IsTrue(values.Contains(0x0F));
}
[TestMethod]
public void IsValid_ValidCapabilities_ReturnsTrue()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse(DellU3011Capabilities);
// Assert
Assert.IsTrue(result.IsValid);
Assert.IsFalse(result.HasErrors);
}
[TestMethod]
public void IsValid_EmptyVcpCodes_ReturnsFalse()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse("(prot(monitor)type(lcd))");
// Assert - No VCP codes = not valid
Assert.IsFalse(result.IsValid);
}
[TestMethod]
public void Capabilities_RawProperty_ContainsOriginalString()
{
// Arrange & Act
var result = MccsCapabilitiesParser.Parse(SimpleCapabilities);
// Assert
Assert.AreEqual(SimpleCapabilities, result.Capabilities.Raw);
}
[TestMethod]
public void Parse_Window1Segment_ParsesCorrectly()
{
// Arrange - Full window segment with all fields
var input = "(vcp(10)window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
var window = result.Capabilities.Windows[0];
Assert.AreEqual(1, window.WindowNumber);
Assert.AreEqual("PIP", window.Type);
Assert.AreEqual(25, window.Area.X1);
Assert.AreEqual(25, window.Area.Y1);
Assert.AreEqual(1895, window.Area.X2);
Assert.AreEqual(1175, window.Area.Y2);
Assert.AreEqual(640, window.MaxSize.Width);
Assert.AreEqual(480, window.MaxSize.Height);
Assert.AreEqual(10, window.MinSize.Width);
Assert.AreEqual(10, window.MinSize.Height);
Assert.AreEqual(10, window.WindowId);
}
[TestMethod]
public void Parse_MultipleWindows_ParsesAll()
{
// Arrange - Two windows (PIP and PBP)
var input = "(window1(type(PIP) area(0 0 640 480))window2(type(PBP) area(640 0 1280 480)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(2, result.Capabilities.Windows.Count);
var window1 = result.Capabilities.Windows[0];
Assert.AreEqual(1, window1.WindowNumber);
Assert.AreEqual("PIP", window1.Type);
Assert.AreEqual(0, window1.Area.X1);
Assert.AreEqual(640, window1.Area.X2);
var window2 = result.Capabilities.Windows[1];
Assert.AreEqual(2, window2.WindowNumber);
Assert.AreEqual("PBP", window2.Type);
Assert.AreEqual(640, window2.Area.X1);
Assert.AreEqual(1280, window2.Area.X2);
}
[TestMethod]
public void Parse_WindowWithMissingFields_HandlesGracefully()
{
// Arrange - Window with only type and area (missing max, min, window)
var input = "(window1(type(PIP) area(0 0 640 480)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
var window = result.Capabilities.Windows[0];
Assert.AreEqual(1, window.WindowNumber);
Assert.AreEqual("PIP", window.Type);
Assert.AreEqual(640, window.Area.X2);
Assert.AreEqual(480, window.Area.Y2);
// Default values for missing fields
Assert.AreEqual(0, window.MaxSize.Width);
Assert.AreEqual(0, window.MinSize.Width);
Assert.AreEqual(0, window.WindowId);
}
[TestMethod]
public void Parse_WindowWithOnlyType_ParsesType()
{
// Arrange
var input = "(window1(type(PBP)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual(1, result.Capabilities.Windows.Count);
Assert.AreEqual("PBP", result.Capabilities.Windows[0].Type);
}
[TestMethod]
public void Parse_NoWindowSegment_HasWindowSupportFalse()
{
// Arrange
var input = "(prot(monitor)vcp(10 12))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsFalse(result.Capabilities.HasWindowSupport);
Assert.AreEqual(0, result.Capabilities.Windows.Count);
}
[TestMethod]
public void Parse_WindowAreaDimensions_CalculatesCorrectly()
{
// Arrange
var input = "(window1(area(100 200 500 600)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
var area = result.Capabilities.Windows[0].Area;
Assert.AreEqual(400, area.Width); // 500 - 100
Assert.AreEqual(400, area.Height); // 600 - 200
}
[TestMethod]
public void Parse_RealWorldMccsWindowExample_ParsesCorrectly()
{
// Arrange - Example from MCCS 2.2a specification
var input = "(prot(display)type(lcd)model(PD3220U)cmds(01 02 03)vcp(10 12)mccs_ver(2.2)window1(type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.AreEqual("lcd", result.Capabilities.Type);
Assert.AreEqual("PD3220U", result.Capabilities.Model);
Assert.AreEqual("2.2", result.Capabilities.MccsVersion);
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual("PIP", result.Capabilities.Windows[0].Type);
}
[TestMethod]
public void Parse_WindowWithExtraSpaces_HandlesCorrectly()
{
// Arrange - Extra spaces in content
var input = "(window1( type( PIP ) area( 0 0 640 480 ) ))";
// Act
var result = MccsCapabilitiesParser.Parse(input);
// Assert
Assert.IsTrue(result.Capabilities.HasWindowSupport);
Assert.AreEqual("PIP", result.Capabilities.Windows[0].Type);
Assert.AreEqual(640, result.Capabilities.Windows[0].Area.X2);
}
}

View File

@@ -0,0 +1,94 @@
// 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.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Unit tests for MonitorMatchingHelper class.
/// Tests monitor key generation and matching logic.
/// </summary>
[TestClass]
public class MonitorMatchingHelperTests
{
[TestMethod]
public void GetMonitorKey_WithMonitor_ReturnsId()
{
// Arrange
var monitor = new Monitor { Id = "DDC_GSM5C6D_1", Name = "LG Monitor" };
// Act
var result = MonitorMatchingHelper.GetMonitorKey(monitor);
// Assert
Assert.AreEqual("DDC_GSM5C6D_1", result);
}
[TestMethod]
public void GetMonitorKey_NullMonitor_ReturnsEmptyString()
{
// Act
var result = MonitorMatchingHelper.GetMonitorKey(null);
// Assert
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void GetMonitorKey_EmptyId_ReturnsEmptyString()
{
// Arrange
var monitor = new Monitor { Id = string.Empty, Name = "Display Name" };
// Act
var result = MonitorMatchingHelper.GetMonitorKey(monitor);
// Assert
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void AreMonitorsSame_SameId_ReturnsTrue()
{
// Arrange
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
var monitor2 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 2" };
// Act
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, monitor2);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public void AreMonitorsSame_DifferentId_ReturnsFalse()
{
// Arrange
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
var monitor2 = new Monitor { Id = "DDC_GSM5C6D_2", Name = "Monitor 2" };
// Act
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, monitor2);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void AreMonitorsSame_NullMonitor_ReturnsFalse()
{
// Arrange
var monitor1 = new Monitor { Id = "DDC_GSM5C6D_1", Name = "Monitor 1" };
// Act
var result = MonitorMatchingHelper.AreMonitorsSame(monitor1, null!);
// Assert
Assert.IsFalse(result);
}
}

View File

@@ -0,0 +1,41 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
<!-- Licensed under the MIT License. See LICENSE file in the project root for license information. -->
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>PowerDisplay.UnitTests</RootNamespace>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\PowerDisplay.Lib.UnitTests\</OutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- Hide build log files from Solution Explorer -->
<None Remove="*.log" />
<None Remove="*.binlog" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<PackageReference Include="Moq" />
<PackageReference Include="System.CodeDom">
<!-- This package is a transitive dependency, but we need to set it here so we can exclude the assets,
so it doesn't conflict with the dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="System.Diagnostics.EventLog">
<!-- This package is a transitive dependency, but we need to set it here so we can exclude the assets,
so it doesn't conflict with the dll coming from .NET SDK. -->
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,658 @@
// 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 Polly;
using Polly.Retry;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.NativeDelegates;
using static PowerDisplay.Common.Drivers.PInvoke;
using Monitor = PowerDisplay.Common.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.Common.Drivers.MonitorInfoEx;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
using RECT = PowerDisplay.Common.Drivers.Rect;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// DDC/CI monitor controller for controlling external monitors
/// </summary>
public partial class DdcCiController : IMonitorController, IDisposable
{
/// <summary>
/// Represents a candidate monitor discovered during Phase 1 of monitor enumeration.
/// </summary>
/// <param name="Handle">Physical monitor handle for DDC/CI communication</param>
/// <param name="PhysicalMonitor">Native physical monitor structure with description</param>
/// <param name="MonitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
private readonly record struct CandidateMonitor(
IntPtr Handle,
PHYSICAL_MONITOR PhysicalMonitor,
MonitorDisplayInfo MonitorInfo);
/// <summary>
/// Delay between retry attempts for DDC/CI operations (in milliseconds)
/// </summary>
private const int RetryDelayMs = 100;
/// <summary>
/// Retry pipeline for getting capabilities string length (3 retries).
/// </summary>
private static readonly ResiliencePipeline<uint> CapabilitiesLengthRetryPipeline =
new ResiliencePipelineBuilder<uint>()
.AddRetry(new RetryStrategyOptions<uint>
{
MaxRetryAttempts = 2, // 2 retries = 3 total attempts
Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
ShouldHandle = new PredicateBuilder<uint>().HandleResult(len => len == 0),
OnRetry = static args =>
{
Logger.LogWarning($"[Retry] GetCapabilitiesStringLength returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
return default;
},
})
.Build();
/// <summary>
/// Retry pipeline for getting capabilities string (5 retries).
/// </summary>
private static readonly ResiliencePipeline<string?> CapabilitiesStringRetryPipeline =
new ResiliencePipelineBuilder<string?>()
.AddRetry(new RetryStrategyOptions<string?>
{
MaxRetryAttempts = 4, // 4 retries = 5 total attempts
Delay = TimeSpan.FromMilliseconds(RetryDelayMs),
ShouldHandle = new PredicateBuilder<string?>().HandleResult(static str => string.IsNullOrEmpty(str)),
OnRetry = static args =>
{
Logger.LogWarning($"[Retry] GetCapabilitiesString returned invalid result on attempt {args.AttemptNumber + 1}, retrying...");
return default;
},
})
.Build();
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>
/// Get monitor brightness using VCP code 0x10
/// </summary>
public async Task<VcpFeatureValue> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await GetVcpFeatureAsync(monitor, VcpCodeBrightness, "Brightness", cancellationToken);
}
/// <summary>
/// Set monitor brightness using VCP code 0x10
/// </summary>
public Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeBrightness, brightness, cancellationToken);
/// <summary>
/// Set monitor contrast
/// </summary>
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeContrast, contrast, cancellationToken);
/// <summary>
/// Set monitor volume
/// </summary>
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, NativeConstants.VcpCodeVolume, volume, 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<VcpFeatureValue> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await GetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, "Color temperature", cancellationToken);
}
/// <summary>
/// Set monitor color temperature using VCP code 0x14 (Select Color Preset)
/// </summary>
public Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, VcpCodeSelectColorPreset, colorTemperature, cancellationToken);
/// <summary>
/// Get current input source using VCP code 0x60
/// Returns the raw VCP value (e.g., 0x11 for HDMI-1)
/// </summary>
public async Task<VcpFeatureValue> GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await GetVcpFeatureAsync(monitor, VcpCodeInputSource, "Input source", cancellationToken);
}
/// <summary>
/// Set input source using VCP code 0x60
/// </summary>
public Task<MonitorOperationResult> SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
=> SetVcpFeatureAsync(monitor, VcpCodeInputSource, inputSource, cancellationToken);
/// <summary>
/// Get monitor capabilities string with retry logic.
/// Uses cached CapabilitiesRaw if available to avoid slow I2C operations.
/// </summary>
public async Task<string> GetCapabilitiesStringAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
// Check if capabilities are already cached
if (!string.IsNullOrEmpty(monitor.CapabilitiesRaw))
{
return monitor.CapabilitiesRaw;
}
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return string.Empty;
}
try
{
// Step 1: Get capabilities string length with retry
var length = CapabilitiesLengthRetryPipeline.Execute(() =>
{
if (GetCapabilitiesStringLength(monitor.Handle, out uint len) && len > 0)
{
return len;
}
return 0u;
});
if (length == 0)
{
Logger.LogWarning("[Retry] GetCapabilitiesStringLength failed after 3 attempts");
return string.Empty;
}
// Step 2: Get actual capabilities string with retry
var capsString = CapabilitiesStringRetryPipeline.Execute(
() => TryGetCapabilitiesString(monitor.Handle, length));
if (!string.IsNullOrEmpty(capsString))
{
return capsString;
}
Logger.LogWarning("[Retry] GetCapabilitiesString failed after 5 attempts");
}
catch (Exception ex)
{
Logger.LogError($"Exception getting capabilities string: {ex.Message}");
}
return string.Empty;
},
cancellationToken);
}
/// <summary>
/// Try to get capabilities string from monitor handle.
/// </summary>
private string? TryGetCapabilitiesString(IntPtr handle, uint length)
{
var buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)length);
try
{
if (CapabilitiesRequestAndCapabilitiesReply(handle, buffer, length))
{
return System.Runtime.InteropServices.Marshal.PtrToStringAnsi(buffer);
}
return null;
}
finally
{
System.Runtime.InteropServices.Marshal.FreeHGlobal(buffer);
}
}
/// <summary>
/// Discover supported monitors using a three-phase approach:
/// Phase 1: Enumerate and collect candidate monitors with their handles
/// Phase 2: Fetch DDC/CI capabilities in parallel (slow I2C operations)
/// Phase 3: Create Monitor objects for valid DDC/CI monitors
/// </summary>
public async Task<IEnumerable<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
try
{
// Get monitor display info from QueryDisplayConfig, keyed by device path (unique per target)
var allMonitorDisplayInfo = DdcCiNative.GetAllMonitorDisplayInfo();
// Phase 1: Collect candidate monitors
var monitorHandles = EnumerateMonitorHandles();
if (monitorHandles.Count == 0)
{
return Enumerable.Empty<Monitor>();
}
var candidateMonitors = await CollectCandidateMonitorsAsync(
monitorHandles, allMonitorDisplayInfo, cancellationToken);
if (candidateMonitors.Count == 0)
{
return Enumerable.Empty<Monitor>();
}
// Phase 2: Fetch capabilities in parallel
var fetchResults = await FetchCapabilitiesInParallelAsync(
candidateMonitors, cancellationToken);
// Phase 3: Create monitor objects
return CreateValidMonitors(fetchResults);
}
catch (Exception ex)
{
Logger.LogError($"DDC: DiscoverMonitorsAsync exception: {ex.Message}\nStack: {ex.StackTrace}");
return Enumerable.Empty<Monitor>();
}
}
/// <summary>
/// Enumerate all logical monitor handles using Win32 API.
/// </summary>
private List<IntPtr> EnumerateMonitorHandles()
{
var handles = new List<IntPtr>();
bool EnumProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData)
{
handles.Add(hMonitor);
return true;
}
if (!EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, EnumProc, IntPtr.Zero))
{
Logger.LogWarning("DDC: EnumDisplayMonitors failed");
}
return handles;
}
/// <summary>
/// Get GDI device name for a monitor handle (e.g., "\\.\DISPLAY1").
/// </summary>
private unsafe string? GetGdiDeviceName(IntPtr hMonitor)
{
var monitorInfo = new MONITORINFOEX { CbSize = (uint)sizeof(MONITORINFOEX) };
if (GetMonitorInfo(hMonitor, ref monitorInfo))
{
return monitorInfo.GetDeviceName();
}
return null;
}
/// <summary>
/// Phase 1: Collect all candidate monitors with their physical handles.
/// Matches physical monitors with MonitorDisplayInfo using GDI device name and friendly name.
/// Supports mirror mode where multiple physical monitors share the same GDI name.
/// </summary>
private async Task<List<CandidateMonitor>> CollectCandidateMonitorsAsync(
List<IntPtr> monitorHandles,
Dictionary<string, MonitorDisplayInfo> allMonitorDisplayInfo,
CancellationToken cancellationToken)
{
var candidates = new List<CandidateMonitor>();
foreach (var hMonitor in monitorHandles)
{
// Get GDI device name for this monitor (e.g., "\\.\DISPLAY1")
var gdiDeviceName = GetGdiDeviceName(hMonitor);
if (string.IsNullOrEmpty(gdiDeviceName))
{
Logger.LogWarning($"DDC: Failed to get GDI device name for hMonitor 0x{hMonitor:X}");
continue;
}
var physicalMonitors = await GetPhysicalMonitorsWithRetryAsync(hMonitor, cancellationToken);
if (physicalMonitors == null || physicalMonitors.Length == 0)
{
Logger.LogWarning($"DDC: Failed to get physical monitors for {gdiDeviceName} after retries");
continue;
}
// Find all MonitorDisplayInfo entries that match this GDI device name
// In mirror mode, multiple targets share the same GDI name
var matchingInfos = allMonitorDisplayInfo.Values
.Where(info => string.Equals(info.GdiDeviceName, gdiDeviceName, StringComparison.OrdinalIgnoreCase))
.ToList();
if (matchingInfos.Count == 0)
{
Logger.LogWarning($"DDC: No QueryDisplayConfig info for {gdiDeviceName}, skipping");
continue;
}
for (int i = 0; i < physicalMonitors.Length; i++)
{
var physicalMonitor = physicalMonitors[i];
if (i >= matchingInfos.Count)
{
Logger.LogWarning($"DDC: Physical monitor index {i} exceeds available QueryDisplayConfig entries ({matchingInfos.Count}) for {gdiDeviceName}");
break;
}
var monitorInfo = matchingInfos[i];
candidates.Add(new CandidateMonitor(physicalMonitor.HPhysicalMonitor, physicalMonitor, monitorInfo));
}
}
return candidates;
}
/// <summary>
/// Phase 2: Fetch DDC/CI capabilities in parallel for all candidate monitors.
/// This is the slow I2C operation (~4s per monitor), but parallelization
/// significantly reduces total time when multiple monitors are connected.
/// </summary>
private async Task<(CandidateMonitor Candidate, DdcCiValidationResult Result)[]> FetchCapabilitiesInParallelAsync(
List<CandidateMonitor> candidates,
CancellationToken cancellationToken)
{
Logger.LogInfo($"DDC: Phase 2 - Fetching capabilities for {candidates.Count} monitors in parallel");
var tasks = candidates.Select(candidate =>
Task.Run(
() => (Candidate: candidate, Result: DdcCiNative.FetchCapabilities(candidate.Handle)),
cancellationToken));
var results = await Task.WhenAll(tasks);
Logger.LogInfo($"DDC: Phase 2 completed - Got results for {results.Length} monitors");
return results;
}
/// <summary>
/// Phase 3: Create Monitor objects for valid DDC/CI monitors.
/// A monitor is valid if it has capabilities with brightness support.
/// </summary>
private List<Monitor> CreateValidMonitors(
(CandidateMonitor Candidate, DdcCiValidationResult Result)[] fetchResults)
{
var monitors = new List<Monitor>();
var newHandleMap = new Dictionary<string, IntPtr>();
foreach (var (candidate, capResult) in fetchResults)
{
if (!capResult.IsValid)
{
continue;
}
var monitor = _discoveryHelper.CreateMonitorFromPhysical(
candidate.PhysicalMonitor,
candidate.MonitorInfo);
if (monitor == null)
{
continue;
}
// Set capabilities data
if (!string.IsNullOrEmpty(capResult.CapabilitiesString))
{
monitor.CapabilitiesRaw = capResult.CapabilitiesString;
}
if (capResult.VcpCapabilitiesInfo != null)
{
monitor.VcpCapabilitiesInfo = capResult.VcpCapabilitiesInfo;
UpdateMonitorCapabilitiesFromVcp(monitor, capResult.VcpCapabilitiesInfo);
// Initialize input source if supported
if (monitor.SupportsInputSource)
{
InitializeInputSource(monitor, candidate.Handle);
}
// Initialize color temperature if supported
if (monitor.SupportsColorTemperature)
{
InitializeColorTemperature(monitor, candidate.Handle);
}
}
// Initialize brightness (always supported for DDC/CI monitors)
InitializeBrightness(monitor, candidate.Handle);
monitors.Add(monitor);
newHandleMap[monitor.Id] = candidate.Handle;
Logger.LogInfo($"DDC: Added monitor {monitor.Id} with {monitor.VcpCapabilitiesInfo?.SupportedVcpCodes.Count ?? 0} VCP codes");
}
_handleManager.UpdateHandleMap(newHandleMap);
return monitors;
}
/// <summary>
/// Initialize input source value for a monitor using VCP 0x60.
/// </summary>
private static void InitializeInputSource(Monitor monitor, IntPtr handle)
{
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeInputSource, IntPtr.Zero, out uint current, out uint _))
{
monitor.CurrentInputSource = (int)current;
}
}
/// <summary>
/// Initialize color temperature value for a monitor using VCP 0x14.
/// </summary>
private static void InitializeColorTemperature(Monitor monitor, IntPtr handle)
{
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeSelectColorPreset, IntPtr.Zero, out uint current, out uint _))
{
monitor.CurrentColorTemperature = (int)current;
}
}
/// <summary>
/// Initialize brightness value for a monitor using VCP 0x10.
/// </summary>
private static void InitializeBrightness(Monitor monitor, IntPtr handle)
{
if (GetVCPFeatureAndVCPFeatureReply(handle, VcpCodeBrightness, IntPtr.Zero, out uint current, out uint max))
{
var brightnessInfo = new VcpFeatureValue((int)current, 0, (int)max);
monitor.CurrentBrightness = brightnessInfo.ToPercentage();
}
}
/// <summary>
/// Update monitor capability flags based on parsed VCP capabilities.
/// </summary>
private static void UpdateMonitorCapabilitiesFromVcp(Monitor monitor, VcpCapabilities vcpCaps)
{
// Check for Contrast support (VCP 0x12)
if (vcpCaps.SupportsVcpCode(VcpCodeContrast))
{
monitor.Capabilities |= MonitorCapabilities.Contrast;
}
// Check for Volume support (VCP 0x62)
if (vcpCaps.SupportsVcpCode(VcpCodeVolume))
{
monitor.Capabilities |= MonitorCapabilities.Volume;
}
// Check for Color Temperature support (VCP 0x14)
if (vcpCaps.SupportsVcpCode(VcpCodeSelectColorPreset))
{
monitor.SupportsColorTemperature = true;
}
}
/// <summary>
/// Get physical monitors with retry logic to handle Windows API occasionally returning NULL handles.
/// NULL handles are automatically filtered out by GetPhysicalMonitors; retry if any were filtered.
/// </summary>
/// <param name="hMonitor">Handle to the monitor</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Array of valid 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, out bool hasNullHandles);
// Success: got valid monitors with no NULL handles filtered out
if (monitors != null && !hasNullHandles)
{
return monitors;
}
// Got monitors but some had NULL handles - retry to see if API stabilizes
if (monitors != null && hasNullHandles && attempt < maxRetries - 1)
{
Logger.LogWarning($"DDC: Some monitors had NULL handles on attempt {attempt + 1}, will retry");
continue;
}
// No monitors returned - retry
if (monitors == null && attempt < maxRetries - 1)
{
Logger.LogWarning($"DDC: GetPhysicalMonitors returned null on attempt {attempt + 1}, will retry");
continue;
}
// Last attempt - return whatever we have (may have NULL handles filtered)
if (monitors != null && hasNullHandles)
{
Logger.LogWarning($"DDC: NULL handles still present after {maxRetries} attempts, using filtered result");
}
return monitors;
}
return null;
}
/// <summary>
/// Generic method to get VCP feature value with optional logging.
/// </summary>
/// <param name="monitor">Monitor to query</param>
/// <param name="vcpCode">VCP code to read</param>
/// <param name="featureName">Optional feature name for logging (e.g., "color temperature", "input source")</param>
/// <param name="cancellationToken">Cancellation token</param>
private async Task<VcpFeatureValue> GetVcpFeatureAsync(
Monitor monitor,
byte vcpCode,
string? featureName = null,
CancellationToken cancellationToken = default)
{
return await Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return VcpFeatureValue.Invalid;
}
if (GetVCPFeatureAndVCPFeatureReply(monitor.Handle, vcpCode, IntPtr.Zero, out uint current, out uint max))
{
return new VcpFeatureValue((int)current, 0, (int)max);
}
return VcpFeatureValue.Invalid;
},
cancellationToken);
}
/// <summary>
/// Generic method to set VCP feature value directly.
/// </summary>
private Task<MonitorOperationResult> SetVcpFeatureAsync(
Monitor monitor,
byte vcpCode,
int value,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return Task.Run(
() =>
{
if (monitor.Handle == IntPtr.Zero)
{
return MonitorOperationResult.Failure("Invalid monitor handle");
}
try
{
if (SetVCPFeature(monitor.Handle, vcpCode, (uint)value))
{
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);
}
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,374 @@
// 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.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.PInvoke;
// Type aliases for Windows API naming conventions compatibility
using LUID = PowerDisplay.Common.Drivers.Luid;
#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.Common.Drivers.DDC
{
/// <summary>
/// DDC/CI validation result containing both validation status and cached capabilities data.
/// This allows reusing capabilities data retrieved during validation, avoiding duplicate I2C calls.
/// </summary>
public struct DdcCiValidationResult
{
/// <summary>
/// Gets a value indicating whether the monitor has a valid DDC/CI connection with brightness support.
/// </summary>
public bool IsValid { get; }
/// <summary>
/// Gets the raw capabilities string retrieved during validation.
/// Null if retrieval failed.
/// </summary>
public string? CapabilitiesString { get; }
/// <summary>
/// Gets the parsed VCP capabilities info retrieved during validation.
/// Null if parsing failed.
/// </summary>
public Models.VcpCapabilities? VcpCapabilitiesInfo { get; }
/// <summary>
/// Gets a value indicating whether capabilities retrieval was attempted.
/// True means the result is from an actual attempt (success or failure).
/// </summary>
public bool WasAttempted { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DdcCiValidationResult"/> struct.
/// </summary>
public DdcCiValidationResult(bool isValid, string? capabilitiesString = null, Models.VcpCapabilities? vcpCapabilitiesInfo = null, bool wasAttempted = true)
{
IsValid = isValid;
CapabilitiesString = capabilitiesString;
VcpCapabilitiesInfo = vcpCapabilitiesInfo;
WasAttempted = wasAttempted;
}
/// <summary>
/// Gets an invalid validation result with no cached data.
/// </summary>
public static DdcCiValidationResult Invalid => new(false, null, null, true);
/// <summary>
/// Gets a result indicating validation was not attempted yet.
/// </summary>
public static DdcCiValidationResult NotAttempted => new(false, null, null, false);
}
/// <summary>
/// DDC/CI native API wrapper
/// </summary>
public static class DdcCiNative
{
/// <summary>
/// Fetches VCP capabilities string from a monitor and returns a validation result.
/// This is the slow I2C operation (~4 seconds per monitor) that should only be done once.
/// The result is cached regardless of success or failure.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>Validation result with capabilities data (or failure status)</returns>
public static DdcCiValidationResult FetchCapabilities(IntPtr hPhysicalMonitor)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return DdcCiValidationResult.Invalid;
}
try
{
// Try to get capabilities string (slow I2C operation)
var capsString = TryGetCapabilitiesString(hPhysicalMonitor);
if (string.IsNullOrEmpty(capsString))
{
return DdcCiValidationResult.Invalid;
}
// Parse the capabilities string
var parseResult = Utils.MccsCapabilitiesParser.Parse(capsString);
var capabilities = parseResult.Capabilities;
if (capabilities == null || capabilities.SupportedVcpCodes.Count == 0)
{
return DdcCiValidationResult.Invalid;
}
// Check if brightness (VCP 0x10) is supported - determines DDC/CI validity
bool supportsBrightness = capabilities.SupportsVcpCode(NativeConstants.VcpCodeBrightness);
return new DdcCiValidationResult(supportsBrightness, capsString, capabilities);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return DdcCiValidationResult.Invalid;
}
}
/// <summary>
/// Try to get capabilities string from a physical monitor handle.
/// </summary>
/// <param name="hPhysicalMonitor">Physical monitor handle</param>
/// <returns>Capabilities string, or null if failed</returns>
private static string? TryGetCapabilitiesString(IntPtr hPhysicalMonitor)
{
if (hPhysicalMonitor == IntPtr.Zero)
{
return null;
}
try
{
// Get capabilities string length
if (!GetCapabilitiesStringLength(hPhysicalMonitor, out uint length) || length == 0)
{
return null;
}
// Allocate buffer and get capabilities string
var buffer = Marshal.AllocHGlobal((int)length);
try
{
if (!CapabilitiesRequestAndCapabilitiesReply(hPhysicalMonitor, buffer, length))
{
return null;
}
return Marshal.PtrToStringAnsi(buffer);
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return null;
}
}
/// <summary>
/// Gets GDI device name for a source (e.g., "\\.\DISPLAY1").
/// </summary>
/// <param name="adapterId">Adapter ID</param>
/// <param name="sourceId">Source ID</param>
/// <returns>GDI device name, or null if retrieval fails</returns>
private static unsafe string? GetSourceGdiDeviceName(LUID adapterId, uint sourceId)
{
try
{
var sourceName = new DISPLAYCONFIG_SOURCE_DEVICE_NAME
{
Header = new DISPLAYCONFIG_DEVICE_INFO_HEADER
{
Type = DisplayconfigDeviceInfoGetSourceName,
Size = (uint)sizeof(DISPLAYCONFIG_SOURCE_DEVICE_NAME),
AdapterId = adapterId,
Id = sourceId,
},
};
var result = DisplayConfigGetDeviceInfo(ref sourceName);
if (result == 0)
{
return sourceName.GetViewGdiDeviceName();
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
}
return null;
}
/// <summary>
/// Gets friendly name, hardware ID, and device path for a monitor target.
/// </summary>
/// <param name="adapterId">Adapter ID</param>
/// <param name="targetId">Target ID</param>
/// <returns>Tuple of (friendlyName, hardwareId, devicePath), any may be null if retrieval fails</returns>
private static unsafe (string? FriendlyName, string? HardwareId, string? DevicePath) GetTargetDeviceInfo(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)
{
// Extract friendly name
var friendlyName = deviceName.GetMonitorFriendlyDeviceName();
// Extract device path (unique per target, used as key)
var devicePath = deviceName.GetMonitorDevicePath();
// Extract hardware ID from EDID data
var manufacturerId = deviceName.EdidManufactureId;
var manufactureCode = ConvertManufactureIdToString(manufacturerId);
var productCode = deviceName.EdidProductCodeId.ToString("X4", System.Globalization.CultureInfo.InvariantCulture);
var hardwareId = $"{manufactureCode}{productCode}";
return (friendlyName, hardwareId, devicePath);
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
}
return (null, null, null);
}
/// <summary>
/// Converts manufacturer ID to 3-character manufacturer code
/// </summary>
/// <param name="manufacturerId">Manufacturer ID</param>
/// <returns>3-character manufacturer code</returns>
private static string ConvertManufactureIdToString(ushort manufacturerId)
{
// EDID manufacturer ID requires byte order swap first
manufacturerId = (ushort)(((manufacturerId & 0xff00) >> 8) | ((manufacturerId & 0x00ff) << 8));
// Extract 3 5-bit characters (each character is A-Z, where 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));
// Combine characters in correct order
return $"{char3}{char2}{char1}";
}
/// <summary>
/// Gets complete information for all monitors, keyed by GDI device name (e.g., "\\.\DISPLAY1").
/// This allows reliable matching with GetMonitorInfo results.
/// </summary>
/// <returns>Dictionary keyed by GDI device name containing monitor information</returns>
public static unsafe Dictionary<string, MonitorDisplayInfo> GetAllMonitorDisplayInfo()
{
var monitorInfo = new Dictionary<string, MonitorDisplayInfo>(StringComparer.OrdinalIgnoreCase);
try
{
// Get buffer sizes
var result = GetDisplayConfigBufferSizes(QdcOnlyActivePaths, out uint pathCount, out uint modeCount);
if (result != 0)
{
return monitorInfo;
}
// Allocate buffers
var paths = new DISPLAYCONFIG_PATH_INFO[pathCount];
var modes = new DISPLAYCONFIG_MODE_INFO[modeCount];
// Query display configuration using fixed pointer
fixed (DISPLAYCONFIG_PATH_INFO* pathsPtr = paths)
{
fixed (DISPLAYCONFIG_MODE_INFO* modesPtr = modes)
{
result = QueryDisplayConfig(QdcOnlyActivePaths, ref pathCount, pathsPtr, ref modeCount, modesPtr, IntPtr.Zero);
if (result != 0)
{
return monitorInfo;
}
}
}
// Get information for each path
// The path index corresponds to Windows Display Settings "Identify" number
for (int i = 0; i < pathCount; i++)
{
var path = paths[i];
// Get GDI device name from source info (e.g., "\\.\DISPLAY1")
var gdiDeviceName = GetSourceGdiDeviceName(path.SourceInfo.AdapterId, path.SourceInfo.Id);
if (string.IsNullOrEmpty(gdiDeviceName))
{
continue;
}
// Get target info (friendly name, hardware ID, device path)
var (friendlyName, hardwareId, devicePath) = GetTargetDeviceInfo(path.TargetInfo.AdapterId, path.TargetInfo.Id);
// Use device path as key - unique per target, supports mirror mode
if (string.IsNullOrEmpty(devicePath))
{
continue;
}
monitorInfo[devicePath] = new MonitorDisplayInfo
{
DevicePath = devicePath,
GdiDeviceName = gdiDeviceName,
FriendlyName = friendlyName ?? string.Empty,
HardwareId = hardwareId ?? string.Empty,
AdapterId = path.TargetInfo.AdapterId,
TargetId = path.TargetInfo.Id,
MonitorNumber = i + 1, // 1-based, matches Windows Display Settings
};
}
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
}
return monitorInfo;
}
}
/// <summary>
/// Monitor display information structure
/// </summary>
public struct MonitorDisplayInfo
{
/// <summary>
/// Gets or sets the monitor device path (e.g., "\\?\DISPLAY#DELA1D8#...").
/// This is unique per target and used as the primary key.
/// </summary>
public string DevicePath { get; set; }
/// <summary>
/// Gets or sets the GDI device name (e.g., "\\.\DISPLAY1").
/// This is used to match with GetMonitorInfo results from HMONITOR.
/// In mirror mode, multiple targets may share the same GDI name.
/// </summary>
public string GdiDeviceName { get; set; }
/// <summary>
/// Gets or sets the friendly display name from EDID.
/// </summary>
public string FriendlyName { get; set; }
/// <summary>
/// Gets or sets the hardware ID derived from EDID manufacturer and product code.
/// </summary>
public string HardwareId { get; set; }
public LUID AdapterId { get; set; }
public uint TargetId { get; set; }
/// <summary>
/// Gets or sets the monitor number based on QueryDisplayConfig path index.
/// This matches the number shown in Windows Display Settings "Identify" feature.
/// 1-based index (paths[0] = 1, paths[1] = 2, etc.)
/// </summary>
public int MonitorNumber { get; set; }
}
}

View File

@@ -0,0 +1,154 @@
// 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.Common.Models;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.PInvoke;
using PHYSICAL_MONITOR = PowerDisplay.Common.Drivers.PhysicalMonitor;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Helper class for discovering and creating monitor objects
/// </summary>
public class MonitorDiscoveryHelper
{
public MonitorDiscoveryHelper()
{
}
/// <summary>
/// Get physical monitors for a logical monitor.
/// Filters out any monitors with NULL handles (Windows API bug workaround).
/// </summary>
/// <param name="hMonitor">Handle to the logical monitor</param>
/// <param name="hasNullHandles">Output: true if any NULL handles were filtered out</param>
/// <returns>Array of valid physical monitors, or null if API call failed</returns>
internal PHYSICAL_MONITOR[]? GetPhysicalMonitors(IntPtr hMonitor, out bool hasNullHandles)
{
hasNullHandles = false;
try
{
if (!GetNumberOfPhysicalMonitorsFromHMONITOR(hMonitor, out uint numMonitors))
{
Logger.LogWarning($"GetPhysicalMonitors: GetNumberOfPhysicalMonitorsFromHMONITOR failed for 0x{hMonitor:X}");
return null;
}
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);
}
}
if (!apiResult)
{
Logger.LogWarning($"GetPhysicalMonitors: GetPhysicalMonitorsFromHMONITOR failed");
return null;
}
// Filter out NULL handles and log each physical monitor
var validMonitors = new List<PHYSICAL_MONITOR>();
for (int i = 0; i < numMonitors; i++)
{
IntPtr handle = physicalMonitors[i].HPhysicalMonitor;
if (handle == IntPtr.Zero)
{
Logger.LogWarning($"GetPhysicalMonitors: Monitor [{i}] has NULL handle, filtering out");
hasNullHandles = true;
continue;
}
validMonitors.Add(physicalMonitors[i]);
}
return validMonitors.Count > 0 ? validMonitors.ToArray() : null;
}
catch (Exception ex)
{
Logger.LogError($"GetPhysicalMonitors: Exception: {ex.Message}");
return null;
}
}
/// <summary>
/// Create Monitor object from physical monitor and display info.
/// Uses MonitorDisplayInfo directly from QueryDisplayConfig for stable identification.
/// Note: Brightness is not initialized here - MonitorManager handles brightness initialization
/// after discovery to avoid slow I2C operations during the discovery phase.
/// </summary>
/// <param name="physicalMonitor">Physical monitor structure with handle and description</param>
/// <param name="monitorInfo">Display info from QueryDisplayConfig (HardwareId, FriendlyName, MonitorNumber)</param>
internal Monitor? CreateMonitorFromPhysical(
PHYSICAL_MONITOR physicalMonitor,
MonitorDisplayInfo monitorInfo)
{
try
{
// Get hardware ID and friendly name directly from MonitorDisplayInfo
string edidId = monitorInfo.HardwareId ?? string.Empty;
string name = physicalMonitor.GetDescription() ?? string.Empty;
// Use FriendlyName from QueryDisplayConfig if available and not generic
if (!string.IsNullOrEmpty(monitorInfo.FriendlyName) &&
!monitorInfo.FriendlyName.Contains("Generic"))
{
name = monitorInfo.FriendlyName;
}
// Generate unique monitor Id: "DDC_{EdidId}_{MonitorNumber}"
string monitorId = !string.IsNullOrEmpty(edidId)
? $"DDC_{edidId}_{monitorInfo.MonitorNumber}"
: $"DDC_Unknown_{monitorInfo.MonitorNumber}";
// If still no good name, use default value
if (string.IsNullOrEmpty(name) || name.Contains("Generic") || name.Contains("PnP"))
{
name = "External Display";
}
var monitor = new Monitor
{
Id = monitorId,
Name = name.Trim(),
CurrentBrightness = 50, // Default value, will be updated by MonitorManager after discovery
MinBrightness = 0,
MaxBrightness = 100,
IsAvailable = true,
Handle = physicalMonitor.HPhysicalMonitor,
Capabilities = MonitorCapabilities.DdcCi,
CommunicationMethod = "DDC/CI",
MonitorNumber = monitorInfo.MonitorNumber,
GdiDeviceName = monitorInfo.GdiDeviceName ?? string.Empty,
Orientation = DmdoDefault, // Orientation will be set separately if needed
};
// 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;
}
}
}
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using ManagedCommon;
using static PowerDisplay.Common.Drivers.PInvoke;
namespace PowerDisplay.Common.Drivers.DDC
{
/// <summary>
/// Manages physical monitor handles - reuse, cleanup, and validation
/// </summary>
public partial class PhysicalMonitorHandleManager : IDisposable
{
// Mapping: monitorId -> physical handle (thread-safe)
private readonly ConcurrentDictionary<string, IntPtr> _monitorIdToHandleMap = new();
private readonly object _handleLock = new();
private bool _disposed;
/// <summary>
/// Update the handle mapping with new handles
/// </summary>
public void UpdateHandleMap(Dictionary<string, IntPtr> newHandleMap)
{
// Lock to ensure atomic update (cleanup + replace)
lock (_handleLock)
{
// Clean up unused handles before updating
CleanupUnusedHandles(newHandleMap);
// Update the device key map
_monitorIdToHandleMap.Clear();
foreach (var kvp in newHandleMap)
{
_monitorIdToHandleMap[kvp.Key] = kvp.Value;
}
}
}
/// <summary>
/// Clean up handles that are no longer in use.
/// Called within lock context. Optimized to O(n) using HashSet lookup.
/// </summary>
private void CleanupUnusedHandles(Dictionary<string, IntPtr> newHandles)
{
if (_monitorIdToHandleMap.IsEmpty)
{
return;
}
// Build HashSet of handles that will be reused (O(m))
var reusedHandles = new HashSet<IntPtr>(newHandles.Values);
// Find handles to destroy: in old map but not reused (O(n) with O(1) lookup)
var handlesToDestroy = _monitorIdToHandleMap.Values
.Where(h => h != IntPtr.Zero && !reusedHandles.Contains(h))
.ToList();
// Destroy unused handles
foreach (var handle in handlesToDestroy)
{
try
{
DestroyPhysicalMonitor(handle);
}
catch
{
// Silently ignore cleanup failures
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
// Release all physical monitor handles - get snapshot to avoid holding lock during cleanup
var handles = _monitorIdToHandleMap.Values.ToList();
foreach (var handle in handles)
{
if (handle != IntPtr.Zero)
{
try
{
DestroyPhysicalMonitor(handle);
}
catch
{
// Silently ignore cleanup failures
}
}
}
_monitorIdToHandleMap.Clear();
_disposed = true;
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,425 @@
// 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.Common.Drivers
{
/// <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: Input Source (0x60)
/// Standard VESA MCCS input source selection.
/// Supports values like: 0x0F=DisplayPort-1, 0x10=DisplayPort-2, 0x11=HDMI-1, 0x12=HDMI-2, 0x1B=USB-C.
/// Note: Actual supported values depend on monitor capabilities.
/// </summary>
public const byte VcpCodeInputSource = 0x60;
/// <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 source name (GDI device name like "\\.\DISPLAY1")
/// </summary>
public const uint DisplayconfigDeviceInfoGetSourceName = 1;
/// <summary>
/// Get target name (monitor friendly name and hardware ID)
/// </summary>
public const uint DisplayconfigDeviceInfoGetTargetName = 2;
/// <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;
/// <summary>
/// Retrieve the current settings for the display device.
/// </summary>
public const int EnumCurrentSettings = -1;
/// <summary>
/// Retrieve the settings for the display device that are stored in the registry.
/// </summary>
public const int EnumRegistrySettings = -2;
/// <summary>
/// The display is in the natural orientation of the device.
/// </summary>
public const int DmdoDefault = 0;
/// <summary>
/// The display is rotated 90 degrees (measured clockwise) from its natural orientation.
/// </summary>
public const int Dmdo90 = 1;
/// <summary>
/// The display is rotated 180 degrees (measured clockwise) from its natural orientation.
/// </summary>
public const int Dmdo180 = 2;
/// <summary>
/// The display is rotated 270 degrees (measured clockwise) from its natural orientation.
/// </summary>
public const int Dmdo270 = 3;
// ==================== DEVMODE field flags ====================
/// <summary>
/// DmDisplayOrientation field is valid.
/// </summary>
public const int DmDisplayOrientation = 0x00000080;
/// <summary>
/// DmPelsWidth field is valid.
/// </summary>
public const int DmPelsWidth = 0x00080000;
/// <summary>
/// DmPelsHeight field is valid.
/// </summary>
public const int DmPelsHeight = 0x00100000;
// ==================== ChangeDisplaySettings flags ====================
/// <summary>
/// The settings change is temporary. Not saved to registry.
/// </summary>
public const uint CdsUpdateregistry = 0x00000001;
/// <summary>
/// Test the graphics mode but don't actually set it.
/// </summary>
public const uint CdsTest = 0x00000002;
/// <summary>
/// The mode is fullscreen.
/// </summary>
public const uint CdsFullscreen = 0x00000004;
/// <summary>
/// The settings apply to all users.
/// </summary>
public const uint CdsGlobal = 0x00000008;
/// <summary>
/// Set the primary display.
/// </summary>
public const uint CdsSetPrimary = 0x00000010;
/// <summary>
/// Reset the mode after a dynamic mode change.
/// </summary>
public const uint CdsReset = 0x40000000;
/// <summary>
/// Don't reset the mode.
/// </summary>
public const uint CdsNoreset = 0x10000000;
// ==================== ChangeDisplaySettings result codes ====================
/// <summary>
/// The settings change was successful.
/// </summary>
public const int DispChangeSuccessful = 0;
/// <summary>
/// The computer must be restarted for the graphics mode to work.
/// </summary>
public const int DispChangeRestart = 1;
/// <summary>
/// The display driver failed the specified graphics mode.
/// </summary>
public const int DispChangeFailed = -1;
/// <summary>
/// The graphics mode is not supported.
/// </summary>
public const int DispChangeBadmode = -2;
/// <summary>
/// Unable to write settings to the registry.
/// </summary>
public const int DispChangeNotupdated = -3;
/// <summary>
/// An invalid set of flags was passed in.
/// </summary>
public const int DispChangeBadflags = -4;
/// <summary>
/// An invalid parameter was passed in.
/// </summary>
public const int DispChangeBadparam = -5;
}
}

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.Runtime.InteropServices;
namespace PowerDisplay.Common.Drivers;
/// <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,484 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
#pragma warning disable SA1649 // File name should match first type name - Multiple related P/Invoke structures
namespace PowerDisplay.Common.Drivers
{
/// <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>
/// 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 source device name - contains GDI device name (e.g., "\\.\DISPLAY1")
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct DISPLAYCONFIG_SOURCE_DEVICE_NAME
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
/// <summary>
/// GDI device name - fixed buffer for 32 wide characters (CCHDEVICENAME)
/// </summary>
public fixed ushort ViewGdiDeviceName[32];
/// <summary>
/// Helper method to get GDI device name as string
/// </summary>
public readonly string GetViewGdiDeviceName()
{
fixed (ushort* ptr = ViewGdiDeviceName)
{
return new string((char*)ptr);
}
}
}
/// <summary>
/// Display configuration target device name
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct DISPLAYCONFIG_TARGET_DEVICE_NAME
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint Flags;
public uint OutputTechnology;
public ushort EdidManufactureId;
public ushort EdidProductCodeId;
public uint ConnectorInstance;
/// <summary>
/// Monitor friendly name - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort MonitorFriendlyDeviceName[64];
/// <summary>
/// Monitor device path - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort MonitorDevicePath[128];
/// <summary>
/// Helper method to get monitor friendly name as string
/// </summary>
public readonly string GetMonitorFriendlyDeviceName()
{
fixed (ushort* ptr = MonitorFriendlyDeviceName)
{
return new string((char*)ptr);
}
}
/// <summary>
/// Helper method to get monitor device path as string
/// </summary>
public readonly string GetMonitorDevicePath()
{
fixed (ushort* ptr = MonitorDevicePath)
{
return new string((char*)ptr);
}
}
}
/// <summary>
/// Display configuration SDR white level
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SDR_WHITE_LEVEL
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint SDRWhiteLevel;
}
/// <summary>
/// Display configuration advanced color information
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint AdvancedColorSupported;
public uint AdvancedColorEnabled;
public uint BitsPerColorChannel;
public uint ColorEncoding;
public uint FormatSupport;
}
/// <summary>
/// Custom structure for setting SDR white level
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct DISPLAYCONFIG_SET_SDR_WHITE_LEVEL
{
public DISPLAYCONFIG_DEVICE_INFO_HEADER Header;
public uint SDRWhiteLevel;
public byte FinalValue;
}
/// <summary>
/// Point structure
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
this.X = x;
this.Y = y;
}
}
/// <summary>
/// The DEVMODE structure contains information about the initialization and environment of a printer or a display device.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public unsafe struct DevMode
{
/// <summary>
/// Device name - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DmDeviceName[32];
public short DmSpecVersion;
public short DmDriverVersion;
public short DmSize;
public short DmDriverExtra;
public int DmFields;
public int DmPositionX;
public int DmPositionY;
public int DmDisplayOrientation;
public int DmDisplayFixedOutput;
public short DmColor;
public short DmDuplex;
public short DmYResolution;
public short DmTTOption;
public short DmCollate;
/// <summary>
/// Form name - fixed buffer for LibraryImport compatibility
/// </summary>
public fixed ushort DmFormName[32];
public short DmLogPixels;
public int DmBitsPerPel;
public int DmPelsWidth;
public int DmPelsHeight;
public int DmDisplayFlags;
public int DmDisplayFrequency;
public int DmICMMethod;
public int DmICMIntent;
public int DmMediaType;
public int DmDitherType;
public int DmReserved1;
public int DmReserved2;
public int DmPanningWidth;
public int DmPanningHeight;
}
}

View File

@@ -0,0 +1,151 @@
// 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.Common.Drivers
{
/// <summary>
/// P/Invoke declarations using LibraryImport source generator
/// </summary>
internal static partial class PInvoke
{
// ==================== User32.dll - Display Configuration ====================
[LibraryImport("user32.dll")]
internal static partial int GetDisplayConfigBufferSizes(
uint flags,
out uint numPathArrayElements,
out uint numModeInfoArrayElements);
// Use unsafe pointer to avoid runtime marshalling
[LibraryImport("user32.dll")]
internal static unsafe partial int QueryDisplayConfig(
uint flags,
ref uint numPathArrayElements,
DISPLAYCONFIG_PATH_INFO* pathArray,
ref uint numModeInfoArrayElements,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
IntPtr currentTopologyId);
[LibraryImport("user32.dll")]
internal static partial int DisplayConfigGetDeviceInfo(
ref DISPLAYCONFIG_TARGET_DEVICE_NAME deviceName);
[LibraryImport("user32.dll")]
internal static partial int DisplayConfigGetDeviceInfo(
ref DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName);
// ==================== 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", EntryPoint = "EnumDisplaySettingsW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool EnumDisplaySettings(
[MarshalAs(UnmanagedType.LPWStr)] string? lpszDeviceName,
int iModeNum,
DevMode* lpDevMode);
[LibraryImport("user32.dll", EntryPoint = "ChangeDisplaySettingsExW", StringMarshalling = StringMarshalling.Utf16)]
internal static unsafe partial int ChangeDisplaySettingsEx(
[MarshalAs(UnmanagedType.LPWStr)] string? lpszDeviceName,
DevMode* lpDevMode,
IntPtr hwnd,
uint dwflags,
IntPtr lParam);
[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);
// ==================== Kernel32.dll ====================
[LibraryImport("kernel32.dll")]
internal static partial uint GetLastError();
}
}

View File

@@ -0,0 +1,376 @@
// 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.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Utils;
using WmiLight;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.Common.Drivers.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";
// Common WMI error codes for classification
private const int WbemENotFound = unchecked((int)0x80041002);
private const int WbemEAccessDenied = unchecked((int)0x80041003);
private const int WbemEProviderFailure = unchecked((int)0x80041004);
private const int WbemEInvalidQuery = unchecked((int)0x80041017);
private const int WmiFeatureNotSupported = 0x1068;
/// <summary>
/// Classifies WMI exceptions into user-friendly error messages.
/// </summary>
private static MonitorOperationResult ClassifyWmiError(WmiException ex, string operation)
{
var hresult = ex.HResult;
return hresult switch
{
WbemENotFound => MonitorOperationResult.Failure($"WMI class not found during {operation}. This feature may not be supported on your system.", hresult),
WbemEAccessDenied => MonitorOperationResult.Failure($"Access denied during {operation}. Administrator privileges may be required.", hresult),
WbemEProviderFailure => MonitorOperationResult.Failure($"WMI provider failure during {operation}. The display driver may not support this feature.", hresult),
WbemEInvalidQuery => MonitorOperationResult.Failure($"Invalid WMI query during {operation}. This is likely a bug.", hresult),
WmiFeatureNotSupported => MonitorOperationResult.Failure($"WMI brightness control not supported on this system during {operation}.", hresult),
_ => MonitorOperationResult.Failure($"WMI error during {operation}: {ex.Message}", hresult),
};
}
/// <summary>
/// Escape special characters in WMI query strings.
/// WMI requires backslashes and single quotes to be escaped in WHERE clauses.
/// See: https://learn.microsoft.com/en-us/windows/win32/wmisdk/wql-sql-for-wmi
/// </summary>
/// <param name="value">The string value to escape.</param>
/// <returns>The escaped string safe for use in WMI queries.</returns>
private static string EscapeWmiString(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
// WMI requires backslashes and single quotes to be escaped in WHERE clauses
// Backslash must be escaped first to avoid double-escaping the quote's backslash
return value.Replace("\\", "\\\\").Replace("'", "\\'");
}
/// <summary>
/// Extract hardware ID from WMI InstanceName.
/// InstanceName format: "DISPLAY\BOE0900\4&amp;10fd3ab1&amp;0&amp;UID265988_0"
/// Returns the second segment (e.g., "BOE0900") which is the manufacturer+product code.
/// </summary>
/// <param name="instanceName">The WMI InstanceName.</param>
/// <returns>The hardware ID extracted from the InstanceName, or empty string if extraction fails.</returns>
private static string ExtractHardwareIdFromInstanceName(string instanceName)
{
if (string.IsNullOrEmpty(instanceName))
{
return string.Empty;
}
// Split by backslash: ["DISPLAY", "BOE0900", "4&10fd3ab1&0&UID265988_0"]
var parts = instanceName.Split('\\');
if (parts.Length >= 2 && !string.IsNullOrEmpty(parts[1]))
{
// Return the second part (e.g., "BOE0900")
return parts[1];
}
return string.Empty;
}
/// <summary>
/// Build a WMI query filtered by monitor instance name.
/// </summary>
/// <param name="wmiClass">The WMI class to query.</param>
/// <param name="instanceName">The monitor instance name to filter by.</param>
/// <param name="selectClause">Optional SELECT clause fields (defaults to "*").</param>
/// <returns>The formatted WMI query string.</returns>
private static string BuildInstanceNameQuery(string wmiClass, string instanceName, string selectClause = "*")
{
var escapedInstanceName = EscapeWmiString(instanceName);
return $"SELECT {selectClause} FROM {wmiClass} WHERE InstanceName = '{escapedInstanceName}'";
}
/// <summary>
/// Get MonitorDisplayInfo from dictionary by matching HardwareId.
/// Uses QueryDisplayConfig path index which matches Windows Display Settings "Identify" feature.
/// </summary>
/// <param name="hardwareId">The hardware ID to match (e.g., "LEN4038", "BOE0900").</param>
/// <param name="monitorDisplayInfos">Dictionary of monitor display info from QueryDisplayConfig.</param>
/// <returns>MonitorDisplayInfo if found, or null if not found.</returns>
private static Drivers.DDC.MonitorDisplayInfo? GetMonitorDisplayInfoByHardwareId(string hardwareId, Dictionary<string, Drivers.DDC.MonitorDisplayInfo> monitorDisplayInfos)
{
if (string.IsNullOrEmpty(hardwareId) || monitorDisplayInfos == null || monitorDisplayInfos.Count == 0)
{
return null;
}
var match = monitorDisplayInfos.Values.FirstOrDefault(
v => hardwareId.Equals(v.HardwareId, StringComparison.OrdinalIgnoreCase));
// Check if match was found (struct default has null/empty HardwareId)
if (!string.IsNullOrEmpty(match.HardwareId))
{
return match;
}
Logger.LogWarning($"WMI: Could not find MonitorDisplayInfo for HardwareId '{hardwareId}'");
return null;
}
public string Name => "WMI Monitor Controller";
/// <summary>
/// Get monitor brightness
/// </summary>
public async Task<VcpFeatureValue> GetBrightnessAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
return await Task.Run(
() =>
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = BuildInstanceNameQuery(BrightnessQueryClass, monitor.InstanceName, "CurrentBrightness");
var results = connection.CreateQuery(query);
foreach (var obj in results)
{
var currentBrightness = obj.GetPropertyValue<byte>("CurrentBrightness");
return new VcpFeatureValue(currentBrightness, 0, 100);
}
// No match found - monitor may have been disconnected
}
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 VcpFeatureValue.Invalid;
},
cancellationToken);
}
/// <summary>
/// Set monitor brightness
/// </summary>
public async Task<MonitorOperationResult> SetBrightnessAsync(Monitor monitor, int brightness, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(monitor);
// Validate brightness range
brightness = Math.Clamp(brightness, 0, 100);
return await Task.Run(
() =>
{
try
{
using var connection = new WmiConnection(WmiNamespace);
var query = BuildInstanceNameQuery(BrightnessMethodClass, monitor.InstanceName);
var results = connection.CreateQuery(query);
foreach (var obj in results)
{
// Call WmiSetBrightness method
// Parameters: Timeout (uint32), Brightness (uint8)
// Note: WmiLight requires string values for method parameters
using (WmiMethod method = obj.GetMethod("WmiSetBrightness"))
using (WmiMethodParameters inParams = method.CreateInParameters())
{
inParams.SetPropertyValue("Timeout", "0");
inParams.SetPropertyValue("Brightness", brightness.ToString(CultureInfo.InvariantCulture));
uint result = obj.ExecuteMethod<uint>(
method,
inParams,
out WmiMethodParameters outParams);
// Check return value (0 indicates success)
if (result == 0)
{
return MonitorOperationResult.Success();
}
return MonitorOperationResult.Failure($"WMI method returned error code: {result}", (int)result);
}
}
// No match found - monitor may have been disconnected
Logger.LogWarning($"WMI SetBrightness: No monitor found with InstanceName '{monitor.InstanceName}'");
return MonitorOperationResult.Failure($"No WMI brightness method found for monitor '{monitor.InstanceName}'");
}
catch (UnauthorizedAccessException)
{
return MonitorOperationResult.Failure("Access denied. Administrator privileges may be required.", 5);
}
catch (WmiException ex)
{
return ClassifyWmiError(ex, "SetBrightness");
}
catch (Exception ex)
{
return MonitorOperationResult.Failure($"Unexpected error during SetBrightness: {ex.Message}");
}
},
cancellationToken);
}
/// <summary>
/// Discover supported monitors.
/// WMI brightness control is typically only available on internal laptop displays,
/// which don't have meaningful UserFriendlyName in WmiMonitorID, so we use "Built-in Display".
/// </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);
// Query WMI brightness support - only internal displays typically support this
var brightnessQuery = $"SELECT InstanceName, CurrentBrightness FROM {BrightnessQueryClass}";
var brightnessResults = connection.CreateQuery(brightnessQuery).ToList();
if (brightnessResults.Count == 0)
{
return monitors;
}
// Get MonitorDisplayInfo from QueryDisplayConfig - this provides the correct monitor numbers
var monitorDisplayInfos = Drivers.DDC.DdcCiNative.GetAllMonitorDisplayInfo();
// 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");
// Extract hardware ID from InstanceName
// e.g., "DISPLAY\LEN4038\4&40f4dee&0&UID8388688_0" -> "LEN4038"
var hardwareId = ExtractHardwareIdFromInstanceName(instanceName);
// Get MonitorDisplayInfo from QueryDisplayConfig by matching hardware ID
// This provides MonitorNumber and GdiDeviceName for display settings APIs
var displayInfo = GetMonitorDisplayInfoByHardwareId(hardwareId, monitorDisplayInfos);
int monitorNumber = displayInfo?.MonitorNumber ?? 0;
string gdiDeviceName = displayInfo?.GdiDeviceName ?? string.Empty;
// Generate unique monitor Id: "WMI_{HardwareId}_{MonitorNumber}"
string monitorId = !string.IsNullOrEmpty(hardwareId)
? $"WMI_{hardwareId}_{monitorNumber}"
: $"WMI_Unknown_{monitorNumber}";
// Get display name from PnP manufacturer ID (e.g., "Lenovo Built-in Display")
var displayName = PnpIdHelper.GetBuiltInDisplayName(hardwareId);
var monitor = new Monitor
{
Id = monitorId,
Name = displayName,
CurrentBrightness = currentBrightness,
MinBrightness = 0,
MaxBrightness = 100,
IsAvailable = true,
InstanceName = instanceName,
Capabilities = MonitorCapabilities.Brightness | MonitorCapabilities.Wmi,
CommunicationMethod = "WMI",
SupportsColorTemperature = false,
MonitorNumber = monitorNumber,
GdiDeviceName = gdiDeviceName,
};
monitors.Add(monitor);
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to create monitor from WMI data: {ex.Message}");
}
}
}
catch (WmiException ex)
{
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);
}
// Extended features not supported by WMI
public Task<MonitorOperationResult> SetContrastAsync(Monitor monitor, int contrast, CancellationToken cancellationToken = default)
{
return Task.FromResult(MonitorOperationResult.Failure("Contrast control not supported via WMI"));
}
public Task<MonitorOperationResult> SetVolumeAsync(Monitor monitor, int volume, CancellationToken cancellationToken = default)
{
return Task.FromResult(MonitorOperationResult.Failure("Volume control not supported via WMI"));
}
public Task<VcpFeatureValue> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
return Task.FromResult(VcpFeatureValue.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<VcpFeatureValue> GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default)
{
// Input source switching not supported for internal displays
return Task.FromResult(VcpFeatureValue.Invalid);
}
public Task<MonitorOperationResult> SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default)
{
// Input source switching not supported for internal displays
return Task.FromResult(MonitorOperationResult.Failure("Input source switching not supported via WMI"));
}
public void Dispose()
{
// WmiLight objects are created per-operation and disposed immediately via using statements.
// No instance-level resources require cleanup.
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,105 @@
// 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.Common.Models;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.Common.Interfaces
{
/// <summary>
/// Monitor controller interface
/// </summary>
public interface IMonitorController
{
/// <summary>
/// Gets controller name
/// </summary>
string Name { get; }
/// <summary>
/// Gets monitor brightness
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Brightness information</returns>
Task<VcpFeatureValue> 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>
/// 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>
/// 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 using VCP 0x14 (Select Color Preset)
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature</returns>
Task<VcpFeatureValue> GetColorTemperatureAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets monitor color temperature using VCP 0x14 preset value
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="colorTemperature">VCP preset value (e.g., 0x05 for 6500K), not Kelvin temperature</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetColorTemperatureAsync(Monitor monitor, int colorTemperature, CancellationToken cancellationToken = default);
/// <summary>
/// Gets current input source using VCP 0x60
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>VCP input source value (e.g., 0x11 for HDMI-1)</returns>
Task<VcpFeatureValue> GetInputSourceAsync(Monitor monitor, CancellationToken cancellationToken = default);
/// <summary>
/// Sets input source using VCP 0x60
/// </summary>
/// <param name="monitor">Monitor object</param>
/// <param name="inputSource">VCP input source value (e.g., 0x11 for HDMI-1)</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
Task<MonitorOperationResult> SetInputSourceAsync(Monitor monitor, int inputSource, CancellationToken cancellationToken = default);
/// <summary>
/// Releases resources
/// </summary>
void Dispose();
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace PowerDisplay.Common.Interfaces
{
/// <summary>
/// Core interface representing monitor hardware data.
/// This interface defines the actual hardware values for a monitor.
/// Implementations can add UI-specific properties and use converters for display formatting.
/// </summary>
public interface IMonitorData
{
/// <summary>
/// Gets or sets the unique identifier for the monitor.
/// </summary>
string Id { get; set; }
/// <summary>
/// Gets or sets the display name of the monitor.
/// </summary>
string Name { get; set; }
/// <summary>
/// Gets or sets the current brightness value (0-100).
/// </summary>
int Brightness { get; set; }
/// <summary>
/// Gets or sets the current contrast value (0-100).
/// </summary>
int Contrast { get; set; }
/// <summary>
/// Gets or sets the current volume value (0-100).
/// </summary>
int Volume { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP preset value (raw DDC/CI value from VCP code 0x14).
/// This stores the raw VCP value (e.g., 0x05 for 6500K preset), not the Kelvin temperature.
/// Use MonitorValueConverter to convert to/from human-readable Kelvin values.
/// </summary>
int ColorTemperatureVcp { get; set; }
/// <summary>
/// Gets or sets the monitor number (1, 2, 3...) as assigned by the OS.
/// </summary>
int MonitorNumber { get; set; }
/// <summary>
/// Gets or sets the monitor orientation (0=0, 1=90, 2=180, 3=270).
/// </summary>
int Orientation { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
// 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 PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Interfaces
{
/// <summary>
/// Interface for profile management service.
/// Provides abstraction for loading, saving, and managing PowerDisplay profiles.
/// Enables dependency injection and unit testing.
/// </summary>
public interface IProfileService
{
/// <summary>
/// Loads PowerDisplay profiles from disk.
/// </summary>
/// <returns>PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails.</returns>
PowerDisplayProfiles LoadProfiles();
/// <summary>
/// Saves PowerDisplay profiles to disk.
/// </summary>
/// <param name="profiles">The profiles collection to save.</param>
/// <returns>True if save was successful, false otherwise.</returns>
bool SaveProfiles(PowerDisplayProfiles profiles);
/// <summary>
/// Adds or updates a profile in the collection and persists to disk.
/// </summary>
/// <param name="profile">The profile to add or update.</param>
/// <returns>True if operation was successful, false otherwise.</returns>
bool AddOrUpdateProfile(PowerDisplayProfile profile);
/// <summary>
/// Removes a profile by name and persists to disk.
/// </summary>
/// <param name="profileName">The name of the profile to remove.</param>
/// <returns>True if profile was found and removed, false otherwise.</returns>
bool RemoveProfile(string profileName);
/// <summary>
/// Gets a profile by name.
/// </summary>
/// <param name="profileName">The name of the profile to retrieve.</param>
/// <returns>The profile if found, null otherwise.</returns>
PowerDisplayProfile? GetProfile(string profileName);
/// <summary>
/// Checks if the profiles file exists.
/// </summary>
/// <returns>True if profiles file exists, false otherwise.</returns>
bool ProfilesFileExists();
/// <summary>
/// Gets the path to the profiles file.
/// </summary>
/// <returns>The full path to the profiles file.</returns>
string GetProfilesFilePath();
}
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Represents a color temperature preset item for VCP code 0x14.
/// Used to display available color temperature presets in UI components.
/// </summary>
public partial class ColorPresetItem : INotifyPropertyChanged
{
private int _vcpValue;
private string _displayName = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="ColorPresetItem"/> class.
/// </summary>
public ColorPresetItem()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPresetItem"/> class.
/// </summary>
/// <param name="vcpValue">The VCP value for the color temperature preset.</param>
/// <param name="displayName">The display name for UI.</param>
public ColorPresetItem(int vcpValue, string displayName)
{
_vcpValue = vcpValue;
_displayName = displayName;
}
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Gets or sets the VCP value for this color temperature preset.
/// </summary>
[JsonPropertyName("vcpValue")]
public int VcpValue
{
get => _vcpValue;
set
{
if (_vcpValue != value)
{
_vcpValue = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets the display name for UI.
/// </summary>
[JsonPropertyName("displayName")]
public string DisplayName
{
get => _displayName;
set
{
if (_displayName != value)
{
_displayName = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Represents a pending color temperature change operation
/// </summary>
public class ColorTemperatureOperation
{
[JsonPropertyName("monitor_id")]
public string MonitorId { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP preset value.
/// JSON property name kept as "color_temperature" for IPC compatibility.
/// </summary>
[JsonPropertyName("color_temperature")]
public int ColorTemperatureVcp { get; set; }
public ColorTemperatureOperation()
{
MonitorId = string.Empty;
}
}
}

View File

@@ -0,0 +1,352 @@
// 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 PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Monitor model that implements property change notification.
/// Implements IMonitorData to provide a common interface for monitor hardware values.
/// </summary>
/// <remarks>
/// <para><see cref="Id"/> is the unique identifier used for all purposes: UI lookups, IPC, persistent storage, and handle management.</para>
/// <para>Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").</para>
/// </remarks>
public partial class Monitor : INotifyPropertyChanged, IMonitorData
{
private int _currentBrightness;
private int _currentColorTemperature = 0x05; // Default to 6500K preset (VCP 0x14 value)
private int _currentInputSource; // VCP 0x60 value
private bool _isAvailable = true;
private int _orientation;
/// <summary>
/// Gets or sets unique identifier for all purposes: UI lookups, IPC, persistent storage, and handle management.
/// </summary>
/// <remarks>
/// Format: "{Source}_{EdidId}_{MonitorNumber}" where Source is "DDC" or "WMI".
/// Examples: "DDC_GSM5C6D_1", "WMI_BOE0900_2".
/// Stable across reboots and unique even for multiple identical monitors.
/// </remarks>
public string Id { get; set; } = string.Empty;
/// <summary>
/// Gets or sets display name
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets 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>
/// Gets or sets minimum brightness value
/// </summary>
public int MinBrightness { get; set; }
/// <summary>
/// Gets or sets maximum brightness value
/// </summary>
public int MaxBrightness { get; set; } = 100;
/// <summary>
/// Gets or sets 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>
/// Gets human-readable color temperature preset name (e.g., "6500K (0x05)", "sRGB (0x01)")
/// </summary>
public string ColorTemperaturePresetName =>
VcpNames.GetFormattedValueName(0x14, CurrentColorTemperature);
/// <summary>
/// Gets or sets a value indicating whether the monitor supports color temperature adjustment via VCP 0x14
/// </summary>
public bool SupportsColorTemperature { get; set; }
/// <summary>
/// Gets or sets current input source VCP value (from VCP code 0x60).
/// This stores the raw VCP value (e.g., 0x11 for HDMI-1).
/// Use InputSourceName to get human-readable name.
/// </summary>
public int CurrentInputSource
{
get => _currentInputSource;
set
{
if (_currentInputSource != value)
{
_currentInputSource = value;
OnPropertyChanged();
OnPropertyChanged(nameof(InputSourceName));
}
}
}
/// <summary>
/// Gets human-readable input source name (e.g., "HDMI-1", "DisplayPort-1")
/// Returns just the name without hex value for cleaner UI display.
/// </summary>
public string InputSourceName =>
VcpNames.GetValueName(0x60, CurrentInputSource) ?? $"Source 0x{CurrentInputSource:X2}";
/// <summary>
/// Gets a value indicating whether the monitor supports input source switching via VCP 0x60
/// </summary>
public bool SupportsInputSource => VcpCapabilitiesInfo?.SupportsVcpCode(0x60) ?? false;
/// <summary>
/// Gets get supported input sources from capabilities (as list of VCP values)
/// </summary>
public System.Collections.Generic.IReadOnlyList<int>? SupportedInputSources =>
VcpCapabilitiesInfo?.GetSupportedValues(0x60);
/// <summary>
/// Gets a value indicating whether the monitor supports contrast adjustment
/// </summary>
public bool SupportsContrast => Capabilities.HasFlag(MonitorCapabilities.Contrast);
/// <summary>
/// Gets a value indicating whether the monitor supports volume adjustment (for audio-capable monitors)
/// </summary>
public bool SupportsVolume => Capabilities.HasFlag(MonitorCapabilities.Volume);
private int _currentContrast = 50;
private int _currentVolume = 50;
/// <summary>
/// Gets or sets 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>
/// Gets or sets minimum contrast value
/// </summary>
public int MinContrast { get; set; }
/// <summary>
/// Gets or sets maximum contrast value
/// </summary>
public int MaxContrast { get; set; } = 100;
/// <summary>
/// Gets or sets 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>
/// Gets or sets minimum volume value
/// </summary>
public int MinVolume { get; set; }
/// <summary>
/// Gets or sets maximum volume value
/// </summary>
public int MaxVolume { get; set; } = 100;
/// <summary>
/// Gets or sets a value indicating whether the monitor is available/online
/// </summary>
public bool IsAvailable
{
get => _isAvailable;
set
{
if (_isAvailable != value)
{
_isAvailable = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Gets or sets physical monitor handle (for DDC/CI)
/// </summary>
public IntPtr Handle { get; set; } = IntPtr.Zero;
/// <summary>
/// Gets or sets instance name (used by WMI)
/// </summary>
public string InstanceName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets communication method (DDC/CI, WMI, HDR API, etc.)
/// </summary>
public string CommunicationMethod { get; set; } = string.Empty;
/// <summary>
/// Gets or sets supported control methods
/// </summary>
public MonitorCapabilities Capabilities { get; set; } = MonitorCapabilities.None;
/// <summary>
/// Gets or sets raw DDC/CI capabilities string (MCCS format)
/// </summary>
public string? CapabilitiesRaw { get; set; }
/// <summary>
/// Gets or sets parsed VCP capabilities information
/// </summary>
public VcpCapabilities? VcpCapabilitiesInfo { get; set; }
/// <summary>
/// Gets or sets 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;
}
}
/// <inheritdoc />
int IMonitorData.Brightness
{
get => CurrentBrightness;
set => CurrentBrightness = value;
}
/// <inheritdoc />
int IMonitorData.Contrast
{
get => CurrentContrast;
set => CurrentContrast = value;
}
/// <inheritdoc />
int IMonitorData.Volume
{
get => CurrentVolume;
set => CurrentVolume = value;
}
/// <inheritdoc />
int IMonitorData.ColorTemperatureVcp
{
get => CurrentColorTemperature;
set => CurrentColorTemperature = value;
}
/// <summary>
/// Gets or sets monitor number (1, 2, 3...)
/// </summary>
public int MonitorNumber { get; set; }
/// <summary>
/// Gets or sets the GDI device name (e.g., "\\.\DISPLAY1").
/// This is obtained from QueryDisplayConfig during discovery and should be used
/// for display settings APIs (EnumDisplaySettings, ChangeDisplaySettingsEx).
/// </summary>
public string GdiDeviceName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets monitor orientation (0=0, 1=90, 2=180, 3=270).
/// Fires PropertyChanged when value changes.
/// </summary>
public int Orientation
{
get => _orientation;
set
{
if (_orientation != value)
{
_orientation = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc />
int IMonitorData.MonitorNumber
{
get => MonitorNumber;
set => MonitorNumber = value;
}
/// <inheritdoc />
int IMonitorData.Orientation
{
get => Orientation;
set => Orientation = value;
}
}
}

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.Common.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.Common.Models
{
/// <summary>
/// Monitor operation result
/// </summary>
public readonly struct MonitorOperationResult
{
/// <summary>
/// Gets a value indicating whether the operation was successful
/// </summary>
public bool IsSuccess { get; }
/// <summary>
/// Gets error message
/// </summary>
public string? ErrorMessage { get; }
/// <summary>
/// Gets system error code
/// </summary>
public int? ErrorCode { get; }
/// <summary>
/// Gets 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,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;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Individual monitor state entry for JSON persistence.
/// Stores the current state of a monitor's adjustable parameters.
/// </summary>
public sealed class MonitorStateEntry
{
/// <summary>
/// Gets or sets the brightness level (0-100).
/// </summary>
[JsonPropertyName("brightness")]
public int Brightness { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP value.
/// </summary>
[JsonPropertyName("colorTemperature")]
public int ColorTemperatureVcp { get; set; }
/// <summary>
/// Gets or sets the contrast level (0-100).
/// </summary>
[JsonPropertyName("contrast")]
public int Contrast { get; set; }
/// <summary>
/// Gets or sets the volume level (0-100).
/// </summary>
[JsonPropertyName("volume")]
public int Volume { get; set; }
/// <summary>
/// Gets or sets the raw capabilities string from DDC/CI.
/// </summary>
[JsonPropertyName("capabilitiesRaw")]
public string? CapabilitiesRaw { get; set; }
/// <summary>
/// Gets or sets when this entry was last updated.
/// </summary>
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@@ -0,0 +1,30 @@
// 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.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Monitor state file structure for JSON persistence.
/// Contains all monitor states indexed by Monitor.Id.
/// </summary>
public sealed class MonitorStateFile
{
/// <summary>
/// Gets or sets the monitor states dictionary.
/// Key is the monitor's unique Id (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").
/// </summary>
[JsonPropertyName("monitors")]
public Dictionary<string, MonitorStateEntry> Monitors { get; set; } = new();
/// <summary>
/// Gets or sets when the file was last updated.
/// </summary>
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
// 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.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Represents a PowerDisplay profile containing monitor settings
/// </summary>
public class PowerDisplayProfile
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("monitorSettings")]
public List<ProfileMonitorSetting> MonitorSettings { get; set; }
[JsonPropertyName("createdDate")]
public DateTime CreatedDate { get; set; }
[JsonPropertyName("lastModified")]
public DateTime LastModified { get; set; }
public PowerDisplayProfile()
{
Name = string.Empty;
MonitorSettings = new List<ProfileMonitorSetting>();
CreatedDate = DateTime.UtcNow;
LastModified = DateTime.UtcNow;
}
public PowerDisplayProfile(string name, List<ProfileMonitorSetting> monitorSettings)
{
Name = name;
MonitorSettings = monitorSettings ?? new List<ProfileMonitorSetting>();
CreatedDate = DateTime.UtcNow;
LastModified = DateTime.UtcNow;
}
/// <summary>
/// Validates that the profile has at least one monitor configured
/// </summary>
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(Name) && MonitorSettings != null && MonitorSettings.Count > 0;
}
/// <summary>
/// Updates the last modified timestamp
/// </summary>
public void Touch()
{
LastModified = DateTime.UtcNow;
}
}
}

View File

@@ -0,0 +1,100 @@
// 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.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Container for all PowerDisplay profiles
/// </summary>
public class PowerDisplayProfiles
{
// NOTE: Custom profile concept has been removed. Profiles are now templates, not states.
// This constant is kept for backward compatibility (cleaning up legacy Custom profiles).
public const string CustomProfileName = "Custom";
[JsonPropertyName("profiles")]
public List<PowerDisplayProfile> Profiles { get; set; }
[JsonPropertyName("lastUpdated")]
public DateTime LastUpdated { get; set; }
public PowerDisplayProfiles()
{
Profiles = new List<PowerDisplayProfile>();
LastUpdated = DateTime.UtcNow;
}
/// <summary>
/// Gets the profile by name
/// </summary>
public PowerDisplayProfile? GetProfile(string name)
{
return Profiles.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Adds or updates a profile
/// </summary>
public void SetProfile(PowerDisplayProfile profile)
{
if (profile == null || !profile.IsValid())
{
throw new ArgumentException("Profile is invalid");
}
var existing = GetProfile(profile.Name);
if (existing != null)
{
Profiles.Remove(existing);
}
profile.Touch();
Profiles.Add(profile);
LastUpdated = DateTime.UtcNow;
}
/// <summary>
/// Removes a profile by name
/// </summary>
public bool RemoveProfile(string name)
{
var profile = GetProfile(name);
if (profile != null)
{
Profiles.Remove(profile);
LastUpdated = DateTime.UtcNow;
return true;
}
return false;
}
/// <summary>
/// Checks if a profile name is valid and available
/// </summary>
public bool IsNameAvailable(string name, string? excludeName = null)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}
// Check if name is already used (excluding the profile being renamed)
var existing = GetProfile(name);
if (existing != null && (excludeName == null || !existing.Name.Equals(excludeName, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,51 @@
// 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 PowerDisplay.Common.Models
{
/// <summary>
/// Monitor settings for a specific profile
/// </summary>
public class ProfileMonitorSetting
{
/// <summary>
/// Gets or sets the monitor's unique identifier.
/// Format: "{Source}_{EdidId}_{MonitorNumber}" (e.g., "DDC_GSM5C6D_1").
/// </summary>
[JsonPropertyName("monitorId")]
public string MonitorId { get; set; }
[JsonPropertyName("brightness")]
public int? Brightness { get; set; }
[JsonPropertyName("contrast")]
public int? Contrast { get; set; }
[JsonPropertyName("volume")]
public int? Volume { get; set; }
/// <summary>
/// Gets or sets the color temperature VCP preset value.
/// JSON property name kept as "colorTemperature" for backward compatibility.
/// </summary>
[JsonPropertyName("colorTemperature")]
public int? ColorTemperatureVcp { get; set; }
public ProfileMonitorSetting()
{
MonitorId = string.Empty;
}
public ProfileMonitorSetting(string monitorId, int? brightness = null, int? colorTemperatureVcp = null, int? contrast = null, int? volume = null)
{
MonitorId = monitorId;
Brightness = brightness;
ColorTemperatureVcp = colorTemperatureVcp;
Contrast = contrast;
Volume = volume;
}
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// Represents a pending profile operation to be applied by PowerDisplay
/// </summary>
public class ProfileOperation
{
[JsonPropertyName("profileName")]
public string ProfileName { get; set; }
[JsonPropertyName("monitorSettings")]
public List<ProfileMonitorSetting> MonitorSettings { get; set; }
public ProfileOperation()
{
ProfileName = string.Empty;
MonitorSettings = new List<ProfileMonitorSetting>();
}
public ProfileOperation(string profileName, List<ProfileMonitorSetting> monitorSettings)
{
ProfileName = profileName;
MonitorSettings = monitorSettings ?? new List<ProfileMonitorSetting>();
}
}
}

View File

@@ -0,0 +1,314 @@
// 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;
namespace PowerDisplay.Common.Models
{
/// <summary>
/// DDC/CI VCP capabilities information
/// </summary>
public class VcpCapabilities
{
/// <summary>
/// Gets or sets raw capabilities string (MCCS format)
/// </summary>
public string Raw { get; set; } = string.Empty;
/// <summary>
/// Gets or sets monitor model name from capabilities
/// </summary>
public string? Model { get; set; }
/// <summary>
/// Gets or sets monitor type from capabilities (e.g., "LCD")
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Gets or sets mCCS protocol version
/// </summary>
public string? Protocol { get; set; }
/// <summary>
/// Gets or sets mCCS version (e.g., "2.2", "2.1")
/// </summary>
public string? MccsVersion { get; set; }
/// <summary>
/// Gets or sets supported command codes
/// </summary>
public List<byte> SupportedCommands { get; set; } = new();
/// <summary>
/// Gets or sets supported VCP codes with their information
/// </summary>
public Dictionary<byte, VcpCodeInfo> SupportedVcpCodes { get; set; } = new();
/// <summary>
/// Gets or sets window capabilities for PIP/PBP support
/// </summary>
public List<WindowCapability> Windows { get; set; } = new();
/// <summary>
/// Gets a value indicating whether check if display supports PIP/PBP windows
/// </summary>
public bool HasWindowSupport => Windows.Count > 0;
/// <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>
/// Get all VCP codes as hex strings, sorted by code value.
/// </summary>
/// <returns>List of hex strings like ["0x10", "0x12", "0x14"]</returns>
public List<string> GetVcpCodesAsHexStrings()
{
var result = new List<string>(SupportedVcpCodes.Count);
foreach (var kvp in SupportedVcpCodes)
{
result.Add($"0x{kvp.Key:X2}");
}
result.Sort(StringComparer.Ordinal);
return result;
}
/// <summary>
/// Get all VCP codes sorted by code value.
/// </summary>
/// <returns>Sorted list of VcpCodeInfo</returns>
public IEnumerable<VcpCodeInfo> GetSortedVcpCodes()
{
var sortedKeys = new List<byte>(SupportedVcpCodes.Keys);
sortedKeys.Sort();
foreach (var key in sortedKeys)
{
yield return SupportedVcpCodes[key];
}
}
/// <summary>
/// Gets 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>
/// Gets vCP code (e.g., 0x10 for brightness)
/// </summary>
public byte Code { get; }
/// <summary>
/// Gets human-readable name of the VCP code
/// </summary>
public string Name { get; }
/// <summary>
/// Gets supported discrete values (empty if continuous range)
/// </summary>
public IReadOnlyList<int> SupportedValues { get; }
/// <summary>
/// Gets a value indicating whether this VCP code has discrete values
/// </summary>
public bool HasDiscreteValues => SupportedValues.Count > 0;
/// <summary>
/// Gets a value indicating whether this VCP code supports a continuous range
/// </summary>
public bool IsContinuous => SupportedValues.Count == 0;
/// <summary>
/// Gets the VCP code formatted as a hex string (e.g., "0x10").
/// </summary>
public string FormattedCode => $"0x{Code:X2}";
/// <summary>
/// Gets the VCP code formatted with its name (e.g., "Brightness (0x10)").
/// </summary>
public string FormattedTitle => $"{Name} ({FormattedCode})";
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";
}
}
/// <summary>
/// Window size (width and height)
/// </summary>
public readonly struct WindowSize
{
/// <summary>
/// Gets width in pixels
/// </summary>
public int Width { get; }
/// <summary>
/// Gets height in pixels
/// </summary>
public int Height { get; }
public WindowSize(int width, int height)
{
Width = width;
Height = height;
}
public override string ToString() => $"{Width}x{Height}";
}
/// <summary>
/// Window area coordinates (top-left and bottom-right)
/// </summary>
public readonly struct WindowArea
{
/// <summary>
/// Gets top-left X coordinate
/// </summary>
public int X1 { get; }
/// <summary>
/// Gets top-left Y coordinate
/// </summary>
public int Y1 { get; }
/// <summary>
/// Gets bottom-right X coordinate
/// </summary>
public int X2 { get; }
/// <summary>
/// Gets bottom-right Y coordinate
/// </summary>
public int Y2 { get; }
/// <summary>
/// Gets width of the area
/// </summary>
public int Width => X2 - X1;
/// <summary>
/// Gets height of the area
/// </summary>
public int Height => Y2 - Y1;
public WindowArea(int x1, int y1, int x2, int y2)
{
X1 = x1;
Y1 = y1;
X2 = x2;
Y2 = y2;
}
public override string ToString() => $"({X1},{Y1})-({X2},{Y2})";
}
/// <summary>
/// Window capability information for PIP/PBP displays
/// </summary>
public readonly struct WindowCapability
{
/// <summary>
/// Gets window number (1, 2, 3, etc.)
/// </summary>
public int WindowNumber { get; }
/// <summary>
/// Gets window type (e.g., "PIP", "PBP")
/// </summary>
public string Type { get; }
/// <summary>
/// Gets window area coordinates
/// </summary>
public WindowArea Area { get; }
/// <summary>
/// Gets maximum window size
/// </summary>
public WindowSize MaxSize { get; }
/// <summary>
/// Gets minimum window size
/// </summary>
public WindowSize MinSize { get; }
/// <summary>
/// Gets window identifier
/// </summary>
public int WindowId { get; }
public WindowCapability(
int windowNumber,
string type,
WindowArea area,
WindowSize maxSize,
WindowSize minSize,
int windowId)
{
WindowNumber = windowNumber;
Type = type ?? string.Empty;
Area = area;
MaxSize = maxSize;
MinSize = minSize;
WindowId = windowId;
}
public override string ToString() =>
$"Window{WindowNumber}: Type={Type}, Area={Area}, Max={MaxSize}, Min={MinSize}";
}
}

View File

@@ -0,0 +1,77 @@
// 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.Common.Models
{
/// <summary>
/// VCP feature value information structure.
/// Represents the current, minimum, and maximum values for a VCP (Virtual Control Panel) feature.
/// </summary>
public readonly struct VcpFeatureValue
{
/// <summary>
/// Gets current value
/// </summary>
public int Current { get; }
/// <summary>
/// Gets minimum value
/// </summary>
public int Minimum { get; }
/// <summary>
/// Gets maximum value
/// </summary>
public int Maximum { get; }
/// <summary>
/// Gets a value indicating whether the value information is valid
/// </summary>
public bool IsValid { get; }
/// <summary>
/// Gets timestamp when the value information was obtained
/// </summary>
public DateTime Timestamp { get; }
public VcpFeatureValue(int current, int minimum, int maximum)
{
Current = current;
Minimum = minimum;
Maximum = maximum;
IsValid = current >= minimum && current <= maximum && maximum > minimum;
Timestamp = DateTime.Now;
}
public VcpFeatureValue(int current, int maximum)
: this(current, 0, maximum)
{
}
/// <summary>
/// Gets creates invalid value information
/// </summary>
public static VcpFeatureValue Invalid => new(-1, -1, -1);
/// <summary>
/// Converts 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));
}
public override string ToString()
{
return IsValid ? $"{Current}/{Maximum} ({ToPercentage()}%)" : "Invalid";
}
}
}

View File

@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace PowerDisplay.Common
{
/// <summary>
/// Centralized path constants for PowerDisplay module.
/// Provides unified access to all file and folder paths used by PowerDisplay and related integrations.
/// </summary>
public static class PathConstants
{
private static readonly Lazy<string> _localAppDataPath = new Lazy<string>(
() => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData));
private static readonly Lazy<string> _powerToysBasePath = new Lazy<string>(
() => Path.Combine(_localAppDataPath.Value, "Microsoft", "PowerToys"));
/// <summary>
/// Gets the base PowerToys settings folder path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys
/// </summary>
public static string PowerToysBasePath => _powerToysBasePath.Value;
/// <summary>
/// Gets the PowerDisplay module folder path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay
/// </summary>
public static string PowerDisplayFolderPath => Path.Combine(PowerToysBasePath, "PowerDisplay");
/// <summary>
/// Gets the PowerDisplay profiles file path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\profiles.json
/// </summary>
public static string ProfilesFilePath => Path.Combine(PowerDisplayFolderPath, ProfilesFileName);
/// <summary>
/// Gets the PowerDisplay settings file path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\settings.json
/// </summary>
public static string SettingsFilePath => Path.Combine(PowerDisplayFolderPath, SettingsFileName);
/// <summary>
/// Gets the LightSwitch module folder path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\LightSwitch
/// </summary>
public static string LightSwitchFolderPath => Path.Combine(PowerToysBasePath, "LightSwitch");
/// <summary>
/// Gets the LightSwitch settings file path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\LightSwitch\settings.json
/// </summary>
public static string LightSwitchSettingsFilePath => Path.Combine(LightSwitchFolderPath, SettingsFileName);
/// <summary>
/// The name of the profiles file.
/// </summary>
public const string ProfilesFileName = "profiles.json";
/// <summary>
/// The name of the settings file.
/// </summary>
public const string SettingsFileName = "settings.json";
/// <summary>
/// The name of the monitor state file.
/// </summary>
public const string MonitorStateFileName = "monitor_state.json";
/// <summary>
/// Gets the monitor state file path.
/// Example: C:\Users\{User}\AppData\Local\Microsoft\PowerToys\PowerDisplay\monitor_state.json
/// </summary>
public static string MonitorStateFilePath => Path.Combine(PowerDisplayFolderPath, MonitorStateFileName);
/// <summary>
/// Event name for LightSwitch light theme change notifications.
/// Signaled when LightSwitch switches to light mode.
/// Must match CommonSharedConstants::LIGHT_SWITCH_LIGHT_THEME_EVENT in shared_constants.h.
/// </summary>
public const string LightSwitchLightThemeEventName = "Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
/// <summary>
/// Event name for LightSwitch dark theme change notifications.
/// Signaled when LightSwitch switches to dark mode.
/// Must match CommonSharedConstants::LIGHT_SWITCH_DARK_THEME_EVENT in shared_constants.h.
/// </summary>
public const string LightSwitchDarkThemeEventName = "Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
/// <summary>
/// Ensures the PowerDisplay folder exists. Creates it if necessary.
/// </summary>
/// <returns>The PowerDisplay folder path</returns>
public static string EnsurePowerDisplayFolderExists()
=> EnsureFolderExists(PowerDisplayFolderPath);
/// <summary>
/// Ensures the LightSwitch folder exists. Creates it if necessary.
/// </summary>
/// <returns>The LightSwitch folder path</returns>
public static string EnsureLightSwitchFolderExists()
=> EnsureFolderExists(LightSwitchFolderPath);
/// <summary>
/// Ensures the specified folder exists. Creates it if necessary.
/// </summary>
/// <param name="folderPath">The folder path to ensure exists</param>
/// <returns>The folder path</returns>
private static string EnsureFolderExists(string folderPath)
{
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
return folderPath;
}
}
}

View File

@@ -0,0 +1,40 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
<!-- Licensed under the MIT License. See LICENSE file in the project root for license information. -->
<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.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<RootNamespace>PowerDisplay.Common</RootNamespace>
<Platforms>x64;ARM64</Platforms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<AssemblyName>PowerDisplay.Lib</AssemblyName>
</PropertyGroup>
<!-- Native AOT Configuration -->
<PropertyGroup>
<PublishSingleFile>false</PublishSingleFile>
<DisableRuntimeMarshalling>false</DisableRuntimeMarshalling>
<IsAotCompatible>true</IsAotCompatible>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Polly.Core" />
<PackageReference Include="WmiLight" />
<PackageReference Include="System.Collections.Immutable" />
</ItemGroup>
<ItemGroup>
<!-- Hide build log files from Solution Explorer -->
<None Remove="*.log" />
<None Remove="*.binlog" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,38 @@
// 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;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Serialization
{
/// <summary>
/// JSON serialization context for PowerDisplay Profile types.
/// Provides source-generated serialization for Native AOT compatibility.
/// </summary>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IncludeFields = true)]
// Profile Types
[JsonSerializable(typeof(ProfileMonitorSetting))]
[JsonSerializable(typeof(List<ProfileMonitorSetting>))]
[JsonSerializable(typeof(PowerDisplayProfile))]
[JsonSerializable(typeof(List<PowerDisplayProfile>))]
[JsonSerializable(typeof(PowerDisplayProfiles))]
[JsonSerializable(typeof(ProfileOperation))]
[JsonSerializable(typeof(List<ProfileOperation>))]
[JsonSerializable(typeof(ColorTemperatureOperation))]
[JsonSerializable(typeof(List<ColorTemperatureOperation>))]
// Monitor State Types
[JsonSerializable(typeof(MonitorStateEntry))]
[JsonSerializable(typeof(MonitorStateFile))]
[JsonSerializable(typeof(Dictionary<string, MonitorStateEntry>))]
public partial class ProfileSerializationContext : JsonSerializerContext
{
}
}

View File

@@ -0,0 +1,174 @@
// 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 ManagedCommon;
using PowerDisplay.Common.Models;
using static PowerDisplay.Common.Drivers.NativeConstants;
using static PowerDisplay.Common.Drivers.PInvoke;
using DevMode = PowerDisplay.Common.Drivers.DevMode;
namespace PowerDisplay.Common.Services
{
/// <summary>
/// Service for controlling display rotation/orientation.
/// Uses ChangeDisplaySettingsEx API to change display orientation.
/// </summary>
public class DisplayRotationService
{
/// <summary>
/// Set display rotation for a specific monitor.
/// Uses GdiDeviceName from the Monitor object for accurate adapter targeting.
/// </summary>
/// <param name="monitor">Monitor object with GdiDeviceName</param>
/// <param name="newOrientation">New orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
/// <returns>Operation result</returns>
public MonitorOperationResult SetRotation(Monitor monitor, int newOrientation)
{
ArgumentNullException.ThrowIfNull(monitor);
if (newOrientation < 0 || newOrientation > 3)
{
return MonitorOperationResult.Failure($"Invalid orientation value: {newOrientation}. Must be 0-3.");
}
if (string.IsNullOrEmpty(monitor.GdiDeviceName))
{
return MonitorOperationResult.Failure("Monitor has no GdiDeviceName");
}
return SetRotationByGdiDeviceName(monitor.GdiDeviceName, newOrientation);
}
/// <summary>
/// Set display rotation by GDI device name.
/// </summary>
/// <param name="gdiDeviceName">GDI device name (e.g., "\\.\DISPLAY1")</param>
/// <param name="newOrientation">New orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
/// <returns>Operation result</returns>
public unsafe MonitorOperationResult SetRotationByGdiDeviceName(string gdiDeviceName, int newOrientation)
{
if (string.IsNullOrEmpty(gdiDeviceName))
{
return MonitorOperationResult.Failure("GDI device name is required");
}
try
{
Logger.LogInfo($"SetRotation: Setting {gdiDeviceName} to orientation {newOrientation}");
// 1. Get current display settings
DevMode devMode = default;
devMode.DmSize = (short)sizeof(DevMode);
if (!EnumDisplaySettings(gdiDeviceName, EnumCurrentSettings, &devMode))
{
var error = GetLastError();
Logger.LogError($"SetRotation: EnumDisplaySettings failed for {gdiDeviceName}, error: {error}");
return MonitorOperationResult.Failure($"Failed to get current display settings for {gdiDeviceName}", (int)error);
}
int currentOrientation = devMode.DmDisplayOrientation;
// If already at target orientation, return success
if (currentOrientation == newOrientation)
{
return MonitorOperationResult.Success();
}
// 2. Determine if we need to swap width and height
// When switching between landscape (0°/180°) and portrait (90°/270°), swap dimensions
bool currentIsLandscape = currentOrientation == DmdoDefault || currentOrientation == Dmdo180;
bool newIsLandscape = newOrientation == DmdoDefault || newOrientation == Dmdo180;
if (currentIsLandscape != newIsLandscape)
{
// Swap width and height
int temp = devMode.DmPelsWidth;
devMode.DmPelsWidth = devMode.DmPelsHeight;
devMode.DmPelsHeight = temp;
}
// 3. Set new orientation
devMode.DmDisplayOrientation = newOrientation;
devMode.DmFields = DmDisplayOrientation | DmPelsWidth | DmPelsHeight;
// 4. Test the settings first using CDS_TEST flag
int testResult = ChangeDisplaySettingsEx(gdiDeviceName, &devMode, IntPtr.Zero, CdsTest, IntPtr.Zero);
if (testResult != DispChangeSuccessful)
{
string errorMsg = GetChangeDisplaySettingsErrorMessage(testResult);
Logger.LogError($"SetRotation: Test failed for {gdiDeviceName}: {errorMsg}");
return MonitorOperationResult.Failure($"Display settings test failed: {errorMsg}", testResult);
}
// 5. Apply the settings (without CDS_UPDATEREGISTRY to make it temporary)
int result = ChangeDisplaySettingsEx(gdiDeviceName, &devMode, IntPtr.Zero, 0, IntPtr.Zero);
if (result != DispChangeSuccessful)
{
string errorMsg = GetChangeDisplaySettingsErrorMessage(result);
Logger.LogError($"SetRotation: Apply failed for {gdiDeviceName}: {errorMsg}");
return MonitorOperationResult.Failure($"Failed to apply display settings: {errorMsg}", result);
}
Logger.LogInfo($"SetRotation: Successfully set {gdiDeviceName} to orientation {newOrientation}");
return MonitorOperationResult.Success();
}
catch (Exception ex)
{
Logger.LogError($"SetRotation: Exception for {gdiDeviceName}: {ex.Message}");
return MonitorOperationResult.Failure($"Exception while setting rotation: {ex.Message}");
}
}
/// <summary>
/// Get current orientation for a GDI device name.
/// </summary>
/// <param name="gdiDeviceName">GDI device name (e.g., "\\.\DISPLAY1")</param>
/// <returns>Current orientation (0-3), or -1 if query failed</returns>
public unsafe int GetCurrentOrientation(string gdiDeviceName)
{
if (string.IsNullOrEmpty(gdiDeviceName))
{
return -1;
}
try
{
DevMode devMode = default;
devMode.DmSize = (short)sizeof(DevMode);
if (!EnumDisplaySettings(gdiDeviceName, EnumCurrentSettings, &devMode))
{
return -1;
}
return devMode.DmDisplayOrientation;
}
catch
{
return -1;
}
}
/// <summary>
/// Get human-readable error message for ChangeDisplaySettings result code.
/// </summary>
private static string GetChangeDisplaySettingsErrorMessage(int resultCode)
{
return resultCode switch
{
DispChangeSuccessful => "Success",
DispChangeRestart => "Computer must be restarted",
DispChangeFailed => "Display driver failed the specified graphics mode",
DispChangeBadmode => "Graphics mode is not supported",
DispChangeNotupdated => "Unable to write settings to registry",
DispChangeBadflags => "Invalid flags",
DispChangeBadparam => "Invalid parameter",
_ => $"Unknown error code: {resultCode}",
};
}
}
}

View File

@@ -0,0 +1,300 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Serialization;
using PowerDisplay.Common.Utils;
namespace PowerDisplay.Common.Services
{
/// <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 ConcurrentDictionary<string, MonitorState> _states = new();
private readonly object _statesLock = new();
private readonly SimpleDebouncer _saveDebouncer;
private bool _disposed;
private bool _isDirty; // Track pending changes for flush on dispose
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 ColorTemperatureVcp { get; set; }
public int Contrast { get; set; }
public int Volume { get; set; }
public string? CapabilitiesRaw { get; set; }
}
/// <summary>
/// Initializes a new instance of the <see cref="MonitorStateManager"/> class.
/// Uses PathConstants for consistent path management.
/// </summary>
public MonitorStateManager()
{
// Use PathConstants for consistent path management
PathConstants.EnsurePowerDisplayFolderExists();
_stateFilePath = PathConstants.MonitorStateFilePath;
// Initialize debouncer for batching rapid updates (e.g., slider drag)
_saveDebouncer = new SimpleDebouncer(SaveDebounceMs);
// Load existing state if available
LoadStateFromDisk();
Logger.LogInfo($"MonitorStateManager initialized with debounced-save strategy (debounce: {SaveDebounceMs}ms), state file: {_stateFilePath}");
}
/// <summary>
/// Update monitor parameter and schedule debounced save to disk.
/// Uses Monitor.Id as the stable key (e.g., "DDC_GSM5C6D_1", "WMI_BOE0900_2").
/// Debounced-save strategy reduces disk I/O by batching rapid updates (e.g., during slider drag).
/// </summary>
/// <param name="monitorId">The monitor's unique Id (e.g., "DDC_GSM5C6D_1").</param>
/// <param name="property">The property name to update (Brightness, ColorTemperature, Contrast, or Volume).</param>
/// <param name="value">The new value.</param>
public void UpdateMonitorParameter(string monitorId, string property, int value)
{
try
{
if (string.IsNullOrEmpty(monitorId))
{
Logger.LogWarning($"Cannot update monitor parameter: monitorId is empty");
return;
}
var state = _states.GetOrAdd(monitorId, _ => new MonitorState());
// Update the specific property
bool shouldSave = true;
switch (property)
{
case "Brightness":
state.Brightness = value;
break;
case "ColorTemperature":
state.ColorTemperatureVcp = value;
break;
case "Contrast":
state.Contrast = value;
break;
case "Volume":
state.Volume = value;
break;
default:
Logger.LogWarning($"Unknown property: {property}");
shouldSave = false;
break;
}
if (shouldSave)
{
// Mark dirty for flush on dispose
_isDirty = true;
}
// Schedule debounced save (SimpleDebouncer handles cancellation of previous calls)
if (shouldSave)
{
_saveDebouncer.Debounce(SaveStateToDiskAsync);
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to update monitor parameter: {ex.Message}");
}
}
/// <summary>
/// Get saved parameters for a monitor using Monitor.Id.
/// </summary>
/// <param name="monitorId">The monitor's unique Id (e.g., "DDC_GSM5C6D_1").</param>
/// <returns>A tuple of (Brightness, ColorTemperatureVcp, Contrast, Volume) or null if not found.</returns>
public (int Brightness, int ColorTemperatureVcp, int Contrast, int Volume)? GetMonitorParameters(string monitorId)
{
if (string.IsNullOrEmpty(monitorId))
{
return null;
}
if (_states.TryGetValue(monitorId, out var state))
{
return (state.Brightness, state.ColorTemperatureVcp, state.Contrast, state.Volume);
}
return null;
}
/// <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, ProfileSerializationContext.Default.MonitorStateFile);
if (stateFile?.Monitors != null)
{
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,
ColorTemperatureVcp = entry.ColorTemperatureVcp,
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.
/// </summary>
private async Task SaveStateToDiskAsync()
{
try
{
if (_disposed)
{
return;
}
var (json, monitorCount) = BuildStateJson();
// Write to disk asynchronously
await File.WriteAllTextAsync(_stateFilePath, json);
// Clear dirty flag after successful save
_isDirty = false;
}
catch (Exception ex)
{
Logger.LogError($"Failed to save monitor state: {ex.Message}");
}
}
/// <summary>
/// Save current state to disk synchronously.
/// Called during Dispose to flush pending changes without risk of deadlock.
/// </summary>
private void SaveStateToDiskSync()
{
try
{
var (json, monitorCount) = BuildStateJson();
// Write to disk synchronously - safe for Dispose
File.WriteAllText(_stateFilePath, json);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save monitor state (sync): {ex.Message}");
}
}
/// <summary>
/// Build the JSON string for state file.
/// Shared logic between async and sync save methods.
/// </summary>
/// <returns>Tuple of (JSON string, monitor count)</returns>
private (string Json, int MonitorCount) BuildStateJson()
{
var now = DateTime.Now;
var stateFile = new MonitorStateFile
{
LastUpdated = now,
};
foreach (var kvp in _states)
{
var monitorId = kvp.Key;
var state = kvp.Value;
stateFile.Monitors[monitorId] = new MonitorStateEntry
{
Brightness = state.Brightness,
ColorTemperatureVcp = state.ColorTemperatureVcp,
Contrast = state.Contrast,
Volume = state.Volume,
CapabilitiesRaw = state.CapabilitiesRaw,
LastUpdated = now,
};
}
var json = JsonSerializer.Serialize(stateFile, ProfileSerializationContext.Default.MonitorStateFile);
return (json, stateFile.Monitors.Count);
}
/// <summary>
/// Disposes the MonitorStateManager, flushing any pending state changes.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
bool wasDirty = _isDirty;
_disposed = true;
_isDirty = false;
// Dispose debouncer first to cancel any pending saves
_saveDebouncer?.Dispose();
// Flush any pending changes before disposing using sync method to avoid deadlock
if (wasDirty)
{
Logger.LogInfo("Flushing pending state changes before dispose");
SaveStateToDiskSync();
}
Logger.LogInfo("MonitorStateManager disposed");
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,261 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Text.Json;
using ManagedCommon;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Serialization;
namespace PowerDisplay.Common.Services
{
/// <summary>
/// Centralized service for managing PowerDisplay profiles storage and retrieval.
/// Provides unified access to profile data for PowerDisplay, Settings UI, and LightSwitch modules.
/// Thread-safe and AOT-compatible.
/// </summary>
public class ProfileService : IProfileService
{
private const string LogPrefix = "[ProfileService]";
private static readonly object _lock = new object();
/// <summary>
/// Gets the singleton instance of the ProfileService.
/// Use this for dependency injection or when interface-based access is needed.
/// </summary>
public static IProfileService Instance { get; } = new ProfileService();
/// <summary>
/// Initializes a new instance of the <see cref="ProfileService"/> class.
/// Private constructor to enforce singleton pattern for instance-based access.
/// Static methods remain available for backward compatibility.
/// </summary>
private ProfileService()
{
}
/// <summary>
/// Loads PowerDisplay profiles from disk.
/// Thread-safe operation with automatic legacy profile cleanup.
/// </summary>
/// <returns>PowerDisplayProfiles object, or a new empty instance if file doesn't exist or load fails</returns>
public static PowerDisplayProfiles LoadProfiles()
{
lock (_lock)
{
var (profiles, _) = LoadProfilesInternal();
return profiles;
}
}
/// <summary>
/// Saves PowerDisplay profiles to disk.
/// Thread-safe operation with automatic timestamp update and legacy profile cleanup.
/// </summary>
/// <param name="profiles">The profiles collection to save</param>
/// <returns>True if save was successful, false otherwise</returns>
public static bool SaveProfiles(PowerDisplayProfiles profiles)
{
lock (_lock)
{
if (profiles == null)
{
Logger.LogWarning($"{LogPrefix} Cannot save null profiles");
return false;
}
var (success, _) = SaveProfilesInternal(profiles);
return success;
}
}
/// <summary>
/// Adds or updates a profile in the collection and persists to disk.
/// Thread-safe operation.
/// </summary>
/// <param name="profile">The profile to add or update</param>
/// <returns>True if operation was successful, false otherwise</returns>
public static bool AddOrUpdateProfile(PowerDisplayProfile profile)
{
lock (_lock)
{
if (profile == null || !profile.IsValid())
{
Logger.LogWarning($"{LogPrefix} Cannot add invalid profile");
return false;
}
var (profiles, _) = LoadProfilesInternal();
profiles.SetProfile(profile);
var (success, _) = SaveProfilesInternal(profiles);
if (success)
{
Logger.LogInfo($"{LogPrefix} Profile '{profile.Name}' added/updated with {profile.MonitorSettings.Count} monitors");
}
return success;
}
}
/// <summary>
/// Removes a profile by name and persists to disk.
/// Thread-safe operation.
/// </summary>
/// <param name="profileName">The name of the profile to remove</param>
/// <returns>True if profile was found and removed, false otherwise</returns>
public static bool RemoveProfile(string profileName)
{
lock (_lock)
{
var (profiles, _) = LoadProfilesInternal();
bool removed = profiles.RemoveProfile(profileName);
if (removed)
{
SaveProfilesInternal(profiles);
Logger.LogInfo($"{LogPrefix} Profile '{profileName}' removed");
}
else
{
Logger.LogWarning($"{LogPrefix} Profile '{profileName}' not found or cannot be removed");
}
return removed;
}
}
/// <summary>
/// Gets a profile by name.
/// Thread-safe operation.
/// </summary>
/// <param name="profileName">The name of the profile to retrieve</param>
/// <returns>The profile if found, null otherwise</returns>
public static PowerDisplayProfile? GetProfile(string profileName)
{
lock (_lock)
{
var (profiles, _) = LoadProfilesInternal();
return profiles.GetProfile(profileName);
}
}
/// <summary>
/// Checks if the profiles file exists.
/// </summary>
/// <returns>True if profiles file exists, false otherwise</returns>
public static bool ProfilesFileExists()
{
try
{
return File.Exists(PathConstants.ProfilesFilePath);
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Error checking if profiles file exists: {ex.Message}");
return false;
}
}
/// <summary>
/// Gets the path to the profiles file.
/// </summary>
/// <returns>The full path to the profiles file</returns>
public static string GetProfilesFilePath()
{
return PathConstants.ProfilesFilePath;
}
// Internal methods without lock for use within already-locked contexts
// Returns tuple with result and optional log message
private static (PowerDisplayProfiles Profiles, string? Message) LoadProfilesInternal()
{
try
{
var filePath = PathConstants.ProfilesFilePath;
PathConstants.EnsurePowerDisplayFolderExists();
if (File.Exists(filePath))
{
var json = File.ReadAllText(filePath);
var profiles = JsonSerializer.Deserialize(json, ProfileSerializationContext.Default.PowerDisplayProfiles);
if (profiles != null)
{
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
return (profiles, $"Loaded {profiles.Profiles.Count} profiles from {filePath}");
}
}
else
{
return (new PowerDisplayProfiles(), $"No profiles file found at {filePath}, returning empty collection");
}
return (new PowerDisplayProfiles(), null);
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Failed to load profiles: {ex.Message}");
return (new PowerDisplayProfiles(), null);
}
}
// Returns tuple with success status and optional log message
private static (bool Success, string? Message) SaveProfilesInternal(PowerDisplayProfiles profiles)
{
try
{
if (profiles == null)
{
return (false, null);
}
PathConstants.EnsurePowerDisplayFolderExists();
profiles.Profiles.RemoveAll(p => p.Name.Equals(PowerDisplayProfiles.CustomProfileName, StringComparison.OrdinalIgnoreCase));
profiles.LastUpdated = DateTime.UtcNow;
var json = JsonSerializer.Serialize(profiles, ProfileSerializationContext.Default.PowerDisplayProfiles);
var filePath = PathConstants.ProfilesFilePath;
File.WriteAllText(filePath, json);
return (true, $"Saved {profiles.Profiles.Count} profiles to {filePath}");
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Failed to save profiles: {ex.Message}");
return (false, null);
}
}
// IProfileService Implementation
// Explicit interface implementation to satisfy IProfileService
// These methods delegate to the static methods for backward compatibility
/// <inheritdoc/>
PowerDisplayProfiles IProfileService.LoadProfiles() => LoadProfiles();
/// <inheritdoc/>
bool IProfileService.SaveProfiles(PowerDisplayProfiles profiles) => SaveProfiles(profiles);
/// <inheritdoc/>
bool IProfileService.AddOrUpdateProfile(PowerDisplayProfile profile) => AddOrUpdateProfile(profile);
/// <inheritdoc/>
bool IProfileService.RemoveProfile(string profileName) => RemoveProfile(profileName);
/// <inheritdoc/>
PowerDisplayProfile? IProfileService.GetProfile(string profileName) => GetProfile(profileName);
/// <inheritdoc/>
bool IProfileService.ProfilesFileExists() => ProfilesFileExists();
/// <inheritdoc/>
string IProfileService.GetProfilesFilePath() => GetProfilesFilePath();
}
}

View File

@@ -0,0 +1,79 @@
// 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.Linq;
using PowerDisplay.Common.Drivers;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for color temperature preset computation.
/// Provides shared logic for computing available color presets from VCP capabilities.
/// </summary>
public static class ColorTemperatureHelper
{
/// <summary>
/// Computes available color temperature presets from VCP value data.
/// </summary>
/// <param name="colorTemperatureValues">
/// Collection of tuples containing (VcpValue, Name) for each color temperature preset.
/// The VcpValue is the VCP value, Name is the name from capabilities string if available.
/// </param>
/// <returns>Sorted list of ColorPresetItem objects.</returns>
public static List<ColorPresetItem> ComputeColorPresets(IEnumerable<(int VcpValue, string? Name)> colorTemperatureValues)
{
if (colorTemperatureValues == null)
{
return new List<ColorPresetItem>();
}
var presetList = new List<ColorPresetItem>();
foreach (var item in colorTemperatureValues)
{
var displayName = FormatColorTemperatureDisplayName(item.VcpValue, item.Name);
presetList.Add(new ColorPresetItem(item.VcpValue, displayName));
}
// Sort by VCP value for consistent ordering
return presetList.OrderBy(p => p.VcpValue).ToList();
}
/// <summary>
/// Formats a color temperature display name.
/// Uses VcpNames for standard VCP value mappings if no custom name is provided.
/// </summary>
/// <param name="vcpValue">The VCP value.</param>
/// <param name="customName">Optional custom name from capabilities string.</param>
/// <returns>Formatted display name.</returns>
public static string FormatColorTemperatureDisplayName(int vcpValue, string? customName = null)
{
// Priority: use name from VCP capabilities if available
if (!string.IsNullOrEmpty(customName))
{
return customName;
}
// Fall back to standard VCP value name from shared library
return VcpNames.GetValueName(NativeConstants.VcpCodeSelectColorPreset, vcpValue)
?? "Manufacturer Defined";
}
/// <summary>
/// Formats a display name for a custom (non-preset) color temperature value.
/// Used when the current value is not in the available preset list.
/// </summary>
/// <param name="vcpValue">The VCP value.</param>
/// <returns>Formatted display name with "Custom" indicator.</returns>
public static string FormatCustomColorTemperatureDisplayName(int vcpValue)
{
var standardName = VcpNames.GetValueName(NativeConstants.VcpCodeSelectColorPreset, vcpValue);
return string.IsNullOrEmpty(standardName)
? "Custom"
: $"{standardName} (Custom)";
}
}
}

View File

@@ -0,0 +1,46 @@
// 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;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for Windows named event operations.
/// Provides unified event signaling with consistent error handling and logging.
/// </summary>
public static class EventHelper
{
/// <summary>
/// Signals a named event. Creates the event if it doesn't exist.
/// </summary>
/// <param name="eventName">The name of the event to signal.</param>
/// <returns>True if the event was signaled successfully, false otherwise.</returns>
public static bool SignalEvent(string eventName)
{
if (string.IsNullOrEmpty(eventName))
{
Logger.LogWarning("[EventHelper] SignalEvent called with null or empty event name");
return false;
}
try
{
using var eventHandle = new EventWaitHandle(
false,
EventResetMode.AutoReset,
eventName);
eventHandle.Set();
return true;
}
catch (Exception ex)
{
Logger.LogError($"[EventHelper] Failed to signal event '{eventName}': {ex.Message}");
return false;
}
}
}
}

View File

@@ -0,0 +1,860 @@
// 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.Runtime.CompilerServices;
using ManagedCommon;
using PowerDisplay.Common.Models;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Recursive descent parser for DDC/CI MCCS capabilities strings.
///
/// MCCS Capabilities String Grammar (BNF):
/// <code>
/// capabilities ::= '(' segment* ')'
/// segment ::= identifier '(' segment_content ')'
/// segment_content ::= text | vcp_entries | hex_list
/// vcp_entries ::= vcp_entry*
/// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
/// hex_list ::= hex_byte*
/// hex_byte ::= [0-9A-Fa-f]{2}
/// identifier ::= [a-z_]+
/// text ::= [^()]+
/// </code>
///
/// Example input:
/// (prot(monitor)type(lcd)model(PD3220U)cmds(01 02 03)vcp(10 12 14(04 05) 60(11 12))mccs_ver(2.2))
/// </summary>
public ref struct MccsCapabilitiesParser
{
private readonly List<ParseError> _errors;
private ReadOnlySpan<char> _input;
private int _position;
/// <summary>
/// Parse a capabilities string into structured VcpCapabilities.
/// </summary>
/// <param name="capabilitiesString">Raw MCCS capabilities string</param>
/// <returns>Parsed capabilities object with any parse errors</returns>
public static MccsParseResult Parse(string? capabilitiesString)
{
if (string.IsNullOrWhiteSpace(capabilitiesString))
{
return new MccsParseResult(VcpCapabilities.Empty, new List<ParseError>());
}
var parser = new MccsCapabilitiesParser(capabilitiesString);
return parser.ParseCapabilities();
}
private MccsCapabilitiesParser(string input)
{
_input = input.AsSpan();
_position = 0;
_errors = new List<ParseError>();
}
/// <summary>
/// Main entry point: parse the entire capabilities string.
/// capabilities ::= '(' segment* ')' | segment*
/// </summary>
private MccsParseResult ParseCapabilities()
{
var capabilities = new VcpCapabilities
{
Raw = _input.ToString(),
};
SkipWhitespace();
// Handle optional outer parentheses (some monitors omit them)
bool hasOuterParens = Peek() == '(';
if (hasOuterParens)
{
Advance(); // consume '('
}
// Parse segments until end or closing paren
while (!IsAtEnd())
{
SkipWhitespace();
if (IsAtEnd())
{
break;
}
if (Peek() == ')')
{
if (hasOuterParens)
{
Advance(); // consume closing ')'
}
break;
}
// Parse a segment: identifier(content)
var segment = ParseSegment();
if (segment.HasValue)
{
ApplySegment(capabilities, segment.Value);
}
}
return new MccsParseResult(capabilities, _errors);
}
/// <summary>
/// Parse a single segment: identifier '(' content ')'
/// </summary>
private ParsedSegment? ParseSegment()
{
SkipWhitespace();
int startPos = _position;
// Parse identifier
var identifier = ParseIdentifier();
if (identifier.IsEmpty)
{
// Not a valid segment start - skip this character and continue
if (!IsAtEnd())
{
Advance();
}
return null;
}
SkipWhitespace();
// Expect '('
if (Peek() != '(')
{
AddError($"Expected '(' after identifier '{identifier.ToString()}' at position {_position}");
return null;
}
Advance(); // consume '('
// Parse content until matching ')'
var content = ParseBalancedContent();
// Expect ')'
if (Peek() != ')')
{
AddError($"Expected ')' to close segment '{identifier.ToString()}' at position {_position}");
}
else
{
Advance(); // consume ')'
}
return new ParsedSegment(identifier.ToString(), content);
}
/// <summary>
/// Parse content between balanced parentheses.
/// Handles nested parentheses correctly.
/// </summary>
private string ParseBalancedContent()
{
int start = _position;
int depth = 1;
while (!IsAtEnd() && depth > 0)
{
char c = Peek();
if (c == '(')
{
depth++;
}
else if (c == ')')
{
depth--;
if (depth == 0)
{
break; // Don't consume the closing paren
}
}
Advance();
}
return _input.Slice(start, _position - start).ToString();
}
/// <summary>
/// Parse an identifier (letters, digits, and underscores).
/// identifier ::= [a-zA-Z0-9_]+
/// Note: MCCS uses identifiers like window1, window2, etc.
/// </summary>
private ReadOnlySpan<char> ParseIdentifier()
{
int start = _position;
while (!IsAtEnd() && IsIdentifierChar(Peek()))
{
Advance();
}
return _input.Slice(start, _position - start);
}
/// <summary>
/// Apply a parsed segment to the capabilities object.
/// </summary>
private void ApplySegment(VcpCapabilities capabilities, ParsedSegment segment)
{
switch (segment.Name.ToLowerInvariant())
{
case "prot":
capabilities.Protocol = segment.Content.Trim();
break;
case "type":
capabilities.Type = segment.Content.Trim();
break;
case "model":
capabilities.Model = segment.Content.Trim();
break;
case "mccs_ver":
capabilities.MccsVersion = segment.Content.Trim();
break;
case "cmds":
capabilities.SupportedCommands = ParseHexList(segment.Content);
break;
case "vcp":
capabilities.SupportedVcpCodes = ParseVcpEntries(segment.Content);
break;
case "vcpname":
ParseVcpNames(segment.Content, capabilities);
break;
default:
// Check for windowN pattern (window1, window2, etc.)
if (segment.Name.Length > 6 &&
segment.Name.StartsWith("window", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(segment.Name.AsSpan(6), out int windowNum))
{
var windowParser = new WindowParser(segment.Content);
var windowCap = windowParser.Parse(windowNum);
capabilities.Windows.Add(windowCap);
}
else
{
// Unknown segments are silently ignored
}
break;
}
}
/// <summary>
/// Parse VCP entries: vcp_entry*
/// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
/// </summary>
private Dictionary<byte, VcpCodeInfo> ParseVcpEntries(string content)
{
var vcpCodes = new Dictionary<byte, VcpCodeInfo>();
var parser = new VcpEntryParser(content);
while (parser.TryParseEntry(out var entry))
{
var name = VcpNames.GetCodeName(entry.Code);
vcpCodes[entry.Code] = new VcpCodeInfo(entry.Code, name, entry.Values);
}
return vcpCodes;
}
/// <summary>
/// Parse a hex byte list: hex_byte*
/// Handles both space-separated (01 02 03) and concatenated (010203) formats.
/// </summary>
private static List<byte> ParseHexList(string content)
{
var result = new List<byte>();
var span = content.AsSpan();
int i = 0;
while (i < span.Length)
{
// Skip whitespace
while (i < span.Length && char.IsWhiteSpace(span[i]))
{
i++;
}
if (i >= span.Length)
{
break;
}
// Try to read two hex digits
if (i + 1 < span.Length && IsHexDigit(span[i]) && IsHexDigit(span[i + 1]))
{
if (byte.TryParse(span.Slice(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value))
{
result.Add(value);
}
i += 2;
}
else
{
i++; // Skip invalid character
}
}
return result;
}
/// <summary>
/// Parse vcpname entries: hex_byte '(' name ')'
/// </summary>
private void ParseVcpNames(string content, VcpCapabilities capabilities)
{
// vcpname format: F0(Custom Name 1) F1(Custom Name 2)
var parser = new VcpNameParser(content);
while (parser.TryParseEntry(out var code, out var name))
{
if (capabilities.SupportedVcpCodes.TryGetValue(code, out var existingInfo))
{
// Update existing entry with custom name
capabilities.SupportedVcpCodes[code] = new VcpCodeInfo(code, name, existingInfo.SupportedValues);
}
else
{
// Add new entry with custom name
capabilities.SupportedVcpCodes[code] = new VcpCodeInfo(code, name, Array.Empty<int>());
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _input[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Advance() => _position++;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _input.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
Advance();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIdentifierChar(char c) =>
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
private void AddError(string message)
{
_errors.Add(new ParseError(_position, message));
Logger.LogWarning($"[MccsParser] {message}");
}
}
/// <summary>
/// Sub-parser for VCP entries within the vcp() segment.
/// </summary>
internal ref struct VcpEntryParser
{
private ReadOnlySpan<char> _content;
private int _position;
public VcpEntryParser(string content)
{
_content = content.AsSpan();
_position = 0;
}
/// <summary>
/// Try to parse the next VCP entry.
/// vcp_entry ::= hex_byte [ '(' hex_list ')' ]
/// </summary>
public bool TryParseEntry(out VcpEntry entry)
{
entry = default;
SkipWhitespace();
if (IsAtEnd())
{
return false;
}
// Parse hex byte (VCP code)
if (!TryParseHexByte(out var code))
{
// Skip invalid character and try again
_position++;
return TryParseEntry(out entry);
}
var values = new List<int>();
SkipWhitespace();
// Check for optional value list
if (!IsAtEnd() && Peek() == '(')
{
_position++; // consume '('
// Parse values until ')'
while (!IsAtEnd() && Peek() != ')')
{
SkipWhitespace();
if (Peek() == ')')
{
break;
}
if (TryParseHexByte(out var value))
{
values.Add(value);
}
else
{
_position++; // Skip invalid character
}
}
if (!IsAtEnd() && Peek() == ')')
{
_position++; // consume ')'
}
}
entry = new VcpEntry(code, values);
return true;
}
private bool TryParseHexByte(out byte value)
{
value = 0;
if (_position + 1 >= _content.Length)
{
return false;
}
if (!IsHexDigit(_content[_position]) || !IsHexDigit(_content[_position + 1]))
{
return false;
}
if (byte.TryParse(_content.Slice(_position, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
{
_position += 2;
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _content[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _content.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
_position++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
/// <summary>
/// Sub-parser for vcpname entries.
/// </summary>
internal ref struct VcpNameParser
{
private ReadOnlySpan<char> _content;
private int _position;
public VcpNameParser(string content)
{
_content = content.AsSpan();
_position = 0;
}
/// <summary>
/// Try to parse the next vcpname entry.
/// vcpname_entry ::= hex_byte '(' name ')'
/// </summary>
public bool TryParseEntry(out byte code, out string name)
{
code = 0;
name = string.Empty;
SkipWhitespace();
if (IsAtEnd())
{
return false;
}
// Parse hex byte
if (!TryParseHexByte(out code))
{
_position++;
return TryParseEntry(out code, out name);
}
SkipWhitespace();
// Expect '('
if (IsAtEnd() || Peek() != '(')
{
return false;
}
_position++; // consume '('
// Parse name until ')'
int start = _position;
while (!IsAtEnd() && Peek() != ')')
{
_position++;
}
name = _content.Slice(start, _position - start).ToString().Trim();
if (!IsAtEnd() && Peek() == ')')
{
_position++; // consume ')'
}
return true;
}
private bool TryParseHexByte(out byte value)
{
value = 0;
if (_position + 1 >= _content.Length)
{
return false;
}
if (!IsHexDigit(_content[_position]) || !IsHexDigit(_content[_position + 1]))
{
return false;
}
if (byte.TryParse(_content.Slice(_position, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value))
{
_position += 2;
return true;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _content[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _content.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
_position++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsHexDigit(char c) =>
(c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
/// <summary>
/// Sub-parser for window segment content.
/// Parses: type(PIP) area(25 25 1895 1175) max(640 480) min(10 10) window(10)
/// </summary>
internal ref struct WindowParser
{
private ReadOnlySpan<char> _content;
private int _position;
public WindowParser(string content)
{
_content = content.AsSpan();
_position = 0;
}
/// <summary>
/// Parse window segment content into a WindowCapability.
/// </summary>
public WindowCapability Parse(int windowNumber)
{
string type = string.Empty;
var area = default(WindowArea);
var maxSize = default(WindowSize);
var minSize = default(WindowSize);
int windowId = 0;
// Parse sub-segments: type(...) area(...) max(...) min(...) window(...)
while (!IsAtEnd())
{
SkipWhitespace();
if (IsAtEnd())
{
break;
}
var subSegment = ParseSubSegment();
if (subSegment.HasValue)
{
switch (subSegment.Value.Name.ToLowerInvariant())
{
case "type":
type = subSegment.Value.Content.Trim();
break;
case "area":
area = ParseArea(subSegment.Value.Content);
break;
case "max":
maxSize = ParseSize(subSegment.Value.Content);
break;
case "min":
minSize = ParseSize(subSegment.Value.Content);
break;
case "window":
_ = int.TryParse(subSegment.Value.Content.Trim(), out windowId);
break;
}
}
}
return new WindowCapability(windowNumber, type, area, maxSize, minSize, windowId);
}
private (string Name, string Content)? ParseSubSegment()
{
int start = _position;
// Parse identifier
while (!IsAtEnd() && IsIdentifierChar(Peek()))
{
_position++;
}
if (_position == start)
{
// No identifier found, skip character
if (!IsAtEnd())
{
_position++;
}
return null;
}
var name = _content.Slice(start, _position - start).ToString();
SkipWhitespace();
// Expect '('
if (IsAtEnd() || Peek() != '(')
{
return null;
}
_position++; // consume '('
// Parse content with balanced parentheses
int contentStart = _position;
int depth = 1;
while (!IsAtEnd() && depth > 0)
{
char c = Peek();
if (c == '(')
{
depth++;
}
else if (c == ')')
{
depth--;
if (depth == 0)
{
break;
}
}
_position++;
}
var content = _content.Slice(contentStart, _position - contentStart).ToString();
if (!IsAtEnd() && Peek() == ')')
{
_position++; // consume ')'
}
return (name, content);
}
private static WindowArea ParseArea(string content)
{
var values = ParseIntList(content);
if (values.Length >= 4)
{
return new WindowArea(values[0], values[1], values[2], values[3]);
}
return default;
}
private static WindowSize ParseSize(string content)
{
var values = ParseIntList(content);
if (values.Length >= 2)
{
return new WindowSize(values[0], values[1]);
}
return default;
}
private static int[] ParseIntList(string content)
{
var parts = content.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var result = new List<int>(parts.Length);
foreach (var part in parts)
{
if (int.TryParse(part.Trim(), out int value))
{
result.Add(value);
}
}
return result.ToArray();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private char Peek() => IsAtEnd() ? '\0' : _content[_position];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool IsAtEnd() => _position >= _content.Length;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhitespace()
{
while (!IsAtEnd() && char.IsWhiteSpace(Peek()))
{
_position++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIdentifierChar(char c) =>
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
/// <summary>
/// Represents a parsed segment from the capabilities string.
/// </summary>
internal readonly struct ParsedSegment
{
public string Name { get; }
public string Content { get; }
public ParsedSegment(string name, string content)
{
Name = name;
Content = content;
}
}
/// <summary>
/// Represents a parsed VCP entry.
/// </summary>
internal readonly struct VcpEntry
{
public byte Code { get; }
public IReadOnlyList<int> Values { get; }
public VcpEntry(byte code, IReadOnlyList<int> values)
{
Code = code;
Values = values;
}
}
/// <summary>
/// Represents a parse error with position information.
/// </summary>
public readonly struct ParseError
{
public int Position { get; }
public string Message { get; }
public ParseError(int position, string message)
{
Position = position;
Message = message;
}
public override string ToString() => $"[{Position}] {Message}";
}
/// <summary>
/// Result of parsing MCCS capabilities string.
/// </summary>
public sealed class MccsParseResult
{
public VcpCapabilities Capabilities { get; }
public IReadOnlyList<ParseError> Errors { get; }
public bool HasErrors => Errors.Count > 0;
public bool IsValid => !HasErrors && Capabilities.SupportedVcpCodes.Count > 0;
public MccsParseResult(VcpCapabilities capabilities, IReadOnlyList<ParseError> errors)
{
Capabilities = capabilities;
Errors = errors;
}
}
}

View File

@@ -0,0 +1,108 @@
// 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 PowerDisplay.Common.Drivers;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Unified helper class for parsing monitor feature support from VCP capabilities.
/// This eliminates duplicate VCP parsing logic across PowerDisplay.exe and Settings.UI.
/// </summary>
public static class MonitorFeatureHelper
{
/// <summary>
/// Result of parsing monitor feature support from VCP capabilities
/// </summary>
public readonly struct FeatureSupportResult
{
public bool SupportsBrightness { get; init; }
public bool SupportsContrast { get; init; }
public bool SupportsColorTemperature { get; init; }
public bool SupportsVolume { get; init; }
public bool SupportsInputSource { get; init; }
public static FeatureSupportResult Unavailable => new()
{
SupportsBrightness = false,
SupportsContrast = false,
SupportsColorTemperature = false,
SupportsVolume = false,
SupportsInputSource = false,
};
}
/// <summary>
/// Parse feature support from a list of VCP code strings.
/// This is the single source of truth for determining monitor capabilities.
/// </summary>
/// <param name="vcpCodes">List of VCP codes as strings (e.g., "0x10", "10", "0x12")</param>
/// <param name="capabilitiesRaw">Raw capabilities string, used to determine availability status</param>
/// <returns>Feature support result</returns>
public static FeatureSupportResult ParseFeatureSupport(IReadOnlyList<string>? vcpCodes, string? capabilitiesRaw)
{
// Check capabilities availability
if (string.IsNullOrEmpty(capabilitiesRaw))
{
return FeatureSupportResult.Unavailable;
}
// Convert all VCP codes to integers for comparison
var vcpCodeInts = ParseVcpCodesToIntegers(vcpCodes);
// Determine feature support based on VCP codes
return new FeatureSupportResult
{
SupportsBrightness = vcpCodeInts.Contains(NativeConstants.VcpCodeBrightness),
SupportsContrast = vcpCodeInts.Contains(NativeConstants.VcpCodeContrast),
SupportsColorTemperature = vcpCodeInts.Contains(NativeConstants.VcpCodeSelectColorPreset),
SupportsVolume = vcpCodeInts.Contains(NativeConstants.VcpCodeVolume),
SupportsInputSource = vcpCodeInts.Contains(NativeConstants.VcpCodeInputSource),
};
}
/// <summary>
/// Parse VCP codes from string list to integer set
/// Handles both hex formats: "0x10" and "10"
/// </summary>
private static HashSet<int> ParseVcpCodesToIntegers(IReadOnlyList<string>? vcpCodes)
{
var result = new HashSet<int>();
if (vcpCodes == null)
{
return result;
}
foreach (var code in vcpCodes)
{
if (string.IsNullOrWhiteSpace(code))
{
continue;
}
// Remove "0x" prefix if present and parse as hex
var cleanCode = code.Trim();
if (cleanCode.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
cleanCode = cleanCode[2..];
}
if (int.TryParse(cleanCode, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int codeInt))
{
result.Add(codeInt);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,40 @@
// 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.Common.Interfaces;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for monitor matching and identification.
/// Provides consistent logic for matching monitors across different data sources.
/// </summary>
public static class MonitorMatchingHelper
{
/// <summary>
/// Generate a unique key for monitor matching based on Id.
/// </summary>
/// <param name="monitor">The monitor data to generate a key for.</param>
/// <returns>A unique string key for the monitor.</returns>
public static string GetMonitorKey(IMonitorData? monitor)
=> monitor?.Id ?? string.Empty;
/// <summary>
/// Check if two monitors are considered the same based on their Ids.
/// </summary>
/// <param name="monitor1">First monitor.</param>
/// <param name="monitor2">Second monitor.</param>
/// <returns>True if the monitors have the same Id.</returns>
public static bool AreMonitorsSame(IMonitorData monitor1, IMonitorData monitor2)
{
if (monitor1 == null || monitor2 == null)
{
return false;
}
return !string.IsNullOrEmpty(monitor1.Id) && monitor1.Id == monitor2.Id;
}
}
}

View File

@@ -0,0 +1,23 @@
// 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.Common.Utils
{
/// <summary>
/// Provides conversion utilities for monitor hardware values.
/// Use this class to convert between raw hardware values and display-friendly formats.
/// </summary>
public static class MonitorValueConverter
{
/// <summary>
/// Formats a VCP color temperature value as a display name.
/// </summary>
/// <param name="vcpValue">The VCP preset value (e.g., 0x05).</param>
/// <returns>Display name like "6500K" or "sRGB".</returns>
public static string FormatColorTemperatureDisplay(int vcpValue)
{
return ColorTemperatureHelper.FormatColorTemperatureDisplayName(vcpValue);
}
}
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
namespace PowerDisplay.Common.Utils;
/// <summary>
/// Helper class for mapping PnP (Plug and Play) manufacturer IDs to display names.
/// PnP IDs are 3-character codes assigned by Microsoft to hardware manufacturers.
/// See: https://uefi.org/pnp_id_list
/// </summary>
public static class PnpIdHelper
{
/// <summary>
/// Map of common laptop/monitor manufacturer PnP IDs to display names.
/// Only includes manufacturers known to produce laptops with internal displays.
/// </summary>
private static readonly FrozenDictionary<string, string> ManufacturerNames = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
// Major laptop manufacturers
{ "ACR", "Acer" },
{ "AUO", "AU Optronics" },
{ "BOE", "BOE" },
{ "CMN", "Chi Mei Innolux" },
{ "DEL", "Dell" },
{ "HWP", "HP" },
{ "IVO", "InfoVision" },
{ "LEN", "Lenovo" },
{ "LGD", "LG Display" },
{ "NCP", "Nanjing CEC Panda" },
{ "SAM", "Samsung" },
{ "SDC", "Samsung Display" },
{ "SEC", "Samsung Electronics" },
{ "SHP", "Sharp" },
{ "AUS", "ASUS" },
{ "MSI", "MSI" },
{ "APP", "Apple" },
{ "SNY", "Sony" },
{ "PHL", "Philips" },
{ "HSD", "HannStar" },
{ "CPT", "Chunghwa Picture Tubes" },
{ "QDS", "Quanta Display" },
{ "TMX", "Tianma Microelectronics" },
{ "CSO", "CSOT" },
// Microsoft Surface
{ "MSF", "Microsoft" },
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Extract the 3-character PnP manufacturer ID from a hardware ID.
/// </summary>
/// <param name="hardwareId">Hardware ID like "LEN4038" or "BOE0900".</param>
/// <returns>The 3-character PnP ID (e.g., "LEN"), or null if invalid.</returns>
public static string? ExtractPnpId(string? hardwareId)
{
if (string.IsNullOrEmpty(hardwareId) || hardwareId.Length < 3)
{
return null;
}
// PnP ID is the first 3 characters
return hardwareId.Substring(0, 3).ToUpperInvariant();
}
/// <summary>
/// Get a user-friendly display name for an internal display based on its hardware ID.
/// </summary>
/// <param name="hardwareId">Hardware ID like "LEN4038" or "BOE0900".</param>
/// <returns>Display name like "Lenovo Built-in Display" or "Built-in Display" as fallback.</returns>
public static string GetBuiltInDisplayName(string? hardwareId)
{
var pnpId = ExtractPnpId(hardwareId);
if (pnpId != null && ManufacturerNames.TryGetValue(pnpId, out var manufacturer))
{
return $"{manufacturer} Built-in Display";
}
return "Built-in Display";
}
}

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;
namespace PowerDisplay.Common.Utils
{
/// <summary>
/// Helper class for profile management operations.
/// Provides utilities for profile name generation and validation.
/// </summary>
public static class ProfileHelper
{
private const string DefaultProfileBaseName = "Profile";
/// <summary>
/// Generate a unique profile name that doesn't conflict with existing names.
/// </summary>
/// <param name="existingNames">Set of existing profile names.</param>
/// <param name="baseName">The base name to use (default: "Profile").</param>
/// <returns>A unique profile name like "Profile 1", "Profile 2", etc.</returns>
public static string GenerateUniqueProfileName(ISet<string>? existingNames, string baseName = DefaultProfileBaseName)
{
if (existingNames == null || existingNames.Count == 0)
{
return $"{baseName} 1";
}
int counter = 1;
string name;
do
{
name = $"{baseName} {counter}";
counter++;
}
while (existingNames.Contains(name));
return name;
}
}
}

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.Common.Utils
{
/// <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 (ObjectDisposedException)
{
// Expected if CTS was already disposed
}
}
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,427 @@
// 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.Common.Utils
{
/// <summary>
/// Provides human-readable names for VCP codes and their values based on MCCS v2.2a specification.
/// Combines VCP code names (e.g., 0x10 = "Brightness") and VCP value names (e.g., 0x14:0x05 = "6500K").
/// </summary>
public static class VcpNames
{
/// <summary>
/// VCP code to name mapping
/// </summary>
private static readonly Dictionary<byte, string> CodeNames = new()
{
// Control codes (0x00-0x0F)
{ 0x00, "Code Page" },
{ 0x01, "Degauss" },
{ 0x02, "New Control Value" },
{ 0x03, "Soft Controls" },
// Preset operations (0x04-0x0A)
{ 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 controls (0x20-0x4C)
{ 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" },
{ 0x2E, "Gray Scale Expansion" },
{ 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" },
{ 0x76, "Remote Procedure Call" },
{ 0x78, "Display Identification Data Operation" },
{ 0x7A, "Adjust Focal Plane" },
{ 0x7C, "Adjust Zoom" },
{ 0x7E, "Trapezoid" },
{ 0x80, "Keystone" },
{ 0x82, "Horizontal Mirror (Flip)" },
{ 0x84, "Vertical Mirror (Flip)" },
// Image adjustment codes (0x86-0x9F)
{ 0x86, "Display Scaling" },
{ 0x87, "Sharpness" },
{ 0x88, "Velocity Scan Modulation" },
{ 0x8A, "Color Saturation" },
{ 0x8B, "TV Channel Up/Down" },
{ 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" },
{ 0xA8, "Window Control" },
{ 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" },
{ 0xCD, "Status Indicators" },
{ 0xCE, "Auxiliary Display Size" },
{ 0xCF, "Auxiliary Display Data" },
{ 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" },
{ 0xDB, "Backlight Level: White" },
{ 0xDC, "Display Application" },
{ 0xDD, "Application Enable Key" },
{ 0xDE, "Scratch Pad" },
{ 0xDF, "VCP Version" },
// Manufacturer specific codes (0xE0-0xFF)
// Per MCCS 2.2a: "The 32 control codes E0h through FFh have been
// allocated to allow manufacturers to issue their own specific controls."
{ 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 GetCodeName(byte code)
{
return CodeNames.TryGetValue(code, out var name) ? name : $"Unknown (0x{code:X2})";
}
// 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? GetValueName(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 GetFormattedValueName(byte vcpCode, int value)
{
var name = GetValueName(vcpCode, value);
if (name != null)
{
return $"{name} (0x{value:X2})";
}
return $"0x{value:X2}";
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

View File

@@ -0,0 +1,39 @@
// 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>
/// UI layout and timing constants
/// </summary>
public static class UI
{
// Window dimensions
public const int WindowWidth = 362;
public const int MinWindowHeight = 100;
public const int MaxWindowHeight = 650;
public const int WindowRightMargin = 12;
/// <summary>
/// Debounce delay for slider controls in milliseconds
/// </summary>
public const int SliderDebounceDelayMs = 300;
/// <summary>
/// Icon glyph for internal/laptop displays (WMI)
/// </summary>
public const string InternalMonitorGlyph = "\uE7F8";
/// <summary>
/// Icon glyph for external monitors (DDC/CI)
/// </summary>
public const string ExternalMonitorGlyph = "\uE7F4";
}
}
}

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,268 @@
// 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.UI.Dispatching;
using Windows.Devices.Display;
using Windows.Devices.Enumeration;
namespace PowerDisplay.Helpers;
/// <summary>
/// Watches for display/monitor connection changes using WinRT DeviceWatcher.
/// Triggers DisplayChanged event when monitors are added, removed, or updated.
/// </summary>
public sealed partial class DisplayChangeWatcher : IDisposable
{
private readonly DispatcherQueue _dispatcherQueue;
private readonly TimeSpan _debounceDelay = TimeSpan.FromSeconds(1);
private DeviceWatcher? _deviceWatcher;
private CancellationTokenSource? _debounceCts;
private bool _isRunning;
private bool _disposed;
private bool _initialEnumerationComplete;
/// <summary>
/// Event triggered when display configuration changes (after debounce period).
/// </summary>
public event EventHandler? DisplayChanged;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayChangeWatcher"/> class.
/// </summary>
/// <param name="dispatcherQueue">The dispatcher queue for UI thread marshalling.</param>
public DisplayChangeWatcher(DispatcherQueue dispatcherQueue)
{
_dispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue));
}
/// <summary>
/// Gets a value indicating whether the watcher is currently running.
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// Starts watching for display changes.
/// </summary>
public void Start()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_isRunning)
{
return;
}
try
{
// Get the device selector for display monitors
string selector = DisplayMonitor.GetDeviceSelector();
Logger.LogInfo($"[DisplayChangeWatcher] Using device selector: {selector}");
// Create the device watcher
_deviceWatcher = DeviceInformation.CreateWatcher(selector);
// Subscribe to events
_deviceWatcher.Added += OnDeviceAdded;
_deviceWatcher.Removed += OnDeviceRemoved;
_deviceWatcher.Updated += OnDeviceUpdated;
_deviceWatcher.EnumerationCompleted += OnEnumerationCompleted;
_deviceWatcher.Stopped += OnWatcherStopped;
// Reset state before starting (must be before Start() to avoid race)
_initialEnumerationComplete = false;
_isRunning = true;
// Start watching
_deviceWatcher.Start();
Logger.LogInfo("[DisplayChangeWatcher] Started watching for display changes");
}
catch (Exception ex)
{
Logger.LogError($"[DisplayChangeWatcher] Failed to start: {ex.Message}");
_isRunning = false;
}
}
/// <summary>
/// Stops watching for display changes.
/// </summary>
public void Stop()
{
if (!_isRunning || _deviceWatcher == null)
{
return;
}
try
{
// Cancel any pending debounce
CancelDebounce();
// Stop the watcher
_deviceWatcher.Stop();
Logger.LogInfo("[DisplayChangeWatcher] Stopped watching for display changes");
}
catch (Exception ex)
{
Logger.LogError($"[DisplayChangeWatcher] Error stopping watcher: {ex.Message}");
}
}
private void OnDeviceAdded(DeviceWatcher sender, DeviceInformation args)
{
// Dispatch to UI thread to ensure thread-safe state access
_dispatcherQueue.TryEnqueue(() =>
{
// Ignore events during initial enumeration or after disposal
if (_disposed || !_initialEnumerationComplete)
{
return;
}
Logger.LogInfo($"[DisplayChangeWatcher] Display added: {args.Name}");
ScheduleDisplayChanged();
});
}
private void OnDeviceRemoved(DeviceWatcher sender, DeviceInformationUpdate args)
{
// Dispatch to UI thread to ensure thread-safe state access
_dispatcherQueue.TryEnqueue(() =>
{
// Ignore events during initial enumeration or after disposal
if (_disposed || !_initialEnumerationComplete)
{
return;
}
Logger.LogInfo("[DisplayChangeWatcher] Display removed");
ScheduleDisplayChanged();
});
}
private void OnDeviceUpdated(DeviceWatcher sender, DeviceInformationUpdate args)
{
// Only trigger refresh for significant updates, not every property change.
// For now, we'll skip updates to avoid excessive refreshes.
// The Added and Removed events are the primary triggers for monitor changes.
}
private void OnEnumerationCompleted(DeviceWatcher sender, object args)
{
// Dispatch to UI thread to ensure thread-safe state access
_dispatcherQueue.TryEnqueue(() =>
{
_initialEnumerationComplete = true;
Logger.LogInfo("[DisplayChangeWatcher] Initial enumeration completed, now responding to display changes");
});
}
private void OnWatcherStopped(DeviceWatcher sender, object args)
{
// Dispatch to UI thread to ensure thread-safe state access
_dispatcherQueue.TryEnqueue(() =>
{
_isRunning = false;
_initialEnumerationComplete = false;
Logger.LogInfo("[DisplayChangeWatcher] Watcher stopped");
});
}
/// <summary>
/// Schedules a DisplayChanged event with debouncing.
/// Multiple rapid changes will only trigger one event after the debounce period.
/// </summary>
private void ScheduleDisplayChanged()
{
// Cancel any pending debounce
CancelDebounce();
// Create new cancellation token
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;
// Schedule the event after debounce delay
Task.Run(async () =>
{
try
{
await Task.Delay(_debounceDelay, token);
if (!token.IsCancellationRequested)
{
// Dispatch to UI thread
_dispatcherQueue.TryEnqueue(() =>
{
if (!_disposed)
{
Logger.LogInfo("[DisplayChangeWatcher] Triggering DisplayChanged event");
DisplayChanged?.Invoke(this, EventArgs.Empty);
}
});
}
}
catch (OperationCanceledException)
{
// Debounce was cancelled by a newer event, this is expected
}
catch (Exception ex)
{
Logger.LogError($"[DisplayChangeWatcher] Error in debounce task: {ex.Message}");
}
});
}
private void CancelDebounce()
{
try
{
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_debounceCts = null;
}
catch (ObjectDisposedException)
{
// Already disposed, ignore
}
}
/// <summary>
/// Disposes resources used by the watcher.
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
// Stop watching
Stop();
// Unsubscribe from events
if (_deviceWatcher != null)
{
_deviceWatcher.Added -= OnDeviceAdded;
_deviceWatcher.Removed -= OnDeviceRemoved;
_deviceWatcher.Updated -= OnDeviceUpdated;
_deviceWatcher.EnumerationCompleted -= OnEnumerationCompleted;
_deviceWatcher.Stopped -= OnWatcherStopped;
_deviceWatcher = null;
}
// Cancel debounce
CancelDebounce();
Logger.LogInfo("[DisplayChangeWatcher] Disposed");
}
}

View File

@@ -0,0 +1,188 @@
// 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;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using WinRT.Interop;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Service for handling hotkey registration in-process.
/// Uses RegisterHotKey Win32 API instead of Runner's centralized mechanism
/// to avoid IPC timing issues (CmdPal pattern).
/// </summary>
internal sealed partial class HotkeyService : IDisposable
{
private const int HotkeyId = 9001;
private readonly SettingsUtils _settingsUtils;
private readonly Action _hotkeyAction;
private nint _hwnd;
private nint _originalWndProc;
// Must keep delegate reference to prevent GC collection
private WndProcDelegate? _hotkeyWndProc;
private bool _isRegistered;
private bool _disposed;
public HotkeyService(SettingsUtils settingsUtils, Action hotkeyAction)
{
_settingsUtils = settingsUtils;
_hotkeyAction = hotkeyAction;
}
/// <summary>
/// Initialize the hotkey service with a window handle.
/// Must be called after window is created.
/// </summary>
/// <param name="window">The WinUI window to attach to.</param>
public void Initialize(Microsoft.UI.Xaml.Window window)
{
_hwnd = WindowNative.GetWindowHandle(window);
Logger.LogTrace($"[HotkeyService] Initialize: hwnd=0x{_hwnd:X}");
// LOAD BEARING: If you don't stick the pointer to the WndProc into a
// member (and instead use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our WndProc will explode.
_hotkeyWndProc = HotkeyWndProc;
var wndProcPointer = Marshal.GetFunctionPointerForDelegate(_hotkeyWndProc);
_originalWndProc = SetWindowLongPtrNative(_hwnd, GwlWndProc, wndProcPointer);
Logger.LogTrace($"[HotkeyService] WndProc hooked, original=0x{_originalWndProc:X}");
// Register hotkey based on current settings
ReloadSettings();
}
/// <summary>
/// Reload settings and re-register hotkey.
/// Call this when settings change.
/// </summary>
public void ReloadSettings()
{
Logger.LogTrace("[HotkeyService] ReloadSettings called");
UnregisterHotkey();
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
var hotkey = settings?.Properties?.ActivationShortcut;
if (hotkey == null || !hotkey.IsValid())
{
Logger.LogInfo("[HotkeyService] No valid hotkey configured");
return;
}
RegisterHotkey(hotkey);
}
private void RegisterHotkey(HotkeySettings hotkey)
{
if (_hwnd == 0)
{
Logger.LogWarning("[HotkeyService] Cannot register hotkey: window handle not set");
return;
}
// Build modifiers using bit flags
uint modifiers = ModNoRepeat
| (hotkey.Win ? ModWin : 0)
| (hotkey.Ctrl ? ModControl : 0)
| (hotkey.Alt ? ModAlt : 0)
| (hotkey.Shift ? ModShift : 0);
if (RegisterHotKeyNative(_hwnd, HotkeyId, modifiers, (uint)hotkey.Code))
{
_isRegistered = true;
Logger.LogInfo($"[HotkeyService] Hotkey registered: {hotkey}");
}
else
{
Logger.LogError($"[HotkeyService] Failed to register hotkey: {hotkey}, error={Marshal.GetLastWin32Error()}");
}
}
private void UnregisterHotkey()
{
if (!_isRegistered || _hwnd == 0)
{
return;
}
bool success = UnregisterHotKeyNative(_hwnd, HotkeyId);
if (success)
{
Logger.LogTrace("[HotkeyService] Hotkey unregistered");
}
else
{
var error = Marshal.GetLastWin32Error();
Logger.LogWarning($"[HotkeyService] Failed to unregister hotkey, error={error}");
}
_isRegistered = false;
}
private nint HotkeyWndProc(nint hwnd, uint uMsg, nuint wParam, nint lParam)
{
if (uMsg == WmHotkey && (int)wParam == HotkeyId)
{
Logger.LogInfo("[HotkeyService] WM_HOTKEY received, invoking action");
try
{
_hotkeyAction?.Invoke();
}
catch (Exception ex)
{
Logger.LogError($"[HotkeyService] Hotkey action failed: {ex.Message}");
}
return 0;
}
return CallWindowProcNative(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
public void Dispose()
{
if (_disposed)
{
return;
}
UnregisterHotkey();
_disposed = true;
}
// P/Invoke constants
private const int GwlWndProc = -4;
private const uint WmHotkey = 0x0312;
// HOT_KEY_MODIFIERS flags
private const uint ModAlt = 0x0001;
private const uint ModControl = 0x0002;
private const uint ModShift = 0x0004;
private const uint ModWin = 0x0008;
private const uint ModNoRepeat = 0x4000;
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtrNative(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
private static partial nint CallWindowProcNative(nint lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
[LibraryImport("user32.dll", EntryPoint = "RegisterHotKey", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool RegisterHotKeyNative(nint hWnd, int id, uint fsModifiers, uint vk);
[LibraryImport("user32.dll", EntryPoint = "UnregisterHotKey", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool UnregisterHotKeyNative(nint hWnd, int id);
}
}

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.Common.Drivers;
using PowerDisplay.Common.Drivers.DDC;
using PowerDisplay.Common.Drivers.WMI;
using PowerDisplay.Common.Interfaces;
using PowerDisplay.Common.Models;
using PowerDisplay.Common.Services;
using PowerDisplay.Common.Utils;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay.Helpers
{
/// <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 Dictionary<string, Monitor> _monitorLookup = new();
private readonly SemaphoreSlim _discoveryLock = new(1, 1);
private readonly DisplayRotationService _rotationService = new();
// Controllers stored by type for O(1) lookup based on CommunicationMethod
private DdcCiController? _ddcController;
private WmiController? _wmiController;
private bool _disposed;
public IReadOnlyList<Monitor> Monitors => _monitors.AsReadOnly();
public MonitorManager()
{
// Initialize controllers
InitializeControllers();
}
/// <summary>
/// Initialize controllers
/// </summary>
private void InitializeControllers()
{
try
{
// DDC/CI controller (external monitors)
_ddcController = new DdcCiController();
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize DDC/CI controller: {ex.Message}");
}
try
{
// WMI controller (internal monitors)
// Always create - DiscoverMonitorsAsync returns empty list if WMI is unavailable
_wmiController = new WmiController();
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to initialize WMI controller: {ex.Message}");
}
}
/// <summary>
/// Discover all monitors from all controllers.
/// Each controller is responsible for fully initializing its monitors
/// (including brightness, capabilities, input source, color temperature, etc.)
/// </summary>
public async Task<IReadOnlyList<Monitor>> DiscoverMonitorsAsync(CancellationToken cancellationToken = default)
{
await _discoveryLock.WaitAsync(cancellationToken);
try
{
var discoveredMonitors = await DiscoverFromAllControllersAsync(cancellationToken);
// Update collections
_monitors.Clear();
_monitorLookup.Clear();
var sortedMonitors = discoveredMonitors
.OrderBy(m => m.MonitorNumber)
.ToList();
_monitors.AddRange(sortedMonitors);
foreach (var monitor in sortedMonitors)
{
_monitorLookup[monitor.Id] = monitor;
}
return _monitors.AsReadOnly();
}
finally
{
_discoveryLock.Release();
}
}
/// <summary>
/// Discover monitors from all registered controllers in parallel.
/// </summary>
private async Task<List<Monitor>> DiscoverFromAllControllersAsync(CancellationToken cancellationToken)
{
var tasks = new List<Task<IEnumerable<Monitor>>>();
if (_ddcController != null)
{
tasks.Add(SafeDiscoverAsync(_ddcController, cancellationToken));
}
if (_wmiController != null)
{
tasks.Add(SafeDiscoverAsync(_wmiController, cancellationToken));
}
var results = await Task.WhenAll(tasks);
return results.SelectMany(m => m).ToList();
}
/// <summary>
/// Safely discover monitors from a controller, returning empty list on failure.
/// </summary>
private static async Task<IEnumerable<Monitor>> SafeDiscoverAsync(
IMonitorController controller,
CancellationToken cancellationToken)
{
try
{
return await controller.DiscoverMonitorsAsync(cancellationToken);
}
catch (Exception ex)
{
Logger.LogWarning($"Controller {controller.Name} discovery failed: {ex.Message}");
return Enumerable.Empty<Monitor>();
}
}
/// <summary>
/// Get brightness of the specified monitor
/// </summary>
public async Task<VcpFeatureValue> GetBrightnessAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.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 VcpFeatureValue.Invalid;
}
}
/// <summary>
/// Set brightness of the specified monitor
/// </summary>
public Task<MonitorOperationResult> SetBrightnessAsync(string monitorId, int brightness, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
brightness,
(ctrl, mon, val, ct) => ctrl.SetBrightnessAsync(mon, val, ct),
(mon, val) => mon.UpdateStatus(val, true),
cancellationToken);
/// <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<VcpFeatureValue> GetColorTemperatureAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.Invalid;
}
try
{
return await controller.GetColorTemperatureAsync(monitor, cancellationToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return VcpFeatureValue.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>
/// Get current input source for a monitor
/// </summary>
public async Task<VcpFeatureValue> GetInputSourceAsync(string monitorId, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
return VcpFeatureValue.Invalid;
}
var controller = GetControllerForMonitor(monitor);
if (controller == null)
{
return VcpFeatureValue.Invalid;
}
try
{
return await controller.GetInputSourceAsync(monitor, cancellationToken);
}
catch (Exception ex) when (ex is not OutOfMemoryException)
{
return VcpFeatureValue.Invalid;
}
}
/// <summary>
/// Set input source for a monitor
/// </summary>
public Task<MonitorOperationResult> SetInputSourceAsync(string monitorId, int inputSource, CancellationToken cancellationToken = default)
=> ExecuteMonitorOperationAsync(
monitorId,
inputSource,
(ctrl, mon, val, ct) => ctrl.SetInputSourceAsync(mon, val, ct),
(mon, val) => mon.CurrentInputSource = val,
cancellationToken);
/// <summary>
/// Set rotation/orientation for a monitor.
/// Uses Windows ChangeDisplaySettingsEx API (not DDC/CI).
/// After successful rotation, refreshes orientation for all monitors sharing the same GdiDeviceName
/// (important for mirror/clone mode where multiple monitors share one display source).
/// </summary>
/// <param name="monitorId">Monitor ID</param>
/// <param name="orientation">Orientation: 0=normal, 1=90°, 2=180°, 3=270°</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Operation result</returns>
public Task<MonitorOperationResult> SetRotationAsync(string monitorId, int orientation, CancellationToken cancellationToken = default)
{
var monitor = GetMonitor(monitorId);
if (monitor == null)
{
Logger.LogError($"[MonitorManager] SetRotation: Monitor not found: {monitorId}");
return Task.FromResult(MonitorOperationResult.Failure("Monitor not found"));
}
// Rotation uses Windows display settings API, not DDC/CI controller
// Prefer using Monitor object which contains GdiDeviceName for accurate adapter targeting
var result = _rotationService.SetRotation(monitor, orientation);
if (result.IsSuccess)
{
// Refresh orientation for all monitors - rotation affects the GdiDeviceName (display source),
// and in mirror mode multiple monitors may share the same GdiDeviceName
RefreshAllOrientations();
Logger.LogInfo($"[MonitorManager] SetRotation: Successfully set {monitorId} to orientation {orientation}");
}
else
{
Logger.LogError($"[MonitorManager] SetRotation: Failed for {monitorId}: {result.ErrorMessage}");
}
return Task.FromResult(result);
}
/// <summary>
/// Refresh orientation values for all monitors by querying current display settings.
/// This ensures all monitors reflect the actual system state, which is important
/// in mirror mode where multiple monitors share the same GdiDeviceName.
/// </summary>
public void RefreshAllOrientations()
{
foreach (var monitor in _monitors)
{
if (string.IsNullOrEmpty(monitor.GdiDeviceName))
{
continue;
}
var currentOrientation = _rotationService.GetCurrentOrientation(monitor.GdiDeviceName);
if (currentOrientation >= 0 && currentOrientation != monitor.Orientation)
{
monitor.Orientation = currentOrientation;
monitor.LastUpdate = DateTime.Now;
}
}
}
/// <summary>
/// Get monitor by ID. Uses dictionary lookup for O(1) performance.
/// </summary>
public Monitor? GetMonitor(string monitorId)
{
return _monitorLookup.TryGetValue(monitorId, out var monitor) ? monitor : null;
}
/// <summary>
/// Get controller for the monitor based on CommunicationMethod.
/// O(1) lookup - no async validation needed since controller type is determined at discovery.
/// </summary>
private IMonitorController? GetControllerForMonitor(Monitor monitor)
{
return monitor.CommunicationMethod switch
{
"WMI" => _wmiController,
"DDC/CI" => _ddcController,
_ => 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 = GetControllerForMonitor(monitor);
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}");
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_discoveryLock?.Dispose();
// Release controllers
_ddcController?.Dispose();
_wmiController?.Dispose();
_monitors.Clear();
_monitorLookup.Clear();
_disposed = true;
}
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using ManagedCommon;
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)
{
Logger.LogTrace($"[NativeEventWaiter] Setting up event loop for event: {eventName}");
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
if (dispatcherQueue == null)
{
Logger.LogError($"[NativeEventWaiter] DispatcherQueue is null for event: {eventName}");
return;
}
Logger.LogTrace($"[NativeEventWaiter] DispatcherQueue obtained for event: {eventName}");
var t = new Thread(() =>
{
Logger.LogInfo($"[NativeEventWaiter] Background thread started for event: {eventName}");
try
{
Logger.LogTrace($"[NativeEventWaiter] Creating EventWaitHandle for event: {eventName}");
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
Logger.LogInfo($"[NativeEventWaiter] EventWaitHandle created successfully for event: {eventName}");
int waitCount = 0;
while (!cancellationToken.IsCancellationRequested)
{
// Use 500ms timeout for polling
if (eventHandle.WaitOne(500))
{
waitCount++;
Logger.LogInfo($"[NativeEventWaiter] Event SIGNALED: {eventName} (signal count: {waitCount})");
bool enqueued = dispatcherQueue.TryEnqueue(() =>
{
Logger.LogTrace($"[NativeEventWaiter] Executing callback on UI thread for event: {eventName}");
try
{
callback();
Logger.LogTrace($"[NativeEventWaiter] Callback completed for event: {eventName}");
}
catch (Exception callbackEx)
{
Logger.LogError($"[NativeEventWaiter] Callback exception for event {eventName}: {callbackEx.Message}");
}
});
if (!enqueued)
{
Logger.LogError($"[NativeEventWaiter] Failed to enqueue callback to UI thread for event: {eventName}");
}
}
}
Logger.LogInfo($"[NativeEventWaiter] Event loop ending for event: {eventName} (cancellation requested)");
}
catch (Exception ex)
{
Logger.LogError($"[NativeEventWaiter] Exception in event loop for {eventName}: {ex.Message}\n{ex.StackTrace}");
}
});
t.IsBackground = true;
t.Name = $"NativeEventWaiter_{eventName}";
t.Start();
Logger.LogTrace($"[NativeEventWaiter] Background thread started with name: {t.Name}");
}
}
}

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,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.Diagnostics;
using System.IO;
namespace PowerDisplay.Helpers
{
public static class SettingsDeepLink
{
public enum SettingsWindow
{
PowerDisplay,
}
public static void OpenSettings(SettingsWindow window, bool mainExecutableIsOnTheParentFolder)
{
try
{
var directoryPath = System.AppContext.BaseDirectory;
if (mainExecutableIsOnTheParentFolder)
{
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
directoryPath = Path.Combine(directoryPath, "..");
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
else
{
// PowerToys.exe is in the same path as the application.
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=PowerDisplay" });
}
catch
{
// Silently ignore errors opening settings
}
}
}
}

View File

@@ -0,0 +1,328 @@
// 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.IO;
using System.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT.Interop;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Window procedure delegate for handling window messages.
/// Uses primitive types to avoid accessibility issues with CsWin32-generated types.
/// </summary>
/// <param name="hwnd">Handle to the window.</param>
/// <param name="msg">The message.</param>
/// <param name="wParam">Additional message information.</param>
/// <param name="lParam">Additional message.</param>
/// <returns>The result of the message processing.</returns>
internal delegate nint WndProcDelegate(nint hwnd, uint msg, nuint wParam, nint lParam);
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_*")]
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_*")]
internal sealed partial class TrayIconService
{
private const uint MY_NOTIFY_ID = 1001;
private const uint WM_TRAY_ICON = PInvoke.WM_USER + 1;
private readonly SettingsUtils _settingsUtils;
private readonly Action _toggleWindowAction;
private readonly Action _exitAction;
private readonly Action _openSettingsAction;
private readonly uint WM_TASKBAR_RESTART;
private Window? _window;
private nint _hwnd;
private nint _originalWndProc;
private WndProcDelegate? _trayWndProc;
private NOTIFYICONDATAW? _trayIconData;
private nint _largeIcon;
private nint _popupMenu;
public TrayIconService(
SettingsUtils settingsUtils,
Action toggleWindowAction,
Action exitAction,
Action openSettingsAction)
{
_settingsUtils = settingsUtils;
_toggleWindowAction = toggleWindowAction;
_exitAction = exitAction;
_openSettingsAction = openSettingsAction;
// TaskbarCreated is the message that's broadcast when explorer.exe
// restarts. We need to know when that happens to be able to bring our
// notification area icon back
WM_TASKBAR_RESTART = RegisterWindowMessageNative("TaskbarCreated");
}
public void SetupTrayIcon(bool? showSystemTrayIcon = null)
{
var settings = _settingsUtils.GetSettingsOrDefault<PowerDisplaySettings>(PowerDisplaySettings.ModuleName);
bool shouldShow = showSystemTrayIcon ?? settings.Properties.ShowSystemTrayIcon;
if (shouldShow)
{
if (_window is null)
{
_window = new Window();
_hwnd = WindowNative.GetWindowHandle(_window);
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
// member (and instead like, use a local), then the pointer we marshal
// into the WindowLongPtr will be useless after we leave this function,
// and our **WindProc will explode**.
_trayWndProc = WindowProc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_trayWndProc);
_originalWndProc = SetWindowLongPtrNative(_hwnd, GWL_WNDPROC, hotKeyPrcPointer);
}
if (_trayIconData is null)
{
// We need to stash this handle, so it doesn't clean itself up. If
// explorer restarts, we'll come back through here, and we don't
// really need to re-load the icon in that case. We can just use
// the handle from the first time.
_largeIcon = GetAppIconHandle();
unsafe
{
_trayIconData = new NOTIFYICONDATAW()
{
cbSize = (uint)sizeof(NOTIFYICONDATAW),
hWnd = new HWND(_hwnd),
uID = MY_NOTIFY_ID,
uFlags = NOTIFY_ICON_DATA_FLAGS.NIF_MESSAGE | NOTIFY_ICON_DATA_FLAGS.NIF_ICON | NOTIFY_ICON_DATA_FLAGS.NIF_TIP,
uCallbackMessage = WM_TRAY_ICON,
hIcon = new HICON(_largeIcon),
szTip = GetString("AppName"),
};
}
}
var d = (NOTIFYICONDATAW)_trayIconData;
// Add the notification icon
unsafe
{
bool success = Shell_NotifyIconNative((uint)NOTIFY_ICON_MESSAGE.NIM_ADD, &d);
if (!success)
{
// Shell_NotifyIcon can fail if explorer.exe isn't ready yet (e.g., during system startup)
// Reset _trayIconData to allow retry via WM_WINDOWPOSCHANGING or WM_TASKBAR_RESTART
Logger.LogWarning("[TrayIcon] Shell_NotifyIcon(NIM_ADD) failed, will retry later");
_trayIconData = null;
return;
}
Logger.LogInfo("[TrayIcon] Tray icon created successfully");
}
if (_popupMenu == 0)
{
_popupMenu = CreatePopupMenu();
InsertMenuNative(_popupMenu, 0, (uint)(MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING), PInvoke.WM_USER + 1, GetString("TrayMenu_Settings"));
InsertMenuNative(_popupMenu, 1, (uint)(MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING), PInvoke.WM_USER + 2, GetString("TrayMenu_Exit"));
}
}
else
{
Destroy();
}
}
public void Destroy()
{
if (_trayIconData is not null)
{
var d = (NOTIFYICONDATAW)_trayIconData;
unsafe
{
if (Shell_NotifyIconNative((uint)NOTIFY_ICON_MESSAGE.NIM_DELETE, &d))
{
_trayIconData = null;
}
}
}
if (_popupMenu != 0)
{
DestroyMenu(_popupMenu);
_popupMenu = 0;
}
if (_largeIcon != 0)
{
DestroyIcon(_largeIcon);
_largeIcon = 0;
}
if (_window is not null)
{
_window.Close();
_window = null;
_hwnd = 0;
}
}
private static string GetString(string key)
{
try
{
return ResourceLoaderInstance.ResourceLoader.GetString(key);
}
catch
{
return "unknown";
}
}
private nint GetAppIconHandle()
{
var exePath = Path.Combine(AppContext.BaseDirectory, "PowerToys.PowerDisplay.exe");
ExtractIconExNative(exePath, 0, out var largeIcon, out _, 1);
return largeIcon;
}
private nint WindowProc(
nint hwnd,
uint uMsg,
nuint wParam,
nint lParam)
{
switch (uMsg)
{
case PInvoke.WM_COMMAND:
{
if (wParam == PInvoke.WM_USER + 1)
{
// Settings menu item
Logger.LogInfo("[TrayIcon] Settings menu clicked");
_openSettingsAction?.Invoke();
}
else if (wParam == PInvoke.WM_USER + 2)
{
// Exit menu item
Logger.LogInfo("[TrayIcon] Exit menu clicked");
_exitAction?.Invoke();
}
}
break;
// Shell_NotifyIcon can fail when we invoke it during the time explorer.exe isn't present/ready to handle it.
// We'll also never receive WM_TASKBAR_RESTART message if the first call to Shell_NotifyIcon failed, so we use
// WM_WINDOWPOSCHANGING which is always received on explorer startup sequence.
case PInvoke.WM_WINDOWPOSCHANGING:
{
if (_trayIconData is null)
{
SetupTrayIcon();
}
}
break;
default:
// WM_TASKBAR_RESTART isn't a compile-time constant, so we can't
// use it in a case label
if (uMsg == WM_TASKBAR_RESTART)
{
// Handle the case where explorer.exe restarts.
// Even if we created it before, do it again
Logger.LogInfo("[TrayIcon] Taskbar restarted, recreating tray icon");
SetupTrayIcon();
}
else if (uMsg == WM_TRAY_ICON)
{
switch ((uint)lParam)
{
case PInvoke.WM_RBUTTONUP:
{
if (_popupMenu != 0)
{
GetCursorPos(out var cursorPos);
SetForegroundWindow(_hwnd);
TrackPopupMenuExNative(_popupMenu, (uint)TRACK_POPUP_MENU_FLAGS.TPM_LEFTALIGN | (uint)TRACK_POPUP_MENU_FLAGS.TPM_BOTTOMALIGN, cursorPos.X, cursorPos.Y, _hwnd, 0);
}
}
break;
case PInvoke.WM_LBUTTONUP:
case PInvoke.WM_LBUTTONDBLCLK:
Logger.LogInfo("[TrayIcon] Left click/double click - toggling window");
_toggleWindowAction?.Invoke();
break;
}
}
break;
}
return CallWindowProcIntPtr(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
private static partial nint CallWindowProcIntPtr(IntPtr lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
[LibraryImport("user32.dll", EntryPoint = "RegisterWindowMessageW", StringMarshalling = StringMarshalling.Utf16)]
private static partial uint RegisterWindowMessageNative(string lpString);
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtrNative(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetCursorPos(out POINT lpPoint);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetForegroundWindow(nint hWnd);
// Shell APIs - use uint for enums and unsafe pointer for struct
[LibraryImport("shell32.dll", EntryPoint = "Shell_NotifyIconW")]
[return: MarshalAs(UnmanagedType.Bool)]
private static unsafe partial bool Shell_NotifyIconNative(uint dwMessage, NOTIFYICONDATAW* lpData);
[LibraryImport("shell32.dll", EntryPoint = "ExtractIconExW", StringMarshalling = StringMarshalling.Utf16)]
private static partial uint ExtractIconExNative(string lpszFile, int nIconIndex, out nint phiconLarge, out nint phiconSmall, uint nIcons);
// Menu APIs
[LibraryImport("user32.dll")]
private static partial nint CreatePopupMenu();
[LibraryImport("user32.dll", EntryPoint = "InsertMenuW", StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool InsertMenuNative(nint hMenu, uint uPosition, uint uFlags, nuint uIDNewItem, string? lpNewItem);
[LibraryImport("user32.dll", EntryPoint = "TrackPopupMenuEx")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool TrackPopupMenuExNative(nint hMenu, uint uFlags, int x, int y, nint hwnd, nint lptpm);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DestroyMenu(nint hMenu);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DestroyIcon(nint hIcon);
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X;
public int Y;
}
// GWL_WNDPROC constant
private const int GWL_WNDPROC = -4;
}
}

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,22 @@
// 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.UI.Xaml;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Provides conversion utilities for Visibility binding in x:Bind scenarios.
/// AOT-compatible alternative to IValueConverter implementations.
/// </summary>
public static class VisibilityConverter
{
/// <summary>
/// Converts a boolean value to a Visibility value.
/// </summary>
/// <param name="value">The boolean value to convert.</param>
/// <returns>Visibility.Visible if true, Visibility.Collapsed if false.</returns>
public static Visibility BoolToVisibility(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
}
}

View File

@@ -0,0 +1,261 @@
// 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 Microsoft.UI;
using Microsoft.UI.Windowing;
using WinUIEx;
namespace PowerDisplay.Helpers
{
internal static partial class WindowHelper
{
// Cursor position structure for GetCursorPos
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X;
public int Y;
}
// Cursor position for detecting the monitor with the mouse
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetCursorPos(out POINT lpPoint);
// 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;
private const uint SwpNosize = 0x0001;
private const uint SwpNomove = 0x0002;
private const uint SwpFramechanged = 0x0020;
private const nint HwndTopmost = -1;
private const nint HwndNotopmost = -2;
// ShowWindow commands
private const int SwHide = 0;
private const int SwShow = 5;
// P/Invoke declarations (64-bit only - PowerToys only builds for x64/ARM64)
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLong(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool SetWindowPos(
nint hWnd,
nint hWndInsertAfter,
int x,
int y,
int cx,
int cy,
uint uFlags);
[LibraryImport("user32.dll", EntryPoint = "ShowWindow")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindowNative(nint hWnd, int nCmdShow);
[LibraryImport("user32.dll", EntryPoint = "IsWindowVisible")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool IsWindowVisibleNative(nint hWnd);
/// <summary>
/// Check if window is visible
/// </summary>
public static bool IsWindowVisible(nint hWnd)
{
return IsWindowVisibleNative(hWnd);
}
/// <summary>
/// Disable window moving and resizing functionality
/// </summary>
public static void DisableWindowMovingAndResizing(nint hWnd)
{
// Get current window style
nint style = GetWindowLongPtr(hWnd, GwlStyle);
// 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
_ = SetWindowLong(hWnd, GwlStyle, style);
// Get extended style and remove related borders
nint exStyle = GetWindowLongPtr(hWnd, GwlExstyle);
exStyle &= ~WsExDlgmodalframe;
exStyle &= ~WsExWindowedge;
exStyle &= ~WsExClientedge;
exStyle &= ~WsExStaticedge;
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
// Refresh window frame
SetWindowPos(
hWnd,
0,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
/// <summary>
/// Set whether window is topmost
/// </summary>
public static void SetWindowTopmost(nint hWnd, bool topmost)
{
SetWindowPos(
hWnd,
topmost ? HwndTopmost : HwndNotopmost,
0,
0,
0,
0,
SwpNomove | SwpNosize);
}
/// <summary>
/// Show or hide window
/// </summary>
public static void ShowWindow(nint hWnd, bool show)
{
ShowWindowNative(hWnd, show ? SwShow : SwHide);
}
/// <summary>
/// Hide window from taskbar
/// </summary>
public static void HideFromTaskbar(nint hWnd)
{
// Get current extended style
nint exStyle = GetWindowLongPtr(hWnd, GwlExstyle);
// Add WS_EX_TOOLWINDOW style to hide window from taskbar
exStyle |= WsExToolwindow;
// Set new extended style
_ = SetWindowLong(hWnd, GwlExstyle, exStyle);
// Refresh window frame
SetWindowPos(
hWnd,
0,
0,
0,
0,
0,
SwpNomove | SwpNosize | SwpFramechanged);
}
/// <summary>
/// Get the DPI scale factor for a window (relative to standard 96 DPI)
/// </summary>
/// <param name="window">WinUIEx window</param>
/// <returns>DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%)</returns>
public static double GetDpiScale(WindowEx window)
{
return (float)window.GetDpiForWindow() / 96.0;
}
/// <summary>
/// Convert device-independent units (DIU) to physical pixels
/// </summary>
/// <param name="diu">Device-independent unit value</param>
/// <param name="dpiScale">DPI scale factor</param>
/// <returns>Physical pixel value</returns>
public static int ScaleToPhysicalPixels(int diu, double dpiScale)
{
return (int)Math.Ceiling(diu * dpiScale);
}
/// <summary>
/// Position a window at the bottom-right corner of the monitor where the mouse cursor is located.
/// Uses WinUIEx MonitorInfo API which correctly handles all edge cases:
/// - Multi-monitor setups
/// - Taskbar at any position (top/bottom/left/right)
/// - Different DPI settings
/// </summary>
/// <param name="window">WinUIEx window to position</param>
/// <param name="width">Window width in device-independent units (DIU)</param>
/// <param name="height">Window height in device-independent units (DIU)</param>
/// <param name="rightMargin">Right margin in device-independent units (DIU)</param>
public static void PositionWindowBottomRight(
WindowEx window,
int width,
int height,
int rightMargin = 0)
{
// Use WinUIEx MonitorInfo - RectWork already includes correct offsets for taskbar position
var monitors = MonitorInfo.GetDisplayMonitors();
if (monitors == null || monitors.Count == 0)
{
ManagedCommon.Logger.LogWarning("PositionWindowBottomRight: No monitors found, skipping positioning");
return;
}
// Find the monitor where the mouse cursor is located
var targetMonitor = GetMonitorAtCursor(monitors);
var workArea = targetMonitor.RectWork;
double dpiScale = GetDpiScale(window);
// Calculate bottom-right position
// RectWork.Right/Bottom already account for taskbar position
double x = workArea.Right - (dpiScale * (width + rightMargin));
double y = workArea.Bottom - (dpiScale * height);
window.MoveAndResize(x, y, width, height);
}
/// <summary>
/// Get the monitor where the mouse cursor is currently located.
/// Falls back to primary monitor if cursor position cannot be determined.
/// </summary>
/// <param name="monitors">List of available monitors</param>
/// <returns>MonitorInfo of the monitor containing the cursor</returns>
private static MonitorInfo GetMonitorAtCursor(IList<MonitorInfo> monitors)
{
// Try to get cursor position using Win32 API
if (GetCursorPos(out var cursorPos))
{
// Find the monitor that contains the cursor point
foreach (var monitor in monitors)
{
if (cursorPos.X >= monitor.RectMonitor.Left &&
cursorPos.X < monitor.RectMonitor.Right &&
cursorPos.Y >= monitor.RectMonitor.Top &&
cursorPos.Y < monitor.RectMonitor.Bottom)
{
return monitor;
}
}
}
// Fallback to first monitor (typically primary)
return monitors[0];
}
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"public": true,
"allowMarshaling": false
}

View File

@@ -0,0 +1,17 @@
// Structs and types only - functions use LibraryImport for AOT compatibility
NOTIFYICONDATAW
NOTIFY_ICON_MESSAGE
NOTIFY_ICON_DATA_FLAGS
MENU_ITEM_FLAGS
TRACK_POPUP_MENU_FLAGS
// Window message constants (used by TrayIconService)
WM_USER
WM_COMMAND
WM_RBUTTONUP
WM_LBUTTONUP
WM_LBUTTONDBLCLK
WM_WINDOWPOSCHANGING
// COM wait flags for single instance redirection (constants only)
CWMO_FLAGS

View File

@@ -0,0 +1,104 @@
<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>
<ApplicationIcon>Assets\PowerDisplay\PowerDisplay.ico</ApplicationIcon>
<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>
<!-- Hide build log files from Solution Explorer -->
<None Remove="*.log" />
<None Remove="*.binlog" />
</ItemGroup>
<ItemGroup>
<!-- Add WindowsDesktop.App framework reference to align Microsoft.VisualBasic.dll version
with other projects that use UseWPF/UseWindowsForms. This does NOT enable WPF/WinForms,
it only ensures consistent runtime DLL versions across all WinUI3Apps. -->
<FrameworkReference Include="Microsoft.WindowsDesktop.App" />
</ItemGroup>
<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" />
<PackageReference Include="CommunityToolkit.Mvvm" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<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" />
<!-- Removed ManagedCsWin32 - using CsWin32 directly for TrayIcon APIs -->
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\PowerDisplay.Lib\PowerDisplay.Lib.csproj" />
</ItemGroup>
<!-- Copy Assets folder to output directory -->
<ItemGroup>
<Content Include="Assets\PowerDisplay\*">
<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,333 @@
// 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.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using PowerDisplay.Common;
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
{
private readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
private Window? _mainWindow;
private int _powerToysRunnerPid;
private TrayIconService? _trayIconService;
public App(int runnerPid)
{
Logger.LogInfo($"App constructor: Starting with runnerPid={runnerPid}");
_powerToysRunnerPid = runnerPid;
Logger.LogTrace("App constructor: Calling InitializeComponent");
this.InitializeComponent();
// Ensure types used in XAML are preserved for AOT compilation
TypePreservation.PreserveTypes();
// Note: Logger is already initialized in Program.cs before App constructor
Logger.LogTrace("App constructor: InitializeComponent completed");
// Initialize PowerToys telemetry
try
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
Logger.LogTrace("App constructor: Telemetry event sent");
}
catch (Exception ex)
{
Logger.LogWarning($"App constructor: Telemetry failed: {ex.Message}");
}
// Initialize language settings
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
Logger.LogTrace($"App constructor: Language set to {appLanguage}");
}
// Handle unhandled exceptions
this.UnhandledException += OnUnhandledException;
Logger.LogInfo("App constructor: Completed");
}
/// <summary>
/// Handle unhandled exceptions
/// </summary>
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
/// <summary>
/// Called when the application is launched
/// </summary>
/// <param name="args">Launch arguments</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
Logger.LogInfo("OnLaunched: Application launching");
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)
// Note: PowerDisplay.exe should NOT listen to RefreshMonitorsEvent
// That event is sent BY PowerDisplay TO Settings UI for one-way notification
Logger.LogInfo("OnLaunched: Registering Windows Events for IPC...");
RegisterWindowEvent(Constants.TogglePowerDisplayEvent(), mw => mw.ToggleWindow(), "Toggle");
Logger.LogTrace($"OnLaunched: Registered Toggle event: {Constants.TogglePowerDisplayEvent()}");
RegisterEvent(Constants.TerminatePowerDisplayEvent(), () => Environment.Exit(0), "Terminate");
Logger.LogTrace($"OnLaunched: Registered Terminate event: {Constants.TerminatePowerDisplayEvent()}");
RegisterWindowEvent(
Constants.SettingsUpdatedPowerDisplayEvent(),
mw =>
{
mw.ViewModel.ApplySettingsFromUI();
// Refresh tray icon based on updated settings
_trayIconService?.SetupTrayIcon();
},
"SettingsUpdated");
RegisterWindowEvent(
Constants.HotkeyUpdatedPowerDisplayEvent(),
mw => mw.ReloadHotkeySettings(),
"HotkeyUpdated");
RegisterViewModelEvent(Constants.ApplyColorTemperaturePowerDisplayEvent(), vm => vm.ApplyColorTemperatureFromSettings(), "ApplyColorTemperature");
RegisterViewModelEvent(Constants.ApplyProfilePowerDisplayEvent(), vm => vm.ApplyProfileFromSettings(), "ApplyProfile");
RegisterViewModelEvent(Constants.PowerDisplaySendSettingsTelemetryEvent(), vm => vm.SendSettingsTelemetry(), "SendSettingsTelemetry");
// LightSwitch integration - apply profiles when theme changes
RegisterViewModelEvent(PathConstants.LightSwitchLightThemeEventName, vm => vm.ApplyLightSwitchProfile(isLightMode: true), "LightSwitch-Light");
RegisterViewModelEvent(PathConstants.LightSwitchDarkThemeEventName, vm => vm.ApplyLightSwitchProfile(isLightMode: false), "LightSwitch-Dark");
Logger.LogInfo("OnLaunched: All Windows Events registered");
// Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0)
{
Logger.LogInfo($"OnLaunched: PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("OnLaunched: PowerToys Runner exited. Exiting PowerDisplay");
Environment.Exit(0);
});
}
else
{
Logger.LogInfo("OnLaunched: PowerDisplay started in standalone mode (no runner PID)");
}
// Create main window
Logger.LogInfo("OnLaunched: Creating MainWindow");
_mainWindow = new MainWindow();
Logger.LogInfo("OnLaunched: MainWindow created");
// Initialize tray icon service
Logger.LogTrace("OnLaunched: Initializing TrayIconService");
_trayIconService = new TrayIconService(
_settingsUtils,
ToggleMainWindow,
() => Environment.Exit(0),
OpenSettings);
_trayIconService.SetupTrayIcon();
Logger.LogTrace("OnLaunched: TrayIconService initialized");
// Window visibility depends on launch mode
bool isStandaloneMode = _powerToysRunnerPid <= 0;
Logger.LogInfo($"OnLaunched: isStandaloneMode={isStandaloneMode}");
if (isStandaloneMode)
{
// Standalone mode - activate and show window immediately
Logger.LogInfo("OnLaunched: Activating window (standalone mode)");
_mainWindow.Activate();
Logger.LogInfo("OnLaunched: Window activated (standalone mode)");
}
else
{
// PowerToys mode - window remains hidden until show event received
// Background initialization runs automatically via MainWindow constructor
Logger.LogInfo("OnLaunched: Window created but hidden, waiting for show/toggle event (PowerToys mode)");
}
Logger.LogInfo("OnLaunched: Application launch completed");
}
catch (Exception ex)
{
Logger.LogError($"OnLaunched: PowerDisplay startup failed: {ex.Message}\n{ex.StackTrace}");
}
}
/// <summary>
/// Register a simple event handler (no window access needed)
/// </summary>
private void RegisterEvent(string eventName, Action action, string logName)
{
Logger.LogTrace($"RegisterEvent: Setting up event listener for '{logName}' on event '{eventName}'");
NativeEventWaiter.WaitForEventLoop(
eventName,
() =>
{
Logger.LogInfo($"[EVENT] {logName} event received from event '{eventName}'");
try
{
action();
Logger.LogTrace($"[EVENT] {logName} action completed");
}
catch (Exception ex)
{
Logger.LogError($"[EVENT] {logName} action failed: {ex.Message}");
}
},
CancellationToken.None);
}
/// <summary>
/// Register an event handler that operates on MainWindow directly
/// NativeEventWaiter already marshals to UI thread
/// </summary>
private void RegisterWindowEvent(string eventName, Action<MainWindow> action, string logName)
{
Logger.LogTrace($"RegisterWindowEvent: Setting up window event listener for '{logName}' on event '{eventName}'");
NativeEventWaiter.WaitForEventLoop(
eventName,
() =>
{
Logger.LogInfo($"[EVENT] {logName} window event received from event '{eventName}'");
if (_mainWindow is MainWindow mainWindow)
{
Logger.LogTrace($"[EVENT] {logName}: MainWindow is valid, invoking action");
try
{
action(mainWindow);
Logger.LogTrace($"[EVENT] {logName}: Window action completed");
}
catch (Exception ex)
{
Logger.LogError($"[EVENT] {logName}: Window action failed: {ex.Message}");
}
}
else
{
Logger.LogError($"[EVENT] {logName}: _mainWindow is null or not MainWindow type");
}
},
CancellationToken.None);
}
/// <summary>
/// Register an event handler that operates on ViewModel via DispatcherQueue
/// Used for Settings UI IPC events that need ViewModel access
/// </summary>
private void RegisterViewModelEvent(string eventName, Action<ViewModels.MainViewModel> action, string logName)
{
NativeEventWaiter.WaitForEventLoop(
eventName,
() =>
{
Logger.LogInfo($"[EVENT] {logName} event received");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
action(mainWindow.ViewModel);
}
});
},
CancellationToken.None);
}
/// <summary>
/// Gets the main window instance
/// </summary>
public Window? MainWindow => _mainWindow;
/// <summary>
/// Show the main window
/// </summary>
private void ShowMainWindow()
{
Logger.LogInfo("ShowMainWindow: Called");
if (_mainWindow is MainWindow mainWindow)
{
Logger.LogTrace("ShowMainWindow: MainWindow is valid, calling ShowWindow");
mainWindow.ShowWindow();
}
else
{
Logger.LogError("ShowMainWindow: _mainWindow is null or not MainWindow type");
}
}
/// <summary>
/// Toggle the main window visibility
/// </summary>
private void ToggleMainWindow()
{
Logger.LogInfo("ToggleMainWindow: Called");
if (_mainWindow is MainWindow mainWindow)
{
Logger.LogTrace($"ToggleMainWindow: MainWindow is valid, current visibility={mainWindow.IsWindowVisible()}");
mainWindow.ToggleWindow();
}
else
{
Logger.LogError("ToggleMainWindow: _mainWindow is null or not MainWindow type");
}
}
/// <summary>
/// Open PowerDisplay settings in PowerToys Settings UI
/// </summary>
private void OpenSettings()
{
// mainExecutableIsOnTheParentFolder = true because PowerDisplay is a WinUI 3 app
// deployed in a subfolder (PowerDisplay\) while PowerToys.exe is in the parent folder
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerDisplay, true);
}
/// <summary>
/// Refresh tray icon based on current settings
/// </summary>
public void RefreshTrayIcon()
{
_trayIconService?.SetupTrayIcon();
}
/// <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");
_trayIconService?.Destroy();
Environment.Exit(0);
}
}
}

View File

@@ -0,0 +1,25 @@
<winuiex:WindowEx
x:Class="PowerDisplay.PowerDisplayXAML.IdentifyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False"
mc:Ignorable="d">
<Grid Background="#1A000000">
<TextBlock
x:Name="NumberText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="Segoe UI"
FontSize="200"
FontWeight="Bold"
Foreground="White"
Text="1" />
</Grid>
</winuiex:WindowEx>

View File

@@ -0,0 +1,78 @@
// 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.Threading.Tasks;
using Microsoft.UI.Windowing;
using Windows.Graphics;
using WinUIEx;
namespace PowerDisplay.PowerDisplayXAML
{
/// <summary>
/// Interaction logic for IdentifyWindow.xaml
/// </summary>
public sealed partial class IdentifyWindow : WindowEx
{
// Window size in device-independent units (DIU)
private const int WindowWidthDiu = 300;
private const int WindowHeightDiu = 280;
private double _dpiScale = 1.0;
public IdentifyWindow(string displayText)
{
InitializeComponent();
NumberText.Text = displayText;
// Configure window style
ConfigureWindow();
// Auto close after 3 seconds
Task.Delay(3000).ContinueWith(_ =>
{
DispatcherQueue.TryEnqueue(() =>
{
Close();
});
});
}
private void ConfigureWindow()
{
// Get DPI scale using WinUIEx API
_dpiScale = this.GetDpiForWindow() / 96.0;
// Set window size scaled for DPI
// AppWindow.Resize expects physical pixels
int physicalWidth = (int)(WindowWidthDiu * _dpiScale);
int physicalHeight = (int)(WindowHeightDiu * _dpiScale);
this.AppWindow.Resize(new SizeInt32 { Width = physicalWidth, Height = physicalHeight });
// Window properties (IsResizable, IsMinimizable, IsMaximizable,
// IsTitleBarVisible, IsShownInSwitchers) are set in XAML
// Set window topmost using WinUIEx API
this.IsAlwaysOnTop = true;
}
/// <summary>
/// Position the window at the center of the specified display area
/// </summary>
public void PositionOnDisplay(DisplayArea displayArea)
{
var workArea = displayArea.WorkArea;
// Window size in physical pixels (already scaled for DPI)
int physicalWidth = (int)(WindowWidthDiu * _dpiScale);
int physicalHeight = (int)(WindowHeightDiu * _dpiScale);
// Calculate center position (WorkArea coordinates are in physical pixels)
int x = workArea.X + ((workArea.Width - physicalWidth) / 2);
int y = workArea.Y + ((workArea.Height - physicalHeight) / 2);
// Use WindowEx's AppWindow property
this.AppWindow.Move(new PointInt32(x, y));
}
}
}

View File

@@ -0,0 +1,427 @@
<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:helpers="using:PowerDisplay.Helpers"
xmlns:local="using:PowerDisplay"
xmlns:models="using:PowerDisplay.Common.Models"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:PowerDisplay.ViewModels"
xmlns:winuiex="using:WinUIEx"
MinWidth="0"
MinHeight="0"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False">
<winuiex:WindowEx.SystemBackdrop>
<DesktopAcrylicBackdrop />
</winuiex:WindowEx.SystemBackdrop>
<Grid x:Name="RootGrid" IsTabStop="True">
<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 Content Area with modern design -->
<Border
x:Name="ContentArea"
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<Grid>
<StackPanel
Margin="0,16,0,16"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical"
Spacing="16"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(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"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.ShowNoMonitorsMessage), Mode=OneWay}" />
<!-- Content Area -->
<ScrollViewer
x:Name="MainScrollViewer"
Padding="16,16,16,16"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
HorizontalScrollMode="Disabled"
VerticalScrollBarVisibility="Auto"
ZoomMode="Disabled">
<!-- Monitors List with modern card design -->
<ItemsRepeater
x:Name="MonitorsRepeater"
HorizontalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.Monitors, Mode=OneWay}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(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 Margin="2,0,0,0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="22" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:MonitorIcon
VerticalAlignment="Center"
IsBuiltIn="{x:Bind IsInternal, Mode=OneWay}"
MonitorNumber="{x:Bind MonitorNumber, Mode=OneWay}" />
<TextBlock
Grid.Column="2"
Margin="0,0,0,2"
VerticalAlignment="Center"
Text="{x:Bind DisplayName, Mode=OneWay}" />
<Button
Grid.Column="2"
HorizontalAlignment="Right"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowInputSource), Mode=OneWay}">
<Button.Flyout>
<Flyout>
<StackPanel Orientation="Vertical">
<ListView
ItemsSource="{x:Bind AvailableInputSources, Mode=OneWay}"
SelectionChanged="InputSourceListView_SelectionChanged"
SelectionMode="Single">
<ListView.Header>
<TextBlock
Margin="16,0,8,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Input source" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="vm:InputSourceItem">
<Grid Padding="0,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Text="{x:Bind Name}" />
<FontIcon
Grid.Column="1"
Margin="8,0,0,0"
FontSize="12"
Glyph="&#xE73E;"
Visibility="{x:Bind SelectionVisibility}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Flyout>
</Button.Flyout>
</Button>
</Grid>
<StackPanel
Margin="0,8,0,0"
Padding="8,0,16,8"
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<!-- 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="18"
Glyph="&#xEC8A;" />
<Slider
x:Uid="BrightnessAutomation"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
DataContext="{x:Bind}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Maximum="{x:Bind MaxBrightness, Mode=OneWay}"
Minimum="{x:Bind MinBrightness, Mode=OneWay}"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Brightness"
Value="{x:Bind Brightness, Mode=OneWay}" />
</Grid>
<!-- Contrast Control -->
<Grid
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowContrast), Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="ContrastTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="12"
Glyph="&#xE7A1;" />
<Slider
x:Uid="ContrastAutomation"
Grid.Column="2"
VerticalAlignment="Center"
DataContext="{x:Bind}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Maximum="100"
Minimum="0"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Contrast"
Value="{x:Bind ContrastPercent, Mode=OneWay}" />
</Grid>
<!-- Volume Control -->
<Grid
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowVolume), Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="VolumeTooltip"
Margin="2,0,0,0"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="16"
Glyph="&#xE993;" />
<Slider
x:Uid="VolumeAutomation"
Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
DataContext="{x:Bind}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Maximum="{x:Bind MaxVolume, Mode=OneWay}"
Minimum="{x:Bind MinVolume, Mode=OneWay}"
PointerCaptureLost="Slider_PointerCaptureLost"
Tag="Volume"
Value="{x:Bind Volume, Mode=OneWay}" />
</Grid>
<!-- Rotation Controls -->
<Grid
Margin="0,8,0,0"
HorizontalAlignment="Stretch"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ShowRotation), Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16" />
<ColumnDefinition Width="16" />
<!-- Spacing -->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Uid="RotationTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="16"
Glyph="&#xE7AD;" />
<Grid Grid.Column="2" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Normal (0°) -->
<ToggleButton
x:Uid="RotateNormalTooltip"
Grid.Column="0"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
IsChecked="{x:Bind IsRotation0, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Tag="0">
<FontIcon FontSize="14" Glyph="&#xE74A;" />
</ToggleButton>
<!-- Left (270°) -->
<ToggleButton
x:Uid="RotateLeftTooltip"
Grid.Column="2"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
IsChecked="{x:Bind IsRotation3, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Tag="3">
<FontIcon FontSize="14" Glyph="&#xE76B;" />
</ToggleButton>
<!-- Right (90°) -->
<ToggleButton
x:Uid="RotateRightTooltip"
Grid.Column="4"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
IsChecked="{x:Bind IsRotation1, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Tag="1">
<FontIcon FontSize="14" Glyph="&#xE76C;" />
</ToggleButton>
<!-- Inverted (180°) -->
<ToggleButton
x:Uid="RotateInvertedTooltip"
Grid.Column="6"
HorizontalAlignment="Stretch"
Click="RotationButton_Click"
DataContext="{x:Bind}"
IsChecked="{x:Bind IsRotation2, Mode=OneWay}"
IsEnabled="{x:Bind IsInteractionEnabled, Mode=OneWay}"
Tag="2">
<FontIcon FontSize="14" Glyph="&#xE74B;" />
</ToggleButton>
</Grid>
</Grid>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Grid>
</Border>
<Grid x:Name="StatusBar" Grid.Row="1">
<!-- Action Buttons -->
<StackPanel
Margin="0,0,8,8"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<Button
x:Name="ProfilesButton"
x:Uid="ProfilesTooltip"
Content="{ui:FontIcon Glyph=&#xE748;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind helpers:VisibilityConverter.BoolToVisibility(ViewModel.HasProfiles), Mode=OneWay}">
<Button.Flyout>
<Flyout x:Name="ProfilesFlyout">
<ListView
x:Name="ProfilesListView"
ItemsSource="{x:Bind ViewModel.Profiles, Mode=OneWay}"
SelectionChanged="ProfileListView_SelectionChanged"
SelectionMode="Single">
<ListView.Header>
<TextBlock
Margin="16,0,8,0"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Profiles" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:PowerDisplayProfile">
<TextBlock Padding="0,4" Text="{x:Bind Name}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
</Button.Flyout>
</Button>
<Button
x:Name="RefreshButton"
x:Uid="RefreshTooltip"
Click="OnRefreshClick"
Content="{ui:FontIcon Glyph=&#xE72C;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
<Button
x:Name="IdentifyButton"
x:Uid="IdentifyTooltip"
Command="{x:Bind ViewModel.IdentifyMonitorsCommand}"
Content="{ui:FontIcon Glyph=&#xE9D9;,
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,563 @@
// 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.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using PowerDisplay.Common.Models;
using PowerDisplay.Configuration;
using PowerDisplay.Helpers;
using PowerDisplay.ViewModels;
using Windows.Graphics;
using WinUIEx;
using Monitor = PowerDisplay.Common.Models.Monitor;
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay main window
/// </summary>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)]
public sealed partial class MainWindow : WindowEx, IDisposable
{
private readonly SettingsUtils _settingsUtils = SettingsUtils.Default;
private MainViewModel? _viewModel;
private HotkeyService? _hotkeyService;
// Expose ViewModel as property for x:Bind
public MainViewModel ViewModel => _viewModel ?? throw new InvalidOperationException("ViewModel not initialized");
public MainWindow()
{
Logger.LogInfo("MainWindow constructor: Starting");
try
{
// 1. Create ViewModel BEFORE InitializeComponent to avoid x:Bind failures
// x:Bind evaluates during InitializeComponent, so ViewModel must exist first
Logger.LogTrace("MainWindow constructor: Creating MainViewModel");
_viewModel = new MainViewModel();
Logger.LogTrace("MainWindow constructor: MainViewModel created");
Logger.LogTrace("MainWindow constructor: Calling InitializeComponent");
this.InitializeComponent();
Logger.LogTrace("MainWindow constructor: InitializeComponent completed");
// 2. Configure window immediately (synchronous, no data dependency)
Logger.LogTrace("MainWindow constructor: Configuring window");
ConfigureWindow();
// 3. Set up data context and update bindings
RootGrid.DataContext = _viewModel;
Bindings.Update();
Logger.LogTrace("MainWindow constructor: Data context set and bindings updated");
// 4. Register event handlers
RegisterEventHandlers();
Logger.LogTrace("MainWindow constructor: Event handlers registered");
// 5. Initialize HotkeyService for in-process hotkey handling (CmdPal pattern)
// This avoids IPC timing issues with Runner's centralized hotkey mechanism
Logger.LogTrace("MainWindow constructor: Initializing HotkeyService");
_hotkeyService = new HotkeyService(_settingsUtils, ToggleWindow);
_hotkeyService.Initialize(this);
Logger.LogTrace("MainWindow constructor: HotkeyService initialized");
// Note: ViewModel handles all async initialization internally.
// We listen to InitializationCompleted event to know when data is ready.
// No duplicate initialization here - single responsibility in ViewModel.
Logger.LogInfo("MainWindow constructor: Completed");
}
catch (Exception ex)
{
Logger.LogError($"MainWindow constructor: Initialization failed: {ex.Message}\n{ex.StackTrace}");
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 is guaranteed non-null here as this is called after initialization
if (_viewModel != null)
{
_viewModel.InitializationCompleted += OnViewModelInitializationCompleted;
_viewModel.UIRefreshRequested += OnUIRefreshRequested;
_viewModel.Monitors.CollectionChanged += OnMonitorsCollectionChanged;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
}
}
/// <summary>
/// Called when ViewModel completes initial monitor discovery.
/// This is the single source of truth for initialization state.
/// </summary>
private void OnViewModelInitializationCompleted(object? sender, EventArgs e)
{
_hasInitialized = true;
Logger.LogInfo("MainWindow: Initialization completed via ViewModel event, _hasInitialized=true");
AdjustWindowSizeToContent();
}
private bool _hasInitialized;
private void ShowError(string message)
{
Logger.LogError($"Error: {message}");
}
private void OnWindowActivated(object sender, WindowActivatedEventArgs args)
{
Logger.LogTrace($"OnWindowActivated: WindowActivationState={args.WindowActivationState}");
// Auto-hide window when it loses focus (deactivated)
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
Logger.LogInfo("OnWindowActivated: Window deactivated, hiding window");
HideWindow();
}
}
private void OnWindowClosed(object sender, WindowEventArgs args)
{
// If only user operation (although we hide close button), just hide window
args.Handled = true; // Prevent window closing
HideWindow();
}
public void ShowWindow()
{
Logger.LogInfo($"ShowWindow: Called, _hasInitialized={_hasInitialized}");
try
{
// If not initialized, log warning but continue showing
if (!_hasInitialized)
{
Logger.LogWarning("ShowWindow: Window not fully initialized yet, showing anyway");
}
// Adjust size BEFORE showing to prevent flicker
// This measures content and positions window at correct size
Logger.LogTrace("ShowWindow: Adjusting window size to content");
AdjustWindowSizeToContent();
// CRITICAL: WinUI3 windows must be Activated at least once to display properly.
// In PowerToys mode, window is created but never activated until first show.
// Without Activate(), Show() may not actually render the window on screen.
Logger.LogTrace("ShowWindow: Calling this.Activate()");
this.Activate();
// Now show the window - it should appear at the correct size (WinUIEx simplified)
Logger.LogTrace("ShowWindow: Calling this.Show()");
this.Show();
// Ensure window stays on top of other windows
this.IsAlwaysOnTop = true;
Logger.LogTrace("ShowWindow: IsAlwaysOnTop set to true");
// Clear focus from any interactive element (e.g., Slider) to prevent
// showing the value tooltip when the window opens
RootGrid.Focus(FocusState.Programmatic);
// Verify window is visible
bool isVisible = IsWindowVisible();
Logger.LogInfo($"ShowWindow: Window visibility after show: {isVisible}");
if (!isVisible)
{
Logger.LogError("ShowWindow: Window not visible after show attempt, forcing visibility");
this.Activate();
this.Show();
this.BringToFront();
Logger.LogInfo($"ShowWindow: After forced show, visibility: {IsWindowVisible()}");
}
else
{
Logger.LogInfo("ShowWindow: Window shown successfully");
}
}
catch (Exception ex)
{
Logger.LogError($"ShowWindow: Failed to show window: {ex.Message}\n{ex.StackTrace}");
throw;
}
}
public void HideWindow()
{
Logger.LogInfo("HideWindow: Hiding window");
// Hide window using WinUIEx simplified API
this.Hide();
Logger.LogTrace($"HideWindow: Window hidden, visibility now: {IsWindowVisible()}");
}
/// <summary>
/// Check if window is currently visible
/// </summary>
/// <returns>True if window is visible, false otherwise</returns>
public bool IsWindowVisible()
{
// Use WinUIEx Visible property
bool visible = this.Visible;
Logger.LogTrace($"IsWindowVisible: Returning {visible}");
return visible;
}
/// <summary>
/// Toggle window visibility (show if hidden, hide if visible)
/// </summary>
public void ToggleWindow()
{
bool currentlyVisible = IsWindowVisible();
Logger.LogInfo($"ToggleWindow: Called, current visibility={currentlyVisible}");
try
{
if (currentlyVisible)
{
Logger.LogInfo("ToggleWindow: Window is visible, hiding");
HideWindow();
}
else
{
Logger.LogInfo("ToggleWindow: Window is hidden, showing");
ShowWindow();
}
Logger.LogInfo($"ToggleWindow: Completed, new visibility={IsWindowVisible()}");
}
catch (Exception ex)
{
Logger.LogError($"ToggleWindow: Failed to toggle window: {ex.Message}\n{ex.StackTrace}");
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();
});
}
}
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}");
}
}
private void OnSettingsClick(object sender, RoutedEventArgs e)
{
// Open PowerDisplay settings in PowerToys Settings UI
// mainExecutableIsOnTheParentFolder = true because PowerDisplay is a WinUI 3 app
// deployed in a subfolder (PowerDisplay\) while PowerToys.exe is in the parent folder
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.PowerDisplay, true);
}
/// <summary>
/// Configure window properties (synchronous, no data dependency)
/// </summary>
private void ConfigureWindow()
{
try
{
// Window properties (IsResizable, IsMaximizable, IsMinimizable,
// IsTitleBarVisible, IsShownInSwitchers) are set in XAML
// Set minimal initial window size - will be adjusted before showing
// Using minimal height to prevent "large window shrinking" flicker
this.AppWindow.Resize(new SizeInt32 { Width = AppConstants.UI.WindowWidth, Height = 100 });
// Position window at bottom right corner
PositionWindowAtBottomRight();
// Set window title
this.AppWindow.Title = "PowerDisplay";
// Custom title bar - completely remove all buttons
var titleBar = this.AppWindow.TitleBar;
if (titleBar != null)
{
// Extend content into title bar area
titleBar.ExtendsContentIntoTitleBar = true;
// Completely remove title bar height
titleBar.PreferredHeightOption = 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 (removes WS_CAPTION, WS_SYSMENU, etc.)
var hWnd = this.GetWindowHandle();
WindowHelper.DisableWindowMovingAndResizing(hWnd);
}
catch (Exception ex)
{
// Ignore window setup errors
Logger.LogWarning($"Window configuration error: {ex.Message}");
}
}
private void AdjustWindowSizeToContent()
{
try
{
if (RootGrid == null)
{
return;
}
// Force layout update and measure content height
RootGrid.UpdateLayout();
MainContainer?.Measure(new Windows.Foundation.Size(AppConstants.UI.WindowWidth, double.PositiveInfinity));
var contentHeight = (int)Math.Ceiling(MainContainer?.DesiredSize.Height ?? 0);
// Apply min/max height limits and reposition (WindowEx handles DPI automatically)
// Min height ensures window is visible even if content hasn't loaded yet
var finalHeight = Math.Max(AppConstants.UI.MinWindowHeight, Math.Min(contentHeight, AppConstants.UI.MaxWindowHeight));
Logger.LogTrace($"AdjustWindowSizeToContent: contentHeight={contentHeight}, finalHeight={finalHeight}");
WindowHelper.PositionWindowBottomRight(this, AppConstants.UI.WindowWidth, finalHeight, AppConstants.UI.WindowRightMargin);
}
catch (Exception ex)
{
Logger.LogError($"Error adjusting window size: {ex.Message}");
}
}
private void PositionWindowAtBottomRight()
{
try
{
var windowSize = this.AppWindow.Size;
WindowHelper.PositionWindowBottomRight(
this, // MainWindow inherits from WindowEx
AppConstants.UI.WindowWidth,
windowSize.Height,
AppConstants.UI.WindowRightMargin);
}
catch (Exception)
{
// Window positioning failures are non-critical, silently ignore
}
}
/// <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;
}
}
/// <summary>
/// Input source ListView selection changed handler - switches the monitor input source
/// </summary>
private async void InputSourceListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView)
{
return;
}
// Get the selected input source item
var selectedItem = listView.SelectedItem as InputSourceItem;
if (selectedItem == null)
{
return;
}
Logger.LogInfo($"[UI] InputSourceListView_SelectionChanged: Selected {selectedItem.Name} (0x{selectedItem.Value:X2}) for monitor {selectedItem.MonitorId}");
// Find the monitor by ID
MonitorViewModel? monitorVm = null;
if (!string.IsNullOrEmpty(selectedItem.MonitorId) && _viewModel != null)
{
monitorVm = _viewModel.Monitors.FirstOrDefault(m => m.Id == selectedItem.MonitorId);
}
if (monitorVm == null)
{
Logger.LogWarning("[UI] InputSourceListView_SelectionChanged: Could not find MonitorViewModel");
return;
}
// Set the input source
await monitorVm.SetInputSourceAsync(selectedItem.Value);
}
/// <summary>
/// Rotation button click handler - changes monitor orientation
/// </summary>
private async void RotationButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not Microsoft.UI.Xaml.Controls.Primitives.ToggleButton toggleButton)
{
return;
}
// Get the orientation from the Tag
if (toggleButton.Tag is not string tagStr || !int.TryParse(tagStr, out int orientation))
{
Logger.LogWarning("[UI] RotationButton_Click: Invalid Tag");
return;
}
var monitorVm = toggleButton.DataContext as MonitorViewModel;
if (monitorVm == null)
{
Logger.LogWarning("[UI] RotationButton_Click: Could not find MonitorViewModel");
return;
}
// If clicking the current orientation, restore the checked state and do nothing
if (monitorVm.CurrentRotation == orientation)
{
toggleButton.IsChecked = true;
return;
}
Logger.LogInfo($"[UI] RotationButton_Click: Setting rotation for {monitorVm.Name} to {orientation}");
// Set the rotation
await monitorVm.SetRotationAsync(orientation);
}
/// <summary>
/// Profile selection changed handler - applies the selected profile
/// </summary>
private void ProfileListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView)
{
return;
}
var selectedProfile = listView.SelectedItem as PowerDisplayProfile;
if (selectedProfile == null || !selectedProfile.IsValid())
{
return;
}
Logger.LogInfo($"[UI] ProfileListView_SelectionChanged: Applying profile '{selectedProfile.Name}'");
// Apply profile via ViewModel command
if (_viewModel?.ApplyProfileCommand?.CanExecute(selectedProfile) == true)
{
_viewModel.ApplyProfileCommand.Execute(selectedProfile);
}
// Close the flyout after selection
ProfilesFlyout?.Hide();
// Clear selection to allow reselecting the same profile
listView.SelectedItem = null;
}
public void Dispose()
{
_hotkeyService?.Dispose();
_viewModel?.Dispose();
GC.SuppressFinalize(this);
}
/// <summary>
/// Reload hotkey settings. Call this when settings change.
/// </summary>
public void ReloadHotkeySettings()
{
_hotkeyService?.ReloadSettings();
}
}
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="PowerDisplay.MonitorIcon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:PowerDisplay"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<Viewbox>
<Grid>
<Grid x:Name="MonitorGrid">
<FontIcon
x:Uid="MonitorTooltip"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="22"
Glyph="&#xE7F4;" />
<TextBlock
Margin="0,0,0,4"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
FontWeight="SemiBold"
Text="{x:Bind MonitorNumber, Mode=OneWay}" />
</Grid>
<Grid
x:Name="BuiltInDisplayGrid"
Padding="0,0,0,-4"
Visibility="Collapsed">
<FontIcon
x:Uid="MonitorTooltip"
Margin="0,0,0,0"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="22"
Glyph="&#xE7FB;" />
<TextBlock
Margin="0,0,0,6"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="10"
FontWeight="SemiBold"
Text="{x:Bind MonitorNumber, Mode=OneWay}" />
</Grid>
</Grid>
</Viewbox>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Monitor" />
<VisualState x:Name="BuiltIn">
<VisualState.Setters>
<Setter Target="BuiltInDisplayGrid.Visibility" Value="Visible" />
<Setter Target="MonitorGrid.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,45 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace PowerDisplay;
public sealed partial class MonitorIcon : UserControl
{
public MonitorIcon()
{
InitializeComponent();
}
public bool IsBuiltIn
{
get => (bool)GetValue(IsBuiltInProperty);
set => SetValue(IsBuiltInProperty, value);
}
public static readonly DependencyProperty IsBuiltInProperty = DependencyProperty.Register(nameof(IsBuiltIn), typeof(bool), typeof(MonitorIcon), new PropertyMetadata(false, OnPropertyChanged));
public int MonitorNumber
{
get => (int)GetValue(MonitorNumberProperty);
set => SetValue(MonitorNumberProperty, value);
}
public static readonly DependencyProperty MonitorNumberProperty = DependencyProperty.Register(nameof(MonitorNumber), typeof(int), typeof(MonitorIcon), new PropertyMetadata(0, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var monIcon = (MonitorIcon)d;
if (monIcon.IsBuiltIn)
{
VisualStateManager.GoToState(monIcon, "BuiltIn", true);
}
else
{
VisualStateManager.GoToState(monIcon, "Monitor", true);
}
}
}

View File

@@ -0,0 +1,163 @@
// 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;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.UI.Dispatching;
using Microsoft.Windows.AppLifecycle;
namespace PowerDisplay
{
public static partial class Program
{
private static App? _app;
// LibraryImport for AOT compatibility - COM wait constants
private const uint CowaitDefault = 0;
private const uint InfiniteTimeout = 0xFFFFFFFF;
[LibraryImport("ole32.dll")]
private static partial int CoWaitForMultipleObjects(
uint dwFlags,
uint dwTimeout,
int cHandles,
nint[] pHandles,
out uint lpdwIndex);
[STAThread]
public static int Main(string[] args)
{
// Initialize COM wrappers first (needed for AppInstance)
WinRT.ComWrappersSupport.InitializeComWrappers();
// Single instance check BEFORE logger initialization to avoid creating extra log files
// Command Palette pattern: check for existing instance first
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
var keyInstance = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance");
if (!keyInstance.IsCurrent)
{
// Another instance exists - redirect and exit WITHOUT initializing logger
// This prevents creation of extra log files for short-lived redirect processes
RedirectActivationTo(activationArgs, keyInstance);
return 0;
}
// This is the primary instance - now initialize logger
Logger.InitializeLogger("\\PowerDisplay\\Logs");
Logger.LogInfo("=== PowerDisplay Process Starting (Primary Instance) ===");
Logger.LogInfo($"Main: Process ID = {Environment.ProcessId}");
Logger.LogInfo($"Main: Command line args count = {args.Length}");
for (int i = 0; i < args.Length; i++)
{
Logger.LogInfo($"Main: args[{i}] = '{args[i]}'");
}
// Register activation handler for future redirects
keyInstance.Activated += OnActivated;
// 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($"Main: Parsed runner_pid={runnerPid} from args[0]");
}
else
{
Logger.LogWarning($"Main: Failed to parse PID from args[0]: '{args[0]}'");
}
}
else
{
Logger.LogWarning("Main: No command line args provided. Running in standalone mode.");
}
Logger.LogInfo("Main: Starting application");
Microsoft.UI.Xaml.Application.Start((p) =>
{
Logger.LogTrace("Main: Application.Start callback - setting up SynchronizationContext");
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
Logger.LogTrace("Main: Creating App instance");
_app = new App(runnerPid);
Logger.LogTrace("Main: App instance created");
});
Logger.LogInfo("Main: Application.Start returned, process ending");
return 0;
}
/// <summary>
/// Redirect activation to existing instance (Command Palette pattern)
/// Called BEFORE logger is initialized, so no logging here
/// </summary>
private static void RedirectActivationTo(AppActivationArguments args, AppInstance keyInstance)
{
// Do the redirection on another thread, and use a non-blocking
// wait method to wait for the redirection to complete.
using var redirectSemaphore = new Semaphore(0, 1);
var redirectTimeout = TimeSpan.FromSeconds(10);
_ = Task.Run(() =>
{
using var cts = new CancellationTokenSource(redirectTimeout);
try
{
keyInstance.RedirectActivationToAsync(args)
.AsTask(cts.Token)
.GetAwaiter()
.GetResult();
}
catch
{
// Silently ignore errors - logger not initialized yet
}
finally
{
redirectSemaphore.Release();
}
});
// Use CoWaitForMultipleObjects to pump COM messages while waiting
nint[] handles = [redirectSemaphore.SafeWaitHandle.DangerousGetHandle()];
_ = CoWaitForMultipleObjects(
CowaitDefault,
InfiniteTimeout,
1,
handles,
out _);
}
/// <summary>
/// Called when an existing instance is activated by another process
/// </summary>
private static void OnActivated(object? sender, AppActivationArguments args)
{
Logger.LogInfo("OnActivated: Received activation from another instance");
// Toggle the window visibility when activated by another instance
if (_app?.MainWindow is MainWindow mainWindow)
{
Logger.LogInfo("OnActivated: Showing/toggling main window");
mainWindow.DispatcherQueue.TryEnqueue(() =>
{
Logger.LogTrace("OnActivated: Executing ShowWindow on UI thread");
mainWindow.ShowWindow();
});
}
else
{
Logger.LogWarning("OnActivated: MainWindow not available");
}
}
}
}

View File

@@ -0,0 +1,60 @@
// 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;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerDisplay.Common.Models;
#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.
/// Note: MonitorStateFile and MonitorStateEntry are now in PowerDisplay.Lib
/// and should be serialized using ProfileSerializationContext from the Lib.
/// </summary>
[JsonSerializable(typeof(MonitorInfoData))]
[JsonSerializable(typeof(IPCMessageAction))]
[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; }
}
}

View File

@@ -0,0 +1,77 @@
// 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 ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Settings.UI.Library;
namespace PowerDisplay.Services
{
/// <summary>
/// Service for handling LightSwitch theme change events.
/// Reads LightSwitch settings using the standard PowerToys settings pattern.
/// </summary>
public static class LightSwitchService
{
private const string LogPrefix = "[LightSwitch]";
/// <summary>
/// Get the profile name to apply for the given theme.
/// </summary>
/// <param name="isLightMode">Whether the theme changed to light mode.</param>
/// <returns>The profile name to apply, or null if no profile is configured.</returns>
public static string? GetProfileForTheme(bool isLightMode)
{
try
{
Logger.LogInfo($"{LogPrefix} Processing theme change to {(isLightMode ? "light" : "dark")} mode");
var settings = SettingsUtils.Default.GetSettingsOrDefault<LightSwitchSettings>(LightSwitchSettings.ModuleName);
if (settings?.Properties == null)
{
Logger.LogWarning($"{LogPrefix} LightSwitch settings not found");
return null;
}
string? profileName;
if (isLightMode)
{
if (!settings.Properties.EnableLightModeProfile.Value)
{
Logger.LogInfo($"{LogPrefix} Light mode profile is disabled");
return null;
}
profileName = settings.Properties.LightModeProfile.Value;
}
else
{
if (!settings.Properties.EnableDarkModeProfile.Value)
{
Logger.LogInfo($"{LogPrefix} Dark mode profile is disabled");
return null;
}
profileName = settings.Properties.DarkModeProfile.Value;
}
if (string.IsNullOrEmpty(profileName) || profileName == "(None)")
{
Logger.LogInfo($"{LogPrefix} No profile configured for {(isLightMode ? "light" : "dark")} mode");
return null;
}
Logger.LogInfo($"{LogPrefix} Profile to apply: {profileName}");
return profileName;
}
catch (Exception ex)
{
Logger.LogError($"{LogPrefix} Failed to get profile for theme: {ex.Message}");
return null;
}
}
}
}

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